22 KiB
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:
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.
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.
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
blockerandcatalystto reflect current reality - Every edit must pass
nickel exportbefore continuing to the next node
After completing ontology enrichment, run:
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, usuallyconfig.ncl) - What sections exist (each top-level key in the merged export)
- Who reads each section (Rust structs, Nu scripts, CI pipelines)
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:
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.
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):
pub fn export_with_overrides(
ncl_path: &Path,
import_path: &str,
overrides: &serde_json::Value,
) -> Result<String, ConfigError> {
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():
// 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::<u16>() {
overrides["server"]["port"] = n.into();
}
}
// ... one block per env var, following the NCL schema nesting
overrides
}
Step 3 — Wire through the load path:
pub fn load() -> Result<Self, ConfigError> {
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:
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:
# 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:
# 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):
[dependencies]
ontoref-ontology = { path = "../../{ontoref_source_dir}/crates/ontoref-ontology" }
Annotate config structs:
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:
idmust match the sectionidinmanifest.ncl → config_surface.sections[*].idncl_fileis 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:
#[test]
fn config_fields_registered() {
for entry in inventory::iter::<ontoref_ontology::ConfigFieldsEntry> {
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:
#[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::<ontoref_ontology::ConfigFieldsEntry> {
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:
[dependencies]
ontoref-ontology = { path = "../../{ontoref_source_dir}/crates/ontoref-ontology" }
ontoref-derive = { path = "../../{ontoref_source_dir}/crates/ontoref-derive" }
4b. Annotate HTTP handlers
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-readableauth—"bearer" | "admin" | "none"actors— list of actors allowed:"developer" | "agent" | "ci" | "admin"params— array of(name, type, required, description)tuplestags— grouping tags for catalog filtering
Register inventory collection in the crate's lib.rs or main.rs:
// 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):
// 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<serde_json::Value> {
let routes: Vec<_> = inventory::iter::<ApiRouteEntry>
.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):
/// 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):
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):
# 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:
just export-api-catalog
git add api-catalog.json
Commit api-catalog.json alongside the #[onto_api] annotations — they change together.
4e. Verify
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:
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
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.nclpresent and exports cleanly.ontology/connections.nclpresent and exports cleanlycore.nclnodes reflect current implementation (no aspirational nodes)state.ncldimension states match current reality- All
check_hintfields migrated to typedcheckvariants
Config surface
config_surfacedeclared inmanifest.ncl(if project uses NCL config)- All sections have
id,file,consumerswith 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 sectionscargo test config_fields_registeredpassescargo test ncl_rust_coherencepassesfields = [...]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/catalogreturns non-empty routes list- All routes have accurate
auth,actors,tags
Manifest self-interrogation
descriptionfield populated (non-empty)- At least 1
capabilityentry with non-emptysummary capabilities[].nodes[]verified againstcore.nclnode IDs- At least 1
requirementper relevant environment - All
critical_depshave non-emptyfailure_impact
Delivery
describe projectreturns complete picturedescribe requirementsrenders without errors- No orphaned
describe diffchanges (everything committed or staged intentionally) - Do NOT commit — developer reviews the diff first