Skip to content

tooling(check-adr-anchors): verify a cited Dk names a real decision of the cited ADR - #12785

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-9592-adr-decision-letter
Aug 27, 2026
Merged

tooling(check-adr-anchors): verify a cited Dk names a real decision of the cited ADR#12785
os-zhuang merged 1 commit into
mainfrom
claude/issue-9592-adr-decision-letter

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#9592

Remedy 3 of the maintainer ruling on #9255 (2026-08-18). The number layer already proved that ADR-0057 names exactly one record; nothing ever read the D10 half — the part that says which of that record's decisions the code is standing on. That is the #8386 / #9255 failure family, and every gate called it green.

This adds the layer below the number: for every ADR-NNNN Dk citation on the anchored surface — each anchor's invariant in scripts/adr-anchors/, and the full text of each anchored file — Dk must appear in the cited record's decision index.

The grammar was measured before the parser was written

The judgement in this card is entirely in the heading grammar: too strict reddens correct citations, too loose passes anything. So docs/adr/** was counted first (131 files, 125 numbers), and the check recognises exactly the four shapes that exist:

#ShapeWhere
1### Dk — Title heading (h3 dominant, h4/h5 for sub-decisions)321 tokens, the dominant form
2**Dk — Title.** bold-lead paragraph22 numbers, 10 of them exclusively (ADR-0105's style)
3| Dk | Title | first cell of a decision-table rowADR-0120's decision table
4A Dkheading's own top-level list declares its sub-decisions: an ordered list gives Dk.1, Dk.2, …; a lettered list gives Dka, Dkb, …ADR-0120 D5's five lettered gates, ADR-0020 D3's three numbered steps

Rule 4 is load-bearing, not a convenience. Without it, ADR-0120 D5a/D5b/D5c/D5e and ADR-0020 D3.320 live citations — read as bad letters. Both were red under the first, stricter parser draft, and both turned out to be correct citations: ADR-0120 spells its gates as a./b./c./d./e. under ### D5, and ADR-0020 spells the introspection endpoint as item 3 under ### D3. That is the "too strict" half of this failure, caught by measuring instead of by shipping.

The rule is bounded in both directions: sub-items stop at the next heading of any level (so a list under ## Consequences declares nothing), and a sub-number the list does not reach is still refused.

"Cannot verify" is a third verdict, and it is printed

67 of the 125 ADR numbers declare no machine-readable decision letters at all — the older records number their decisions topically (### Schema) or ordinally (### 2. Commits are atomic), never as Dk. 37 live citations point into six of them.

Those are neither resolvable nor clean, and the run says the difference out loud rather than letting an unrecognised shape read like a pass:

check-adr-anchors: OK (52 anchored file(s), …; 878 decision-letter citation(s) on the
anchored surface name a decision the record makes).
check-adr-anchors: note — 37 decision-letter citation(s) across 12 letter/file site(s)
CANNOT BE VERIFIED — not checked, and NOT counted as resolving above: the 6 id(s)
below name records that declare no decision letters at all (#9592):
· ADR-0006 D4 — 1 site(s): packages/spec/src/data/object.zod.ts
· ADR-0014 D2 — 1 site(s): packages/objectql/src/engine.ts
· ADR-0024 D4 — 2 site(s): …
· ADR-0024 D5.2 — 2 site(s): …
· ADR-0067 D2 — 4 site(s): …
· ADR-0081 D1 — 2 site(s): …

Reported, never failed — for the same reason the ambiguous-number note is: the defect is the record's spelling, and docs/adr/** is governed surface (#6741) that a script gate must not force. Those 37 are filed as a finding rather than baselined away.

An ordinal→letter inference (D2 = the 2nd ### N. heading, which would have made ADR-0067 verifiable) was weighed and rejected: the record never writes D2, so the mapping would be this gate's own invention, and a gate that manufactures the index it checks against cannot fail honestly.

First-run reds: there are none, and that is a measurement

The card warned that first-run reds are their own findings, never a reason to weaken the check. Two candidates appeared under the first parser draft (ADR-0020 D3.3, ADR-0120 D5b); both were parser artifacts, dissolved by reading the sub-decision grammar the records actually use. After that, 0 bad letters remain on the anchored surface. Nothing was baselined, no allowlist was added, and the predicate was not softened — the grammar was corrected to match the corpus.

One citation is skipped by the pre-existing cross-repo rule and correctly so: // [cloud ADR-0024 D5.2] in packages/plugins/plugin-auth/src/auth-plugin.ts names a sibling repo's registry.

Two silent-failure modes this closes on the way

  • Line-wrapped citations. 11 live citations on the anchored surface wrap across a JSDoc continuation — … (ADR-0106 newline * D3) — …. A line-oriented scan misses every one of them silently. The citation regex follows the wrap.
  • A shape scanner degrading to a no-op. Change the heading style and every record parses as "declares no letters", every citation becomes "cannot verify", and the run still exits 0 with a cheerful summary. An empty corpus-wide index is a hard failure in the main run, and --self-test pins measured floors on the index (≥ 40 of 125 numbers), on the scan (> 500 citations read) and on the resolution count (> 500 actually resolved) — so green cannot mean "nothing was checked".

Ablation — proved it can fail, end to end

Mutation and restore both proved on disk, against HEAD (the implementation was committed first, so the restore leg has a real reference):

HEAD blob : ec8252f3f3f47246c3a7862e696a18691910371d
on-disk before : ec8252f3f3f47246c3a7862e696a18691910371d
before: 'ADR-0057 D10'=12 'ADR-0057 D99'=0
after : 'ADR-0057 D10'=11 'ADR-0057 D99'=1 <- mutation landed (both counts moved)
on-disk after : 9c6bc6d8f3ab9ad70b28eb2d5da5d4364ac4d5b3
MUTATED_EXIT=1
check-adr-anchors: 1 problem(s)
• ADR-0057 D99 is cited by 1 file(s), but ADR-0057 declares no D99 —
on-disk restored: ec8252f3f3f47246c3a7862e696a18691910371d
restore proven: git diff HEAD is EMPTY and on-disk hash == HEAD blob
RESTORED_EXIT=0

The restore leg is proved by observed state (hash equality plus an empty git diff HEAD), not by an exit code — a checkout that does nothing also exits 0. --self-test carries the same ablation as an assertion (ablation-a-fabricated-letter-is-red) so it cannot rot.

Scope

  • Onlyscripts/check-adr-anchors.mjs. No ADR was edited — the gate reads docs/adr/**, which stays governed surface.
  • The layer covers the anchored surface deliberately (915 citations). The repo-wide corpus is 7736 same-line ADR-NNNN Dk occurrences across 1609 files and stays unchecked; that boundary is filed as a finding rather than widened here.
  • skip-changeset: the diff is one root scripts/ gate. Nothing published changes.

Verification

Gate union run on the final commit 8071dd26b, clean tree, exit codes captured before any pipe:

adr-anchors exit=0 agent-test-spelling exit=0
bash32-floor exit=0 cli-command-ids exit=0
cross-package-test-inputs exit=0 entry-guard exit=0
parse-guard exit=0 pm-governed-merges exit=0
pnpm-filter-targets exit=0 ci-filter-parity exit=0
cross-package-inputs-direct exit=0 bare-root-worklist exit=0
pm-dispatch-gates exit=0 nul-bytes exit=0
ratchet-remedy-authority exit=0 adr-links exit=0

Each gate's own printed verdict line, not a bare $? — for example:

  • ✓ check-adr-anchors --self-test: 106 assertions over the real auditAdrDirectory() / auditCitedNumbers() / assembleAnchors() paths. (100 before this change)
  • OK self-test: 46 live row(s), 39 unreachable as spelled, 39 recorded verdict(s) — none stale, none missing, none contradicted. (bare-root-worklist)
  • ✓ dispatch-gates self-test: 723 cases pass. (check:pm-dispatch-gates — a convention-triggered obligation the dispatched gate list did not name; node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack surfaced it for this diff)
  • check-nul-bytes: OK (scanned 7118 text file(s) … no raw ASCII control bytes).

Whole-repo pnpm lint (eslint . --no-inline-config) also run on 8071dd26b: LINT_EXIT=0, no findings. This is the full farm scan, not a narrowed one.

Session: https://claude.ai/code/session_01PfaSTikked61BkcsB5Rn69


Generated by Claude Code

… of the cited ADR
Extends `scripts/check-adr-anchors.mjs` one layer below the number: for every
`ADR-NNNN Dk` citation on the anchored surface (each anchor's `invariant` and
each anchored file's text), `Dk` must appear in the cited record's decision
index. The number layer already proved 0057 names one record; nothing read the
`D10` half, which is the #8386 / #9255 failure family.
The heading grammar was measured across `docs/adr/**` before the parser was
written, and the check recognises exactly the four shapes that exist: `### Dk`
headings, `**Dk —**` bold leads, `| Dk |` decision-table rows, and a `Dk`
heading's own top-level list declaring `Dk.1`/`Dka` sub-decisions. That last
rule is load-bearing: 20 live citations into ADR-0120 D5's lettered gates and
ADR-0020 D3's numbered steps resolve only through it.
"Cannot verify" is a third verdict and is printed, never folded into the OK
line: 67 of 125 ADR numbers declare no machine-readable decision letters, and
an unrecognised shape must not read the same as a clean one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PfaSTikked61BkcsB5Rn69
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 27, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 27, 2026 19:35
@os-zhuang
os-zhuang enabled auto-merge August 27, 2026 19:35
@os-zhuang
os-zhuang added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 8c0b5cfAug 27, 2026
34 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9592-adr-decision-letter branch August 27, 2026 19:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tooling: check-adr-anchors should verify that a cited ADR-NNNN Dk decision letter is a real heading of the cited ADR

2 participants

@os-zhuang@claude