Skip to content

fix(ci): ADR-0087 迁移说明探测器读得懂「无箭头的改写表」(#6497) - #6558

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6497-prescription-table-detection
Aug 8, 2026
Merged

fix(ci): ADR-0087 迁移说明探测器读得懂「无箭头的改写表」(#6497)#6558
os-project-manager merged 1 commit into
mainfrom
claude/issue-6497-prescription-table-detection

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #6497

缺陷

not-required (no-migration-prescription) 这条豁免的唯一守门人是 findMigrationPrescription():正文带改写指令 ⇒ 拒绝该豁免。它此前的两条分支 —— #6148FROM/TO 标签约定、#6419 的框架锚定改写 —— 都要求出现箭头(REWRITE_REOPERAND ARROW OPERAND,循环里 if (!REWRITE_RE.test(line)) continue)。

而本仓相当一批 changeset 把改写指令写成迁移框架标题下的两列 markdown 表,一个箭头都没有。探测器判它「无迁移说明」,于是这条自相矛盾的豁免被门禁批准 —— 这是 #6419 那个形状的第三种拼写,它的教训逐字适用:作者写得越认真,探测器越看不见。一张写清了「原来怎么写 / 改成怎么写」的表,比一行箭头信息量更大,却恰恰是探测器唯一读不到的形状。

改法

新增第三条分支 framed-table:迁移框架标题所辖区域内、markdown 表格的数据行两个及以上不同单元格各带一个 OPERAND

表格里由单元格边界承担箭头的职责,所以这条分支对两侧的要求与箭头分支完全一致(代码形操作数:反引号代码段,或点/斜线标识符路径),只是读 | 而不读 。三处收窄都是刻意的,方向都是「宁可漏,不可误」:

  • 必须有分隔行(| --- | --- |)才算表 —— 正文里带竖线的散文不是表,分隔行同时保证表头行永远不会被当成证据(扫到表头时分隔行还没出现);
  • 外侧竖线必须写全 —— GFM 允许 a | b,但认它就等于把每一行带竖线的散文读成表格行;
  • 一个代码单元格不算改写 —— 那是描述、定义或能力行(| \getPort?()` | the real bound port |`),不是处方。

先量误报面,再放宽(#6419 的打法,本单照抄)

误报没有诚实的出口:作者被错误拒掉一条他有权使用的豁免,封闭词表里没有别的选项给他。所以放宽之前先在整个存量上量,而不是在触发本单的那 4 条上量。

口径 数字
存量 1384 条 changeset,241 条声明 breaking
探测器 改前 129 命中 / 97 breaking
探测器 改后 133 命中 / 101 breaking
净变化 +4 / −0
新分支单独命中 7 条(全部声明 breaking),逐条人工核对全部是真改写处方
误报 0 条(breaking 与非 breaking 都是 0)

超集性质在此是结构性的而非测量出来的:箭头两支先判并当场 return,表格命中被扣到循环结束才返回,整个函数就是 arrowBranches(body) ?? tableArm(body) —— 改前命中的每一条,仍以同一分支、同一条证据行命中。将来只动表格分支的编辑无法破坏它。

经测量后拒绝的两条更宽规则

  • 不要框架锚,任何两代码格的表格行都算 —— 新增命中 114 条(33 条 breaking)。changeset 正文里表格的用途很杂(能力矩阵、行为对照、版本对照),这条规则分不开它们。
  • 用表格自己的表头取框架(复用现有 MIGRATION_FRAMING_RE,能正确读出 | 原写法 | 改写为 |)—— 很诱人,不需要新词表。实测:只买到 1 条无判决影响的真阳性(filter-icontains-and-regex-retirement.md,不声明 breaking,门禁从不judge它),却带来 1 条真实误报 —— rate-limit-config-dual-source-c9.md,它那张表是两个类型的对照矩阵,只因表头单元格里有 (renamed) 一词被命中,而那个词在给列命名,不在框改写。在被判决的人群上一无所得、却多一种判错的方式 ⇒ 拒绝。

4 条存量按名核对(--audit-stock)

残差面 92 → 96,恰好 +4,无一移出;exempt-no-prescription 桶 143 → 139。

changeset 改前 改后
runtime-httpserver-wrapper-retired.md exempt-no-prescription 残差 !(framed-table)
etl-author-shape-aliases.md exempt-no-prescription 残差 !(framed-table)
retire-three-deprecated-aliases.md exempt-no-prescription 残差 ~(framed-table)
unknown-key-strictness-ui-batch15.md exempt-no-prescription 残差 !(framed-table)

⚠️ 这是工作清单,不是判决:移入残差面并不决定其中哪一条欠一条台账记录。登记判准本身是 #6350 挂在维护者那儿的未决题,本 PR 不回补任何 ADR-0087 台账条目 —— 那等于用实现去替维护者做决定。

一并改写的自述

脚本正文那段「残余盲区」此前只说未加框架的改写表读不到 —— 而本单这 4 条恰恰是加了框架的。那句自述本身就是 #6497 的成因:它读起来像「凡是作者框起来的都看得见」,于是下一个读者信了它而不去测。现在给的是分支 3 之后剩下的形状,分项带数:

  • 完全无框架old → new 清单/表:132 条 / 21 条 breaking —— 锚定框架的诚实残差,故意保留;
  • 由标签而非标题起框(**Migration.** 独占一行,紧跟表格):4 条 / 2 条 breaking;
  • 只由表头单元格起框、且用的词不在现有框架词表里(| Wrote | Write instead || Removed | Live replacement || you wrote | write instead || Was | Now |):14 条 / 11 条 breaking —— 这是一条真实的房规(4 种拼法),读它需要一套新的老/新列词表,那是它自己的测量任务和自己的误报面,本单刻意不做,已另行归档为 finding。

自测与反向验证

--self-test 从 73 条断言增至 85 条(CI 的 Check Changeset 步骤跑它)。新增:P22-P27(表格分支的能力与三条边界)、R12(#6497 形状走完整 scan() 路径被拒,证据分支为 framed-table)、G8(同一框架下的能力表保持 GREEN)、S7(--audit-stock 分类器把它归入 residue)。

反向验证 —— 红集在运行前就预测好了:删掉表格分支,预测 R12(1 条「expected RED, got green」+ 3 条消息断言)、P22P23P24S7 转红,共 8 条;实跑 8 条,与预测逐条一致,既有断言无一移动。

诚实标注方向:P25 / P26 / P27 / G8误报地板,删掉分支后仍然是绿的。删一条分支只会让探测器匹配得更少,所以任何误报地板在反向验证下都不可能转红 —— 它们钉的是分支停在哪里,不是分支的能力。把它们写进红集预测才是编造证据。

为什么没有 changeset

本 PR 只改 scripts/ 下的 CI 脚本,不属于任何已发布包,不发布任何东西 ⇒ 按 AGENTS.md「纯 bug 修复不需要 changeset」走不写 changeset这一支,并给 PR 打 skip-changeset 标签。

本地门禁

  • node scripts/check-adr-0087-registration.mjs --self-test ✅ 85 assertions
  • node scripts/check-adr-0087-registration.mjs(门禁本体)✅
  • pnpm lint(全仓 ESLint)✅;lint.yml 里的全部 check:* 家族门禁 ✅
  • pnpm check:nul-bytes ✅;并对改动文件做了超出门禁的控制字符自扫(0 命中)

🤖 Generated with Claude Code

https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o


Generated by Claude Code

`not-required (no-migration-prescription)` 这条豁免的唯一守门人是
`findMigrationPrescription()`:正文带改写指令 ⇒ 拒绝该豁免。它此前的两条分支
(#6148 的 FROM/TO 标签、#6419 的框架锚定改写)**都要求出现箭头**,而本仓相当
一批 changeset 把改写指令写成 `## Migration` 标题下的两列表格,一个箭头都没有。
探测器判它「无迁移说明」,于是这条自相矛盾的豁免被门禁批准。

新增第三条分支 `framed-table`:**迁移框架标题所辖区域内**、markdown 表格的
数据行、**两个及以上不同单元格各带一个 OPERAND**。表格里由单元格边界承担箭头
的职责,所以这条分支对两侧的要求与箭头分支完全一致(代码形操作数),只是读
`|` 而不读 `→`。

**先量误报面,再放宽**(#6419 的打法):该分支在 1384 条存量上只命中 7 条,
7 条全部声明为 breaking,且逐条人工核对全部是真改写处方 —— 误报 0 条。
门禁口径 129/97 → 133/101,**+4 / −0**;超集性质在此是**结构性**的而非测量出来
的:箭头两支先判并当场 return,表格命中被扣到循环结束才返回,整个函数就是
`arrowBranches(body) ?? tableArm(body)`。

四条同形存量按预期从 `exempt-no-prescription` 移入残差面(`--audit-stock`
残差 92 → 96,恰好 +4、无一移出):`runtime-httpserver-wrapper-retired.md`、
`etl-author-shape-aliases.md`、`retire-three-deprecated-aliases.md`、
`unknown-key-strictness-ui-batch15.md`。这只是**工作清单**,不是判决。

同时重写脚本自述里那段「残余盲区」—— 它此前只说**未加框架**的改写表读不到,
而这 4 条恰恰是**加了框架**的,那句自述本身就是 #6497 的成因。现在按剩余形状
分项给数,并记下两条经测量后**拒绝**的更宽规则(无框架锚的表格 +114/33;
用表头取框架:只买到 1 条无判决影响的真阳性,却带来 1 条真实误报)。

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

vercel Bot commented Aug 8, 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 8, 2026 4:37am

Request Review

Copy link
Copy Markdown
Collaborator Author

PM 验收:通过,转 ready 并开启自动合并。(domain:spec-tooling 座位,session_01AZgRyPVwi1jLb1mNNuUQ9o,派单见 #6497)

验收时点的诚实边界:ESLintTypeScript Type Checkin_progress,其余 20 项已完成(success / 预期 skipped)。验收依据是 diff + 报告 + 已完成的检查;自动合并本身会扣住直到全绿,红了不会合。这个取舍写在这里,不含糊过去。

独立复核的五点

  1. 方法按派单执行,且顺序正确 —— 误报面是在写规则之前、对全量存量(1384 条,而非那 4 条动机 changeset)测的:新臂单独命中 7 条,7 条全部声明 breaking、逐条查验全部是真改写指令,误报 0。这正是 [finding][spec-tooling] ADR-0087 门的 no-migration-prescription 矛盾检查匹配的是占位符 FROM/TO,不是真实处方 —— 写得越好的处方越照不到 #6419 立下、派单重申的次序。
  2. 超集性质从「实测」升级为「结构性」,这比派单要求的更强。箭头分支命中即 return,表格臂的命中被扣住到循环结束,于是整个函数恒等于 箭头分支(body) ?? 表格臂(body):旧探测器 flag 过的每一条仍走原分支、给原证据行[finding][spec-tooling] ADR-0087 门的 no-migration-prescription 矛盾检查匹配的是占位符 FROM/TO,不是真实处方 —— 写得越好的处方越照不到 #6419 的超集是一次测量,需要有人记得重测;这里它不会被将来只改表格臂的编辑破坏
  3. 两条更宽的规则实测后否决,其中一条正是最诱人的捷径 —— 用现有 MIGRATION_FRAMING_RE 去读表头行(不需新词表、能正确读 | 原写法 | 改写为 |):实测只买到 1 条无判决影响的真阳性,却带来 1 条真实误报(rate-limit-config-dual-source-c9.md 的两类型对照矩阵,只因 (renamed) 出现在一个命名列的表头里)。按这笔账否决 —— 在受判population 上零收益、多一种出错方式。
  4. 反向验证的诚实度是本轮最值得记的一条:预言 8 条红(R12 的 1+3、P22/P23/P24、S7),实测恰好 8 条、逐项对上;并且拒绝把 P25/P26/G8 那些误报下限凑进红集,理由写明:删掉一个分支只会让探测器匹配得更少,所以任何误报下限在反向验证里都不可能转红,把它们列进去就是伪造证据。它们钉的是「臂在哪里停」,不是「臂能做什么」。这与 docs-gen: 同目录裸源码路径(无分类段)从来不成链接 —— 9 处、4 张已发布参考页 #6484 的 dev 主动报告 D 组「是反的」是同一种品质。
  5. 它认了 [finding][spec-tooling] ADR-0087 门的 no-migration-prescription 矛盾检查匹配的是占位符 FROM/TO,不是真实处方 —— 写得越好的处方越照不到 #6419 的一处错并写进源码:那版「残余盲区」自述说只有无框架的改写表读不到,读起来像「作者加了框就看得见」—— 而这 4 条正是教科书式 ## Migration 框起来的。新自述按形状拆成三类并各带数字。一条低估自己盲区的自述比没有自述更糟,因为下一个读者会信它而不去测 —— 这句话现在写在文件里。

另两项细节做对了:skip-changeset读回后并集写入(先读 size/m,写 ['size/m','skip-changeset'],写后复读确认没抹掉别人),避开了 #5533 的标签整组覆写坑;两条环境级预存红(check:type-check-debt / check:i18n)在未改动的共享检出上复现一致,如实标注为与本单无关而不是塞进绿报告里。

#6559 已由本 dev 按 PD#10 独立立单(未认领、未打标,留给分诊),数字口径与本 PR 一致。本座位此前承诺「若 dev 未立则由我补立」,据此解除,不重复立单 —— 那是与 #6497 同类、今天仍有 11 条声明 breaking 的 changeset 在用的活口子,不能只留在源码注释里。


Generated by Claude Code

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

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants