ADR-0042 — The ADR decided-ledger is generated from frontmatter (drift unrepresentable)
-
Status: Accepted
-
Summary: The ADR index + resolved-questions ledger is GENERATED from per-ADR frontmatter; onboarding consults the generated ledger before opening a design question.
-
Depends-on: 0026
-
Layer: C (tooling / doc-discipline — the decided-ledger's single source of truth)
-
Date: 2026-06-24
Context
The "what's-decided" ledger lived in two hand-maintained places —
docs/decisions/README.md (the index table) and docs/notes/OPEN_QUESTIONS.md
(the per-question statuses) — both copies of facts whose authoritative home is
the individual ADR files. Two copies of a fact is a representable illegal state
(the DB update anomaly): they drift.
They did. Q19 was marked · OPEN in OPEN_QUESTIONS even though ADR-0040
resolved it — and the README's own prose said 0040 "Answers Q19". A grilling
session then re-derived an already-decided question because the ledger lied.
(The same sweep surfaced two more: Q3 and Q8 had ✓ RESOLVED body headers but
stale OPEN-less index lines.) This is exactly the failure CLAUDE.md's
"Single source of truth" + the generate>test>convention ladder exist to prevent.
Decision
The decided-ledger is a pure function of the ADR frontmatter — generated, not hand-maintained. Drift moves from "caught if someone notices" to unrepresentable.
- Frontmatter schema. Each
docs/decisions/NNNN-*.mdcarries a machine block after its H1, fenced by ``:Status·Summary·Supersedes·Amends·Resolves(Q-numbers) ·Depends-on. Inverse links (Superseded-by, Amended-by) are not declared — the generator derives them from other ADRs'Supersedes/Amends. The ADR body stays the source of truth for the full rationale; the frontmatter is the one-line + relationship projection of it. - The generator —
tools/gen-adr-index.py— parses the frontmatter + H1 title, derives inverse links, and emits the index table + a resolved-questions table into the README between `` markers (hand-written preamble above the markers is preserved). It is idempotent and supports--check. - The gate.
just adr-checkruns--check: (a) the README generated region matches a fresh regen; (b) a bidirectional cross-reference — every Q marked✓ RESOLVED (ADR-n)in OPEN_QUESTIONS declaresResolves: Qnon ADR-n, and every ADRResolves: Qncorresponds to a non-OPEN Qn; and (c) a Status-consistency leg — an ADR's machine-frontmatter Status must agree (first word) with its prose**Status**narrative bullet, since the two are two copies of a fact that changes (Accepted→Superseded). Wired intojust verify. (b) is the exact check that would have failed on the Q19 drift; (c) guards the residual dual-Status copy the frontmatter sweep introduced. - Onboarding trigger. Before grilling or opening a design question, read the
generated ledger first — a question with an ADR is closed (CLAUDE.md doc
discipline +
development-lifecycle.md).
Why this model
- The top rung of the ladder.
generatemakes drift unrepresentable;test(the--checkgate) catches it at CI;convention(hand-sync) is the anti-pattern we were on. We now run the top two rungs, not the bottom. - The ADR body stays canonical. The frontmatter is a projection, not a second copy of the rationale — the rich "why" remains in the body where it is the only copy.
- Lenient parser, mechanical rewrite avoided. The H1 forms (
ADR-N ·,ADR-N —,N —) and bullet styles (**Status:**vs**Status**:) varied across 33 ADRs; the parser normalises them, so the sweep only adds a clean frontmatter block rather than rewriting every header.
Rejected alternatives
- Test-rung only (keep both hand-maintained tables, add a lint that they
agree) — rejected: it catches drift but still keeps two copies of the fact, so
a writer must remember to update both.
generateremoves the second copy. - Convention only (a doc-discipline note saying "remember to update the index") — rejected: this is the regime that produced the Q19 drift. Hope is not a mechanism.
- Bulk-load all ADRs into context (skip the ledger; let a session read every ADR to learn what's decided) — rejected for Context Rot: ~42 ADRs of coherent prose dilute attention more than a compact generated table, and cost the token budget. The generated ledger is the progressive-disclosure index; the bodies are pulled on demand.
Revisit if
- The frontmatter schema can't express a relationship that recurs (e.g. a partial supersession — currently kept in prose, as with ADR-0023's "supersedes ADR-0022 D3" only — would want a first-class field if it recurs).
- ADRs move out of
docs/decisions/or stop being one-file-per-decision. - A second consumer of the frontmatter appears (e.g. a docs site), which would argue for emitting structured data (JSON) the README renders from, not just the README table.