ontoref/crates/ontoref-daemon/src/registry.rs

808 lines
26 KiB
Rust
Raw Normal View History

2026-03-13 00:18:14 +00:00
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, RwLock};
2026-03-13 00:18:14 +00:00
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use tokio::sync::Semaphore;
2026-03-13 00:18:14 +00:00
use tracing::warn;
use crate::actors::ActorRegistry;
use crate::cache::NclCache;
use crate::notifications::NotificationStore;
feat: config surface, NCL contracts, override-layer mutation, on+re update Config surface — per-project config introspection, coherence verification, and audited mutation without destroying NCL structure (ADR-008): - crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary pattern); all section structs derive ConfigFields + config_section(id, ncl_file) emitting inventory::submit!(ConfigFieldsEntry{...}) at link time - crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde rename support; serde_rename_of() helper extracted to fix excessive_nesting - crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path, loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes &UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui") - crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence, cross-project comparison; index_section_fields() extracted (excessive_nesting) - crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence; merge_meta_into_section() extracted; and() replaces unnecessary and_then NCL contracts for ontoref's own config: - .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and DaemonConfig (Port, optional overrides); std.contract.from_validator throughout - .ontoref/config.ncl — log | C.LogConfig applied - .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section with DaemonRuntimeConfig consumer and 7 declared fields Protocol: - adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts as single validation gate; Rust structs are contract-trusted; override-layer mutation writes {section}.overrides.ncl + _overrides_meta, never touches source on+re update: - .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate, Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref) - .ontology/state.ncl — protocol-maturity blocker and self-description-coverage catalyst updated for session 2026-03-26 - README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
// ── Config surface
// ────────────────────────────────────────────────────────────
/// Which process reads a config section.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConsumerKind {
RustStruct,
NuScript,
CiPipeline,
External,
}
/// A single process that reads fields from a config section.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigConsumer {
/// Identifier for this consumer (e.g. "vapora-backend", "deploy-script").
pub id: String,
pub kind: ConsumerKind,
/// Rust fully-qualified type or script path.
pub reference: String,
/// Fields this consumer reads. Empty = reads all fields.
pub fields: Vec<String>,
}
/// Describes one NCL config file and all processes that read it.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigSection {
pub id: String,
/// Path to the NCL file, relative to `config_root`.
pub file: String,
/// Path to the NCL contract file. Relative to `contracts_path` or project
/// root.
pub contract: String,
pub description: String,
/// Why this section exists and why current values were chosen.
pub rationale: String,
/// When false, ontoref will only read this section, never write.
pub mutable: bool,
pub consumers: Vec<ConfigConsumer>,
}
/// How the project's config files are organised.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConfigKind {
/// Multiple .ncl files merged via & operator.
NclMerge,
/// .typedialog/ structure with form.toml + validators + fragments.
TypeDialog,
/// Single monolithic .ncl file.
SingleFile,
}
/// Project-level config surface metadata — loaded once from manifest.ncl at
/// registration time and stored in `ProjectContext`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigSurface {
/// Directory containing config NCL files, relative to project root.
pub config_root: PathBuf,
/// Main NCL file (entry point for `nickel export`).
pub entry_point: String,
pub kind: ConfigKind,
/// Directory added to NICKEL_IMPORT_PATH when exporting config.
pub contracts_path: String,
/// Where ontoref writes `{section}.overrides.ncl` files.
/// Defaults to `config_root` when empty.
pub overrides_dir: PathBuf,
pub sections: Vec<ConfigSection>,
}
impl ConfigSurface {
/// Resolve the directory where override files are written.
/// Returns `overrides_dir` if set, otherwise `config_root`.
pub fn resolved_overrides_dir(&self) -> &Path {
if self.overrides_dir == PathBuf::new() {
&self.config_root
} else {
&self.overrides_dir
}
}
/// Resolve the absolute path to the config entry point given the project
/// root.
pub fn entry_point_path(&self, project_root: &Path) -> PathBuf {
project_root.join(&self.config_root).join(&self.entry_point)
}
/// Find a section by id.
pub fn section(&self, id: &str) -> Option<&ConfigSection> {
self.sections.iter().find(|s| s.id == id)
}
}
// ── Auth / keys
// ───────────────────────────────────────────────────────────────
2026-03-13 00:18:14 +00:00
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
Admin,
Viewer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyEntry {
pub role: Role,
/// Argon2id PHC string — produced by `ontoref-daemon --hash-password <pw>`
pub hash: String,
/// Human-readable label identifying which key this is (e.g. "developer",
/// "agent", "ci"). Recorded in sessions for audit tracing. Optional —
/// defaults to empty string for keys created before this field existed.
#[serde(default)]
pub label: String,
}
/// Result of a successful key verification — role and the label of the
/// matched key for audit trail purposes.
#[derive(Debug, Clone)]
pub struct KeyMatch {
pub role: Role,
pub label: String,
}
/// Verify `password` against a slice of `KeyEntry`s using argon2id.
///
/// Returns the `KeyMatch` (role + label) of the first matching key, or `None`
/// if no key matches. Unparseable PHC hashes are logged and skipped.
pub fn verify_keys_list(keys: &[KeyEntry], password: &str) -> Option<KeyMatch> {
use argon2::{Argon2, PasswordHash, PasswordVerifier};
for key in keys {
let Ok(parsed) = PasswordHash::new(&key.hash) else {
warn!(role = ?key.role, label = %key.label, "skipping key entry with unparseable PHC hash — key is inactive");
continue;
};
if Argon2::default()
.verify_password(password.as_bytes(), &parsed)
.is_ok()
{
return Some(KeyMatch {
role: key.role,
label: key.label.clone(),
});
}
}
None
2026-03-13 00:18:14 +00:00
}
/// Entry as it arrives from the JSON config pipe (ADR-004).
2026-03-13 00:18:14 +00:00
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryEntry {
pub slug: String,
/// Absolute local path. Empty for push_only projects.
#[serde(default)]
2026-03-13 00:18:14 +00:00
pub root: PathBuf,
/// Import paths for nickel export (local projects only).
#[serde(default)]
pub nickel_import_paths: Vec<String>,
2026-03-13 00:18:14 +00:00
#[serde(default)]
pub keys: Vec<KeyEntry>,
/// Git/SSH/HTTPS URL. Informational — daemon never fetches from it.
#[serde(default)]
pub remote_url: String,
/// When true: no local file watch, no NCL import. Project pushes via POST
/// /sync.
#[serde(default)]
pub push_only: bool,
2026-03-13 00:18:14 +00:00
}
/// Per-project runtime state owned by the registry.
pub struct ProjectContext {
pub slug: String,
pub root: PathBuf,
pub import_path: Option<String>,
pub cache: Arc<NclCache>,
pub actors: Arc<ActorRegistry>,
pub notifications: Arc<NotificationStore>,
/// Mutable at runtime via `PUT /projects/{slug}/keys`.
pub keys: RwLock<Vec<KeyEntry>>,
/// Git/SSH/HTTPS remote URL. Informational.
pub remote_url: String,
/// No local file watch, no NCL import — project pushes via POST /sync.
pub push_only: bool,
/// Ensures only one `seed_ontology` runs at a time per project.
/// Prevents concurrent re-seeds caused by rapid filesystem events.
pub seed_lock: Arc<Semaphore>,
/// Incremented by 1 after each successful `seed_ontology` completion.
/// Clients can compare versions to detect stale local state.
pub ontology_version: Arc<AtomicU64>,
--- feat: API catalog surface, protocol v2 tooling, MCP expansion, on+re update ## Summary Session 2026-03-23. Closes the loop between handler code and discoverability across all three surfaces (browser, CLI, MCP agent) via compile-time inventory registration. Adds protocol v2 update tooling, extends MCP from 21 to 29 tools, and brings the self-description up to date. ## API Catalog Surface (#[onto_api] proc-macro) - crates/ontoref-derive: new proc-macro crate; `#[onto_api(method, path, description, auth, actors, params, tags)]` emits `inventory::submit!(ApiRouteEntry{...})` at link time - crates/ontoref-daemon/src/api_catalog.rs: `catalog()` — pure fn over `inventory::iter::<ApiRouteEntry>()`, zero runtime allocation - GET /api/catalog: returns full annotated HTTP surface as JSON - templates/pages/api_catalog.html: new page with client-side filtering by method, auth, path/description; detail panel per route (params table, feature flag); linked from dashboard card and nav - UI nav: "API" link (</> icon) added to mobile dropdown and desktop bar - inventory = "0.3" added to workspace.dependencies (MIT, zero transitive deps) ## Protocol Update Mode - reflection/modes/update_ontoref.ncl: 9-step DAG (5 detect parallel, 2 update idempotent, 2 validate, 1 report) — brings any project from protocol v1 to v2 by adding manifest.ncl and connections.ncl if absent, scanning ADRs for deprecated check_hint, validating with nickel export - reflection/templates/update-ontology-prompt.md: 8-phase reusable prompt for agent-driven ontology enrichment (infrastructure → audit → core.ncl → state.ncl → manifest.ncl → connections.ncl → ADR migration → validation) ## CLI — describe group extensions - reflection/bin/ontoref.nu: `describe diff [--fmt] [--file]` and `describe api [--actor] [--tag] [--auth] [--fmt]` registered as canonical subcommands with log-action; aliases `df` and `da` added; QUICK REFERENCE and ALIASES sections updated ## MCP — two new tools (21 → 29 total) - ontoref_api_catalog: filters catalog() output by actor/tag/auth; returns { routes, total } — no HTTP roundtrip, calls inventory directly - ontoref_file_versions: reads ProjectContext.file_versions DashMap per slug; returns BTreeMap<filename, u64> reload counters - insert_mcp_ctx: audited and updated from 15 to 28 entries in 6 groups - HelpTool JSON: 8 new entries (validate_adrs, validate, impact, guides, bookmark_list, bookmark_add, api_catalog, file_versions) - ServerHandler::get_info instructions updated to mention new tools ## Web UI — dashboard additions - Dashboard: "API Catalog" card (9th); "Ontology File Versions" section showing per-file reload counters from file_versions DashMap - dashboard_mp: builds BTreeMap<String, u64> from ctx.file_versions and injects into Tera context ## on+re update - .ontology/core.ncl: describe-query-layer and adopt-ontoref-tooling descriptions updated; ontoref-daemon updated ("11 pages", "29 tools", API catalog, per-file versioning, #[onto_api]); new node api-catalog-surface (Yang/Practice) with 3 edges; artifact_paths extended across 3 nodes - .ontology/state.ncl: protocol-maturity blocker updated (protocol v2 complete); self-description-coverage catalyst updated with session 2026-03-23 additions - ADR-007: "API Surface Discoverability via #[onto_api] Proc-Macro" — Accepted ## Documentation - README.md: crates table updated (11 pages, 29 MCP tools, ontoref-derive row); MCP representative table expanded; API Catalog, Semantic Diff, Per-File Versioning paragraphs added; update_ontoref onboarding section added - CHANGELOG.md: [Unreleased] section with 4 change groups - assets/web/src/index.html: tool counts 19→29 (EN+ES), page counts 12→11 (EN+ES), daemon description paragraph updated with API catalog + #[onto_api]
2026-03-23 00:58:27 +01:00
/// Per-file change counters. Keyed by canonical absolute path; incremented
/// on every cache invalidation for that file. Consumers compare snapshots
/// to detect which individual files changed between polls.
pub file_versions: Arc<DashMap<PathBuf, u64>>,
feat: config surface, NCL contracts, override-layer mutation, on+re update Config surface — per-project config introspection, coherence verification, and audited mutation without destroying NCL structure (ADR-008): - crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary pattern); all section structs derive ConfigFields + config_section(id, ncl_file) emitting inventory::submit!(ConfigFieldsEntry{...}) at link time - crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde rename support; serde_rename_of() helper extracted to fix excessive_nesting - crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path, loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes &UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui") - crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence, cross-project comparison; index_section_fields() extracted (excessive_nesting) - crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence; merge_meta_into_section() extracted; and() replaces unnecessary and_then NCL contracts for ontoref's own config: - .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and DaemonConfig (Port, optional overrides); std.contract.from_validator throughout - .ontoref/config.ncl — log | C.LogConfig applied - .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section with DaemonRuntimeConfig consumer and 7 declared fields Protocol: - adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts as single validation gate; Rust structs are contract-trusted; override-layer mutation writes {section}.overrides.ncl + _overrides_meta, never touches source on+re update: - .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate, Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref) - .ontology/state.ncl — protocol-maturity blocker and self-description-coverage catalyst updated for session 2026-03-26 - README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
/// Config surface metadata loaded from the project's manifest.ncl at
/// registration time. `None` when the project's manifest has no
/// `config_surface` field or when the manifest can't be exported.
pub config_surface: Option<ConfigSurface>,
2026-03-13 00:18:14 +00:00
}
impl ProjectContext {
pub fn auth_enabled(&self) -> bool {
!self
.keys
.read()
.unwrap_or_else(|e| e.into_inner())
.is_empty()
2026-03-13 00:18:14 +00:00
}
pub fn verify_key(&self, password: &str) -> Option<KeyMatch> {
let keys = self.keys.read().unwrap_or_else(|e| e.into_inner());
verify_keys_list(&keys, password)
}
/// Replace the current key set. Returns the number of keys now active.
pub fn set_keys(&self, new_keys: Vec<KeyEntry>) -> usize {
let mut guard = self.keys.write().unwrap_or_else(|e| e.into_inner());
*guard = new_keys;
guard.len()
2026-03-13 00:18:14 +00:00
}
}
pub struct ProjectRegistry {
contexts: DashMap<String, Arc<ProjectContext>>,
/// Slug of the primary (local) project — always present in `contexts`.
primary_slug: String,
2026-03-13 00:18:14 +00:00
stale_actor_timeout: u64,
max_notifications: usize,
}
impl ProjectRegistry {
/// Build a registry with a pre-constructed primary project context, plus
/// optional additional entries (service mode). The primary context is
/// inserted under `primary_slug` and accessible via `primary()`.
///
/// Returns `(registry, Arc<ProjectContext>)` so the caller can extract
/// shared `Arc` references (cache, actors, notifications) before consuming
/// the primary context into the registry.
pub fn with_primary(
primary_slug: String,
primary_ctx: Arc<ProjectContext>,
additional_entries: Vec<RegistryEntry>,
2026-03-13 00:18:14 +00:00
stale_actor_timeout: u64,
max_notifications: usize,
) -> Self {
2026-03-13 00:18:14 +00:00
let registry = Self {
contexts: DashMap::new(),
primary_slug: primary_slug.clone(),
2026-03-13 00:18:14 +00:00
stale_actor_timeout,
max_notifications,
};
registry.contexts.insert(primary_slug.clone(), primary_ctx);
2026-03-13 00:18:14 +00:00
for entry in additional_entries {
if entry.slug == primary_slug {
warn!(
slug = %entry.slug,
"additional registry entry collides with primary slug — skipping"
);
continue;
}
if entry.push_only {
let ctx = make_context(ContextSpec {
slug: entry.slug.clone(),
root: PathBuf::new(),
import_path: None,
keys: entry.keys,
remote_url: entry.remote_url,
push_only: true,
stale_actor_timeout,
max_notifications,
ack_required: vec![],
feat: config surface, NCL contracts, override-layer mutation, on+re update Config surface — per-project config introspection, coherence verification, and audited mutation without destroying NCL structure (ADR-008): - crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary pattern); all section structs derive ConfigFields + config_section(id, ncl_file) emitting inventory::submit!(ConfigFieldsEntry{...}) at link time - crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde rename support; serde_rename_of() helper extracted to fix excessive_nesting - crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path, loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes &UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui") - crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence, cross-project comparison; index_section_fields() extracted (excessive_nesting) - crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence; merge_meta_into_section() extracted; and() replaces unnecessary and_then NCL contracts for ontoref's own config: - .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and DaemonConfig (Port, optional overrides); std.contract.from_validator throughout - .ontoref/config.ncl — log | C.LogConfig applied - .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section with DaemonRuntimeConfig consumer and 7 declared fields Protocol: - adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts as single validation gate; Rust structs are contract-trusted; override-layer mutation writes {section}.overrides.ncl + _overrides_meta, never touches source on+re update: - .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate, Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref) - .ontology/state.ncl — protocol-maturity blocker and self-description-coverage catalyst updated for session 2026-03-26 - README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
config_surface: None,
});
registry.contexts.insert(entry.slug, Arc::new(ctx));
continue;
}
2026-03-13 00:18:14 +00:00
let root = match entry.root.canonicalize() {
Ok(p) => p,
Err(e) => {
warn!(
slug = %entry.slug,
path = %entry.root.display(),
error = %e,
"project root not found — skipping"
);
continue;
}
};
2026-03-13 00:18:14 +00:00
let import_path = resolve_import_path(&entry.nickel_import_paths, &root);
feat: config surface, NCL contracts, override-layer mutation, on+re update Config surface — per-project config introspection, coherence verification, and audited mutation without destroying NCL structure (ADR-008): - crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary pattern); all section structs derive ConfigFields + config_section(id, ncl_file) emitting inventory::submit!(ConfigFieldsEntry{...}) at link time - crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde rename support; serde_rename_of() helper extracted to fix excessive_nesting - crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path, loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes &UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui") - crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence, cross-project comparison; index_section_fields() extracted (excessive_nesting) - crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence; merge_meta_into_section() extracted; and() replaces unnecessary and_then NCL contracts for ontoref's own config: - .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and DaemonConfig (Port, optional overrides); std.contract.from_validator throughout - .ontoref/config.ncl — log | C.LogConfig applied - .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section with DaemonRuntimeConfig consumer and 7 declared fields Protocol: - adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts as single validation gate; Rust structs are contract-trusted; override-layer mutation writes {section}.overrides.ncl + _overrides_meta, never touches source on+re update: - .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate, Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref) - .ontology/state.ncl — protocol-maturity blocker and self-description-coverage catalyst updated for session 2026-03-26 - README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
let config_surface = load_config_surface(&root, import_path.as_deref());
let ctx = make_context(ContextSpec {
slug: entry.slug.clone(),
2026-03-13 00:18:14 +00:00
root,
import_path,
keys: entry.keys,
remote_url: entry.remote_url,
push_only: false,
stale_actor_timeout,
max_notifications,
ack_required: vec![],
feat: config surface, NCL contracts, override-layer mutation, on+re update Config surface — per-project config introspection, coherence verification, and audited mutation without destroying NCL structure (ADR-008): - crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary pattern); all section structs derive ConfigFields + config_section(id, ncl_file) emitting inventory::submit!(ConfigFieldsEntry{...}) at link time - crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde rename support; serde_rename_of() helper extracted to fix excessive_nesting - crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path, loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes &UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui") - crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence, cross-project comparison; index_section_fields() extracted (excessive_nesting) - crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence; merge_meta_into_section() extracted; and() replaces unnecessary and_then NCL contracts for ontoref's own config: - .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and DaemonConfig (Port, optional overrides); std.contract.from_validator throughout - .ontoref/config.ncl — log | C.LogConfig applied - .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section with DaemonRuntimeConfig consumer and 7 declared fields Protocol: - adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts as single validation gate; Rust structs are contract-trusted; override-layer mutation writes {section}.overrides.ncl + _overrides_meta, never touches source on+re update: - .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate, Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref) - .ontology/state.ncl — protocol-maturity blocker and self-description-coverage catalyst updated for session 2026-03-26 - README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
config_surface,
});
registry.contexts.insert(entry.slug, Arc::new(ctx));
2026-03-13 00:18:14 +00:00
}
registry
2026-03-13 00:18:14 +00:00
}
/// The slug of the primary (local) project.
pub fn primary_slug(&self) -> &str {
&self.primary_slug
}
/// The primary project context. Panics if not found — invariant guaranteed
/// by `with_primary` constructor.
pub fn primary(&self) -> Arc<ProjectContext> {
self.contexts
.get(&self.primary_slug)
.map(|r| Arc::clone(&*r))
.expect("primary project context missing — registry invariant violated")
}
/// Add a project at runtime (UI-triggered, not from bootstrap).
2026-03-13 00:18:14 +00:00
pub fn add_project(&self, entry: RegistryEntry) -> anyhow::Result<()> {
if self.contexts.contains_key(&entry.slug) {
anyhow::bail!("project '{}' already exists", entry.slug);
}
let (root, import_path) = if entry.push_only {
(PathBuf::new(), None)
} else {
let r = entry
.root
.canonicalize()
.map_err(|e| anyhow::anyhow!("project '{}': root path error: {}", entry.slug, e))?;
let ip = resolve_import_path(&entry.nickel_import_paths, &r);
(r, ip)
};
feat: config surface, NCL contracts, override-layer mutation, on+re update Config surface — per-project config introspection, coherence verification, and audited mutation without destroying NCL structure (ADR-008): - crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary pattern); all section structs derive ConfigFields + config_section(id, ncl_file) emitting inventory::submit!(ConfigFieldsEntry{...}) at link time - crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde rename support; serde_rename_of() helper extracted to fix excessive_nesting - crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path, loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes &UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui") - crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence, cross-project comparison; index_section_fields() extracted (excessive_nesting) - crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence; merge_meta_into_section() extracted; and() replaces unnecessary and_then NCL contracts for ontoref's own config: - .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and DaemonConfig (Port, optional overrides); std.contract.from_validator throughout - .ontoref/config.ncl — log | C.LogConfig applied - .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section with DaemonRuntimeConfig consumer and 7 declared fields Protocol: - adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts as single validation gate; Rust structs are contract-trusted; override-layer mutation writes {section}.overrides.ncl + _overrides_meta, never touches source on+re update: - .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate, Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref) - .ontology/state.ncl — protocol-maturity blocker and self-description-coverage catalyst updated for session 2026-03-26 - README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
let config_surface = if entry.push_only {
None
} else {
load_config_surface(&root, import_path.as_deref())
};
let ctx = make_context(ContextSpec {
slug: entry.slug.clone(),
2026-03-13 00:18:14 +00:00
root,
import_path,
keys: entry.keys,
remote_url: entry.remote_url,
push_only: entry.push_only,
stale_actor_timeout: self.stale_actor_timeout,
max_notifications: self.max_notifications,
ack_required: vec![],
feat: config surface, NCL contracts, override-layer mutation, on+re update Config surface — per-project config introspection, coherence verification, and audited mutation without destroying NCL structure (ADR-008): - crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary pattern); all section structs derive ConfigFields + config_section(id, ncl_file) emitting inventory::submit!(ConfigFieldsEntry{...}) at link time - crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde rename support; serde_rename_of() helper extracted to fix excessive_nesting - crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path, loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes &UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui") - crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence, cross-project comparison; index_section_fields() extracted (excessive_nesting) - crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence; merge_meta_into_section() extracted; and() replaces unnecessary and_then NCL contracts for ontoref's own config: - .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and DaemonConfig (Port, optional overrides); std.contract.from_validator throughout - .ontoref/config.ncl — log | C.LogConfig applied - .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section with DaemonRuntimeConfig consumer and 7 declared fields Protocol: - adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts as single validation gate; Rust structs are contract-trusted; override-layer mutation writes {section}.overrides.ncl + _overrides_meta, never touches source on+re update: - .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate, Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref) - .ontology/state.ncl — protocol-maturity blocker and self-description-coverage catalyst updated for session 2026-03-26 - README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
config_surface,
});
2026-03-13 00:18:14 +00:00
self.contexts.insert(entry.slug, Arc::new(ctx));
Ok(())
2026-03-13 00:18:14 +00:00
}
/// Remove a project by slug.
pub fn remove_project(&self, slug: &str) {
2026-03-13 00:18:14 +00:00
self.contexts.remove(slug);
}
pub fn get(&self, slug: &str) -> Option<Arc<ProjectContext>> {
self.contexts.get(slug).map(|r| Arc::clone(&*r))
}
pub fn all(&self) -> Vec<Arc<ProjectContext>> {
let mut list: Vec<_> = self.contexts.iter().map(|r| Arc::clone(&*r)).collect();
list.sort_by(|a, b| a.slug.cmp(&b.slug));
list
}
pub fn count(&self) -> usize {
self.contexts.len()
}
/// Replace the key set for a registered project.
/// Returns `None` if the slug is not found.
pub fn update_keys(&self, slug: &str, keys: Vec<KeyEntry>) -> Option<usize> {
self.contexts.get(slug).map(|ctx| ctx.set_keys(keys))
2026-03-13 00:18:14 +00:00
}
}
/// Resolve nickel_import_paths entries into a colon-separated
/// NICKEL_IMPORT_PATH string. Relative paths are resolved against the project
/// root.
fn resolve_import_path(paths: &[String], root: &Path) -> Option<String> {
2026-03-13 00:18:14 +00:00
let joined = paths
.iter()
.map(|p| {
let candidate = Path::new(p);
2026-03-13 00:18:14 +00:00
if candidate.is_absolute() {
p.clone()
2026-03-13 00:18:14 +00:00
} else {
root.join(candidate).display().to_string()
}
})
.collect::<Vec<_>>()
.join(":");
if joined.is_empty() {
None
} else {
Some(joined)
}
}
pub struct ContextSpec {
pub slug: String,
pub root: PathBuf,
pub import_path: Option<String>,
pub keys: Vec<KeyEntry>,
pub remote_url: String,
pub push_only: bool,
pub stale_actor_timeout: u64,
pub max_notifications: usize,
/// Directories that require notification acknowledgment.
pub ack_required: Vec<String>,
feat: config surface, NCL contracts, override-layer mutation, on+re update Config surface — per-project config introspection, coherence verification, and audited mutation without destroying NCL structure (ADR-008): - crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary pattern); all section structs derive ConfigFields + config_section(id, ncl_file) emitting inventory::submit!(ConfigFieldsEntry{...}) at link time - crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde rename support; serde_rename_of() helper extracted to fix excessive_nesting - crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path, loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes &UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui") - crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence, cross-project comparison; index_section_fields() extracted (excessive_nesting) - crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence; merge_meta_into_section() extracted; and() replaces unnecessary and_then NCL contracts for ontoref's own config: - .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and DaemonConfig (Port, optional overrides); std.contract.from_validator throughout - .ontoref/config.ncl — log | C.LogConfig applied - .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section with DaemonRuntimeConfig consumer and 7 declared fields Protocol: - adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts as single validation gate; Rust structs are contract-trusted; override-layer mutation writes {section}.overrides.ncl + _overrides_meta, never touches source on+re update: - .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate, Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref) - .ontology/state.ncl — protocol-maturity blocker and self-description-coverage catalyst updated for session 2026-03-26 - README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
/// Pre-loaded config surface from the project's manifest.ncl.
/// Pass `None` for push_only projects or when the manifest has no
/// `config_surface` field.
pub config_surface: Option<ConfigSurface>,
}
/// Attempt to load `ConfigSurface` from a project's `manifest.ncl`
/// synchronously.
///
/// Runs `nickel export` on `.ontology/manifest.ncl` and deserialises the
/// `config_surface` key. Returns `None` when:
/// - the manifest file doesn't exist
/// - the manifest has no `config_surface` field
/// - nickel export or deserialisation fails (logged at warn level)
pub fn load_config_surface(root: &Path, import_path: Option<&str>) -> Option<ConfigSurface> {
let manifest = root.join(".ontology").join("manifest.ncl");
if !manifest.exists() {
return None;
}
let mut cmd = std::process::Command::new("nickel");
cmd.args(["export", "--format", "json"])
.arg(&manifest)
.current_dir(root);
if let Some(ip) = import_path {
cmd.env("NICKEL_IMPORT_PATH", ip);
}
let output = cmd.output().ok()?;
if !output.status.success() {
warn!(
path = %manifest.display(),
stderr = %String::from_utf8_lossy(&output.stderr),
"nickel export of manifest.ncl failed — config_surface not loaded"
);
return None;
}
let json: serde_json::Value = serde_json::from_slice(&output.stdout).ok()?;
let surface_val = json.get("config_surface")?;
// Deserialise into an intermediate form that matches the NCL schema,
// then convert to the canonical ConfigSurface.
#[derive(Deserialize)]
struct NclConfigConsumer {
id: String,
kind: String,
#[serde(default, rename = "ref")]
reference: String,
#[serde(default)]
fields: Vec<String>,
}
#[derive(Deserialize)]
struct NclConfigSection {
id: String,
file: String,
#[serde(default)]
contract: String,
#[serde(default)]
description: String,
#[serde(default)]
rationale: String,
#[serde(default = "default_true")]
mutable: bool,
#[serde(default)]
consumers: Vec<NclConfigConsumer>,
}
fn default_true() -> bool {
true
}
#[derive(Deserialize)]
struct NclConfigSurface {
config_root: String,
#[serde(default = "default_config_ncl")]
entry_point: String,
#[serde(default = "default_ncl_merge")]
kind: String,
#[serde(default)]
contracts_path: String,
#[serde(default)]
overrides_dir: String,
#[serde(default)]
sections: Vec<NclConfigSection>,
}
fn default_config_ncl() -> String {
"config.ncl".to_string()
}
fn default_ncl_merge() -> String {
"NclMerge".to_string()
}
let ncl: NclConfigSurface = match serde_json::from_value(surface_val.clone()) {
Ok(v) => v,
Err(e) => {
warn!(error = %e, "failed to deserialise config_surface from manifest.ncl");
return None;
}
};
let kind = match ncl.kind.as_str() {
"TypeDialog" => ConfigKind::TypeDialog,
"SingleFile" => ConfigKind::SingleFile,
_ => ConfigKind::NclMerge,
};
let config_root = root.join(&ncl.config_root);
let overrides_dir = if ncl.overrides_dir.is_empty() {
PathBuf::new()
} else {
root.join(&ncl.overrides_dir)
};
let sections = ncl
.sections
.into_iter()
.map(|s| {
let consumers = s
.consumers
.into_iter()
.map(|c| {
let consumer_kind = match c.kind.as_str() {
"NuScript" => ConsumerKind::NuScript,
"CiPipeline" => ConsumerKind::CiPipeline,
"External" => ConsumerKind::External,
_ => ConsumerKind::RustStruct,
};
ConfigConsumer {
id: c.id,
kind: consumer_kind,
reference: c.reference,
fields: c.fields,
}
})
.collect();
ConfigSection {
id: s.id,
file: s.file,
contract: s.contract,
description: s.description,
rationale: s.rationale,
mutable: s.mutable,
consumers,
}
})
.collect();
Some(ConfigSurface {
config_root,
entry_point: ncl.entry_point,
kind,
contracts_path: ncl.contracts_path,
overrides_dir,
sections,
})
}
pub fn make_context(spec: ContextSpec) -> ProjectContext {
let sessions_dir = spec.root.join(".ontoref").join("sessions");
let actors =
Arc::new(ActorRegistry::new(spec.stale_actor_timeout).with_persist_dir(sessions_dir));
actors.load_persisted();
let ack_dirs = if spec.ack_required.is_empty() {
vec![".ontology".into(), "adrs".into()]
} else {
spec.ack_required
};
ProjectContext {
slug: spec.slug,
root: spec.root,
import_path: spec.import_path,
cache: Arc::new(NclCache::new()),
actors,
notifications: Arc::new(NotificationStore::new(spec.max_notifications, ack_dirs)),
keys: RwLock::new(spec.keys),
remote_url: spec.remote_url,
push_only: spec.push_only,
seed_lock: Arc::new(Semaphore::new(1)),
ontology_version: Arc::new(AtomicU64::new(0)),
--- feat: API catalog surface, protocol v2 tooling, MCP expansion, on+re update ## Summary Session 2026-03-23. Closes the loop between handler code and discoverability across all three surfaces (browser, CLI, MCP agent) via compile-time inventory registration. Adds protocol v2 update tooling, extends MCP from 21 to 29 tools, and brings the self-description up to date. ## API Catalog Surface (#[onto_api] proc-macro) - crates/ontoref-derive: new proc-macro crate; `#[onto_api(method, path, description, auth, actors, params, tags)]` emits `inventory::submit!(ApiRouteEntry{...})` at link time - crates/ontoref-daemon/src/api_catalog.rs: `catalog()` — pure fn over `inventory::iter::<ApiRouteEntry>()`, zero runtime allocation - GET /api/catalog: returns full annotated HTTP surface as JSON - templates/pages/api_catalog.html: new page with client-side filtering by method, auth, path/description; detail panel per route (params table, feature flag); linked from dashboard card and nav - UI nav: "API" link (</> icon) added to mobile dropdown and desktop bar - inventory = "0.3" added to workspace.dependencies (MIT, zero transitive deps) ## Protocol Update Mode - reflection/modes/update_ontoref.ncl: 9-step DAG (5 detect parallel, 2 update idempotent, 2 validate, 1 report) — brings any project from protocol v1 to v2 by adding manifest.ncl and connections.ncl if absent, scanning ADRs for deprecated check_hint, validating with nickel export - reflection/templates/update-ontology-prompt.md: 8-phase reusable prompt for agent-driven ontology enrichment (infrastructure → audit → core.ncl → state.ncl → manifest.ncl → connections.ncl → ADR migration → validation) ## CLI — describe group extensions - reflection/bin/ontoref.nu: `describe diff [--fmt] [--file]` and `describe api [--actor] [--tag] [--auth] [--fmt]` registered as canonical subcommands with log-action; aliases `df` and `da` added; QUICK REFERENCE and ALIASES sections updated ## MCP — two new tools (21 → 29 total) - ontoref_api_catalog: filters catalog() output by actor/tag/auth; returns { routes, total } — no HTTP roundtrip, calls inventory directly - ontoref_file_versions: reads ProjectContext.file_versions DashMap per slug; returns BTreeMap<filename, u64> reload counters - insert_mcp_ctx: audited and updated from 15 to 28 entries in 6 groups - HelpTool JSON: 8 new entries (validate_adrs, validate, impact, guides, bookmark_list, bookmark_add, api_catalog, file_versions) - ServerHandler::get_info instructions updated to mention new tools ## Web UI — dashboard additions - Dashboard: "API Catalog" card (9th); "Ontology File Versions" section showing per-file reload counters from file_versions DashMap - dashboard_mp: builds BTreeMap<String, u64> from ctx.file_versions and injects into Tera context ## on+re update - .ontology/core.ncl: describe-query-layer and adopt-ontoref-tooling descriptions updated; ontoref-daemon updated ("11 pages", "29 tools", API catalog, per-file versioning, #[onto_api]); new node api-catalog-surface (Yang/Practice) with 3 edges; artifact_paths extended across 3 nodes - .ontology/state.ncl: protocol-maturity blocker updated (protocol v2 complete); self-description-coverage catalyst updated with session 2026-03-23 additions - ADR-007: "API Surface Discoverability via #[onto_api] Proc-Macro" — Accepted ## Documentation - README.md: crates table updated (11 pages, 29 MCP tools, ontoref-derive row); MCP representative table expanded; API Catalog, Semantic Diff, Per-File Versioning paragraphs added; update_ontoref onboarding section added - CHANGELOG.md: [Unreleased] section with 4 change groups - assets/web/src/index.html: tool counts 19→29 (EN+ES), page counts 12→11 (EN+ES), daemon description paragraph updated with API catalog + #[onto_api]
2026-03-23 00:58:27 +01:00
file_versions: Arc::new(DashMap::new()),
feat: config surface, NCL contracts, override-layer mutation, on+re update Config surface — per-project config introspection, coherence verification, and audited mutation without destroying NCL structure (ADR-008): - crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary pattern); all section structs derive ConfigFields + config_section(id, ncl_file) emitting inventory::submit!(ConfigFieldsEntry{...}) at link time - crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde rename support; serde_rename_of() helper extracted to fix excessive_nesting - crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path, loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes &UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui") - crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence, cross-project comparison; index_section_fields() extracted (excessive_nesting) - crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence; merge_meta_into_section() extracted; and() replaces unnecessary and_then NCL contracts for ontoref's own config: - .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and DaemonConfig (Port, optional overrides); std.contract.from_validator throughout - .ontoref/config.ncl — log | C.LogConfig applied - .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section with DaemonRuntimeConfig consumer and 7 declared fields Protocol: - adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts as single validation gate; Rust structs are contract-trusted; override-layer mutation writes {section}.overrides.ncl + _overrides_meta, never touches source on+re update: - .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate, Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref) - .ontology/state.ncl — protocol-maturity blocker and self-description-coverage catalyst updated for session 2026-03-26 - README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
config_surface: spec.config_surface,
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
fn phc_hash(password: &str) -> String {
use argon2::password_hash::{rand_core::OsRng, SaltString};
use argon2::{Argon2, PasswordHasher};
let salt = SaltString::generate(&mut OsRng);
Argon2::default()
.hash_password(password.as_bytes(), &salt)
.unwrap()
.to_string()
}
fn admin_entry(password: &str) -> KeyEntry {
KeyEntry {
role: Role::Admin,
hash: phc_hash(password),
label: String::new(),
}
}
fn viewer_entry(password: &str) -> KeyEntry {
KeyEntry {
role: Role::Viewer,
hash: phc_hash(password),
label: String::new(),
}
}
// ── verify_keys_list ──────────────────────────────────────────────────
#[test]
fn correct_password_returns_role() {
let keys = vec![admin_entry("secret")];
assert_eq!(
verify_keys_list(&keys, "secret").map(|m| m.role),
Some(Role::Admin)
);
}
#[test]
fn wrong_password_returns_none() {
let keys = vec![admin_entry("secret")];
assert!(verify_keys_list(&keys, "wrong").is_none());
}
#[test]
fn empty_keys_returns_none() {
assert!(verify_keys_list(&[], "anything").is_none());
}
#[test]
fn returns_first_matching_role() {
let keys = vec![viewer_entry("pw"), admin_entry("pw")];
// Viewer comes first — that role wins.
assert_eq!(
verify_keys_list(&keys, "pw").map(|m| m.role),
Some(Role::Viewer)
);
}
#[test]
fn invalid_phc_hash_is_skipped_not_panicked() {
let keys = vec![
KeyEntry {
role: Role::Admin,
hash: "not-a-phc-string".to_string(),
label: String::new(),
},
admin_entry("valid"),
];
// Bad entry skipped; valid entry matches.
assert_eq!(
verify_keys_list(&keys, "valid").map(|m| m.role),
Some(Role::Admin)
);
}
#[test]
fn invalid_phc_only_returns_none() {
let keys = vec![KeyEntry {
role: Role::Admin,
hash: "garbage".to_string(),
label: String::new(),
}];
assert!(verify_keys_list(&keys, "anything").is_none());
}
// ── resolve_import_path ───────────────────────────────────────────────
#[test]
fn empty_paths_returns_none() {
assert_eq!(resolve_import_path(&[], Path::new("/root")), None);
}
#[test]
fn absolute_path_returned_as_is() {
let result = resolve_import_path(&["/abs/path".to_string()], Path::new("/root"));
assert_eq!(result, Some("/abs/path".to_string()));
}
#[test]
fn relative_path_joined_with_root() {
let result = resolve_import_path(&["lib".to_string()], Path::new("/root"));
assert_eq!(result, Some("/root/lib".to_string()));
}
#[test]
fn multiple_paths_colon_separated() {
let result =
resolve_import_path(&["/abs".to_string(), "rel".to_string()], Path::new("/root"));
assert_eq!(result, Some("/abs:/root/rel".to_string()));
}
// ── with_primary slug collision ───────────────────────────────────────
fn minimal_ctx(slug: &str) -> Arc<ProjectContext> {
Arc::new(make_context(ContextSpec {
slug: slug.to_string(),
root: PathBuf::new(),
import_path: None,
keys: vec![],
remote_url: String::new(),
push_only: true,
stale_actor_timeout: 300,
max_notifications: 64,
ack_required: vec![],
feat: config surface, NCL contracts, override-layer mutation, on+re update Config surface — per-project config introspection, coherence verification, and audited mutation without destroying NCL structure (ADR-008): - crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary pattern); all section structs derive ConfigFields + config_section(id, ncl_file) emitting inventory::submit!(ConfigFieldsEntry{...}) at link time - crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde rename support; serde_rename_of() helper extracted to fix excessive_nesting - crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path, loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes &UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui") - crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence, cross-project comparison; index_section_fields() extracted (excessive_nesting) - crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence; merge_meta_into_section() extracted; and() replaces unnecessary and_then NCL contracts for ontoref's own config: - .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and DaemonConfig (Port, optional overrides); std.contract.from_validator throughout - .ontoref/config.ncl — log | C.LogConfig applied - .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section with DaemonRuntimeConfig consumer and 7 declared fields Protocol: - adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts as single validation gate; Rust structs are contract-trusted; override-layer mutation writes {section}.overrides.ncl + _overrides_meta, never touches source on+re update: - .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate, Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref) - .ontology/state.ncl — protocol-maturity blocker and self-description-coverage catalyst updated for session 2026-03-26 - README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
config_surface: None,
}))
}
#[test]
fn primary_is_accessible_after_construction() {
let registry =
ProjectRegistry::with_primary("main".to_string(), minimal_ctx("main"), vec![], 300, 64);
assert_eq!(registry.primary().slug, "main");
assert_eq!(registry.primary_slug(), "main");
}
#[test]
fn slug_collision_does_not_overwrite_primary() {
let primary = minimal_ctx("shared");
let primary_ptr = Arc::as_ptr(&primary);
let colliding = RegistryEntry {
slug: "shared".to_string(),
root: PathBuf::new(),
nickel_import_paths: vec![],
keys: vec![],
remote_url: String::new(),
push_only: true,
};
let registry =
ProjectRegistry::with_primary("shared".to_string(), primary, vec![colliding], 300, 64);
// The primary context pointer must survive — not replaced by the colliding
// entry.
assert_eq!(Arc::as_ptr(&registry.primary()), primary_ptr);
assert_eq!(registry.count(), 1);
}
#[test]
fn additional_non_colliding_entry_added() {
let registry = ProjectRegistry::with_primary(
"primary".to_string(),
minimal_ctx("primary"),
vec![RegistryEntry {
slug: "secondary".to_string(),
root: PathBuf::new(),
nickel_import_paths: vec![],
keys: vec![],
remote_url: String::new(),
push_only: true,
}],
300,
64,
);
assert_eq!(registry.count(), 2);
assert!(registry.get("secondary").is_some());
}
}