ontoref/adrs/adr-009-manifest-self-interrogation-layer-three-semantic-axes.ncl

86 lines
8.9 KiB
Plaintext
Raw Normal View History

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,
},
}