# Ontoref Full Adoption Prompt **Purpose:** Complete onboarding of `{project_name}` into the ontoref protocol — or bring an existing adoption up to the current version. Covers all adoption layers in dependency order: protocol infrastructure → ontology enrichment → config surface → API surface → manifest self-interrogation. **Substitutions required before use:** - `{project_name}` — kebab-case project identifier - `{project_dir}` — absolute path to project root - `{ontoref_source_dir}` — absolute path to the ontoref source checkout (only needed for Cargo path dependencies in Phases 3c and 4a; not needed if ontoref crates are not used as Rust dependencies) **Run as:** `ontoref --actor developer` from `{project_dir}` (requires `ontoref` installed globally via `just install-daemon` from the ontoref repo). --- ## Bootstrap — source ontoref env vars Before running any direct `nickel export` command, source the ontoref env into the current shell. This sets `NICKEL_IMPORT_PATH` and `ONTOREF_ROOT` without launching a full command: ```sh cd {project_dir} . $(which ontoref) --env-only # NICKEL_IMPORT_PATH and ONTOREF_ROOT are now available in this shell session ``` All `nickel export` commands in this prompt assume these vars are set. Re-run the source line if you open a new terminal. --- ## Phase 0 — Read the project first Do not write anything until you have read and understood the project. This phase is not optional — subsequent phases require accurate knowledge of what the project actually does. ```sh cd {project_dir} # Purpose, architecture, stack cat README.md cat .claude/CLAUDE.md 2>/dev/null || true cat CLAUDE.md 2>/dev/null || true # Existing ontology state test -f .ontology/core.ncl && \ nickel export --import-path "$NICKEL_IMPORT_PATH" .ontology/core.ncl \ | jq '{nodes: [.nodes[] | {id, name, level}], edge_count: (.edges | length)}' # Manifest if present test -f .ontology/manifest.ncl && \ nickel export --import-path "$NICKEL_IMPORT_PATH" .ontology/manifest.ncl \ | jq '{repo_kind, description}' # Rust crates and their purposes cat Cargo.toml 2>/dev/null | grep -A2 '\[workspace\]' || cat Cargo.toml 2>/dev/null | head -20 ls crates/ 2>/dev/null || true # Config loading pattern: does the project use nickel export + serde? grep -rl 'nickel export\|DaemonNclConfig\|ConfigLoader\|config_from_ncl' \ crates/ src/ 2>/dev/null | head -10 # HTTP handlers: does the project expose an HTTP API? grep -rl '#\[get\|#\[post\|#\[put\|Router::new\|axum\|actix' \ crates/ src/ 2>/dev/null | head -10 # External services: what does it connect to? grep -rl 'SurrealDb\|nats\|postgres\|redis\|reqwest\|http::Client' \ crates/ src/ 2>/dev/null | head -10 ``` Identify: - What the project does for each audience (developer, agent, CI, end user) - Whether it uses NCL schemas for configuration (Nickel-validated-overrides applies) - Whether it exposes an HTTP API (`#[onto_api]` annotation applies) - What external services it depends on (critical_deps candidates) - What the existing ontology covers vs what is missing --- ## Phase 1 — Protocol infrastructure Add missing v2 files. All steps are additive — nothing is overwritten. ```sh cd {project_dir} # Detect missing files test -f .ontology/manifest.ncl && echo "manifest: present" || echo "manifest: MISSING" test -f .ontology/connections.ncl && echo "connections: present" || echo "connections: MISSING" # Add manifest.ncl if missing (template is installed with ontoref) test -f .ontology/manifest.ncl || \ sed "s/{{ project_name }}/{project_name}/g" \ "$ONTOREF_ROOT/templates/ontology/manifest.ncl" > .ontology/manifest.ncl # Add connections.ncl if missing test -f .ontology/connections.ncl || \ sed "s/{{ project_name }}/{project_name}/g" \ "$ONTOREF_ROOT/templates/ontology/connections.ncl" > .ontology/connections.ncl # Validate both parse nickel export --import-path "$NICKEL_IMPORT_PATH" .ontology/manifest.ncl \ > /dev/null && echo "manifest: ok" nickel export --import-path "$NICKEL_IMPORT_PATH" .ontology/connections.ncl \ > /dev/null && echo "connections: ok" ``` Check for deprecated `check_hint` fields in ADRs and migrate to typed `check` variants. See `$ONTOREF_ROOT/reflection/templates/update-ontology-prompt.md` Phase 7 for the migration mapping. --- ## Phase 2 — Ontology enrichment (core.ncl, state.ncl) Follow `$ONTOREF_ROOT/reflection/templates/update-ontology-prompt.md` Phases 2–6 in full. Key rules: - Add nodes only for things that actually exist in code today — no aspirational nodes - Advance dimension states only when transition conditions are demonstrably met - Update `blocker` and `catalyst` to reflect current reality - Every edit must pass `nickel export` before continuing to the next node After completing ontology enrichment, run: ```sh nickel export --import-path "$NICKEL_IMPORT_PATH" .ontology/core.ncl \ | jq '{nodes: (.nodes|length), edges: (.edges|length)}' ``` --- ## Phase 3 — Config surface **Skip this phase if the project has no structured NCL configuration system.** This phase has three parts that build on each other: declare the config surface in the manifest, apply the nickel-validated-overrides pattern to Rust services, and register struct fields via `#[derive(ConfigFields)]`. ### 3a. Declare config_surface in manifest.ncl Open `.ontology/manifest.ncl`. Identify: - Where the project's NCL config files live (`config_root`) - What the entry-point file is (`entry_point`, usually `config.ncl`) - What sections exist (each top-level key in the merged export) - Who reads each section (Rust structs, Nu scripts, CI pipelines) ```nickel config_surface = m.make_config_surface { config_root = "config/", # adjust to project layout entry_point = "config.ncl", kind = 'NclMerge, # 'NclMerge | 'SingleFile | 'TypeDialog contracts_path = "nickel/contracts/", # where NCL contract files live overrides_dir = "", # defaults to config_root sections = [ m.make_config_section { id = "server", # must match top-level NCL key file = "config.ncl", contract = "contracts.ncl -> ServerConfig", description = "...", rationale = "...", mutable = true, consumers = [ m.make_config_consumer { id = "{project_name}-backend", kind = 'RustStruct, ref = "crates/{crate}/src/config.rs -> ServerConfig", fields = [], # leave empty once #[derive(ConfigFields)] is in place }, ], }, ], }, ``` Validate: ```sh nickel export --import-path "$NICKEL_IMPORT_PATH" .ontology/manifest.ncl | jq .config_surface ``` ### 3b. Nickel-validated-overrides pattern **Apply this if the project has Rust services that read NCL config AND accept env var or CLI argument overrides.** Without this, env overrides bypass all NCL contract validation. **The core insight:** JSON is valid Nickel syntax. Write env overrides as JSON to a temp `.ncl` file and pass it as a second argument to `nickel export`. Nickel merges both files and applies contracts to the merged result before any Rust struct is populated. ```text OLD: load_from_ncl() → deserialize → apply_env_overrides(&mut self) ← bypasses Nickel NEW: collect_env_overrides() → nickel export base.ncl overrides.ncl → deserialize ``` **Step 1 — Add `export_with_overrides` to the config loader:** In the crate that calls `nickel export` (typically a platform-config or config-loader crate): ```rust pub fn export_with_overrides( ncl_path: &Path, import_path: &str, overrides: &serde_json::Value, ) -> Result { if overrides.is_null() || overrides == &serde_json::Value::Object(Default::default()) { return plain_nickel_export(ncl_path, import_path); } let tmp = tempfile::NamedTempFile::with_suffix(".ncl")?; serde_json::to_writer(&tmp, overrides)?; let output = std::process::Command::new("nickel") .args(["export", "--format", "json"]) .arg(ncl_path) .arg(tmp.path()) .arg("--import-path").arg(import_path) .output()?; // tmp dropped here — cleaned up automatically if output.status.success() { Ok(String::from_utf8(output.stdout)?) } else { Err(ConfigError::NickelContract(String::from_utf8_lossy(&output.stderr).into())) } } ``` **Step 2 — Replace `apply_env_overrides(&mut self)` with `collect_env_overrides()`:** ```rust // REMOVE: fn apply_env_overrides(&mut self) { self.port = env::var("PORT")... } // ADD: pub fn collect_env_overrides() -> serde_json::Value { let mut overrides = serde_json::json!({}); // JSON shape must match NCL schema nesting exactly if let Ok(port) = std::env::var("SERVER_PORT") { if let Ok(n) = port.parse::() { overrides["server"]["port"] = n.into(); } } // ... one block per env var, following the NCL schema nesting overrides } ``` **Step 3 — Wire through the load path:** ```rust pub fn load() -> Result { let overrides = Self::collect_env_overrides(); let json = export_with_overrides(&config_path, &import_path, &overrides)?; Ok(serde_json::from_str(&json)?) } ``` **Step 4 — Verify NCL schemas have real constraints, not bare type annotations:** ```sh grep -n '| Number\b\|| String\b' {project_dir}/nickel/**/*.ncl 2>/dev/null || \ grep -rn '| Number\b\|| String\b' {project_dir}/config/ 2>/dev/null ``` Bare `| Number` passes any number. Constraints must use `from_validator` or `from_predicate`: ```nickel # Weak — any number passes port | Number = 8080 # Strong — contract rejects values outside 1024-65535 port | std.contract.from_validator (fun port => if port >= 1024 && port <= 65535 then 'Ok else 'Error { message = "port must be 1024-65535, got #{port}" }) = 8080 ``` Without real constraints in NCL, the overrides pattern has no enforcement teeth. **Validation tests:** ```sh # Invalid override must produce a Nickel contract error, NOT start silently SERVER_PORT=99999 cargo run -- 2>&1 | grep -i "nickel\|contract\|error" # Valid override must start with the overridden value SERVER_PORT=8090 cargo run -- 2>&1 | grep "port.*8090" cargo clippy -- -D warnings ``` ### 3c. `#[derive(ConfigFields)]` for compile-time field registration Annotate every Rust struct that deserialises a config section. This gives the ontoref daemon accurate field lists without hand-maintaining `fields[]` in the manifest. **Add dependency** (in the crate containing config structs; path is relative to that crate, assuming ontoref source is checked out as a sibling of this project): ```toml [dependencies] ontoref-ontology = { path = "../../{ontoref_source_dir}/crates/ontoref-ontology" } ``` **Annotate config structs:** ```rust use ontoref_ontology::ConfigFields; #[derive(Deserialize, ConfigFields)] #[config_section(id = "server", ncl_file = "config/config.ncl")] pub struct ServerConfig { pub host: String, pub port: u16, // #[serde(rename = "tls_enabled")] is respected — renamed name is registered pub tls: bool, } ``` Rules: - `id` must match the section `id` in `manifest.ncl → config_surface.sections[*].id` - `ncl_file` is relative to the project root - Only top-level fields of the annotated struct are registered; annotate nested structs separately if their fields need to appear in the coherence report **Verify registration:** ```rust #[test] fn config_fields_registered() { for entry in inventory::iter:: { assert!(!entry.fields.is_empty(), "section {} has no fields", entry.section_id); eprintln!("section={} struct={} fields={:?}", entry.section_id, entry.struct_name, entry.fields); } } ``` **Coherence integration test:** ```rust #[test] fn ncl_rust_coherence() { let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .ancestors().find(|p| p.join(".ontology").exists()) .expect("project root not found"); for entry in inventory::iter:: { let ncl_path = root.join(entry.ncl_file); let out = std::process::Command::new("nickel") .args(["export", "--format", "json"]) .arg(&ncl_path).current_dir(root).output().unwrap(); assert!(out.status.success(), "nickel export failed: {}", entry.ncl_file); let json: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap(); let section = json.get(entry.section_id).or(Some(&json)) .and_then(|v| v.as_object()) .unwrap_or_else(|| panic!("section '{}' missing", entry.section_id)); let ncl_keys: std::collections::BTreeSet<&str> = section.keys().map(String::as_str).collect(); let rust_fields: std::collections::BTreeSet<&str> = entry.fields.iter().copied().collect(); let missing: Vec<_> = rust_fields.difference(&ncl_keys).collect(); assert!(missing.is_empty(), "{} declares fields not in NCL: {:?}", entry.struct_name, missing); } } ``` Once `#[derive(ConfigFields)]` is in place, remove the `fields = [...]` lists from the corresponding `config_consumer` entries in `manifest.ncl` — the derive macro is now the source of truth. --- ## Phase 4 — API surface **Skip this phase if the project exposes no HTTP API.** If the project has an HTTP API served by axum or actix-web, annotate each handler with `#[onto_api]` so the daemon can surface the full annotated catalog at `GET /api/catalog`. ### 4a. Add ontoref-derive dependency Path is relative to the consuming crate, assuming ontoref source is a sibling project: ```toml [dependencies] ontoref-ontology = { path = "../../{ontoref_source_dir}/crates/ontoref-ontology" } ontoref-derive = { path = "../../{ontoref_source_dir}/crates/ontoref-derive" } ``` ### 4b. Annotate HTTP handlers ```rust use ontoref_derive::onto_api; #[onto_api( method = "GET", path = "/api/things", description = "List all things with optional filter.", auth = "bearer", actors = ["developer", "agent"], params = [("filter", "string", false, "optional substring filter")], tags = ["things", "read"] )] async fn list_things(/* axum extractors */) -> impl IntoResponse { /* ... */ } ``` Fields: - `method` — HTTP verb in caps: `"GET" | "POST" | "PUT" | "DELETE" | "PATCH"` - `path` — route path as registered in the router (e.g. `"/projects/{slug}/things"`) - `description` — one sentence, agent-readable - `auth` — `"bearer" | "admin" | "none"` - `actors` — list of actors allowed: `"developer" | "agent" | "ci" | "admin"` - `params` — array of `(name, type, required, description)` tuples - `tags` — grouping tags for catalog filtering **Register inventory collection** in the crate's `lib.rs` or `main.rs`: ```rust // In ontoref-ontology already: inventory::collect!(ApiRouteEntry); // In your crate: this is automatic — inventory::submit! is emitted by the macro. ``` ### 4c. Expose the catalog endpoint Add the catalog route to the router (if not already provided by ontoref-daemon): ```rust // If embedding ontoref-daemon routes: // GET /api/catalog is already registered by ontoref-daemon's router. // If building a standalone service with its own router, add: use ontoref_ontology::ApiRouteEntry; async fn api_catalog() -> axum::Json { let routes: Vec<_> = inventory::iter:: .map(|r| serde_json::json!({ "method": r.method, "path": r.path, "description": r.description, "auth": r.auth, "actors": r.actors, "params": r.params, "tags": r.tags, })) .collect(); axum::Json(serde_json::json!({ "routes": routes })) } ``` ### 4d. Export the catalog for ontoref UI visibility The ontoref daemon reads `#[onto_api]` entries from its own `inventory` at runtime. Consumer projects run as separate binaries — their entries are never linked into the ontoref-daemon process. To make the API surface visible in the ontoref UI, generate a static `api-catalog.json` in the project root and commit it. **Add `--dump-api-catalog` to the daemon binary's `Cli` struct** (in `main.rs`): ```rust /// Print all #[onto_api] registered routes as JSON and exit. /// Pipe to api-catalog.json so the ontoref UI can display this project's /// API surface when registered as a non-primary slug. #[arg(long)] dump_api_catalog: bool, ``` Add an early-exit handler before the server starts (same pattern as `--hash-password`): ```rust if cli.dump_api_catalog { println!("{}", ontoref_ontology::api::dump_catalog_json()); return; } ``` **Add the just recipe** — in `justfiles/assets.just` (create if absent, import in root `justfile`): ```just # Export this daemon's API catalog to api-catalog.json. # Run after any #[onto_api] annotation is added or changed. # Read by the ontoref UI for non-primary project slugs. [doc("Export #[onto_api] routes to api-catalog.json")] export-api-catalog: cargo run -p {daemon_crate} --no-default-features -- --dump-api-catalog > api-catalog.json @echo "exported routes to api-catalog.json" ``` Replace `{daemon_crate}` with the crate name of the project's HTTP daemon binary. **Run and commit:** ```sh just export-api-catalog git add api-catalog.json ``` Commit `api-catalog.json` alongside the `#[onto_api]` annotations — they change together. ### 4e. Verify ```sh cargo check --all-targets just export-api-catalog # Confirm routes were captured: cat api-catalog.json | jq '[.[] | {method, path, tags}]' ``` --- ## Phase 5 — Manifest self-interrogation Populate `capabilities[]`, `requirements[]`, and `critical_deps[]` in `.ontology/manifest.ncl`. Follow `$ONTOREF_ROOT/reflection/templates/manifest-self-interrogation-prompt.md` in full. **Quick reference for the three types:** ```nickel capabilities = [ m.make_capability { id = "kebab-id", name = "Name", summary = "One line: what does this capability do?", rationale = "Why it exists. What was rejected.", how = "Key patterns, entry points, data flows.", artifacts = ["crates/foo/", "GET /api/foo"], adrs = ["adr-001"], # IDs that formalize decisions here nodes = ["practice-node-id"], # IDs from .ontology/core.ncl }, ], requirements = [ m.make_requirement { id = "id", name = "Name", env = 'Both, # 'Production | 'Development | 'Both kind = 'Tool, # 'Tool | 'Service | 'EnvVar | 'Infrastructure version = "", required = true, impact = "What breaks if absent.", provision = "How to install/set/provision.", }, ], critical_deps = [ m.make_critical_dep { id = "id", name = "crate-or-service", ref = "crates.io: foo", used_for = "Which capabilities depend on this.", failure_impact = "What breaks if this dep disappears or breaks its contract.", mitigation = "Feature flags, fallback builds, alternatives.", }, ], ``` --- ## Phase 6 — Final validation ```sh cd {project_dir} # All .ontology/ files for f in .ontology/core.ncl .ontology/state.ncl .ontology/gate.ncl \ .ontology/manifest.ncl .ontology/connections.ncl; do test -f "$f" && \ nickel export --import-path "$NICKEL_IMPORT_PATH" "$f" > /dev/null \ && echo "ok: $f" || echo "FAIL: $f" done # All ADRs for f in adrs/adr-*.ncl; do nickel export --import-path "$NICKEL_IMPORT_PATH" "$f" > /dev/null \ && echo "ok: $f" || echo "FAIL: $f" done # Rust: build, lint, tests cargo check --all-targets --all-features cargo clippy --all-targets --all-features -- -D warnings cargo test config_fields_registered -- --nocapture 2>/dev/null || true cargo test ncl_rust_coherence -- --nocapture 2>/dev/null || true # Describe output ontoref --actor developer describe project ontoref --actor developer describe requirements ONTOREF_ACTOR=agent ontoref describe capabilities | from json | get capabilities | length ``` --- ## Checklist ### Protocol layer - [ ] `.ontology/manifest.ncl` present and exports cleanly - [ ] `.ontology/connections.ncl` present and exports cleanly - [ ] `core.ncl` nodes reflect current implementation (no aspirational nodes) - [ ] `state.ncl` dimension states match current reality - [ ] All `check_hint` fields migrated to typed `check` variants ### Config surface - [ ] `config_surface` declared in `manifest.ncl` (if project uses NCL config) - [ ] All sections have `id`, `file`, `consumers` with accurate kinds - [ ] Nickel-validated-overrides: `collect_env_overrides()` implemented (if applicable) - [ ] Nickel-validated-overrides: `apply_env_overrides(&mut self)` removed - [ ] NCL schemas have real constraints (`from_validator`, not bare `| Number`) - [ ] `#[derive(ConfigFields)]` on all config structs that read NCL sections - [ ] `cargo test config_fields_registered` passes - [ ] `cargo test ncl_rust_coherence` passes - [ ] `fields = [...]` removed from manifest consumers once derive is in place ### API surface - [ ] `#[onto_api]` on all HTTP handlers (if project has an HTTP API) - [ ] `GET /api/catalog` returns non-empty routes list - [ ] All routes have accurate `auth`, `actors`, `tags` ### Manifest self-interrogation - [ ] `description` field populated (non-empty) - [ ] At least 1 `capability` entry with non-empty `summary` - [ ] `capabilities[].nodes[]` verified against `core.ncl` node IDs - [ ] At least 1 `requirement` per relevant environment - [ ] All `critical_deps` have non-empty `failure_impact` ### Delivery - [ ] `describe project` returns complete picture - [ ] `describe requirements` renders without errors - [ ] No orphaned `describe diff` changes (everything committed or staged intentionally) - [ ] Do NOT commit — developer reviews the diff first