PR 验证 grep 与 Required Changes 各自维护——「PASS」背后的静默漏检
做完一次文档大清理 PR,所有检查绿了,合并进去,感觉很舒服。几周后有同事问:「这段描述已经过期了,为什么 PR 里没删?」翻回去一看,PR 的 Required Changes 确实列了这一条,但验证 grep 里根本没有这个词——post-commit 跑出来空输出,机器认为 PASS,人也认为 PASS,就这样静默漏掉了。
现象
一个文档清理 PR 的目标是「从 README / runbook 中移除 N 个过期锚点」,PR body 里写了两块内容:
- Required Changes:编号列表,每条给出文件行号 + 待删除的原文短语。
- Verification grep:post-commit 用
rg -nE "phrase1|phrase2|..."验证,期望返回空。
PR 被 reviewer 扩展过一次:v0 列了 4 条,reviewer 评论「还有 6 条同类锚点」,作者补全为 10 条 Required Changes,但验证 grep 的交替列表还停留在 8 个 token。
合并后跑 grep:8 个已删的 token 都不在文件里,输出为空 → 报 PASS。实际上 2 条锚点在文件里原封不动地活着。
根因
两个数据结构描述同一件事,却各自独立维护:
Required Changes (10 条)
──────────────────────
1. file.md:125 "过期短语 A"
2. file.md:165 "过期短语 B"
...
10. file.md:379 "过期短语 J"
Verification grep (只有 8 个 token)
──────────────────────────────────
rg -nE "过期短语 A|过期短语 B|...|过期短语 H" file.md
PR 每次修订都只有作者手动同步两侧,没有任何机制保证它们一致。reviewer 加了 6 条,作者复制进 Required Changes,忘了同步 grep——grep 对此毫无感知,照样跑出空输出。
post-commit grep 返回空输出只能说明「被 grep 盯住的那些词确实不见了」,无法证明 Required Changes 里的所有词都被删干净。两份列表只要有一条不一致,验证就在技术上正确、语义上撒谎。
正解
1. 机械地从 Required Changes 导出 grep
不要手写 grep 交替列表,而是把 Required Changes 里的每条短语原文 copy-paste 过来。
# Required Changes:
# 1. docs/runbook.md:80 "初始化 Phase 1 仪式"
# 2. docs/runbook.md:125 "待迁移模块 × 4"
# Verification grep(从 RC 机械导出):
rg -nE "初始化 Phase 1 仪式|待迁移模块 × 4" docs/runbook.md关键:逐字拷贝原文。哪怕一个全半角差异、一个空格差异,都会让这个 token 变成无效匹配,变相变回静默漏检。推导前先在原始文件上单独跑每个 token,确认命中行号与 RC 里写的一致。
2. 显式断言交替数 == RC 条数
在 PR marker 字段里并排写两个数字,让 reviewer 一眼看出是否对齐:
stale_anchors_removed=10 · verification_regex_alternation=10
验证 grep 里有 N-1 个 | 分隔符,则 alternation 数量 = N。把这个计数器加进 PR 描述,强迫自己在每次扩展 RC 时同步更新。
3. 提交前先跑「应命中检查」
post-commit 验证期望输出空;提交前应该先反向跑一次——验证 grep 在未修改的文件上应该命中 RC_count 次:
# 提交前:grep 应命中 10 次(RC 里每条都在文件里)
rg -nE "短语A|短语B|...|短语J" docs/runbook.md | wc -l # 应等于 10
# 提交后:grep 应返回空(全部删干净)
rg -nE "短语A|短语B|...|短语J" docs/runbook.md # 期望为空如果提交前命中数 < RC_count,说明 grep 里有 token 写错了或漏了,还没到提交阶段就能发现。
4. Review 时的 4 步快速核验
| 步骤 | 动作 | 期望 |
|------|------|------|
| ① | 数 Required Changes 条数 → RC_count | 得到一个数字 |
| ② | 数 verification grep 交替项数(| 个数 + 1) | 应等于 RC_count |
| ③ | 在当前文件跑 grep(未 merge 状态) | 命中数 = RC_count |
| ④ | 逐条比对 RC 短语与 grep token | 每条都能在 grep 里找到对应 token |
任何一步不符,打 NEEDS-WORK,等作者对齐后再合。
导出方向:Required Changes → grep(不要反向)。提交前跑「应命中 = RC_count」,提交后跑「应为空」。在 PR marker 里并排写 stale_anchors_removed=N · verification_regex_alternation=N,两个数字不等时直接可见。
一句话外卖
验证 grep 只证明「它自己盯住的词不见了」——要想证明「PR 要删的东西都删了」,必须保证 grep 的交替项与 Required Changes 一一对应,且数量相等。