feat: manifest self-interrogation layer — capabilities, requirements, critical deps (ADR-009)

Three new typed arrays in manifest_type answering operational self-knowledge queries:
  capability_type (what/why/how + nodes[]/adrs[] DAG cross-refs), requirement_type
  (env_target: Production/Development/Both; kind: Tool/Service/EnvVar/Infrastructure),
  critical_dep_type (failure_impact required, mitigation). describe requirements new
  subcommand; describe capabilities and describe guides extended with manifest data.
  Bug fix: collect-identity was reading manifest.kind? (absent) instead of manifest.repo_kind?.
  Ontoref self-described with 3 capabilities, 5 requirements, 3 critical deps.
  on+re: manifest-self-description node (29 nodes, 59 edges), ADR-009 accepted.
This commit is contained in:
Jesús Pérez 2026-03-26 21:01:39 +00:00
parent 401294de5d
commit 502b5f0caa
Signed by: jesus
GPG Key ID: 9F243E355E0BC939
9 changed files with 505 additions and 12 deletions

View File

@ -81,9 +81,10 @@ let d = import "../ontology/defaults/core.ncl" in
"adrs/adr-006-nushell-0111-string-interpolation-compat.ncl",
"adrs/adr-007-api-surface-discoverability-onto-api-proc-macro.ncl",
"adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl",
"adrs/adr-009-manifest-self-interrogation-layer-three-semantic-axes.ncl",
"CHANGELOG.md",
],
adrs = ["adr-001", "adr-002", "adr-003", "adr-004", "adr-005", "adr-006", "adr-007", "adr-008"],
adrs = ["adr-001", "adr-002", "adr-003", "adr-004", "adr-005", "adr-006", "adr-007", "adr-008", "adr-009"],
},
d.make_node {
@ -369,6 +370,22 @@ let d = import "../ontology/defaults/core.ncl" in
],
},
d.make_node {
id = "manifest-self-description",
name = "Manifest Self-Interrogation Layer",
pole = 'Yang,
level = 'Practice,
description = "Three typed arrays added to manifest_type: capabilities[] (what the project does, why, how — with explicit ontology node and ADR cross-references), requirements[] (prerequisites classified by env_target_type: Production/Development/Both and requirement_kind_type: Tool/Service/EnvVar/Infrastructure), and critical_deps[] (external dependencies with documented blast radius distinct from startup prerequisites). describe requirements new subcommand surfaces these. describe guides gains capabilities/requirements/critical_deps keys — agents on cold start receive full self-interrogation context without extra tool calls. Also fixes the collect-identity bug where manifest.kind? was read (field did not exist) instead of manifest.repo_kind?, and adds description | String | default = '' to manifest_type.",
invariant = false,
artifact_paths = [
"ontology/schemas/manifest.ncl",
"ontology/defaults/manifest.ncl",
".ontology/manifest.ncl",
"reflection/modules/describe.nu",
],
adrs = ["adr-009"],
},
d.make_node {
id = "config-surface",
name = "Config Surface",
@ -474,6 +491,18 @@ let d = import "../ontology/defaults/core.ncl" in
note = "--gen-keys bootstraps the first keys into project.ncl during setup." },
{ from = "project-onboarding", to = "daemon-config-management", kind = 'DependsOn, weight = 'Medium },
# Manifest Self-Interrogation Layer edges
{ from = "manifest-self-description", to = "self-describing", kind = 'Complements, weight = 'High,
note = "capabilities/requirements/critical_deps in the manifest are the typed operational answer to 'what IS this project' — complementing the architectural answer in core.ncl Practice nodes." },
{ from = "manifest-self-description", to = "adopt-ontoref-tooling", kind = 'Complements, weight = 'High,
note = "Consumer projects declare their own capabilities, requirements, and critical deps — the self-interrogation layer is part of the adoption surface." },
{ from = "manifest-self-description", to = "describe-query-layer", kind = 'ManifestsIn, weight = 'High,
note = "describe capabilities renders manifest_capabilities; describe requirements surfaces requirements + critical_deps; describe guides extends its output with all three arrays." },
{ from = "manifest-self-description", to = "dag-formalized", kind = 'Complements, weight = 'Medium,
note = "capabilities.nodes[] cross-references ontology node IDs; capabilities.adrs[] cross-references ADR IDs — bridging the manifest into the queryable DAG." },
{ from = "manifest-self-description", to = "adr-lifecycle", kind = 'Complements, weight = 'Medium,
note = "capabilities.adrs[] creates explicit typed links from capabilities to the ADRs that formalize them — the ADR→Node linkage pattern extended to the manifest layer." },
# Config Surface edges
{ from = "config-surface", to = "ontoref-daemon", kind = 'ManifestsIn, weight = 'High },
{ from = "config-surface", to = "ontoref-ontology-crate", kind = 'DependsOn, weight = 'High,

View File

@ -1,8 +1,9 @@
let m = import "../ontology/defaults/manifest.ncl" in
m.make_manifest {
project = "ontoref",
repo_kind = 'DevWorkspace,
project = "ontoref",
repo_kind = 'DevWorkspace,
description = "Protocol specification and tooling layer for structured self-knowledge in software projects. Provides schemas, Nushell automation, and Rust crates so projects can describe what they are, record architectural decisions, track operational state, and execute formalized procedures as typed, queryable artifacts.",
content_assets = [
m.make_asset {
@ -213,6 +214,123 @@ m.make_manifest {
],
},
capabilities = [
m.make_capability {
id = "protocol-spec",
name = "Protocol Specification",
summary = "Typed NCL schemas for nodes, edges, ADRs, state, gates, and manifests.",
rationale = "Projects need a contract layer to describe what they are — not just code comments. NCL provides typed, queryable, git-versionable schemas with contract enforcement at export time. Alternatives (TOML/JSON/YAML) lack contracts; Rust-only structs are not adoption-friendly.",
how = "ontology/schemas/ defines all type contracts. adrs/adr-schema.ncl defines the ADR lifecycle contract. ontology/defaults/ exposes builders (make_node, make_edge, make_adr) so consumer projects never write raw NCL records. nickel export validates against declared contracts before any JSON reaches Rust or Nushell.",
artifacts = ["ontology/schemas/", "ontology/defaults/", "adrs/adr-schema.ncl", "adrs/adr-defaults.ncl"],
nodes = ["dag-formalized", "protocol-not-runtime", "adr-lifecycle"],
},
m.make_capability {
id = "daemon-api",
name = "Daemon HTTP + MCP Surface",
summary = "HTTP UI (11 pages), 29 MCP tools, annotated API catalog, Q&A store, search bookmarks, config surface, per-file versioning.",
rationale = "Agents and developers need a queryable interface to ontology state without spawning nickel on every request. The NCL export cache reduces full-sync from ~2m42s to <30s. 29 MCP tools give agents structured access to every capability without screen-scraping the CLI. ADR-002 records the architectural decision to extract the daemon; ADR-007 covers the #[onto_api] catalog pattern; ADR-008 covers the config override layer.",
how = "crates/ontoref-daemon uses axum for HTTP. #[onto_api(...)] proc-macro + inventory::submit! registers every route at link time; GET /api/catalog aggregates via inventory::collect!. NclCache (DashMap<PathBuf, CachedExport>) keyed on path + mtime. File watcher (notify) triggers cache invalidation and drift detection after 15s debounce. MCP over stdio and streamable-HTTP.",
artifacts = [
"crates/ontoref-daemon/",
"GET /api/catalog",
"GET /projects/{slug}/ontology",
"GET /projects/{slug}/config/coherence",
"MCP: ontoref_guides, ontoref_api_catalog, ontoref_validate, ontoref_impact",
],
adrs = ["adr-002", "adr-004", "adr-007", "adr-008"],
nodes = ["ontoref-daemon", "api-catalog-surface", "config-surface"],
},
m.make_capability {
id = "reflection-modes",
name = "Reflection Mode Executor",
summary = "NCL DAG workflow engine executing ADR lifecycle, project adoption, content modes, and operational procedures.",
rationale = "Structured procedures expressed as typed DAGs rather than ad-hoc scripts. Every step has a declared dep graph — the executor validates it before running. Agent-safe: modes are NCL contracts, not imperative scripts, so agents can read and reason about them before execution.",
how = "crates/ontoref-reflection loads a mode NCL file, validates the DAG contract (no cycles, declared deps exist), then executes steps via Nushell subprocesses. reflection/modules/ contains 16 Nushell modules that implement the actual step logic. reflection/modes/ contains the typed NCL DAG definitions.",
artifacts = ["reflection/modes/", "reflection/modules/", "crates/ontoref-reflection/"],
nodes = ["reflection-modes", "adopt-ontoref-tooling"],
},
],
requirements = [
m.make_requirement {
id = "nushell",
name = "Nushell",
env = 'Both,
kind = 'Tool,
version = "0.110.0",
required = true,
impact = "All reflection modes and the ./ontoref dispatcher are Nushell scripts. Without Nu nothing executes — no mode runs, no describe subcommands, no sync.",
provision = "https://www.nushell.sh/ — cargo install nu or OS package manager.",
},
m.make_requirement {
id = "nickel",
name = "Nickel",
env = 'Both,
kind = 'Tool,
version = "",
required = true,
impact = "All schema evaluation, ADR parsing, config export blocked. Daemon NCL cache inoperable. Config surface mutation cannot validate overrides.",
provision = "https://nickel-lang.org/ — cargo install nickel-lang-cli or nix flake.",
},
m.make_requirement {
id = "rust-nightly",
name = "Rust nightly toolchain",
env = 'Development,
kind = 'Tool,
version = "",
required = true,
impact = "cargo +nightly fmt fails — pre-commit hook blocks all commits.",
provision = "rustup toolchain install nightly",
},
m.make_requirement {
id = "surrealdb",
name = "SurrealDB",
env = 'Production,
kind = 'Service,
version = "",
required = false,
impact = "Daemon db feature disabled; ontology not projected into DB. Daemon still works via --no-default-features for local-only use.",
provision = "https://surrealdb.com/ — binary or container. Feature-gated: cargo build -p ontoref-daemon --no-default-features omits it.",
},
m.make_requirement {
id = "stratumiops",
name = "stratumiops repo checkout",
env = 'Development,
kind = 'Infrastructure,
version = "",
required = false,
impact = "ontoref-daemon (db/nats features) and ontoref-reflection (nats feature) cannot build. Build with --no-default-features to work without it.",
provision = "git clone at ../../../stratumiops relative to this repo root.",
},
],
critical_deps = [
m.make_critical_dep {
id = "nickel-lang",
name = "nickel-lang",
ref = "crates.io: nickel-lang-core / nickel-lang-cli",
used_for = "Schema evaluation, contract enforcement, config export, ADR parsing. Every nickel export call in the daemon cache and in Nushell modules.",
failure_impact = "Total loss of typed schema layer. All NCL export operations fail. Daemon cache inoperable. Config override mutation cannot validate before committing. describe guides and ontoref_guides MCP tool return empty data.",
mitigation = "Pin to a specific Nickel release in PATH. Nickel is a subprocess dep (never linked into Rust binary) — breakage manifests at runtime as nickel export exit code != 0, which all callers handle gracefully (daemon-export-safe returns null, callers use | default []).",
},
m.make_critical_dep {
id = "inventory",
name = "inventory",
ref = "crates.io: inventory 0.3",
used_for = "#[onto_api] HTTP catalog registration and #[derive(ConfigFields)] config coherence registry. Both use inventory::submit! at link time; GET /api/catalog and GET /config/coherence use inventory::iter! at runtime.",
failure_impact = "GET /api/catalog returns empty. Config coherence endpoint loses Rust struct field data. ontoref_api_catalog MCP tool blind. API version change would require updating both onto_api and ConfigFields derive macros simultaneously.",
mitigation = "inventory uses linker sections (zero runtime overhead). Version is pinned in Cargo.toml. ontoref-derive and ontoref-ontology both declare it — version must stay in sync.",
},
m.make_critical_dep {
id = "axum",
name = "axum",
ref = "crates.io: axum",
used_for = "All 11 UI pages and REST API endpoints in ontoref-daemon. Router, handlers, extractors, middleware.",
failure_impact = "Daemon does not compile. Full HTTP surface down: UI, REST API, MCP over HTTP, session management, config surface endpoints.",
mitigation = "ontoref-ontology and the ./ontoref CLI are axum-free. Reflection modes, ADR tooling, and describe subcommands continue working without the daemon.",
},
],
layers = [
m.make_layer {
id = "protocol",

View File

@ -25,7 +25,7 @@ let d = import "../ontology/defaults/state.ncl" in
to = "protocol-stable",
condition = "ADR-001 accepted, ontoref.dev published, at least two external projects consuming the protocol.",
catalyst = "First external adoption.",
blocker = "ontoref.dev not yet published; no external consumers yet. Auth model complete. Install pipeline complete. Personal/career schema layer present; content modes operational. Nu 0.111 compat fixed (ADR-006). Protocol v2 complete: manifest.ncl + connections.ncl templates, update_ontoref mode, API catalog via #[onto_api], describe diff, describe api, per-file versioning. Config surface complete (ADR-008): typed DaemonNclConfig, #[derive(ConfigFields)] inventory coherence registry, NCL contracts (LogConfig/DaemonConfig in .ontoref/contracts.ncl), override-layer mutation API, multi-consumer manifest schema. Syntaxis syntaxis-ontology crate has pending ES→EN migration errors.",
blocker = "ontoref.dev not yet published; no external consumers yet. Auth model complete. Install pipeline complete. Personal/career schema layer present; content modes operational. Nu 0.111 compat fixed (ADR-006). Protocol v2 complete: manifest.ncl + connections.ncl templates, update_ontoref mode, API catalog via #[onto_api], describe diff, describe api, per-file versioning. Config surface complete (ADR-008): typed DaemonNclConfig, #[derive(ConfigFields)] inventory coherence registry, NCL contracts (LogConfig/DaemonConfig in .ontoref/contracts.ncl), override-layer mutation API, multi-consumer manifest schema. Manifest self-interrogation layer complete (ADR-009): capability_type, requirement_type (env_target: Production/Development/Both, kind: Tool/Service/EnvVar/Infrastructure), critical_dep_type — describe requirements new subcommand, describe guides extended. Syntaxis syntaxis-ontology crate has pending ES→EN migration errors.",
horizon = 'Months,
},
],
@ -52,7 +52,7 @@ let d = import "../ontology/defaults/state.ncl" in
from = "modes-and-web-present",
to = "fully-self-described",
condition = "At least 3 ADRs accepted, reflection/backlog.ncl present, describe project returns complete picture.",
catalyst = "ADR-001ADR-006 authored (6 ADRs present). Auth model, project onboarding, and session management nodes added in 2026-03-13. Personal/career/project-card schemas, 5 content modes, search bookmarks, and ADR-006 (Nu 0.111 compat) added in session 2026-03-15. Session 2026-03-23: api-catalog-surface node added (#[onto_api] proc-macro + inventory catalog), describe-query-layer updated (diff + api subcommands), adopt-ontoref-tooling updated (update_ontoref mode + manifest/connections templates + enrichment prompt), ontoref-daemon updated (11 pages, 29 MCP tools, per-file versioning, API catalog endpoint). Session 2026-03-26: config-surface node added — typed DaemonNclConfig (parse-at-boundary pattern), #[derive(ConfigFields)] coherence registry, override-layer mutation API (PUT /config/{section}), NCL contracts (.ontoref/contracts.ncl: LogConfig + DaemonConfig), manifest config_surface with multi-consumer sections. ADR-007 (inventory/onto_api) extended to ConfigFields; ADR-008 (NCL-first config validation + override-layer mutation).",
catalyst = "ADR-001ADR-006 authored (6 ADRs present). Auth model, project onboarding, and session management nodes added in 2026-03-13. Personal/career/project-card schemas, 5 content modes, search bookmarks, and ADR-006 (Nu 0.111 compat) added in session 2026-03-15. Session 2026-03-23: api-catalog-surface node added (#[onto_api] proc-macro + inventory catalog), describe-query-layer updated (diff + api subcommands), adopt-ontoref-tooling updated (update_ontoref mode + manifest/connections templates + enrichment prompt), ontoref-daemon updated (11 pages, 29 MCP tools, per-file versioning, API catalog endpoint). Session 2026-03-26: config-surface node added — typed DaemonNclConfig (parse-at-boundary pattern), #[derive(ConfigFields)] coherence registry, override-layer mutation API (PUT /config/{section}), NCL contracts (.ontoref/contracts.ncl: LogConfig + DaemonConfig), manifest config_surface with multi-consumer sections. ADR-007 (inventory/onto_api) extended to ConfigFields; ADR-008 (NCL-first config validation + override-layer mutation). Session 2026-03-26 (2nd): manifest-self-description node added — capability_type (id/name/summary/rationale/how/artifacts/adrs/nodes), requirement_type (env_target: Production/Development/Both; kind: Tool/Service/EnvVar/Infrastructure; impact/provision), critical_dep_type (failure_impact required; mitigation). describe requirements new subcommand. describe guides extended with capabilities/requirements/critical_deps. Bug fix: collect-identity read manifest.kind? (never existed) instead of manifest.repo_kind?; description field added to manifest_type. Ontoref self-described with 3 capabilities, 5 requirements, 3 critical deps. ADR-009.",
blocker = "none",
horizon = 'Weeks,
},

View File

@ -7,6 +7,58 @@ ADRs referenced below live in `adrs/` as typed Nickel records.
## [Unreleased]
### Manifest Self-Interrogation Layer — capabilities, requirements, critical deps (ADR-009)
Three new typed arrays in `manifest_type` answering operational self-knowledge queries distinct from
ontology Practice nodes. `describe requirements` new subcommand; `describe guides` extended.
#### `ontology/schemas/manifest.ncl` — three new types
- `capability_type``id`, `name`, `summary`, `rationale`, `how`, `artifacts[]`, `adrs[]`, `nodes[]`.
`nodes[]` cross-references ontology node IDs; `adrs[]` cross-references ADR IDs.
- `env_target_type``'Production | 'Development | 'Both` classification axis for requirements.
- `requirement_kind_type``'Tool | 'Service | 'EnvVar | 'Infrastructure`.
- `requirement_type``id`, `name`, `env`, `kind`, `version`, `required`, `impact`, `provision`.
- `critical_dep_type``id`, `name`, `ref`, `used_for`, `failure_impact` (required), `mitigation`.
- `manifest_type` gains `description | String | default = ""` (bug fix — `collect-identity` was reading
a field that didn't exist), `capabilities[]`, `requirements[]`, `critical_deps[]` (all `default = []`).
- New exports: `EnvTarget`, `RequirementKind`, `Capability`, `Requirement`, `CriticalDep`.
#### `ontology/defaults/manifest.ncl` — three new builders
- `make_capability`, `make_requirement`, `make_critical_dep` added alongside existing builders.
- New type re-exports: `EnvTarget`, `RequirementKind`, `Capability`, `Requirement`, `CriticalDep`.
#### `reflection/modules/describe.nu` — new subcommand + extended outputs
- `describe requirements` — renders requirements grouped by env (Production / Development / Both) and
critical deps table (name, ref, used_for, failure_impact, mitigation). `--environment` flag filters.
- `describe capabilities` extended — loads `manifest.capabilities?` and renders a `PROJECT CAPABILITIES
(manifest)` section with name, summary, artifacts per entry.
- `describe guides` output gains `capabilities`, `requirements`, `critical_deps` keys — agents on cold
start via `ontoref_guides` MCP tool receive full self-interrogation context without extra tool calls.
- Bug fix: `collect-identity` was reading `manifest.kind?` (field absent from schema, always returned
`""`) — changed to `manifest.repo_kind?`. Same fix for `manifest.description?` (now exists).
#### `.ontology/manifest.ncl` — ontoref self-described
- `description` field populated.
- 3 capabilities: `protocol-spec`, `daemon-api`, `reflection-modes` — each with rationale, how,
artifacts, adrs, nodes cross-references.
- 5 requirements: `nushell` (both), `nickel` (both), `rust-nightly` (dev), `surrealdb` (prod optional),
`stratumiops` (dev optional) — each with impact and provision.
- 3 critical deps: `nickel-lang`, `inventory`, `axum` — each with failure_impact and mitigation.
#### on+re update
| Artifact | Change |
|----------|--------|
| `adrs/adr-009-...ncl` | Created — manifest self-interrogation layer, three semantic axes |
| `.ontology/core.ncl` | `manifest-self-description` node added (29 nodes, 59 edges); `adr-lifecycle` updated with ADR-009 |
| `.ontology/state.ncl` | `protocol-maturity` blocker + `self-description-coverage` catalyst updated |
---
### Config Surface — typed config, NCL contracts, override-layer mutation
Per-project config introspection, coherence verification, and audited mutation. NCL contracts are the single

View File

@ -131,6 +131,14 @@ gate — Rust structs are contract-trusted readers with `#[serde(default)]`.
Ontoref demonstrates the pattern on itself: `.ontoref/contracts.ncl` applies `LogConfig` and
`DaemonConfig` contracts to `.ontoref/config.ncl`. ([ADR-008](adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl))
**Manifest Self-Interrogation** — `manifest_type` gains three typed arrays that answer self-knowledge
queries agents and operators need on cold start: `capabilities[]` (what the project does, why it was
built, how it works — with explicit `nodes[]` and `adrs[]` cross-references into the DAG),
`requirements[]` (prerequisites classified by environment: `'Production | 'Development | 'Both` and
kind: `'Tool | 'Service | 'EnvVar | 'Infrastructure`), `critical_deps[]` (external dependencies with
required `failure_impact` and optional `mitigation`). `describe requirements` surfaces these; `describe
guides` and `ontoref_guides` include all three arrays in their output. ([ADR-009](adrs/adr-009-manifest-self-interrogation-layer-three-semantic-axes.ncl))
## Install
```sh

View File

@ -0,0 +1,85 @@
let d = import "adr-defaults.ncl" in
d.make_adr {
id = "adr-009",
title = "Manifest Self-Interrogation Layer — Three Semantic Axes",
status = 'Accepted,
date = "2026-03-26",
context = "The manifest.ncl schema described structural facts (layers, modes, consumption modes, tools, config surface) but had no typed layer for self-interrogation: agents and operators could not query why a capability exists, what it needs to run, or what external dependencies have a documented blast radius. The existing tools[] field covered only dev tooling (install_method, version) — no prod/dev classification, no services, no environment variables, no infrastructure dependencies. Practice/node descriptions in core.ncl carry architectural meaning (invariant=true, ADR-backed) but are not the right home for operational, audience-facing descriptions of what a project offers. Capabilities, requirements, and dependency blast-radius analysis are three orthogonal concerns that needed typed, queryable homes in the manifest schema.",
decision = "Three new typed arrays are added to manifest_type: capabilities[] (capability_type), requirements[] (requirement_type), and critical_deps[] (critical_dep_type). These are semantically distinct layers: capabilities answer 'what does this project do, why does it exist, and how does it work' with explicit cross-references to ontology node IDs and ADR IDs; requirements classify prerequisites by env_target_type ('Production | 'Development | 'Both) and requirement_kind_type ('Tool | 'Service | 'EnvVar | 'Infrastructure); critical_deps document blast radius — what breaks when an external dependency disappears or breaks its contract — distinct from requirements because the concern is runtime failure impact, not startup prerequisites. A description | String | default = '' field is also added to manifest_type, fixing a pre-existing bug where collect-identity in describe.nu read manifest.description? (always null) instead of a field that existed. describe requirements is added as a new subcommand; describe capabilities is extended to render manifest capabilities; describe guides output gains capabilities/requirements/critical_deps keys so agents on cold start receive full self-interrogation context.",
rationale = [
{
claim = "Capabilities are operationally distinct from Practice nodes in core.ncl",
detail = "Practice nodes are architectural artifacts: they carry invariant=true, are protected by Hard ADR constraints, and describe structural patterns that must never be violated. A 'capability' is operational and audience-facing — it answers what a project offers, why it was built, and how to find the relevant code. Merging these into core.ncl would inflate the invariant-protected graph with non-architectural facts. The manifest is the right home: it is per-project, optional, and already carries operational metadata (layers, modes, config surface).",
},
{
claim = "requirements[] supercedes tools[] with a principled classification axis",
detail = "The legacy tools[] field (name, install_method, version, required) modeled only dev tooling. Production deployments need SurrealDB; CI needs the Rust nightly toolchain; the daemon needs NATS_STREAMS_CONFIG in certain topologies. env_target_type ('Production | 'Development | 'Both) and requirement_kind_type ('Tool | 'Service | 'EnvVar | 'Infrastructure) make these distinctions queryable. The tools[] field is kept for backward compatibility but requirements[] is the complete model.",
},
{
claim = "critical_deps[] separates blast-radius documentation from prerequisites",
detail = "A requirement says 'you need X to run'. A critical dep says 'X failing at runtime breaks these capabilities in these ways'. inventory (crates.io) not present as a requirement — ontoref compiles without it. But if inventory's API breaks, GET /api/catalog goes silent and ontoref_api_catalog becomes blind. This distinction matters for operational runbooks and for agents reasoning about degraded-mode operation. failure_impact is required (no default) because undocumented blast radius defeats the purpose of the type.",
},
],
consequences = {
positive = [
"describe guides output now includes capabilities, requirements, and critical_deps — agents on cold start receive complete self-interrogation context without extra tool calls",
"describe requirements new subcommand answers 'what does this project need?' for both developer setup and production deployment",
"capabilities.nodes[] and capabilities.adrs[] create explicit bidirectional cross-references between the manifest and the ontology DAG",
"env_target_type makes prod vs dev prerequisite separation queryable — CI can verify only its subset of requirements",
"critical_dep mitigation field enables agents to reason about build flags (--no-default-features) and fallback paths when a dep is unavailable",
"Bug fixed: collect-identity in describe.nu was reading manifest.kind? (always null) instead of manifest.repo_kind?; adding description | String | default = '' fixes the identity description gap",
],
negative = [
"Consumer projects must populate capabilities/requirements/critical_deps manually — there is no automatic extraction; an empty manifest still validates (all fields default = [])",
"capabilities.nodes[] cross-references must be kept in sync with core.ncl node IDs manually — no DAG consistency check at nickel export time",
"Three new array fields increase manifest.ncl verbosity; projects with complex capability/dependency landscapes will have large manifest files",
],
},
alternatives_considered = [
{
option = "Extend tools[] with env and kind fields",
why_rejected = "tools[] is semantically 'dev tooling required to build'. Extending it to cover SurrealDB (a production service) or ONTOREF_ADMIN_TOKEN_FILE (an env var) would violate the field's implied meaning. A type named tool_requirement_type that has kind = 'Service is confusing. Separate types with clear names are preferable to an overloaded catch-all.",
},
{
option = "Add capability descriptions to Practice nodes in core.ncl",
why_rejected = "Practice nodes are architectural: invariant=true nodes are ADR-protected and represent constraints future contributors must follow. Adding 'what does this offer to end users' to the invariant graph would force every capability description through the ADR lifecycle. capabilities[] is per-project, evolves freely, and belongs to the manifest (the operational layer), not the ontology (the architectural layer).",
},
{
option = "Merge critical_deps into requirements with an is_critical flag",
why_rejected = "The concern is different: requirements are prerequisites (the system cannot start without them). critical_deps are runtime load-bearing with a documented blast radius. A requirement that is optional (required = false) can still be a critical dep. The is_critical flag on requirement_type would blur this distinction and make failure_impact logically optional (not required for non-critical items) — creating a type that is partially applicable based on a flag, which is a code smell in typed schemas.",
},
],
constraints = [
{
id = "capabilities-nodes-are-ontology-ids",
claim = "capability_type.nodes[] entries must reference valid node IDs declared in .ontology/core.ncl",
scope = "ontology/schemas/manifest.ncl (capability_type definition)",
severity = 'Soft,
check = { tag = 'Grep, pattern = "nodes.*=.*\\[", paths = [".ontology/manifest.ncl"], must_be_empty = false },
rationale = "Cross-references to non-existent node IDs produce silent broken links in the graph UI and describe output. Soft because nickel export cannot verify cross-file ID consistency; responsibility lies with the author.",
},
{
id = "failure-impact-required-in-critical-deps",
claim = "critical_dep_type.failure_impact must be a non-empty string — undocumented blast radius defeats the purpose of the type",
scope = "ontology/schemas/manifest.ncl (critical_dep_type definition)",
severity = 'Hard,
check = { tag = 'Grep, pattern = "failure_impact.*=.*\"\"", paths = [".ontology/manifest.ncl"], must_be_empty = true },
rationale = "A critical dep entry with no failure_impact is indistinguishable from a regular requirement. The type's value is entirely in the blast-radius documentation.",
},
],
related_adrs = ["adr-001", "adr-009"],
ontology_check = {
decision_string = "manifest_type gains three typed self-interrogation arrays (capabilities, requirements, critical_deps) with orthogonal semantic axes; describe.nu gains describe requirements and extend describe guides; collect-identity bug fixed",
invariants_at_risk = ["dag-formalized", "self-describing"],
verdict = 'Safe,
},
}

View File

@ -40,4 +40,13 @@ let c = import "content.ncl" in
make_config_surface = fun data => s.ConfigSurface & data,
make_config_section = fun data => s.ConfigSection & data,
make_config_consumer = fun data => s.ConfigConsumer & data,
# Capability / requirement / critical-dep builders
EnvTarget = s.EnvTarget,
RequirementKind = s.RequirementKind,
Capability = s.Capability,
Requirement = s.Requirement,
CriticalDep = s.CriticalDep,
make_capability = fun data => s.Capability & data,
make_requirement = fun data => s.Requirement & data,
make_critical_dep = fun data => s.CriticalDep & data,
}

View File

@ -209,11 +209,88 @@ let claude_baseline_type = {
stratum_commands | Bool | default = true,
} in
# ── Capabilities ────────────────────────────────────────────────────────────
# Declares what the project does, why it was built, how it works, and what
# artifacts it produces. Answers the "what IS this, why does it exist, how
# does it work" questions that Practice nodes in core.ncl don't — those are
# architectural; capabilities are operational and audience-facing.
let capability_type = {
# Unique identifier, e.g. "protocol-spec", "daemon-api".
id | String,
name | String,
# One-line answer to "what does this capability do?" for quick scanning.
summary | String,
# The WHY: motivation, problem solved, alternatives consciously rejected.
rationale | String | default = "",
# Implementation level: key patterns, entry points, data flows.
how | String | default = "",
# Observable artifacts: crate paths, API routes, NCL schemas, CLI commands.
artifacts | Array String | default = [],
# ADR IDs that formalize architectural decisions in this capability.
adrs | Array String | default = [],
# Ontology node IDs this capability manifests in the DAG.
nodes | Array String | default = [],
} in
# ── Requirements ─────────────────────────────────────────────────────────────
# Declares what the project needs to run (production) or develop (development).
# Covers tools, external services, environment variables, and infrastructure —
# not just dev tooling. Enables agents and operators to audit readiness.
let env_target_type = [|
'Production, # required only in production deployments
'Development, # required only during development / CI
'Both, # required in all environments
|] in
let requirement_kind_type = [|
'Tool, # executable on PATH (e.g. nushell, nickel, just)
'Service, # external service (e.g. surrealdb, nats, postgres)
'EnvVar, # environment variable that must be set
'Infrastructure, # filesystem layout, network, or platform dependency
|] in
let requirement_type = {
id | String,
name | String,
env | env_target_type | default = 'Both,
kind | requirement_kind_type,
# Minimum version or value constraint. Empty means no constraint.
version | String | default = "",
required | Bool | default = true,
# What breaks or degrades if this requirement is absent.
impact | String | default = "",
# How to install, set, or provision this requirement.
provision | String | default = "",
} in
# ── Critical Dependencies ────────────────────────────────────────────────────
# External dependencies (crates, services, infra) whose contract breakage or
# disappearance has a documented blast radius. Distinct from requirements:
# requirements are prerequisites to run; critical_deps are runtime load-bearing
# dependencies whose failure affects specific capabilities.
let critical_dep_type = {
id | String,
name | String,
# Canonical reference: crates.io identifier, GitHub URL, or service name.
ref | String,
# Which capabilities or features depend on this dependency.
used_for | String,
# What breaks if this dep disappears or breaks its API contract.
failure_impact | String,
# Mitigation: feature flags, fallbacks, alternative build targets.
mitigation | String | default = "",
} in
# ── Root manifest ───────────────────────────────────────────────────────────
let manifest_type = {
project | String,
repo_kind | repo_kind_type,
# Human-readable project description. Consumed by collect-identity in describe.nu.
description | String | default = "",
layers | Array layer_type | default = [],
operational_modes | Array op_mode_type | default = [],
consumption_modes | Array consumption_mode_type,
@ -237,6 +314,14 @@ let manifest_type = {
# consumers read each section, and mutation rules. Optional — projects
# without a structured config system omit this field.
config_surface | config_surface_type | optional,
# What the project does, why it was built, how each capability works,
# and which artifacts/ADRs/nodes it manifests.
capabilities | Array capability_type | default = [],
# Prerequisites for production and/or development: tools, services,
# env vars, infrastructure. Superset of the legacy `tools` field.
requirements | Array requirement_type | default = [],
# External dependencies with documented failure blast radius.
critical_deps | Array critical_dep_type | default = [],
} in
{
@ -261,4 +346,9 @@ let manifest_type = {
ConfigConsumer = config_consumer_type,
ConfigSection = config_section_type,
ConfigSurface = config_surface_type,
EnvTarget = env_target_type,
RequirementKind = requirement_kind_type,
Capability = capability_type,
Requirement = requirement_type,
CriticalDep = critical_dep_type,
}

View File

@ -105,6 +105,8 @@ export def "describe capabilities" [
let claude = (scan-claude-capabilities $root)
let ci_tools = (scan-ci-tools $root)
let manifest_modes = (scan-manifest-modes $root)
let manifest = (load-manifest-safe $root)
let manifest_capabilities = ($manifest.capabilities? | default [])
let data = {
just_modules: $just_modules,
@ -113,11 +115,93 @@ export def "describe capabilities" [
claude_capabilities: $claude,
ci_tools: $ci_tools,
manifest_modes: $manifest_modes,
manifest_capabilities: $manifest_capabilities,
}
emit-output $data $f { || render-capabilities-text $data $a $root }
}
# ── describe requirements ────────────────────────────────────────────────────────
# "What does this project need to run? What are the prod/dev prerequisites?"
export def "describe requirements" [
--fmt: string = "",
--actor: string = "",
--environment: string = "", # filter by environment: production | development | both
]: nothing -> nothing {
let root = (project-root)
let a = if ($actor | is-not-empty) { $actor } else { (actor-default) }
let f = if ($fmt | is-not-empty) { $fmt } else if $a == "agent" { "json" } else { "text" }
let manifest = (load-manifest-safe $root)
let all_reqs = ($manifest.requirements? | default [])
let critical = ($manifest.critical_deps? | default [])
let requirements = if ($environment | is-not-empty) {
$all_reqs | where { |r| ($r.env? | default "Both") == ($environment | str capitalize) }
} else {
$all_reqs
}
let data = {
requirements: $requirements,
critical_deps: $critical,
}
emit-output $data $f {||
print ""
print "REQUIREMENTS"
print "══════════════════════════════════════════════════════════════════"
if ($requirements | is-not-empty) {
let prod = ($requirements | where { |r| ($r.env? | default "Both") in ["Production", "Both"] })
let dev = ($requirements | where { |r| ($r.env? | default "Both") in ["Development", "Both"] })
if ($prod | is-not-empty) {
print ""
print "PRODUCTION"
print "──────────────────────────────────────────────────────────────────"
for r in $prod {
let req_flag = if ($r.required? | default true) { "(required)" } else { "(optional)" }
let ver = if ($r.version? | default "" | is-not-empty) { $" >= ($r.version)" } else { "" }
print $" ($r.name)($ver) [($r.kind? | default '')] ($req_flag)"
if ($r.impact? | default "" | is-not-empty) { print $" Impact: ($r.impact)" }
if ($r.provision? | default "" | is-not-empty) { print $" Provision: ($r.provision)" }
}
}
if ($dev | is-not-empty) {
print ""
print "DEVELOPMENT"
print "──────────────────────────────────────────────────────────────────"
for r in $dev {
let req_flag = if ($r.required? | default true) { "(required)" } else { "(optional)" }
let ver = if ($r.version? | default "" | is-not-empty) { $" >= ($r.version)" } else { "" }
print $" ($r.name)($ver) [($r.kind? | default '')] ($req_flag)"
if ($r.impact? | default "" | is-not-empty) { print $" Impact: ($r.impact)" }
if ($r.provision? | default "" | is-not-empty) { print $" Provision: ($r.provision)" }
}
}
} else {
print " (no requirements declared in manifest)"
}
if ($critical | is-not-empty) {
print ""
print "CRITICAL DEPENDENCIES"
print "──────────────────────────────────────────────────────────────────"
for d in $critical {
print $" ($d.name) [($d.ref? | default '')]"
print $" Used for: ($d.used_for? | default '')"
print $" Failure: ($d.failure_impact? | default '')"
if ($d.mitigation? | default "" | is-not-empty) {
print $" Mitigation: ($d.mitigation)"
}
}
}
print ""
}
}
# ── describe constraints ────────────────────────────────────────────────────────
# "What can I NOT do? What are the Hard rules?"
@ -1083,8 +1167,11 @@ export def "describe guides" [
let constraints = (collect-constraint-summary $root)
let actor_policy = (derive-actor-policy $gates $a)
let content_assets = ($manifest.content_assets? | default [])
let templates = ($manifest.templates? | default [])
let content_assets = ($manifest.content_assets? | default [])
let templates = ($manifest.templates? | default [])
let manifest_capabilities = ($manifest.capabilities? | default [])
let manifest_requirements = ($manifest.requirements? | default [])
let manifest_critical_deps = ($manifest.critical_deps? | default [])
# Fetch API surface from daemon; empty list if daemon is not reachable.
let daemon_url = ($env.ONTOREF_DAEMON_URL? | default "http://127.0.0.1:7891")
@ -1114,10 +1201,13 @@ export def "describe guides" [
available_modes: $modes,
actor_policy: $actor_policy,
language_guides: $claude,
content_assets: $content_assets,
templates: $templates,
connections: $conns,
api_surface: $api_surface,
content_assets: $content_assets,
templates: $templates,
connections: $conns,
api_surface: $api_surface,
capabilities: $manifest_capabilities,
requirements: $manifest_requirements,
critical_deps: $manifest_critical_deps,
}
emit-output $data $f {||
@ -1389,7 +1479,7 @@ def collect-identity [root: string]: nothing -> record {
}
let manifest = (load-manifest-safe $root)
let kind = if ($manifest | is-not-empty) { $manifest.kind? | default "" } else { "" }
let kind = if ($manifest | is-not-empty) { $manifest.repo_kind? | default "" } else { "" }
let description = if ($manifest | is-not-empty) { $manifest.description? | default "" } else { "" }
{
@ -2089,6 +2179,18 @@ def render-capabilities-text [data: record, actor: string, root: string]: nothin
}
}
if ($data.manifest_capabilities? | default [] | is-not-empty) {
print ""
print "PROJECT CAPABILITIES (manifest)"
print "──────────────────────────────────────────────────────────────────"
for c in ($data.manifest_capabilities? | default []) {
print $" ($c.name): ($c.summary)"
if ($c.artifacts? | default [] | is-not-empty) {
print $" Artifacts: ($c.artifacts | str join ', ')"
}
}
}
print ""
}