Skip to content

docs(adr),chore(devx): tombstone ADR-0107 — 撤回后退役的编号,附考古证据 (#6676) - #7316

Draft
os-project-manager wants to merge 1 commit into
mainfrom
claude/issue-6676-adr-0107-hole
Draft

docs(adr),chore(devx): tombstone ADR-0107 — 撤回后退役的编号,附考古证据 (#6676)#7316
os-project-manager wants to merge 1 commit into
mainfrom
claude/issue-6676-adr-0107-hole

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #6676

结论先说:是「起草后撤回」,不是丢失,也不是从未分配

卡片列了三种可能,考古结果唯一且无歧义:ADR-0107 曾经真实存在于 main 上九个小时,随后被 owner 决定删除

时间(UTC+8) 事件 证据
2026-07-27 #3700 上做出决定:接受 hook body 写入面的静态分析缺口(三选项里的 (a)) 审计 §5 D4
2026-07-28 00:25 ADR-0107《The Hook-Body Write Set Is an Accepted Static-Analysis Gap》合入,Status Proposed 53d37f1ae#3716
2026-07-28 09:24 Owner 改变决定:记录与其 changeset 一并删除,#3700 关为 not planned 3bb382b67#3735
2026-07-31 14:25 被撤回记录的核心立场被推翻(见下) c1d44f7dc#4271
2026-08-08 裸编号被 #6634 挂到 check-adr-anchors 的引用 allowlist 上过渡 #6634

我搜了什么,以及为什么负结果是可信的

先确认克隆不是浅克隆(9,829 commits;#6634 曾被 106 commit 的浅克隆误导过,会把「从未存在」当成事实答出来):

git log --all --diff-filter=AD -- 'docs/adr/0107*'   ->  恰好 2 个 commit,即上表两行
git log --all --oneline -S'ADR-0107'                 ->  5 个 commit,全部可归因
git log --all --oneline -S'0107' -- docs/            ->  同样那 2 个 commit
git log --all --oneline --diff-filter=ADMR -- '*0107*' -> 同样那 2 个

没有第二个东西曾经占用过这个编号——任何分支、任何时间点都没有。所以不存在「两个 ADR-0107 时代」这一停止条件。被撤回的正文也没有丢,git show 53d37f1ae:docs/adr/0107-hook-body-write-set-accepted-static-gap.md 可完整取回;tombstone 里故意不转载它,因为把被撤回的记录重印在自己的墓碑里,读起来就还是一份记录。

为什么不恢复——恢复会是错的

不只是「被撤回了」。被撤回记录的 D2 把「不用启发式分析源码」写成了 permanent posture, not a deferral:永远不会用正则或 AST 去猜 Turing-complete body 的写入集。

撤回三天后,#4271c1d44f7dc,2026-07-31)做的恰恰就是这件事:validateHookBodyWrites 解析 body.source,把字面写入解析到目标对象字段上,hook-body-write-unknown-field 带 did-you-mean 告警。那个 commit 的标题自己就记着这次反转——「L2 hook body 写不存在字段从 accepted gap 变为作者时 lint 告警」。

也就是说,恢复正文等于往决策日志里种一句和已发布代码矛盾的话。今天这块面的真实说法在 content/docs/automation/hook-bodies.mdx「Write-set checking」,tombstone 把读者指过去。

改动

1. docs/adr/0107-withdrawn-hook-body-write-set-static-gap.md(新增)

按邻居 0106-* / 0108-* 的 front-matter 与标题形状写的(粗体 **Status**: 行组 + --- + ## TL;DR)。明说编号已退役、不得复用,记下撤回、后续反转、以及上面这段考古,让下一个人不用重做。

2. scripts/check-adr-anchors.mjs(allowlist 收缩一条 + 三处注释)

这一处不是顺手扩大范围,而是 tombstone 强制的#6634 把裸编号 0107 放进了 UNRESOLVED_ADR_CITATIONS,而这张表是双向审计的:编号一旦获得记录,条目就以 stale 失败。所以加了 tombstone 就必须删这条——这正是 gate 自己在悬空引用报错里排第一的补救方式「(a) write the record」。

而且它严格更强:allowlist 条目只在还有人引用该编号时存活(方向 B「nothing cites it any more」),而 0107 今天的三个引用里有一个是等待发布消费的 changeset——.changeset/ 是设计上就会被 changeset version 删掉的。等它被消费掉,这条 grandfather clause 会自己过期、被要求删除,编号就悄悄重新空出来了,也就是这张卡担心的那件事。记录不会过期;而且复用现在会撞进编号唯一性审计,红得直白。

反向验证(方向是先预测再跑的,两侧都红,但红的理由不同)

① 拿掉 tombstone,allowlist 条目保持已删除
   -> RED:ADR-0107 is cited by 3 file(s) but names no record under docs/adr/
           ……Fix, in order of preference: (a) write the record
   (gate 自己点名了本 PR 采取的路线)

② 保留 tombstone,把 allowlist 条目放回去
   -> RED:the UNRESOLVED_ADR_CITATIONS entry for 0107 is stale —
           docs/adr/ now HAS a record for that number

两处改动因此是耦合的,任何一边单独存在都是红的;没有一处是死重。

Gates(本地实测输出)

pnpm check:adr-anchors  OK (47 anchored file(s); 119 decision number(s); 20916 citation(s) across 3486 file(s) resolve)
                        --self-test: 51 assertions
pnpm check:adr-links    OK (532 relative link destination(s) resolve; 8 frozen on baseline)
pnpm check:nul-bytes    OK (scanned 6722 text file(s); no raw ASCII control bytes) + 75 assertions
check:doc-authoring     OK (375 files clean)
check:empty-changeset   OK (0 declaring changeset(s) added)
check:adr-merge-approval --self-test: 27 assertions
eslint scripts/check-adr-anchors.mjs --no-inline-config   exit=0

编号数从 118 变 119,docs/adr/ 里 0106 → 0107 → 0108 连续。

Changeset:走 skip-changeset 标签

docs/adr/**scripts/** 不在任何已发布包里,本 PR 不发布任何东西。空 frontmatter 的 changeset 已被 check:empty-changeset#5471)明确拒绝——它对 changesets/action 是真实输入,能触发 #4898 那种静默停发,而标签不会。所以按仓库文档化的路线打 skip-changeset 标签,不写 changeset。

⚠️ 落地纪律:本 PR 不由 AI 合并

按维护者在 #6015 / #6741 的裁定,本 PR 触及 docs/adr/**,因此只起草和推送没有开启 auto-merge,也没有进 merge queueADR maintainer approval 检查在维护者本人 approve 之前会是红的——这是设计如此,不是失败。合并键在维护者手里。

顺带测到、但本 PR 不动的事

docs/adr/ 一共有 4 个洞,而且成因各不相同,这正好是卡片里「gaps 是否合法」那个可选 gate 问题的输入:

  • 0001 —— 记录于 2026-02-11(9da8e3e72)permission-protocol 重写时删除,至今在 allowlist 上;
  • 0075 —— 只存在于未合入的分支148f4c8f5 / 617f3f8ae,均 git merge-base --is-ancestor 判否),即 main 上从未分配;
  • 0083 —— 任何分支、任何时间都没有过文件,也无人引用,真正的「从未分配」;
  • 0107 —— 本 PR,落地后撤回。

四个洞四种故事,其中至少两个(0075/0083)是完全正常的编号跳过。所以「给 gaps 加告警」不能是一刀切的,已按卡片要求记为 out-of-scope finding 交分诊判断,本 PR 不实现。


Generated by Claude Code

…#6676)

docs/adr/ ran 0106-* straight to 0108-*. Archaeology on full history (9,829
commits, clone verified unshallowed) settles which of the three cases it is:
the number was DRAFTED AND WITHDRAWN, not lost and not never-assigned.

  53d37f1 (#3716) 2026-07-28 00:25 +0800  adds 0107-hook-body-write-set-
                                            accepted-static-gap.md, Proposed
  3bb382b (#3735) 2026-07-28 09:24 +0800  owner decision deletes it;
                                            #3700 closed as not planned

Nothing else ever claimed the number: `git log --all --diff-filter=AD --
'docs/adr/0107*'` returns exactly those two commits, `-S'ADR-0107'` five (both
of the above, the two later gate commits, #6634), `-S'0107' -- docs/` the same
two. No restoration is wanted — and one would be actively wrong: the withdrawn
record's D2 declared "no heuristic source analysis" a PERMANENT posture, and
c1d44f7 (#4271, 2026-07-31) reversed exactly that, shipping the parser-based
`hook-body-write-unknown-field` lint. Its subject says so: 从 accepted gap 变
为作者时 lint 告警.

So: a tombstone, recording the withdrawal, the later reversal, and the
archaeology, so the next reader does not redo it.

The gate edit is forced by the tombstone, not scope creep. #6634 had
grandfathered the bare number onto `UNRESOLVED_ADR_CITATIONS`, which is audited
in BOTH directions — a number that gains a record fails the entry as stale.
Removing it is the (a) remedy the gate's own dangling-citation message
recommends over the allowlist, and it is strictly stronger: the entry would
have expired on its own once the withdrawal changeset is consumed by a release
(direction B: "nothing cites it any more"), quietly re-freeing the number,
whereas a record makes re-use collide in the number-uniqueness audit.

Reverse-verified in both directions, predicted before running:
  - tombstone removed, entry stays removed -> RED, "ADR-0107 is cited by 3
    file(s) but names no record", fix (a) named in the message
  - tombstone kept, entry restored         -> RED, "entry for 0107 is stale —
    docs/adr/ now HAS a record"

Gates: check:adr-anchors OK (119 decision numbers, 20916 citations resolve),
check:adr-links OK (532 destinations), check:nul-bytes OK, check:doc-authoring
OK (375 files), check:empty-changeset OK, eslint clean.

No changeset: docs/adr/** and scripts/** ship in no published package, so this
releases nothing — the `skip-changeset` label is the documented route and an
empty-frontmatter changeset is rejected by check:empty-changeset (#5471).

Fixes #6676

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
@vercel

vercel Bot commented Aug 10, 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 10, 2026 6:29am

Request Review

@os-project-manager os-project-manager added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 10, 2026 — with Claude
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

PM 验收:实质 ACCEPT。唯一的红是设计如此的人工门 —— ⛔ 无可修,等维护者。

红的那一项不是缺陷

ADR maintainer approval failure(job 93373084272)。门禁自己的输出:

ADR merges are reserved to the maintainer in person. Drafting this PR was fine and stays fine — only the MERGE is reserved.
Green path: the maintainer (@hotlong) reviews and approves; the approval re-runs this check via the pull_request_review trigger, and it goes green with no further action.

这正是派单里原文照抄的落地约束的门禁形态 —— 推与开草稿 PR 都合规,只有 merge 被保留。按 drive-to-green 纪律,这类阻塞的正确动作是回帖说明而不是推"修复":任何让它变绿的改动都是在削弱门禁。⛔ 未武装 auto-merge、未入队。其余 23 项中 ESLint / TypeScript Type Checkin_progress,本贴的验收以它们转绿为前提。

考古 —— 我独立抽验过,不是采信报告

dev 报告尚未送达,下列事实是我自己在 origin/main 上验的:

断言 实测
53d37f1ae 落地 ADR-0107 2026-07-28 00:25:25 +0800 docs: accept the hook-body write-set static-analysis gap (ADR-0107) (#3716)
3bb382b67 九小时后撤回 2026-07-28 09:24:39 +0800 docs: withdraw ADR-0107, drop the structured writes proposal (#3700) (#3735)
c1d44f7dc 推翻其核心立场 2026-07-31 14:25:26 +0800 feat(lint,spec): L2 hook body 写不存在字段从 accepted gap 变为作者时 lint 告警 (#4271) (#4305)
文件确实存在过又被删 --diff-filter=AD 只命中这两笔,增删配对
号今天仍缺 docs/adr/ 上 0106 直接接 0108

三岔判定正确:这是「撤回」,不是「丢失」,更不是「从未分配」—— 删除是有意的、理由写在撤回提交里。派单里把「把从未分配写成已撤回」定为一票否决,因为那等于在决策日志里伪造历史;这里的方向相反且有据,墓碑写的是事实。

没有复活它,而且给出了不该复活的硬理由:该记录的 D2 宣称"永不用启发式源码分析"是永久立场而非延期,而三天后 #4271validateHookBodyWrites 恰恰做了那件事。恢复原文等于在决策日志里植入一条已被出厂代码推翻的陈述。派单的停手条件是"若恢复需要判断该决策是否仍然有效则停手报告"——dev 没有停在那里,而是查清了它确已失效,于是立墓碑。这是比停手更强的解法。

范围纪律:碰了 check-adr-anchors.mjs,但没越界

我派单时写明「⛔ 本次不做门禁扩展(教它查号段空洞)」,所以看到这个文件被改时先按越界处理复核了 diff。结论:改的不是那件事

0107 本就在 UNRESOLVED_ADR_CITATIONS 白名单上,原注释写明是 grandfathered「pending this tombstone」。号一旦有了记录,这条白名单必须移除 —— 该列表是双向审计的,留着反而会让门禁自己红。这是墓碑落地的机械后果,不是新功能。没有新增任何空洞探测逻辑。

顺带一处清醒:dev 在锚点解析处加了警示 —— 该处只按文件名判定记录是否存在,所以墓碑在它眼里"是一份存在的 ADR",把代码锚到墓碑上抓不住;真正拦住这件事的是 invariant 字段,它要求写明该 ADR 决定了什么,而一个什么都没决定的号写不出真话。把自己新引入的盲点写在盲点旁边,这是对的做法。

待办

ESLint / TypeScript Type Check 转绿后,本 PR 即处于只差 @hotlong 批准的状态。批准会自动复跑那道门并转绿,无需任何人再动手。⛔ 本席核不合。


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

验收转为无条件:上贴挂起的两项已转绿 —— ESLint 06:33:36Z success、TypeScript Type Check 06:44:58Z success。23 项里唯一的非绿是 ADR maintainer approval,红得其所@hotlong 批准即由 pull_request_review 自动复跑转绿,无需任何人再动手。

我上贴对 check-adr-anchors.mjs 的"耦合改动"判断,dev 用双向反证证死了

我当时是推理说那条白名单必须删(列表双向审计);dev 做的是两个方向各跑一次,而且两边红的理由不同——这正是"两处改动缺一不可"的证明形态:

  • 只留白名单、不写墓碑 ⇒ 红:ADR-0107 is cited by 3 file(s) but names no record under docs/adr/ … Fix, in order of preference: (a) write the record —— 门禁自己把这张 PR 走的路列为首选解。
  • 写了墓碑、又把白名单加回来 ⇒ 红:the UNRESOLVED_ADR_CITATIONS entry for 0107 is stale — docs/adr/ now HAS a record for that number

两边都红、且红的原因互不相同,说明两处改动是同一个动作的两半,不是一处主改 + 一处顺手。范围纪律的疑虑到此清零。

另有一条我上贴没看到的强论据:那条白名单条目本来就会自己过期 —— 它的存活条件是"仍有东西引用该号",而 0107 的引用是一份待发布的 changeset 加一份审计,消费掉就没了,届时号会悄悄重新变成可用。墓碑不会过期,且复用会在 auditAdrDirectory 里响亮撞车。所以这不只是"换个写法",是把一个会自行失效的护栏换成不会失效的

墓碑不重印被撤回的原文,是对的

dev 明确不把撤回记录的正文抄进墓碑(需要时 git show 53d37f1ae:… 可取),理由是「a withdrawn record reprinted inside its own tombstone still reads as a record」。同意 —— 墓碑的全部作用是让号解析到一句解释,而不是让被撤回的主张借墓碑还魂。

顺带发现已立 #7329,并且比卡片要求的更有用

#6676 正文把"教门禁查号段空洞"列为可选且独立判断,派单里我明令本次不做。dev 没做,但把卡片缺的那个测量补齐了:全history(9,829 commits)追下来,docs/adr/4 个空洞、3 种成因 —— 0001 删除后被特赦、0075 只存在于未合并分支(git merge-base --is-ancestor = false 实证)、0083 从未在任何分支存在过且无人引用、0107 落地后撤回。

结论是**"一刀切标记所有空洞"会是错的**:只有 0107 那种"曾经是活记录"的号才带 #6634 说的占号风险。这把一个原本要靠猜的判断变成了有数据的判断,而且没有顺手把门禁改掉 —— 该做的做满,不该做的一行没碰。


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs/adr has a number hole at 0107 — either a withdrawn record with no tombstone, or a lost file

2 participants