A consumer's reflection schema is INHERITED from the protocol, never copied — the mode contract a project types against is the shipped one, and a copy is a version nobody can state
Accepted
Context
MEASURED 2026-09-02, while expressing a verified plan as a Mode in a consumer spine (jpl/personal, whose reflection layer re-exports personal-ontoref).
· The mode used three fields the base schema documents — `scope` (adr-076), `vars_required` (governed-delivery T1), `postchecks` (T4) — and failed to export: «contract broken by a value: extra fields `scope`, `vars_required`, `postchecks`», raised at personal-ontoref/.ontoref/reflection/schema.ncl:44.
· That file is 82 lines. The base `.ontoref/reflection/schema.ncl` is 193. A field diff (`comm -23` over `^\s{2}[a-z_]+\s+\|`) lists what the consumer lacks: `converge`, `extends`, `max_iterations`, `postchecks`, `scope`, `vars_required`, `guard.needs`, `guard.on_unknown`, `postcheck.desc`. Nothing the consumer has is missing from the base.
· It is not one consumer. stratumiops, rustelo and provisioning/stratumiops carry 78-line copies; website-htmx-rustelo 134. Only the ontoref checkouts carry 193. The copies are dated by content: each froze on the day its project adopted the protocol.
· `ontoref migrate list` reported personal-ontoref at 67/67 applied and jpl/personal at 59/59. No migration in `reflection/migrations/` reads `reflection/schema.ncl`; the eight that mention a schema address `reflection/schemas/*.ncl` (interaction, bundle, cell). `update_ontoref` validates `manifest.ncl` and `connections.ncl` against `{ontoref_dir}` and never the reflection schema. `adopt_ontoref` does not copy it either — the copies predate the consolidated layout and were carried across by migration 0023.
· HOW A MODE IS EVALUATED. Every mode file opens with `let d = import "../defaults.ncl"`, and `defaults.ncl` opens with `let s = import "schema.ncl"` — both relative, both resolving inside the consumer's own tree before any import path is consulted. The exporter (`store.nu::daemon-export-subprocess`, `run.nu::load-mode-dag`) does set NICKEL_IMPORT_PATH from `nickel-import-path`, and that list already ends with the tool root. The path to the shipped schema was open the whole time; the consumer's local file answered first because relative resolution precedes it, and the local file was a copy.
· THE PRECEDENT IS IN THE TREE. Migration 0066 / adr-092 made a biop resolve through two rungs — `reflection/biop/` shipped and inherited, `.ontoref/biop/` a local overlay that wins per id — stated as «a copy is what witness-not-clone refuses». The schema every consumer's modes are typed against had no rung: it was the anti-pattern adr-074 names, a carrier holding its own copy of a governed fact, and adr-074's own reason applies — the copy was true the day it was made and nobody was in a position to notice when it stopped being true.
Decision
THE REFLECTION SCHEMA A CONSUMER TYPES ITS MODES AGAINST IS THE SHIPPED ONE.
(1) THE CONSUMER FILE BECOMES A SHIM. `.ontoref/reflection/schema.ncl` in a consumer is `let base = import "reflection/protocol-schema.ncl" in base`. The shipped tree gains `reflection/protocol-schema.ncl`, a one-line re-export of `schema.ncl` under a name NO consumer carries. The name is load-bearing and was measured, not chosen: the first shim imported "reflection/schema.ncl", and because `nickel-import-path` lists the consumer's `.ontoref` root before the tool root, it resolved to ITSELF — «infinite recursion … recursive reference» on personal-ontoref, 2026-09-02. A name that exists only in the shipped tree cannot resolve to a consumer file unless the consumer copies it, and a copy is what the migration reports. Verified on a fixture: the shim exports the base with a local field merged in; with no import path it fails «could not find import».
(2) A DOMAIN EXTENDS BY MERGING, NEVER BY COPYING. `base & { ... }` adds or replaces exported contracts. This is what «personal-ontoref includes the whole base and overrides what it needs as a domain» means mechanically; today it overrides nothing, which is the honest state and is written as such.
(3) `defaults.ncl` AND `constraints.ncl` STAY LOCAL. They are the tissue: the factory a mode calls and the project's own step constraints. They import `schema.ncl` relatively and now reach the shim. Mode files do not change at all.
(4) THE CHECK IS THE MIGRATION, AND THE MIGRATION IS ADVISORY. Migration 0095 goes red on any consumer whose `reflection/schema.ncl` neither imports the shipped schema nor matches it byte for byte, and prints the fields the copy lacks. A byte-identical copy passes today and fails the day the base moves — which is when the difference between a copy and an import first costs something, and is therefore when it should be reported. `update_ontoref` gains a detection step with the same predicate. No migration rewrites the file: a project that extended its copy by hand has to merge, and that is a decision, not a sed.
(5) NOT DECIDED HERE. Whether `defaults.ncl` should also ship as a shim (its drift is a missing `Converge` re-export no mode uses). It waits for a measured case.
ENGAGED TENSION: formalization-vs-adoption — the one the backlog already names for consumers that cannot state the protocol version they stand at (bl-1081's measurement of 19 spines with no origin). The protocol side moved; the adoption side stood still and reported itself current. The decision holds both poles: the protocol keeps moving, and a consumer is NOT declared non-conformant for lacking a field nobody asked it to carry — it is handed a single-line shim and a check that tells it, in field names, what it has been missing. The forbidden collapses are the two obvious ones: freezing the base schema so the copies stay true, or bulk-rewriting every consumer's file from ontoref's side. Direction of motion: from replica to inheritance, the road migration 0066 already walked for biops.
Constraints
- Hard A consumer schema written as the one-line shim exports a mode that uses the newest base fields, and rejects a field the base does not name — the contract reached through the shim is the shipped one, closed.
- Hard Migration 0095 is present and reads `reflection/schema.ncl` — the file every mode depends on is no longer outside the migration chain.
Alternatives considered
- Keep copying; add a migration that refreshes every consumer's copy from the base. — rejected: It preserves the replica adr-074 forbids and merely shortens the drift window; every hand extension would be overwritten; and it needs a release step per schema change. The biop work already measured this shape and refused it (0066: «the only way to obtain one would have been to copy the file»).
- Have `nickel-import-path` list the consumer's own `.ontoref/reflection/` so nothing needs to change in consumer files. — rejected: It changes nothing: the local file still answers first by relative resolution, and listing the consumer's reflection dir would make the shim import ITSELF. The consumer file has to stop being a copy; no import-path order makes a copy inherit.
- Import the shipped file under its own name, `reflection/schema.ncl`, and keep the tree as it is. — rejected: Tried first and measured: the consumer's `.ontoref` root precedes the tool root on the import path, so the shim resolved to itself and nickel reported infinite recursion. The dedicated name is not a second copy of the schema — it is a one-line re-export — so the «second thing to keep in step» objection does not apply.
- Rewrite consumer files from ontoref's side in one pass. — rejected: The bulk-rewrite collapse of the tension. It would erase any deliberate extension and it would make ontoref the author of files in trees it does not own; adr-066's executor stance is that the check decides, and here the check is enough.
Anti-patterns
- A consumer carries a copy of a protocol schema taken on the day it adopted the protocol — The copy is correct when made, is never read by any migration, and reports itself current through every later `migrate list`. Growth in the protocol schema reaches no consumer, and the first symptom is a mode that fails to export in a project whose spine claims to be fully migrated.
Related ADRs
ADR-011 · ADR-018 · ADR-066 · ADR-074 · ADR-087 · ADR-089 · ADR-092