返回博客

PR 验证 grep 与 Required Changes 各自维护——「PASS」背后的静默漏检

做完一次文档大清理 PR,所有检查绿了,合并进去,感觉很舒服。几周后有同事问:「这段描述已经过期了,为什么 PR 里没删?」翻回去一看,PR 的 Required Changes 确实列了这一条,但验证 grep 里根本没有这个词——post-commit 跑出来空输出,机器认为 PASS,人也认为 PASS,就这样静默漏掉了。

现象

一个文档清理 PR 的目标是「从 README / runbook 中移除 N 个过期锚点」,PR body 里写了两块内容:

  1. Required Changes:编号列表,每条给出文件行号 + 待删除的原文短语。
  2. 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 对此毫无感知,照样跑出空输出。

坑:验证 grep 的交替项 < Required Changes 条数时,会静默放行

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,等作者对齐后再合。

正解:让 grep 从 RC 机械导出,并做双向命中数断言

导出方向:Required Changes → grep(不要反向)。提交前跑「应命中 = RC_count」,提交后跑「应为空」。在 PR marker 里并排写 stale_anchors_removed=N · verification_regex_alternation=N,两个数字不等时直接可见。

一句话外卖

验证 grep 只证明「它自己盯住的词不见了」——要想证明「PR 要删的东西都删了」,必须保证 grep 的交替项与 Required Changes 一一对应,且数量相等。