> 手写的自检清单永远是全绿的——它只包含你刚修过的东西

Research Tools Intermediate 2026-10-08 05:57 2026-10-08
#tooling #audit #self-check #failure-modes #provenance

给工具链写自检时,检查项的覆盖面必须从被检查的东西推导出来,不能手写清单。

结论先行

给工具链写自检时,检查项的覆盖面必须从被检查的东西推导出来,不能手写清单。

手写清单有一个躲不掉的偏差:你想得起来的就是你刚改过的, 于是清单里装的正好是刚修好的那批,跑出来必然满分, 而没改的那批既不在清单里,也就永远不会变红。

一次实测,同一天同一套工具:

自检项写法 报告 真实情况
写死 7 个工具名 7/7 通过 另有 19 个文件 36 处是旧写法
扫全树枚举 当场报 36 处失败 同一时刻的同一批代码

假绿比没有检查更糟,因为它会让你停止找。

为什么

统一 --json 接口那轮改了 9 个工具,自检项照着改动写: 「这 7 个工具在 --json 不带值时必须把合法 JSON 打到 stdout」。 它一直是绿的,直到下一轮把它改成「扫 ~/Tools 与 ~/.claude/skills 下所有 .py, 任何一处 add_argument("--json" 不是 nargs="?" 就算失败」。

同一个毛病在另一处更严重:43 个技能里有 17 个声明了后端脚本却没有任何自检项。 自检有 40 项,看着覆盖得很密,但那 40 项是一条一条手写的, 覆盖的是做过工单的那些技能。改成「从每个 SKILL.md 里把它声明的后端路径抄出来, 逐个实跑」之后,覆盖数从 11 个技能跳到 36 条后端路径, 而且以后新加技能自动进入覆盖,不需要记得来补。

这一类和 最危险的工具故障是"静默少做"——技能在、后端不在 是同一个根: 故障不会报错,只会少做一层,报告照常是绿的。

怎么用

判断标准:这个检查项的覆盖面,是从被检查物推导的,还是我打字打出来的?

三种可枚举的推导源,按优先级:

  1. 从声明推导——工具/技能的定义文件里写了什么依赖,就检查什么。
    grep -rhoE '(~|\$HOME)/(Tools|\.claude/skills)/[A-Za-z0-9._/-]+\.(py|sh)' */SKILL.md \
      | sort -u | while read p; do timeout 30 python3 "$p" --help >/dev/null || echo "BROKEN $p"; done
    
  2. 从代码树推导——接口一致性这种,直接扫全树 AST/正则,跳过 vendored 目录。
  3. 从产物推导——比如「每个 .md 报告都必须能被 json.load 的兄弟命令解析」。

豁免必须写在被检查的源文件里,不能写在检查器里。 一个工具的 --json 是读入后端报告的参数而不是输出开关,它在自己的声明处写:

ap.add_argument(  # --json: input(读入参数,不走全局输出约定,枚举检查按这行跳过)
    "--json", required=True, help="后端的 JSON 报告,'-' 表示 stdin")

检查器只认这行注释。要是把豁免写进检查器的白名单, 白名单就会跟着改动走,又回到清单模式的假绿。

最后一条:自检项本身要在修复之前改成枚举式。 先改检查器,让它报出真实的失败数,再去修; 反过来做(先修再补检查)永远不知道分母是多少。

三个后来才踩到的坑(2026-09-07)

改成枚举之后又栽了三次,都是同一个道理的下一层:

一、枚举源本身也可能是清单。 「从每个 SKILL.md 里抄出声明的后端」听起来 够枚举了,但它只覆盖有人声明过的东西。一个工具当天被改坏、带着坏状态提交, 一直到手工排查才发现——它谁都没声明,所以谁都没在看它。把枚举源换成 「版本库跟踪的、带 __main__ 的 .py」之后覆盖才是全集,而且不依赖有人记得去声明。

判据升级:这个检查的范围,是从「谁登记过」推导的,还是从「实际存在什么」推导的? 前者仍然是清单,只是清单换了个人维护。

二、检查声明不等于检查行为。 有一项断言所有工具的 --json 都声明成 「可选值」形态,一路全绿;而其中三个子命令声明对了、代码还在走旧分支, 给了路径也不写文件。接口的声明和接口的实现是两个东西,要分别断言。

三、间歇性红和假绿一样有害。 一项安全扫描的检查每次都去远端拉规则集, 拉不到就红:完整跑五次红两次,单独跑三次全绿。它教会你的行为是「重跑到绿为止」, 而那恰好也是真出故障时你会做的事——于是这一项两种情况下都不起作用。 拆成两项:引擎用本地规则离线实跑(红=真坏了),网络那层单独查、重试一次才算数。

出处