Three-Layer Model for Project Ontoref Instances — the Export Axis Declared on manifest.layers

Accepted

ontoref
Multiple projects now adopt the ontoref protocol (lian-build is the explicit

Context

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).

WHY IT SAT IN 'Proposed FOR TWO MONTHS, AND WHAT UNSTUCK IT (2026-07-13).

The gate was never a judgement call. The constraints could not run clean because the executor that runs them was broken (ADR-070 / migration 0044: check paths resolved against the caller's cwd, and `.ontoref/`-prefixed paths came out double-prefixed and missed). With the executor anchored, the four Layer 1 checks run clean on ontoref, lian-build and provisioning as written. The Layer 1 half of this ADR was correct and unenforced, which is the worst state a constraint can be in — it looks satisfied because nothing evaluates it.

The Layer 2 half was wrong, and the question that surfaced it was a user's: "ontoref carries ADRs and modes about its own site, doors, campaigns and CV — what use are those to a project that merely adopts ontoref?" The empirical answer is: none, and they do not travel — provisioning's spine carries 44 ADRs of its own and not one of ours. But the question exposed that the protocol had no TYPED answer to "what of me is yours". It had a paragraph in CLAUDE.md (the migration-trigger table) doing the job of a `pub` keyword.

Two defects followed from that:

1. This ADR located Layer 2 by SNIFFING A PATH (`catalog/domains/`), which is provisioning's spelling generalised too early. ontoref publishes its domain artifacts from `code/domains/` (ADR-012 domain extensions, and `code/` only because ADR-052 split the constellation). lian-build spells it differently again. A protocol check that hard-codes one member's layout is the very confusion this ADR exists to end. Worse, patching the check to also look in `code/domains/` would have BAKED ONTOREF'S CONSTELLATION SHAPE INTO A PROTOCOL CONSTRAINT.

2. The mechanism already existed and had rotted unseen. `manifest.ncl::layers` has always declared "a named region of the repo with visibility rules per mode", and ontoref's own manifest already named a `protocol` layer ("the contract layer that projects implement") beside a `self-description` layer ("ontoref consuming ontoref"). That IS Layer 2 beside Layer 1. Nobody noticed, because nothing checked it — its paths still pointed at the pre-ADR-032 layout (`ontology/`, `.ontology/`), naming directories that had not existed for months. A third meaning of the word "layer" sat in the schema while this ADR worried only about colliding with "level" (ADR-018).

So Layer 2 is not discovered, it is DECLARED — on the mechanism that was already there. The layer taxonomy stops being prose about where files sit and becomes the export axis of an ontoref instance: the `pub` the user asked for, checkable.

Decision

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 DECLARED, never sniffed — the paths of any manifest layer whose kind = 'IntegrationSurface, plus 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 declares an 'IntegrationSurface layer if and only if it declares registry_provides. 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.

THE MECHANISM — manifest.ncl::layers CARRIES THE AXIS

`layer_type` gains one field, `kind`:

'SelfManagement Layer 1. Never travels. 'IntegrationSurface Layer 2. What a consumer binds to. 'Internal Neither — reached through a released binary or artifact, never imported as source. The DEFAULT, so a manifest that never mentions `kind` keeps validating unchanged.

ADDITIVE, BUT ORDERED — and the ordering is not optional. Nickel record contracts are CLOSED: a manifest that declares `kind` against a data-dir schema predating the field fails outright with `extra field `kind``. So the field costs nothing to IGNORE and requires a reinstall to USE. Reinstall precedes tagging; migration 0046 says so first, and it says so because the first attempt at tagging the two consumer projects failed exactly this way. ontoref itself did not catch it, because ontoref's manifest imports the schema SOURCE by relative path (`../../code/ontology/defaults/manifest.ncl`) while every consumer imports the INSTALLED copy — the self-hosting project is the one project that cannot feel this class of break. Worth remembering the next time an "additive" schema change is asserted rather than tested on a consumer.

Layer 3 has no `kind` BY CONSTRUCTION: it has no region in this repo. A project that finds itself wanting to tag one has put caller cabling in its own tree.

The consequence is that "what of me is yours" stops being prose. It is a query:

the union of the paths of every layer with kind = 'IntegrationSurface

and everything else — the site, the doors, the campaigns, the CV modes, the constellation instance — is 'SelfManagement or 'Internal, and is nobody's business but this project's. That is the whole content of the answer to "of what use are ontoref's own site ADRs to provisioning?": none, and the manifest now says so in a form a validator can read.

THE RULING ON `constellation` — THE CASE THAT DECIDED THE AXIS

`constellation` was the test case, because it is the one artifact of ontoref's that has demonstrably reached another project. It is not one thing but three, and they split exactly along the layers:

reflection/schemas/constellation.ncl → LAYER 2. Protocol. The taxonomy ('Primary/'Addon/'Projection), the forge/mirror/sovereign vocabularies, and the cross-field rule that a 'Projection owns no repo and a 'Never member carries no mirror. These are generic. The proof is empirical, not argued: provisioning BOUND to this schema, EXTENDED it (a `distribution`/'OCI axis and a `workspaces` block) and ADR'd it (its own adr-052). A second project binding and extending an artifact is what an integration surface IS. It stays.

ontology/constellation.ncl → LAYER 1. Self. WHICH members ontoref has (code, outreach, vault, desktop) and where they push. Per project. Provisioning has its own, listing its own.

ontoref's forge POLICY → LAYER 1 SITTING INSIDE LAYER 2. "Forgejo always canonical · GitHub always a one-way mirror · vault never mirrored" is ontoref's own constellation policy, and it was written into the comments of the shared schema and into the error message of its contract. This is the ONLY ontoref-self content that has ever reached a consumer — and it did: provisioning's copy repeats the `vault` rule although vault is not a member of provisioning. It comes out of the schema.

The schema is protocol. What leaves it is the policy, not the contract. And note HOW it travelled: provisioning did not import the schema, it COPIED it — 118 lines here, 164 there, the comments already diverged. That is the drift this ADR predicted in its own context, arriving on schedule, and it is now an anti-pattern with a name (`layer-2-adopted-by-copy`).

A type cannot stop a comment from lying. That is precisely why the export axis has to be a FIELD and not a paragraph.

Constraints

  • Hard Every ontoref-onboarded project has .ontoref/ontology/core.ncl
  • Hard Every ontoref-onboarded project has .ontoref/reflection/qa.ncl
  • Hard Every ontoref-onboarded project has .ontoref/reflection/backlog.ncl
  • Hard Every ontoref-onboarded project has an .ontoref/adrs/ directory
  • Hard A manifest layer with kind = 'IntegrationSurface is declared if and only if the manifest declares registry_provides — and every path that layer names resolves from the project root
  • Soft qa entries whose primary topic is a Layer N concern carry the layer-N tag (anchors only; satellites inherit by association)

Alternatives considered

  • Keep sniffing a path, and teach the check about code/domains/ as well as catalog/domains/rejected: The cheap fix, and it inverts the ADR. `code/domains/` is where ontoref's domains sit BECAUSE ADR-052 split the constellation — a fact about one member's repo layout. Teaching a protocol constraint to look there bakes ontoref's own shape into a check every consumer runs, which is precisely the contamination this ADR exists to prevent, committed by the ADR itself. It also fails on contact with the next adopter who spells it a third way, and it can never verify that the surface resolves. Rejected as anti-PAP: the self-hosting project must not privilege its own layout in the protocol.
  • A new top-level `exports` block in the manifest, separate from layersrejected: A fourth region-of-the-repo concept beside `layers`, `levels` and this ADR's own layers. manifest.layers ALREADY means 'a named region of the repo with visibility rules per mode' and already carried `protocol` beside `self-description` — the axis existed and had only rotted for want of a reader. Adding a parallel block would leave two declarations of the same regions, free to disagree, which is the ADR-070 pathology (declared twice, no witness) reproduced one layer down. One typed field on the existing mechanism, not a second mechanism.
  • Single 'ontoref content' namespace, no layeringrejected: 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.
  • Two-layer model collapsing self-management with integration-surfacerejected: 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.
  • Make Layer 3 (caller-side) optionally co-resident with Layer 2 in the producer's reporejected: 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.
  • Numbered layers (Layer 1 / 2 / 3) vs named layers ('self' / 'integration-surface' / 'caller-side')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.

Anti-patterns

  • One Project's Own Policy Written Into the Contract Everyone Binds To — A Layer 2 artifact — a schema, a template, a default — carries the authoring project's Layer 1 policy in its comments, its error strings or its examples. The contract is generic; the prose around it is not. Consumers inherit a rule about a thing they do not have, and cannot tell which half was meant for them. Measured, not hypothetical: reflection/schemas/constellation.ncl carried ontoref's forge policy ('vault never mirrored'), and provisioning's copy repeats the vault rule although vault is not one of its members. A type cannot stop a comment from lying — which is the whole argument for making the export axis a field.
  • Binding to an Integration Surface by Forking It — A consumer adopts a Layer 2 artifact by copying the file into its own tree and editing it, instead of importing it and extending it. The copy is a fork with no upstream: it drifts silently, and the drift is invisible precisely because both sides still look like the same file. Observed: provisioning's constellation.ncl is 164 lines against ontoref's 118, the comments already divergent, with no mechanism that could ever report the gap. This is the drift this ADR predicted in its own context, arriving on schedule.
  • The Protocol Surface Defined by a Paragraph Nothing Checks — What a protocol exports to its consumers is recorded in documentation — a migration-trigger table in CLAUDE.md, a README section, a convention — rather than in the data. It works exactly as long as everyone remembers, and it fails silently: the project's own site ADRs, campaign modes and CV workflows accumulate in the same directories as the protocol's schemas and adoption modes, with nothing able to state which is which. The self-hosting project is most exposed, because it is the one whose own concerns share a spine with the contract.

Related ADRs

ADR-001 · ADR-012 · ADR-016 · ADR-018 · ADR-032 · ADR-052 · ADR-062 · ADR-070

Was this useful? Rate it
Got something to add? Tell me what you think, what you'd suggest, or whether we should keep exploring this topic.
· reads

We use cookies to help this site function, understand service usage, and support marketing efforts. Cookie Policy for more info.