ontoref/reflection/templates/project-full-adoption-prompt.md
Jesús Pérez da083fb9ec
Some checks failed
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
.coder/m
2026-03-29 00:19:56 +00:00

626 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 26 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<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()`:**
```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::<u16>() {
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<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:**
```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::<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:**
```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::<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:
```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<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`):
```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