ontoref/adrs/adr-020-three-layer-ontoref-instance-model.ncl

222 lines
15 KiB
Text
Raw Normal View History

feat: #[onto_mcp_tool] catalog, OCI credential vault layer, validate ADR-018 mode hierarchy ontoref-derive: #[onto_mcp_tool] attribute macro registers MCP tool unit-structs in the catalog at link time via inventory::submit!; annotated item is emitted unchanged, ToolBase/AsyncTool impls stay on the struct. All 34 tools migrated from manual wiring (net +5: ontoref_list_projects, ontoref_search, ontoref_describe, ontoref_list_ontology_extensions, ontoref_get_ontology_extension). validate modes (ADR-018): reads level_hierarchy from workflow.ncl and checks every .ncl mode for level declared, strategy declared, delegate chain coherent, compose extends valid. mode resolve <id> shows which hierarchy level handles a mode and why. --self-test generates synthetic fixtures in a temp dir for CI smoke-testing. validate run-cargo: two-step Cargo.toml resolution — workspace layout first (crates/<check.crate>/Cargo.toml), single-crate fallback by package name or repo basename. Lets the same ADR constraint shape apply to workspace and single-crate repos. ontology/schemas/manifest.ncl: registry_topology_type contract — multi-registry coordination, push targets, participant scopes, per-namespace capability. reflection/requirements/base.ncl: oras ≥1.2.0, cosign ≥2.0.0, sops ≥3.9.0, age ≥1.1.0, restic declared as Hard/Soft requirements with version_min, check_cmd, and install_hint (ADR-017 toolchain surface). ADR-019: per-file recipient routing for tenant isolation without multi-vault. Schema additions: sops.recipient_groups + sops.recipient_rules in ontoref-project.ncl. secrets-bootstrap generates .sops.yaml from project.ncl in declarative mode. Three new secrets-audit checks: recipient-routing-coherent, recipient-routing-coverage, no-multi-vault. Adoption templates: single-team/, multi-tenant/, agent-first/. Integration templates: domain-producer/, mode-producer/, mode-consumer/. UI: project_picker surfaces registry badge (⟳ participant) and vault badge (⛁ vault_id · N, green=declarative / amber=legacy) per project card. Expanded panel adds collapsible Registry section with namespace, endpoint, and push/pull capability. manage.html gains Runtime Services card — MCP and GraphQL toggleable without restart via HTMX POST /ui/manage/services/{service}/toggle. describe.nu: capabilities JSON includes registry_topology and vault_state per project. sync.nu: drift check extended to detect //! absence on newly registered crates. qa.ncl: six entries — credential-vault-best-practice (layered data-flow diagram), credential-vault-templates (paths A/B/C), credential-vault-troubleshooting (15 named errors), integration-what-and-why (ADR-042 OCI federation), integration-how-to-implement, integration-troubleshooting. on+re: core.ncl + manifest.ncl updated to reflect OCI, MCP, and mode-hierarchy nodes. Deleted stale presentation assets (2026-02 slides + voice notes).
2026-05-12 04:46:15 +01:00
let d = import "defaults.ncl" in
d.make_adr {
id = "adr-020",
title = "Three-Layer Model for Project Ontoref Instances",
status = 'Proposed,
date = "2026-05-03",
context = m%"
Multiple projects now adopt the ontoref protocol (lian-build is the explicit
external case under bl-002 / bl-008). Field experience across ontoref +
lian-build + provisioning shows that an ontoref-onboarded project's
repository carries TWO distinct ontoref-shaped layers, while a third layer
exists outside the project (in caller repositories). Without codification,
adopters re-derive the model per project, mix layers accidentally, and lose
boundaries silently:
- Layer 3 content (caller-side cabling) drifts into a project's qa.ncl,
making the FAQ a how-to-deploy guide for one specific caller.
- Layer 2 schemas live under reflection/ as 'project notes', drifting
from the binary because they're not on the contract path.
- Layer 1 architectural rationale ends up in catalog/domains/<id>/
contract.ncl, where consumers expecting a typed shape get prose.
The model has been observed (lian-build/reflection/qa.ncl::lian-build-what-
and-why), described (ontoref/reflection/qa.ncl::ontoref-three-layer-model),
and queued for codification (bl-009). This ADR commits the model with
machine-checkable constraints. Acceptance is gated on the constraints
running clean across the existing ontoref-onboarded projects (ontoref
itself, lian-build, provisioning).
"%,
decision = m%"
A project's ontoref instance has THREE distinct layers — but only the first
two live in the project's own repository:
LAYER 1 — Self-management ontoref (about the project itself)
paths .ontology/ reflection/ adrs/
audience this project's developers and maintainers
purpose describe the project to itself — axioms, FSM dimensions,
binding decisions, open questions, accepted knowledge
presence MANDATORY on every ontoref-onboarded project
LAYER 2 — Specialized domain/mode ontoref (the integration surface)
paths schemas/ catalog/{domains,modes}/
manifest.ncl::registry_provides
audience OTHER projects that want to integrate this project
purpose the contract surface other projects bind to — typed domain
artifacts, orchestration mode artifacts, registry-namespace
claim
presence OPTIONAL but BICONDITIONAL — a project either has all of
{schemas/, catalog/, registry_provides} or none. Half-Layer-2
is a contract violation.
LAYER 3 — Caller-side implementations (NOT in this project)
paths <caller>/extensions/<this-project>/
<caller>/catalog/components/<...>/ (when consuming)
<workspace>/infra/<ws>/integrations/
audience operators and CI of caller projects
presence PER CALLER, NEVER in this project's repo. Cross-references
from this project to Layer 3 are explicit pointers, never
copy-paste.
The three-layer axis is ORTHOGONAL to ADR-018's level hierarchy
(Base/Domain/Instance). A project at any level may have any combination of
Layer 1 (always) and Layer 2 (sometimes). Layer 3 is the boundary outward,
not a property of the project. The 3-layer × 3-level matrix is navigable in
both axes: 'where in the protocol hierarchy' (level) is independent of
'where in the project's repo' (layer).
Cross-layer references inside a project carry an explicit layer-N tag on
qa entries. A qa entry whose primary topic is a Layer N concern wears
the layer-N tag; satellite entries (operational how-to, troubleshooting)
inherit the layer of their anchor without re-tagging.
"%,
rationale = [
{
claim = "Audience separation requires lifecycle separation",
detail = "Layer 1 evolves with the project's decisions (ADR cadence). Layer 2 evolves with the binary (schemas in lock-step with code that produces and consumes them). Layer 3 evolves with caller infrastructure (workspace cabling, deployment changes). Three lifecycles in one namespace produces drift in two of them — observed in pre-codification cases where schemas drifted because they were treated as project notes.",
},
{
claim = "Layer 2 biconditional is what makes a federated peer detectable",
detail = "If catalog/ exists without manifest.ncl::registry_provides, the project has artifacts but no namespace claim — consumers can't resolve where to pull from. If registry_provides exists without catalog/, the project claims a namespace it doesn't fill. Either half-state breaks integration. Treating the pair as biconditional makes 'is this a federated peer?' a single boolean check.",
},
{
claim = "Layer 3 isolation is project-specific in spelling, universal in principle",
detail = "What counts as caller-specific differs per project (lian-build forbids `provisioning_workspace|vapora_|woodpecker_` per its adr-001; another project would forbid different patterns). The protocol-level constraint cannot enumerate; instead, each project carries its own constraint-bearing ADR and the protocol verifies presence of SUCH a constraint indirectly via cross-layer-tag-discipline. Project-specific spelling, ecosystem-wide presence.",
},
{
claim = "Cross-layer tag discipline makes filtering scalable",
detail = "An ontoref instance accumulates dozens to hundreds of qa entries over its life. 'Filter the qa for what other projects can integrate' (Layer 2) becomes a tag query rather than a full-text search. Tagging only anchors keeps the result set small and authoritative; satellites don't dilute the filter.",
},
{
claim = "Orthogonality with ADR-018 lets the model expand without conflict",
detail = "ADR-018's level hierarchy describes WHERE a project sits in the protocol specialization (Base = ontoref itself; Domain = a project-type domain; Instance = a concrete workspace). The three-layer model describes WHERE inside a project's repo content sits (self / integration-surface / caller-side). Different axes; no terminology collision (level vs layer); a 3x3 matrix populated empirically. Treating them as orthogonal avoids re-litigating ADR-018 every time the layer model evolves.",
},
],
consequences = {
positive = [
"ore describe project can statically answer 'is this a federated peer?' (Layer 2 biconditional) and 'is the project minimally onboarded?' (Layer 1 mandatory)",
"Adopters discover the model from documentation (qa::ontoref-three-layer-model) plus this ADR, not by osmosis from existing projects",
"qa-tag-based filtering (layer-1, layer-2) becomes a navigable surface as ecosystem corpus grows",
"The 3-layer × 3-level matrix is explicit, removing a recurring source of design ambiguity (e.g. 'should this go in .ontology or in catalog?')",
"ontoref setup gains a clear acceptance criterion: it produces a Layer 1-compliant scaffold by construction",
],
negative = [
"Existing projects must audit their structure for Layer 1 compliance — typically reflection/qa.ncl and reflection/backlog.ncl are missing on early adopters",
"Six new constraints add per-project CI overhead (4x FileExists, 2x NuCmd) — running cheap but additive across the ecosystem",
"Terminology discipline required: 'layer' (this ADR) vs 'level' (ADR-018) are different concepts and prose must not collapse them",
"Layer 2 biconditional rejects half-states that some projects might want as an interim — projects mid-Layer-2-rollout must either complete or not start until ready",
],
},
alternatives_considered = [
{
option = "Single 'ontoref content' namespace, no layering",
why_rejected = "Observed drift outcomes (Layer 3 in qa, Layer 2 schemas treated as notes, etc.) are caused precisely by absence of layering. The single-namespace alternative is what we're correcting; rejecting it is the entire decision.",
},
{
option = "Two-layer model collapsing self-management with integration-surface",
why_rejected = "lian-build's adoption explicitly experienced the schema-vs-rationale split: schemas/build_directives.ncl (Layer 2 contract) and adrs/adr-001-lian-build-as-standalone.ncl (Layer 1 rationale) have different audiences, different change cadences, different validation rules. Collapsing them produced the FAQ-vs-contract confusion that retiring lian-build/.ontology/FAQ.md addressed. The two-layer alternative re-creates that confusion.",
},
{
option = "Make Layer 3 (caller-side) optionally co-resident with Layer 2 in the producer's repo",
why_rejected = "Creates ambiguity about who owns the cabling. If a producer's repo carries `extensions/<self>/`, who maintains it when a caller's workspace evolves? The producer doesn't know about the caller's infrastructure; the caller can't depend on the producer to update the cabling on its schedule. Co-residence inverts the integration arrow.",
},
{
option = "Numbered layers (Layer 1 / 2 / 3) vs named layers ('self' / 'integration-surface' / 'caller-side')",
why_rejected = "Named layers are more descriptive but verbose; numbered layers are more precise but flat. The compromise (this ADR): use 'Layer N — descriptive name' in prose, 'layer-N' in tags. Tag economy wins; prose retains the descriptive name.",
},
],
constraints = [
{
id = "layer-1-self-ontology-core",
claim = "Every ontoref-onboarded project has .ontology/core.ncl",
scope = ".ontology/",
severity = 'Hard,
check = {
tag = 'FileExists,
path = ".ontology/core.ncl",
present = true,
},
rationale = "Layer 1's irreducible minimum: a project with no axioms, tensions, or practices has no ontoref instance. Overlaps with adr-016's lift-out check by intention — same observable, two architectural decisions resting on it.",
},
{
id = "layer-1-reflection-qa",
claim = "Every ontoref-onboarded project has reflection/qa.ncl",
scope = "reflection/",
severity = 'Hard,
check = {
tag = 'FileExists,
path = "reflection/qa.ncl",
present = true,
},
rationale = "Layer 1's accepted-knowledge surface. A project without qa.ncl cannot accumulate verified Q&A — adopters re-derive answers each time. ontoref setup must scaffold this; existing projects backfill.",
},
{
id = "layer-1-reflection-backlog",
claim = "Every ontoref-onboarded project has reflection/backlog.ncl",
scope = "reflection/",
severity = 'Hard,
check = {
tag = 'FileExists,
path = "reflection/backlog.ncl",
present = true,
},
rationale = "Layer 1's open-question surface. Projects without backlog.ncl bury unresolved alternatives in chat threads, defeating the protocol's routing mechanism (graduates_to). Required so every undecided question has a place to land.",
},
{
id = "layer-1-adrs-directory",
claim = "Every ontoref-onboarded project has an adrs/ directory",
scope = "adrs/",
severity = 'Hard,
check = {
tag = 'FileExists,
path = "adrs/",
present = true,
},
rationale = "Layer 1's binding-decisions slot. The directory may be empty for a brand-new project, but its presence is what makes 'where do ADRs go?' a settled question.",
},
{
id = "layer-2-biconditional",
claim = "A federated-peer catalog (catalog/domains/ or catalog/modes/) exists if and only if manifest.ncl declares registry_provides",
scope = "catalog/domains/, catalog/modes/, manifest.ncl, .ontology/manifest.ncl",
severity = 'Hard,
check = {
tag = 'NuCmd,
cmd = "let fed = (('catalog/domains' | path exists) or ('catalog/modes' | path exists)); let m_root = (try { open manifest.ncl | str contains 'registry_provides' } catch { false }); let m_onto = (try { open .ontology/manifest.ncl | str contains 'registry_provides' } catch { false }); let prv = ($m_root or $m_onto); if $fed == $prv { 0 } else { error make {msg: $'Layer 2 biconditional violated: federated_catalog=($fed) registry_provides=($prv) — half-states advertise integration the consumer cannot complete'} }",
expect_exit = 0,
},
rationale = "Federated-peer status is detectable by a single biconditional. The marker is catalog/domains/ or catalog/modes/ specifically — not catalog/ as a whole, because some projects (provisioning) carry catalog/components/, catalog/providers/, etc. that serve workspace composition rather than federated artifact publication. The manifest may live at root or under .ontology/ depending on project convention; bl-010 (forthcoming) tracks canonicalization of that path. Half-states (one side without the other) are the violation.",
},
{
id = "cross-layer-tag-discipline",
claim = "qa entries whose primary topic is a Layer N concern carry the layer-N tag (anchors only; satellites inherit by association)",
scope = "reflection/qa.ncl",
severity = 'Soft,
check = {
tag = 'NuCmd,
cmd = "let entries = (nickel export reflection/qa.ncl --format json | from json | get entries); let suspicious = ($entries | where {|e| (($e.answer | str contains 'LAYER 1') or ($e.answer | str contains 'LAYER 2')) and ($e.tags | where {|t| ($t | str starts-with 'layer-')} | is-empty) and ($e.id | str contains 'what-and-why')}); if ($suspicious | is-empty) { 0 } else { error make {msg: $'Anchor entries discussing layers must carry layer-N tag: ($suspicious | get id | str join \", \")'} }",
expect_exit = 0,
},
rationale = "Tag-based filtering scales as the corpus grows. Without discipline, querying for 'Layer 2 concerns' returns either too few entries (false negatives because anchors aren't tagged) or too many (false positives because every entry that mentions a layer gets tagged). Anchors-only is the sustainable middle path; this constraint enforces it for the most common drift case.",
},
],
related_adrs = [
"adr-001-protocol-as-standalone-project",
"adr-016-component-lift-out-pattern",
"adr-018-level-hierarchy-mode-resolution-strategy",
],
ontology_check = {
decision_string = "An ontoref-onboarded project's repository carries Layer 1 (mandatory self-management) and optionally Layer 2 (integration surface, biconditional with registry_provides). Layer 3 lives in caller projects, not in the producer. The three-layer axis is orthogonal to ADR-018's level hierarchy.",
invariants_at_risk = [],
verdict = 'Safe,
},
}