Skip to content

fix(devx): teach check:adr-anchors that a tombstone is not a decision (#7329) - #7838

Merged
huangyiirene merged 2 commits into
mainfrom
claude/issue-7329-adr-anchors-non-decision
Aug 11, 2026
Merged

fix(devx): teach check:adr-anchors that a tombstone is not a decision (#7329)#7838
huangyiirene merged 2 commits into
mainfrom
claude/issue-7329-adr-anchors-non-decision

Conversation

@huangyiirene

@huangyiirene huangyiirene commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7329

The premise, re-measured before writing code

The issue's comment history disagreed with itself, so the first job was to settle it empirically rather than pick a comment.

Measured on a clean origin/main @ 69fde55. Dropped one probe shard into scripts/adr-anchors/ anchoring a real tracked file to ADR-0107, the tombstone number:

check-adr-anchors: OK (49 anchored file(s), … 119 decision number(s) …)
EXIT=0

Fact 2 is still open. #7316 landed the tombstone and documented this hole inline at the guard (check-adr-anchors.mjs:600-608, verbatim: "Anchoring code to one is not caught mechanically") — it did not close it. The 03:10Z triage conflated three things #7316 genuinely did fix (citations resolve, number re-use collides loudly in auditAdrDirectory, the allowlist shrank) with the one it did not: the anchor loop still accepts the tombstone. So this PR takes the route the card and the PM ruling describe.

The same probe against this branch:

scripts/adr-anchors/…json: ADR-0107 names a WITHDRAWN record, not a decision —
nothing may be anchored to it (#7329).
EXIT=1

What changed

records was doing two jobs that want opposite answers about the same file. It now returns a second set:

set contains 0107? why
records ✅ yes resolving the historical citations is the job the tombstone exists for — unchanged
nonDecisions ✅ yes anchoring live code to it is refused — the gap this closes

No live record's treatment changes. A number counts as a non-decision only when every stem claiming it is a tombstone: a tombstone sharing a number with a live record is a collision, the existing audit already reports that loudly and about the right fact, and the live record keeps its anchors until it is resolved.

The anchor loop's ADR-id judgement (parses / names a record / is a decision) moved into a pure anchorIdProblem, so --self-test drives the real function instead of an imitation — the file's own standard, "a gate whose red path was never exercised is not a gate".

The contract choice, and why not front-matter

The card named three options. Filename convention (NNNN-withdrawn-<slug>.md) — same shape as the .vN version rule and the cross-repo qualifier, both structural spellings rather than lists, and available to every future record without editing the gate.

This diverges from the PM's stated preference for a front-matter status: field. Three measurements decided it:

  1. Zero of the 119 records carry front-matter. It is a new repo-wide convention introduced for one file, not a property of the corpus.
  2. The records' Status lines are free prose and already deliberately outside this gate's rules — the header says so, and the survey backs it: Proposed, Draft (2026-05-24), Accepted in part, Superseded by v4, and multi-sentence audit paragraphs.
  3. ⛔ Discipline: ADRs are confirmed and merged by the maintainer only — no AI seat may merge, queue, or auto-merge a docs/adr/** PR #6741 routes any diff touching docs/adr/** to the maintainer's own mergecheck-adr-merge-approval.mjs matches on the docs/adr/ path prefix with no size exemption. Marking the tombstone would couple a script-only fix to a governance approval. (Confirmed on this PR: ADR maintainer approval is green precisely because nothing under docs/adr/ is touched.)

And front-matter fails open in exactly the way the filename does — a future tombstone can forget either one — so it buys no robustness for that cost. The third option, an explicit NON_DECISION_RECORDS list in the script, is the only one needing a gate edit per future tombstone, which is the property the other two are chosen for.

The one real hazard of the filename route is handled: the marker is a slug prefix, not a substring, so a live decision about withdrawal (0123-withdrawn-plugin-cleanup-policy.md) keeps its anchors. Pinned as an assertion.

Out of scope, with the reasoning recorded

No blanket "flag every gap in the ADR numbering" check. The card's own census is the rejection: four holes, three causes.

hole cause would a gap check help?
0001 record deleted 2026-02-11, cited as history no — already grandfathered on UNRESOLVED_ADR_CITATIONS
0075 drafted on an unmerged branch, never assigned on main no — blameless skip
0083 never existed, anywhere, ever; never cited no — blameless skip
0107 landed, withdrawn 9h later this is the squat shape, and this PR covers it

A gate that alarms on three innocent skips to re-cover one risk already covered trades noise for nothing.

The ADR-0001 tombstone upgrade (03:10Z triage's re-scope) is untouched. It was the deliverable only if Fact 2 had turned out closed; it did not. It needs a new docs/adr/0001-*.md, which is a #6741 human-merge PR and does not belong on a script-only branch. Still open, unclaimed by this card.

Verification

  • pnpm check:adr-anchors70 assertions (55 → 70), gate green: 48 anchored files, 119 decision numbers, 22,444 citations across 3,709 files resolve.
  • New assertions cover both directions over the same file (a tombstone still resolves citations and is refused as an anchor — a test of only the red half would pass on an implementation that broke the thing tombstones exist for), the .vN case, the prefix-not-substring boundary, the contested-number case, the three anchorIdProblem verdicts, and a live-tree ablation replaying the exact probe that came back green on main.
  • pnpm check:adr-links ✅ · pnpm check:doc-authoring ✅ · eslint clean.

Changeset: route 2 (skip-changeset)

This PR touches scripts/ only and releases nothing. It first carried an empty-frontmatter changeset, following the three existing adr-anchors-* ones — but those are grandfathered base-commit files, and Check Changeset correctly refused a newly introduced one (#4898: an empty changeset is a real input to changesets/action, and an all-empty set stalls the release silently and greenly; #5471: the gate judges only what a PR adds). Taken to route 2 as the failure prescribes: changeset deleted, skip-changeset applied. The reasoning it carried is in the commit message and this body.

⛔ No ADR's decision content is touched; no file under docs/adr/ is touched at all.

…on (#7329)

`docs/adr/0107-withdrawn-hook-body-write-set-static-gap.md` (#6676) is the
corpus's first tombstone — a file whose entire content is "this number is
withdrawn, do not reuse". It was written so the historical `ADR-0107` citations
resolve, and it does that. But the gate assembled its record set from filenames
alone, so the same file also satisfied the anchor loop's `records.has(...)`
guard, which was never meant to accept it.

Measured on `main` @ 69fde55: a shard in `scripts/adr-anchors/` citing
`ADR-0107` made the gate print OK and exit 0 — live content anchored to a
number whose own record says nothing in it is in force. The only thing standing
in the way was a reader noticing that the required `invariant` field cannot be
written truthfully for a number that decided nothing.

The two audits want opposite answers about the same file, so one set was not
enough. A tombstone number is now in `records` (citations resolve — the job it
exists for) and in `nonDecisions` (anchors refused). No live record's treatment
changes.

The marker is the filename, `NNNN-withdrawn-<slug>.md`, for the reason the `.vN`
rule and the cross-repo qualifier are also spellings rather than lists: a
structural signal reaches every future record without editing the gate. A
`status:` field in YAML front-matter was the more obvious design and lost on
three measurements — zero of the 119 records carry front-matter today; the
records' status lines are free prose and already outside this gate's rules; and
#6741 routes any `docs/adr/**` diff to the maintainer's own merge, coupling a
script-only fix to a governance approval while failing open the same way. An
explicit list was rejected as the one option needing a gate edit per tombstone.

The anchor loop's ADR-id judgement is extracted into `anchorIdProblem` so
`--self-test` drives the real function: 15 new assertions cover both directions
over the same file, the `.vN` case, the prefix-not-substring boundary, the
contested-number case (a tombstone sharing a number with a live record is a
collision, and the live record keeps its anchors), and an ablation on the real
tree replaying the exact probe that came back green on `main`.

Refs #6676, #6634, #5992.
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 8:08pm

Request Review

This PR touches `scripts/` only and releases nothing, so it takes the
`skip-changeset` route the gate names rather than declaring a release it
does not make. An empty-frontmatter changeset is a real input to
changesets/action and an all-empty set stalls the release silently and
greenly (#4898); the pre-existing empty ones on the base commit are
grandfathered and this gate judges only what a PR newly introduces (#5471).

The reasoning that changeset carried is in the commit message and the PR
body, which is where it is readable without a release cycle.
@huangyiirene huangyiirene added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 11, 2026 — with Claude
@github-actions github-actions Bot removed the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 11, 2026
@huangyiirene huangyiirene added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 11, 2026 — with Claude
@huangyiirene
huangyiirene marked this pull request as ready for review August 11, 2026 20:39
@huangyiirene
huangyiirene added this pull request to the merge queue Aug 11, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31534161719 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/3) — 失败步骤: Run this shard's tests

    �[41m�[1m FAIL �[22m�[49m src/lint-startup-registry-verdict.corpus.test.ts�[2m > �[22mstartup open-vocabulary verdicts across packages/ (#4776)�[2m > �[22mno package records a verdict the boot can sti
    

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 70 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check:adr-anchors has no model of a "non-decision" ADR number — gaps go unflagged, and a tombstone is anchorable

2 participants