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

@huangyiirenehuangyiirene 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:

setcontains 0107?why
records✅ yesresolving the historical citations is the job the tombstone exists for — unchanged
nonDecisions✅ yesanchoring 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.

holecausewould a gap check help?
0001record deleted 2026-02-11, cited as historyno — already grandfathered on UNRESOLVED_ADR_CITATIONS
0075drafted on an unmerged branch, never assigned on mainno — blameless skip
0083never existed, anywhere, ever; never citedno — blameless skip
0107landed, withdrawn 9h laterthis 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 newdocs/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

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 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.
@huangyiirenehuangyiirene added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 11, 2026 — with Claude
@github-actionsgithub-actionsBot removed the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 11, 2026
@huangyiirenehuangyiirene 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 queueAug 11, 2026
@github-merge-queue
github-merge-queueBot 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)

@huangyiirene
huangyiirene added this pull request to the merge queueAug 11, 2026
Merged via the queue into main with commit 3553298Aug 11, 2026
26 of 28 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-7329-adr-anchors-non-decision branch August 11, 2026 21:35
hotlong added a commit that referenced this pull request Aug 13, 2026
…the citation allowlist (#7866) (#7960)
ADR-0001 sat on the weaker of two remedies: an `UNRESOLVED_ADR_CITATIONS`
entry in `check-adr-anchors.mjs`, which says "this citation resolves to
nothing, and that is knowingly tolerated". The gate's own prose prefers a
tombstone record, and since #6676 there is a working template for one.
This lands that upgrade, and with it the allowlist becomes empty.
Both halves are in one commit because the gate refuses either order:
* tombstone first, entry kept -> red, "the UNRESOLVED_ADR_CITATIONS
entry for 0001 is stale — docs/adr/ now HAS a record"
* entry removed, no tombstone -> red, "ADR-0001 is cited by 3 file(s)
but names no record under docs/adr/"
Both measured on this branch before writing the final state.
The record that held the number was `0001-metadata-service-architecture.md`,
merged 2026-02-10 (`908d95c82`) and deleted 2026-02-11 (`9da8e3e72`). The
deletion was NOT a withdrawal on the merits, which is the substantive
difference from ADR-0107: it was collateral in a 37-path documentation
sweep that removed the whole `docs/adr/` registry, README and all, under a
subject about permission-protocol analysis, with an empty body and no
mention of any ADR. The tombstone reconstructs that from the tree rather
than quoting a withdrawal that was never written.
The record must not be resurrected either way: its selected option was a
hybrid dual-provider architecture in which ObjectQL registers `metadata`
as a fallback, and the code has since gone single-provider — the one
`registerService('metadata', ...)` in the tree is in
`packages/metadata/src/plugin.ts`, while `packages/objectql/src/plugin.ts`
registers `objectql`, `data`, `manifest` and `lifecycle` only.
Corrects a claim the removed allowlist entry carried. It said the number
was "cited as history by ADR-0002". ADR-0002 does cite `ADR-0001`, but for
"one global DB + tenant column ... already discarded in v3.4's ADR-0001" —
a tenancy decision, not the metadata-service one this number held, and not
a document that ever existed in this repository. Verified against full
history with the clone unshallowed: nothing matching `*0001-*` was ever
added under any path, on any branch, other than the record above. The
tombstone dissects the discrepancy; ADR-0002's own wording is left alone,
being an accepted record.
`--self-test` gains the live-tree pin the card asks for, mirroring #7838's
0107 pin: 0001 must land in both `records` (citations resolve) and
`nonDecisions` (anchors refused). Pinned by name for 0001 and 0107, plus a
structural sweep so a future `NNNN-withdrawn-*.md` inherits the rule
without editing the gate, and a non-empty guard so that sweep cannot pass
by reading nothing. 70 -> 74 assertions.
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

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

@huangyiirene@claude