Skip to content

机械自检脚本:cite + state 两类可判定错误,走 CI 强制 - #54

Open
jhfnetboy wants to merge 1 commit into
mainfrom
chore/pr-selfcheck
Open

机械自检脚本:cite + state 两类可判定错误,走 CI 强制#54
jhfnetboy wants to merge 1 commit into
mainfrom
chore/pr-selfcheck

Conversation

@jhfnetboy

Copy link
Copy Markdown
Member

从 PR-Daemon 一千余轮 review 的记录里按错误类别聚合,只挑出判错了一定是脚本 bug、而不是口味问题的两类落地:

检查 抓什么 为什么可判定
cite 引用指向不存在的文件 / 解析不到的 commit 路径存不存在没有解释空间
state jq … > tmp; mv&&-s 守卫 jq 失败时 shell 已建好空 tmp,mv 照样成功 → 状态被清空

故意没收的三类(尽管出现频率更高):测试存在但不承重 35.1%、常量跨文件只改一处 5.4%、set -e 下退出码被吞 6.5%。它们都需要读懂业务才能判,放进来必然误报——一个被普遍 --no-verify 掉的闸门比没有闸门更糟,它让人以为已经检查过了。

作用域是双向的

cite 既扫「新增行引了不存在的路径」,也扫「本次删除/改名的文件仍被存量 md 引用」。只做前者会漏掉最典型的一类:引用行是存量、被引文件是本次删掉的——实测在 airaccount-contract#199 上就是这么漏的。两边都不扫全仓存量,否则每个 PR 把仓库历史债务重报一遍(实测 26 报里 25 条与本次改动无关)。

为什么是 CI 不是 pre-commit

--no-verify 一句就绕过去,而且要每人各装一次、装了不会更新。CI 每个 PR 强制一次,升级只改这里一处。本地想提前跑:bash scripts/pr-selfcheck.sh

实测

  • YAML 用 yaml.safe_load 真解析过,不是只跑了步骤命令
  • 正/负控:无守卫 jq→tmp→mvexit 1;带 && 的同一写法 → exit 0
  • 删除正控:删掉一个被 3 个 md 引用的脚本 → 3 条 FAIL;不删 → 0 FAIL
  • 在 AirAccount#196 head 上跑:1 FAIL 0 误报,命中的正是我几轮 review 里反复提的那条 carry-over 根因
  • 脚本自己不用 set -uwhile read 到 EOF 时会误退出)——注释里写明了,这正是 state 要抓的同一种毛病

不要 merge,等你确认。

从 PR-Daemon 一千余轮 review 记录里按类聚合,只有 cite / state 两类是判错了
一定是脚本 bug 而不是口味问题。高频的另外三类(测试不承重 35.1%、常量半扫
5.4%、set -e 吞退出码 6.5%)都需要语义判断,放进来必然误报——一个被普遍
--no-verify 掉的闸门比没有闸门更糟,它让人以为已经检查过了。

cite 两个方向都扫:新增行引不存在的路径,以及本次删掉的文件仍被存量 md 引用。
只做前者会漏掉最典型的一类(实测在 airaccount-contract#199 上漏过)。

走 CI 不走 pre-commit hook:后者一句 --no-verify 就绕过去了。

@clestons clestons left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ REQUEST_CHANGES — 2a23b653 首审 [4-round]

利益冲突,先说清楚:这个 PR 的内容来自 PR-Daemon 自己的 review 记录,脚本形态也是我出的,
上一轮 watcher 因此把它标成了 SKIPPED_SELF_AUTHORED。所以下面没有一条是"我觉得" ——
全是可复跑的机械证据(探针文件 + 实跑输出 + CI 实况),口味类意见一条都不提。
最硬的一条是 GitHub 自己给的:这个闸门在它自己的 PR 上是红的。


🔴 [Blocking] 闸门在自己身上就是红的,而且红在一行注释上

$ gh api .../commits/2a23b653/check-runs
selfcheck  completed  failure          ← 不是我构造的,是 PR 现在的真实状态

$ cd <PR head> && bash scripts/pr-selfcheck.sh --base $(git merge-base HEAD origin/main)
FAIL  scripts/pr-selfcheck.sh:113 有 `jq … > *.tmp` 后直接 mv,缺 && 串联或 -s 非空守卫
汇总  FAIL=1  rc=1

$ sed -n '113p' scripts/pr-selfcheck.sh
# `jq … "$F" > "$F.tmp"; mv "$F.tmp" "$F"` 里,jq 失败时 shell 已经把 .tmp 建成空文件,

第 113 行是这个检查自己的说明注释。 state 那段完全没有注释过滤,所以任何文档化了这个反模式的
shell 文件
都会被判成缺陷 —— 包括写它的那个文件。

这条不是"上线后再迭代"能扛过去的,因为它直接踩中脚本自己第 7-8 行写的那句话:

误报……第一天就会被 --no-verify / [skip ci] 绕过去,那比没有更糟 —— 一个被普遍绕过的闸门
会让人以为已经检查过了。

第一个 PR 就误报,而且误报的是它自己。


🔴 [Blocking] state 的判据是词法共现,不是它声称的守卫关系 —— 实测三个场景全判反

第 126-127 行的实际控制流是「窗口三行内出现任意 &&[ -s ] → 无条件放行;否则出现任意 mv → 报错」。
它从不要求那个 && 串联的是这一对 jq/mv,也不要求 -s 检查的是对应那个 tmp。

我用一个文件同时打三发:

 1  #!/bin/bash
 2  # 场景1:真 bug,但窗口内有一行无关的 &&
 3  jq '.a=1' "$F" > "$F.tmp"
 4  [ -n "$X" ] && echo "无关的一行"        ← 与这次写入毫无关系
 5  mv "$F.tmp" "$F"
 6
 7  # 场景2:真 bug,但 mv 在 +3 行
 8  jq '.b=1' "$G" > "$G.tmp"
 9  echo pad1
10  echo pad2
11  mv "$G.tmp" "$G"
12
13  # 场景3:纯注释里提到这个反模式(不该报)
14  # 千万别写 jq . x > x.tmp 然后 mv x.tmp x

实跑:

FAIL  t.sh:14        ← 注释,误报
(第 3 行:漏)          ← 真 bug
(第 8 行:漏)          ← 真 bug

真 bug 0/2,误报 1/1。 而第 116 行写的是「判据只有一条:mv 之前有没有 && 串联或 -s 非空检查。
零解释空间」—— 实现和这句话不是同一件事。

场景 1 一点也不刁钻:任何在写状态文件前后有一行 [ -f x ] && ... / [ "$v" = y ] && ... 的脚本都会中招,
而这类行在状态机脚本里到处都是。场景 2 更常见 —— 中间隔一行 chmodsync 就超出 3 行窗口了。

Codex PK 独立复核结论:CONFIRM(攻不动),并给了一个比我更准的说法:

它实现的是词法共现,不是注释声称的守卫关系。两次 grep 没有要求 && 串联的是目标 jq
目标 mv-s 检查的是对应 tmp、mv 搬运的是对应 tmp —— 三者甚至不需要位于可执行代码中。

Codex 也替我否掉了「注释误报是有意为之(想连文档里的坏示范一起禁)」这个辩护:检查对象被明确限定成
*.sh|*.bash、根本不扫 markdown,所以那个解释与实现范围不符。

可以怎么修(我把它跑通了,不是纸上建议)

窗口 + 关键字这个形状本身是病根 —— 我第一版修复(把窗口从 3 行放宽到 8 行)自己又漏了一发:
它捞到了下一个代码块[ -s ] 当成守卫。放宽窗口只是把假阴换个地方。

真正跑通的形状是按 tmp 路径配对:从 jq 行提取那个 tmp token,向前找引用同一 token 的行,
只在这两行之间判守卫;顺带跳过注释行、且守卫分支要先于 mv 分支判(否则 [ -s "$H.tmp" ] && mv … 这种
安全写法会被误报 —— 这是我第二版踩的坑)。四个用例全对:

t.sh   → 3, 8 红            14(注释) 不红      ✅
u.sh   → 只有第 1 行红       && 串联、[ -s ] 两种安全写法都不红   ✅
本 PR 的 pr-selfcheck.sh    → 0 条                            ✅
最小正控 jq→tmp / mv        → 红                              ✅

🟠 [Medium] cite 方向①:basename 回退被无条件套到了完整路径

第 79 行「散文里常只写文件名不写完整路径,仓内存在同名文件就不算悬空」这个回退是对的,但它对
带斜杠的完整路径也照样生效。实测:

① `docs/completely/made/up/README.md`  (纯编造,但 basename 全仓有同名) → 漏 ❌
② `docs/made-up/nowhere-xyz.md`        (basename 全仓无同名)            → 报 ✅

于是凡是编造路径时用了常见文件名(README.md / index.ts / package.json / config.json),一律漏
—— 而这恰恰是 PR 描述里说方向①存在的理由(「新写的文档里编造的路径」)。

修法是一行:回退只在 $p 不含 / 时才走。

-        git ls-files "*/$bn" "$bn" 2>/dev/null | grep -q . && continue
+        case "$p" in */*) ;; *) git ls-files "$bn" 2>/dev/null | grep -q . && continue ;; esac

🟡 [Low] cite 只认反引号,markdown 链接形式不扫

③ [看这里](scripts/does-not-exist-at-all.sh)   → 漏
④ 见 ./scripts/also-missing-zzz.sh              → 漏

第 74 行的正则要求两侧反引号。而 md 里引用文件最常见的写法恰恰是链接。这条我不当阻塞
(可以是有意的取值范围),但建议在脚本头注释里写明"只扫反引号内的引用" —— 否则第一个被漏掉的人
会以为闸门覆盖了链接。

ℹ️ 顺带:R1b 报的 workflow 注入是假阳,但方向对

DeepSeek 安全轮报 ${{ github.base_ref }} 插进 run: 是脚本注入。这个模式类别是对的,但落错了字段:
base_ref 只能是目标仓库里已存在的分支名,fork 的 PR 作者控制不了它;真正可被攻击者控制的是
head_ref(fork 分支名)和 pull_request.title。本 workflow 没用到那两个,所以现状安全。
不阻塞,但值得记一笔:如果以后往这个 run: 里加 head_ref,就变成真洞了。


我认可的部分

  • 双向扫描的论证是对的,而且理由有实测支撑(只做方向①会漏「存量文档 + 本次删掉被引文件」)。
  • 「不扫全仓存量」是正确取舍 —— 26 报 25 误的话闸门第一周就死了,这个判断和上面 Blocking 那条
    是同一个道理,只是这个 PR 自己在 state 上没做到。
  • 短 SHA 要求含 a-f 字母 防住 chainId 纯数字误报,这类细节是踩过才知道的。
  • 走 CI 不走 pre-commit 的理由成立,fetch-depth: 0 也确实带上了(少了它 merge-base 会静默退化成
    "无改动 → exit 0",闸门就变成永远绿的摆设 —— 这一点作者已经注意到了)。

结论:方向对、取舍对,但两个检查里目前有一个的判据与它要表达的语义没有可靠对应,而且第一个 PR 就
误伤了自己。
state 换成 tmp-token 配对(上面已跑通)、cite 的 basename 回退加上"仅无斜杠"限定,
就可以重跑。


各轮结论

轮次 模型 结果
R1a deepseek-v4-flash 2 条,均未命中要害;且明确写下 "The state check correctly guards against empty-file overwrites" —— 与实跑相反
R1b deepseek-v4-flash 1 条 workflow 注入:模式类别正确,字段判错(base_ref 非攻击者可控)
核验 本轮主体 3 个 state 场景探针 + 4 个 cite 引用形态探针 + 在真 PR head 上实跑 + CI check-runs 实况 + 两版候选修复(第一版自己也漏,第二版四例全对)
R3 PK Codex gpt-5.6-sol CONFIRM(攻不动);四个攻击角度逐条否掉,并把结论精确化为"词法共现而非守卫关系"
裁决 Sonnet REQUEST_CHANGES

机械证据:worktree HEAD 2a23b653 与 REST pulls/54 核对一致;在真 PR head + 真 merge-base
(ae441b23)上实跑 → FAIL=1 rc=1,命中行经 sed -n 113p 确认为注释;GitHub check-runs 实测
selfcheck completed failure;state 三场景探针(2 真 bug + 1 注释)实跑输出如上;cite 四形态探针
(编造完整路径/编造罕见路径/md 链接/相对路径)实跑输出如上;候选修复两版分别在
t.sh/u.sh/本 PR 脚本自身/最小正控 四个用例上回归。所有探针跑在 scratch 仓库与一次性 worktree 里,
Brood 主 checkout 未改动,worktree 已清理。

PR-Daemon v4 · 4-round · 本 PR 内容源自本 daemon 自身的 review 记录,冲突已在开头声明

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants