One name per tree: ONTOREF_ROOT is retired for ONTOREF_TOOL_ROOT and ONTOREF_PROTOCOL_ROOT
Accepted
Context
ONTOREF_ROOT answered two different questions with one word.
what build is running — the tool's material: ontology/ schemas, templates/, install/, domains/, the bundled bin/ what governs this run — the protocol tree: reflection/ (modules, modes, migrations, schemas, bin, tests) and adrs/
On an installed consumer the two are the same directory, the data dir, which is why the name served for years. On ontoref's own checkout they are not: since the constellation split (adr-062) the tool lives in code/ and the protocol in the spine .ontoref/ beside it. The single name could not be read as either without guessing, and the two places that set it guessed differently — the source wrapper exported code/ while env.nu, when it set the variable itself, derived the spine from its own path. The same variable meant opposite trees depending on who set it.
The cost was measured repeatedly and each time repaired where it surfaced:
- 2026-08-08 adr-070 red over a variable collision, not a reproduction failure (site justfile) - 2026-07-06 nested `^ontoref` in audit criteria inheriting the wrong root (bl-038, hide-env rule) - 2026-08-20 a release cut blocked: `chain-ok` reported "reflection scripts not found" over a valid chain because an inherited code/ carried no reflection tree - 2026-09-03 `sync assets` hashing a digest over an empty set and recording it green (bl-056) - 2026-09-13 `migrate pending` answering [] for every project from the source CLI (bl-056) - 2026-09-19 adr-029 `tier-transition-requires-clean-migration-state` reporting GREEN because `migrate pending` failed, produced no stdout, and `"" | length == 0` was true — while one migration (0044) was in fact pending
Every repair chose correctly for its own reader and left the name overloaded for the next one. That is the mechanism by which the defect stayed invisible: bl-056 listed three instances and each had been closed.
The same overload was carried into the mode step language. The placeholder {ontoref_dir} named templates/ and ontology/ in 46 places and reflection/ in 10.
Decision
ONTOREF_ROOT is RETIRED and replaced by two names, each naming one tree:
ONTOREF_TOOL_ROOT ontology/, templates/, install/, domains/, bin/ ONTOREF_PROTOCOL_ROOT reflection/, adrs/
On an installation both are the data dir; on ontoref's checkout they are code/ and the spine. A reader chooses by WHAT THE PATH NAMES, never by where the reader lives.
NO ALIAS. A caller still setting ONTOREF_ROOT is refused BY NAME — by env.nu, by the checkout wrapper and by the installed wrapper — with the two replacements in the message. An alias would preserve exactly the overload being removed and defer a second break to a later release.
The mode step placeholder {ontoref_dir} is retired with it for {tool_dir} and {protocol_dir}; a step still carrying it is refused before it reaches the shell as a literal path.
ROOTS ARE DERIVED FROM CONTENT OR NOT AT ALL. env.nu sets the protocol root from its own path (it IS the protocol tree being executed) and the tool root only when a marker proves it: `ontology/ schemas` with `install/` in the same tree (a data dir) or in a sibling `code/` (the constellation). When neither holds, `tool-root` names the absence instead of choosing a tree.
CALLER-SET ROOTS ARE RESPECTED, AND AN INVALID ONE IS REFUSED — NOT REPLACED. Respecting them is what lets an unreleased protocol be tried against a real consumer before it is installed; before this the only way to see a protocol change from anywhere was to install it, which delivers it to every project on the machine at once. A root that does not carry what its name promises is refused by name, so a typo in a deliberate override cannot silently test the installation.
DOMAINS DO NOT GET THE SPLIT. A domain's provider root is resolved per relationship, from the bond and the project registry, and its material has one home in the provider's spine (adr-102). There is no global name answering two questions there, which is what this decision removes. The review found the opposite asymmetry instead — a consumer reads its provider's LIVE checkout with no install step between them — and it is recorded as a backlog item, not decided here.
This ADR is the deprecation record adr-029 `additive-stack-no-breaking-tier-changes` requires for a removal. The path is migration 0101 and the refusal that names the replacement.
Constraints
- Hard A caller setting ONTOREF_ROOT is refused by name, before any module runs, by env.nu and by both wrappers — never read as either tree.
- Hard No runtime module, dispatcher, library, hook or handler in the protocol tree reads ONTOREF_ROOT; the only reader left is env.nu's refusal.
- Hard A caller-set ONTOREF_PROTOCOL_ROOT without reflection/bin/ontoref.nu, or ONTOREF_TOOL_ROOT without ontology/schemas, is refused by name by both wrappers — never silently replaced by another tree.
- Hard No protocol mode step carries {ontoref_dir}; steps name {tool_dir} or {protocol_dir}, and the executor refuses the retired placeholder.
Alternatives considered
- Keep ONTOREF_ROOT as an alias of one of the two during a compatibility window — rejected: Whichever tree the alias named, readers of the other would keep working by accident or failing by surprise — the overload preserved under a softer name, and a second break deferred to the release that removes the alias. Sacrifices the closure the decision exists for.
- Redefine ONTOREF_ROOT to mean the protocol tree (the spine) — rejected: Every tool reader — schemas, templates, domains, the bundled nu — would silently read the wrong tree on the checkout. It picks a pole of a two-tree fact and hides the other. Sacrifices every reader of the tool.
- Resolve the protocol tree in each reader that needs it (e.g. `../.ontoref` in migrate.nu) — rejected: Tried in migrate.nu and rejected there: the migrations' own checks read the variable, so that root reported 22 pending instead of 9. It is the reader-by-reader repair that produced six incidents. Sacrifices the single point of truth.
- Extend the split to domain providers and their consumers — rejected: A domain has no global root: its provider is resolved per bond from the project registry, and its material has one home (adr-102). Adding two names there would add a question nobody asks. Sacrifices nothing to reject it; the real domain asymmetry (no staging between a provider and its consumers) is a different problem, recorded separately.
Anti-patterns
- A name with two meanings, repaired reader by reader — A variable, placeholder or field answers two questions. Each time it surfaces, the reader that surfaced it is fixed to choose the right meaning for itself. Every fix is correct and the name stays overloaded for the next reader, so the defect is invisible to anyone reading any single fix.
- Keeping an overloaded name alive as an alias of one of its meanings — The split is made but the old name is kept pointing at one tree for compatibility. Readers of the other tree keep working by accident or fail by surprise, and the break is only deferred to the release that removes the alias.
- Silently replacing a caller-set value that does not validate — A value a caller set on purpose is swapped for a default when it fails validation. Harmless when the value only ever arrived by leak; a trap once setting it is a supported way to choose a tree, because the caller believes their choice took effect.
Related ADRs
ADR-029 · ADR-032 · ADR-038 · ADR-048 · ADR-062 · ADR-072 · ADR-074 · ADR-102 · ADR-115
Perspectives