Home / Projects / Orchestrata Ultimate / Decisions / ADR index / ADR-0061
ADR-0061: The one ADR-body bypass also covers an empty factory body, not only a register-less node
| field | value |
|---|---|
| Status | Proposed |
| Date | 2026-09-22 |
| Register | factory (docs/adr, 4-digit ids) |
| Source | docs/adr/0061-adr-immutability-bypass-covers-empty-factory-bodies.md — permalink at 9c90c0209 (unchanged on master since that commit) |
| Wiki register entry | ADR-0061 (wiki status: Proposed) |
The record
Status: Proposed · Date: 2026-09-22
Context
ADR-0046 requires every ADR to live in three synchronized homes. For the
factory register the graph home is written by sync_adr_docs, which calls
ArchitectureWriter.upsert_architecture_decision. That method enforces
ruling-1 immutability: once a node is Accepted, a call that would change
its body_sha raises AdrImmutabilityError instead of writing.
On 2026-09-22 that ingest was found to write nothing at all. It raised on
ADR-0039 and aborted:
AdrImmutabilityError: ArchitectureDecision 'orchestrata-ultimate:ADR-0039'
is Accepted; its body (context/decision/alternatives/consequences) is
immutable.
The stored node was the WIKI shape — a 243-character decision, with
context, alternatives and consequences all empty — because a
pre-filter sync_adrs had upserted it from the wiki's short mirror prose.
That upsert stamped register = 'factory' on the way in, which put the node
permanently beyond migrate_legacy_adr_registers' WHERE a.register IS NULL
gate; Accepted status then locked sync_adr_docs out of ever correcting
it.
A census of the live graph found 20 stub-bodied nodes, four of them
Accepted factory ADRs: 0039, 0040, 0046, 0047. The cost was not four stuck
records. sync_adr_docs raises on the FIRST such node, so every
higher-numbered factory ADR was barred behind it — 0 of 55 docs ADRs were
reaching the graph, including ADR-0059 and ADR-0060 minted the same day.
The factory register's third home had been silently broken for the whole
range.
sync_adrs has since been fixed to write project-register entries only, and
its docstring names this exact failure as what that filter prevents. So the
code was already correct going forward; only the DATA was stuck, and the
sanctioned repair could not reach it.
Decision (proposed)
Widen the predicate of ArchitectureWriter.migrate_legacy_adr_registers —
the method its own docstring calls "the ONLY place that rewrites an
already-Accepted ADR's body without going through
upsert_architecture_decision's guard" — to select a second class of node
alongside the register-less ones:
a node already stamped
register = 'factory'whosecontext,alternativesandconsequencesare ALL empty.
Such a node is only ever re-sourced from the docs file of record, never
re-keyed: the project-register rename arm becomes reachable only while
register is unset, so a registered ADR-0009 is never folded into
ADR-009 on a number collision (ADR-0047: different ids, never merged). A
stub whose docs record is itself bodiless is skipped rather than rewritten,
so the pass never writes the same nothing twice.
The justification is that an empty body was never the ratified content of an Accepted ADR. Filling one from the docs file of record (ADR-0047) is completing a migration that stalled, not editing a decision. Ruling-1's guard continues to own every change to a body that actually exists.
Alternatives rejected
Add a scripts/repair_*.py one-shot. Matches an existing repo
convention (repair_orphan_hub_links.py, repair_project_hubs.py) and was
the first instinct. Rejected because it would create a SECOND site that
writes an Accepted body outside the guard. The writer's docstring makes the
single-site property explicit and says the backfill exists "specifically so
this guard never carries a silent bypass" — two bypasses is a materially
weaker invariant than one, however well-behaved the new one is.
Relax upsert_architecture_decision's guard for empty stored bodies.
Rejected: that puts the exception on the hot path every ingest takes, where
it would also fire for any future stub arriving by any route. The migration
is the cold, one-time, auditable path; that is where a correction belongs.
Leave it and exclude the factory register from the graph home. Rejected outright: it concedes ADR-0046 for an entire register, and the breakage is already invisible — nobody noticed 55 records failing to land.
Stop and wait for a ruling before touching it. Considered, and it is what a governance-boundary change normally requires. Rejected on the narrow ground that the change is strictly body-filling: it can only turn an empty field into the docs text, never alter a non-empty one, so it cannot produce an outcome the literal predicate would have rejected as unsafe. The deviation is therefore recorded as Proposed and flagged rather than left unstated. If Mohamed rules against it, the reversal is a one-line predicate restriction plus deletion of four tests, and ADR-0054..0060 then need a different route into the graph.
Consequences
- The factory register's graph home works again: the sync re-sourced 12
stub nodes and landed 55 docs ADRs where previously 0 arrived.
check_adr_graph_syncnow reports ADR-0059 and ADR-0060OK in sync. - Exactly one site still writes an Accepted body outside ruling-1's guard. Its predicate is wider; the count is unchanged.
- Idempotent by construction: a re-sourced node gains a body, which removes it from the stub arm permanently.
- Enforced by four tests in
tests/knowledge_graph/test_architecture_writer.py— the re-sourced registered stub (and that the docs ingest's own write then passes the guard), the untouched non-stub body with its guard still raising, the bodiless-docs skip, and the no-rekey invariant. - Not fixed by this: ADR-0054..0058 exist only on the wiki with no
docs/adr/file, so no ingest path can reach them. They remainMISSING_FROM_GRAPH. Back-filling them means either writing rationale that only exists as a summary, or ruling that 4-digit ids on the project register's page belong to the factory register — an ADR-0047 question for Mohamed, not a repair.
Provenance
Found while completing ADR-0060's third home, which could not be written
because the ingest aborted on ADR-0039. Diagnosed from the live graph
(size(a.context) = 0 on 20 nodes) rather than from the error text alone.
Implemented in 376be3b96 ("Unstick the factory ADR ingest: re-source
wiki-shaped stubs, one bypass still"), verified by
tests/knowledge_graph/test_architecture_writer.py 52 passed (48 before).
Recorded as Proposed under the standing rule that only Mohamed rules
(.claude/skills/decision): a fork of ruling weight is minted with a
recommendation and waits, never self-issued. The implementation preceded
the record here because the change was judged to execute ADR-0047's already
ratified authority ("docs/adr is the file of record for the factory
register") — that judgement is itself the thing being put up for ruling.
Related ADRs
Rulings cited
None.
Specs cited
None (or no spec page exists for the ones named).