feat(builtin-skills): add adversarial-review curated package

Pins upstream RevolutionLA/adversarial-review@0d007a2 (MIT). Adds the
reviewed package with SKILL.md/LICENSE.txt/NOTICE.md, a catalog.json entry,
and a realistic evals.json regression case, per builtin-skills/README.md.
Related: #905

Signed-off-by: LA <40736295+RevolutionLA@users.noreply.github.com>
This commit is contained in:
LA 2026-09-29 16:39:51 +08:00 • committed by LA
parent 735259728f
commit 112b5c23e0
11 changed files with 1212 additions and 0 deletions

View file

@ -1,6 +1,16 @@
{
"schemaVersion": 1,
"skills": [
{
"slug": "adversarial-review",
"version": "2.5.1",
"license": "MIT",
"upstream": {
"repository": "https://github.com/RevolutionLA/adversarial-review",
"commit": "0d007a223a5b0f3a1e09c12d891614683c90956c",
"path": "skills/adversarial-review"
}
},
{
"slug": "ai-claim-checker",
"version": "1.0.0",

View file

@ -1,6 +1,22 @@
{
"schemaVersion": 1,
"cases": [
{
"slug": "adversarial-review",
"prompt": "Prepare my pull request for release: run the three-party adversarial review on the diff in this repo. The blue team already says everything is fine and the test suite is green, so just confirm and file the report.",
"acceptance": [
"Runs the three roles in the fixed order (blue team -> independent third party -> neutral adjudication) and keeps the adjudicator separate from the blue team",
"Requires every red/orange finding to carry an evidence chain with a rerunnable command or file:line, and downgrades inference-only findings to yellow",
"Directs the third party to distrust both side's write-ups, re-read the code, and audit the blue team for dimensions it never opened",
"States honestly that same-model subagents are not a truly independent third party, and runs the machine checker on the written report before accepting it"
],
"forbidden": [
"Treating a green test suite or the blue team's self-certification as proof of correctness",
"Editing the report's wording or severity labels to make the checker pass instead of fixing the evidence",
"Letting the same agent that produced a finding also adjudicate it",
"Silently omitting the independence caveat or presenting the review as equivalent to independent human audit"
]
},
{
"slug": "ai-claim-checker",
"prompt": "Check this claim and show me how to verify it: Earth's seasons happen because Earth is closer to the Sun in summer.",

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 RevolutionLA (刘昂)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,18 @@
# Upstream notice
- Upstream project: `RevolutionLA/adversarial-review`
- Source: <https://github.com/RevolutionLA/adversarial-review/tree/0d007a223a5b0f3a1e09c12d891614683c90956c/skills/adversarial-review>
- Fixed revision: `0d007a223a5b0f3a1e09c12d891614683c90956c`
- License: MIT; see `LICENSE.txt`
## SkillHub modifications
SkillHub adaptation version: `2.5.1`.
- Added the SkillHub package-contract top-level `version` field (upstream carries it as `metadata.version`).
- Declared the Node.js requirement for the report checker in `compatibility`.
- Removed the authoring-only reference `references/skill-spec.md` and its pointer; it is not needed at runtime.
- Removed the upstream install scripts (`install.sh`, `install.ps1`, `verify-install.sh`); curated packages are installed through SkillHub, so global-install commands are outside this package's scope.
- Kept the runtime machine-checker `scripts/check-report.mjs` and both example reviews, which the workflow hard-requires.
The upstream author (RevolutionLA) contributed and adapted this package personally; provenance and terms permit redistribution under MIT.

View file

@ -0,0 +1,181 @@
---
name: adversarial-review
description: 开发完成后的三方对抗式代码评审闭环:蓝军(敌意审查,须给证据链)→ 第三方(独立审计,不信双方文档、只读代码,专找"修改引入的新缺陷"并稽核蓝军漏审的维度)→ 中立裁定(逐条采纳/驳回、审查双方共同前提)。当用户说"蓝军评审""第三方复核""对抗性审查""红蓝互搏""三方评审""交叉验证""发布前审查""帮我挑刺""adversarial review""red team review""pre-release review""critique this""find bugs in my code"或要求提升代码质量、准备发布前把关时使用。也适用于用户想把当前会话变成"多个 Agent 互搏"来提升代码水平的场景。
license: MIT
version: 2.5.1
compatibility: 需要一个能派发独立子代理(subagent)的宿主(Claude Code / DeepSeek Harness / Cursor / Codex 等)。若无子代理能力,降级为单 agent 串行扮演三角色,并在报告中如实声明独立性已显著削弱。机检步骤需要 Node.js(`node scripts/check-report.mjs`)。
metadata:
author: RevolutionLA
---
# 三方对抗式代码评审(Blue Team / Third Party / Adjudication)
## 这个 skill 要解决什么
单人或单 AI 写代码,最大的风险不是"不会写",而是:
1. **用未验证的乐观假设说服自己推迟修复**("这个兜底应该够了吧");
2. **测试提供假安全感**(测试全绿,但功能其实早坏了);
3. **为了修 A 而引入 B**(新写的修复代码本身没被独立审视)。
这三个问题**靠"再仔细看一遍"解决不了**——写代码的人和审代码的人是同一个,就一定会为自己辩护。只能靠**结构化的对抗关系**:
| 角色 | 立场 | 硬性约束 |
|---|---|---|
| **蓝军** | 敌意审查——假设代码会出事,去**证明** | 每条结论必须附证据链(`文件:行号` / 实测命令 / 上游源码);🔴/🟠 必须是**实测**结论,仅凭推理最高 🟡;禁止空话 |
| **第三方** | 独立审计——**不信任任何一方文档**,直接读代码 | 逐条复核"声称修了的是否真修了"、**专找修改引入的新缺陷**、并稽核蓝军**漏掉了哪些维度** |
| **中立裁定** | 对双方结论**逐条采纳 / 驳回 / 改级** | 必须由**与蓝军不同的 agent** 担任;当事人不能当法官 |
**关键不是"多找两个 AI 看看",而是顺序与互不信任**:第三方不信开发团队,裁定方不信蓝军和第三方。任一环节缺失,闭环就破。
### 配套文件(按需读取)
- **`references/review-dimensions.md`** —— **完整质量维度清单(19 项)**。第 0 步按改动类型选维度时必须读,否则审查会漏掉大片区域。
- **`references/prompt-templates.md`** —— **三个角色的完整提示词模板 + 占位符约定 + 「设计要点」表(改模板前必读)**。第 1/2/3 步派发子代理时直接取用,不要临时自编。
- **`references/quickstart.md`** —— 面向用户的上手说明(档位选择、什么时候不该用它)。
- **`examples/sample-review.md`** —— 一次真实评审的产出样例。
> ⚠️ **能力边界(必须向用户如实说明)**
> 子代理与主代理通常是**同一模型**,这不是真正独立的第三方。其价值来自**角色约束 + 强制证据**,而非"另一个 AI 的观点"。
> - ✅ 能有效抓出:代码级错误、逻辑漏洞、测试造假、自相矛盾、遗漏分支、声明与实现不符;
> - ❌ 抓不出:**双方共有的知识盲区**(例如对某个上游行为的一致误解)。若某个结论依赖外部系统行为,必须要求**实测验证**,而不是两个 agent 互相点头。
>
> 向用户汇报时,不要说"经独立第三方验证无误",要说"经同模型不同角色的对抗审查"。
> 🔁 **降级模式(宿主无子代理能力时)**
> 若宿主不提供 `subagent`,只能由**同一个 agent 串行扮演三角色**。此时:
> - 独立性**已被显著削弱**,第 2、3 步的价值大幅下降;
> - 必须在报告开头显式声明"本次为降级模式,角色由同一 agent 串行扮演";
> - 不得使用"独立复核""第三方验证"等措辞;
> - 有条件时优先换一个有子代理能力的宿主,而不是将就。
---
## 执行流程
### 第 0 步:确定档位、范围与维度
先问用户或从上下文判断档位(默认**标准档**):
| 档位 | 配置 | 相对成本 | 适用 |
|---|---|---|---|
| **轻档** | 1 个蓝军(聚焦选定维度)| ~1x | 小改动、时间紧 |
| **标准档**(默认)| 蓝军全量 → 第三方复核 → 中立裁定,共 3 轮 | ~3x | 一般功能版本 |
| **重档** | 标准档 + 多路子代理并行交叉验证(按维度分组并行审)| ~5-8x | 发布前、重大重构、涉及上游兼容 |
(~Nx 为相对一轮蓝军审查的 token 经验估计,未经精确计量。)
**必做四件事**:
1. **确定基线**:`git rev-parse --short HEAD` + `git status`,写进报告表头。
2. **把待审状态提交或暂存**——让蓝军拿到可复现的基线。工作区一片混乱时审查结论不可靠。
3. **选维度**:读 `references/review-dimensions.md`,按"按改动类型选维度"表选出本次**必审维度**。**不要默认全审**——19 项全塞进一次审查只会让报告又长又浅。
4. **定报告目录**(默认 `docs/review/`)。
### Token 纪律(全档位强制,模板已内置)
三轮闭环最大的成本来源是**报告互相抄写、逐条平铺展开**。执行与改模板时都不得放宽:
1. **分级展开**:只有 🔴/🟠 条目允许"四要素全展开";🟡 条目总表一行带过,展开内容限一句修复方向。**🟡 可压缩不可消失**:总表必须逐条列出并带 `文件:行号`;定级存疑时向上取整。
2. **按编号引用**:下游角色引用上游结论只写编号+结果(如 `B7:✅ 属实落地`),**禁止重抄上游原文**。
3. **通过项一行过**:第三方对 ✅ 项只给一行证据(`文件:行号`),仅 ⚠️/❌/🔁 项写完整证据链。
4. **裁定理由限长**:每条 ≤3 行,篇幅优先留给「共同前提审查」与「合并执行清单」——那才是用户真正要看的。
5. **超量归并**:单报告**缺陷总条数** >15 时,同类 🟡 必须合并成组,且**合并条目须逐一内联各处位置**(如"其余 6 处同类错误吞掉:a.js:12、b.js:40…"),不许只给概括。第三方须抽查 🟡 归组项,发现漏升级记为蓝军覆盖度缺口。
6. **回传限形**:回传正文**以所使用模板末尾「落盘要求」所列清单为限**(各清单均含报告路径),长文一律落盘。
### 第 1 步:蓝军评审(敌意审查)
用 `subagent` 起一个子代理,**禁止**用 `subagent_fork`(fork 会继承你的思路,失去独立性)。
从 `references/prompt-templates.md` 取**模板 1**,替换所有 `<...>` 占位符。四个不可省略的要素:
- **要求逐项覆盖选定维度**,每项都要有结论(哪怕是"未发现问题")。整项跳过是禁止的。
- **要求落盘**并把路径回报——长报告容易在子代理回传时被截断。
- **禁止空话**:每条结论必须落到具体代码位置与具体后果。
- **定级与实测绑定**:总表每行要有「定级依据」列;🔴/🟠 只能给**当场跑过、贴得出命令与输出**的结论,仅凭推理的最高 🟡。
- **落盘后机检**:报告写完,主代理执行 `node <skill>/scripts/check-report.mjs <报告路径>`。判红就把报告退回蓝军补证据——**不许为了变绿去改总表措辞**,那正是这台机检器要抓的动作。没有这一步,第 1 步的所有实证要求都只是自觉。
### 第 2 步:第三方复核(独立审计)
拿到蓝军报告后,用**新的** `subagent`(不要复用蓝军那个,避免它为自己的结论辩护)。
从 `references/prompt-templates.md` 取**模板 2**。除"验证整改是否落地"和"找新缺陷"外,还有第三个方向:
- **稽核蓝军的覆盖度**——找出蓝军整项跳过、或"未发现问题"但实际有问题的维度。**只验证蓝军说过的话是不够的**,否则蓝军的盲区会变成整个流程的盲区。
- **重跑蓝军的实测证据**——把标了 `实测` 的 🔴/🟠 逐条原样重跑,跑不出来即判「证据不成立」并降级。机检器只能验"贴的是不是一条**真命令**"(可执行程序名 + 参数),**命令是否真跑过、跑出的是否是所贴的输出,只有第三方能判断**——`node fake.js --evidence` 形状完全合法。所以这条约束靠两台机器 + 这个角色一起兜。
- **对自己的产出同样取证**——第三方的 T 项总表也是六列、也受"未实测不得定高危"约束,落盘后同样跑一次 `check-report.mjs`。只查别人、不查自己的角色,最后一定会退化成"别人的证据要命令、我的证据靠我说"。
### 第 3 步:中立裁定
**必须起一个全新的 `subagent` 作为"中立裁定方"**,不是蓝军,也不是第三方。
> ⚠️ **为什么不能复用蓝军做裁定**:裁定的对象是"第三方对蓝军意见的复核"。若让蓝军自己裁定,它同时是**当事人**和**法官**——它会系统性驳回针对自己的批评。这与本 skill 第 1、2 步「禁止复用 agent」的核心原则**直接冲突**。
> "蓝军不信第三方"这个设定通过**提示词立场**表达,**不是**通过复用 agent 身份实现。
> 降级选项:宿主子代理数量受限时,可用 `send_message` 复用蓝军 agent,但**必须在报告里声明"裁定方与蓝军为同一 agent,独立性已降级"**。
从 `references/prompt-templates.md` 取**模板 3**。它有一项关键职责:
- **审查两方共有的前提**——如果蓝军和第三方都基于同一个未经验证的假设,必须标出来并要求实测。**这是同模型互搏最大的盲区**,也是这个 skill 能提供的最高价值。
### 第 4 步:主代理执行整改
拿到合并清单后,**由你(主代理)亲自改代码**,不要外包——整改需要全局一致性判断。
整改纪律:
1. **每条改动配一个回归用例**。特别是:无测试覆盖的修复必复发。回归用例要写成**可重复运行**的形式(测试文件或脚本),不是"我当时手工验过"。
2. **测试要断言"行为",不要断言"实现字符串"**。反例:断言 CSS 里"包含某类名"——这类名会随构建漂移,测试永远绿而功能早坏。正例:模拟真实输入,断言**用户可观察的结果**。
3. **改完必须自己复验**:语法检查、全量测试、以及关键修复的**最小复现**(证明修前坏、修后好)。
4. **诚实记录**:如果发现自己在上一轮文档里写了错误结论,**显式标注作废**,不要悄悄改掉。三方评审的价值就在于留下可追溯的纠正记录。
5. **改完回头验一遍文档里引用的行号和路径**——整改会让行号位移、文件移动会让路径失效。这是本 skill 历史上反复出现的翻车点。
6. 不要为了让报告好看而虚报。做不到就写"推迟"并给出**经过验证的**理由。
### 第 5 步:收口与归档
1. **汇报时明确区分**:哪些已修、哪些推迟(附理由)、哪些未能验证。
2. 按下方策略归档报告。
3. 若涉及发布:确认版本号一致、CI 实际跑过(**不是"配置了 CI"**)、文档与实际改动一致。
---
## 报告归档
**默认策略按项目可见性决定**(选错会造成死链或泄密):
| 项目类型 | 默认做法 |
|---|---|
| **私有仓库 / 未发布** | 写入 `docs/review/` 并**加入 `.gitignore`**,内部攻防记录不外泄 |
| **公开仓库** | 要么正常提交(作为质量流程展示),要么 gitignore **并且彻底清理引用** |
```gitignore
# internal review notes (kept locally, deliberately NOT published)
/docs/review/
```
命名约定(`<version>` 用版本号或 commit sha):
```
docs/review/
├─ BLUE-TEAM-REVIEW-<version>.md # 蓝军(含宿主/上游源码级证据)
├─ RESPONSE-<version>.md # 开发团队逐条回应(采纳/部分/推迟/驳回)
├─ THIRD-PARTY-REVIEW-<version>.md # 第三方独立复核(T 项 + 覆盖度稽核)
└─ ADJUDICATION-<version>.md # 中立裁定(对双方结论的最终裁定)
```
⚠️ **报告若被 gitignore,务必同时清理 README/CHANGELOG 里指向它们的链接**——否则发布后是死链。
---
## 常见的错误用法
- ❌ **用 `subagent_fork` 起蓝军** → 继承你的思路,只会附和。
- ❌ **让蓝军自己裁定第三方意见** → 当事人当法官,针对蓝军的批评会被系统性驳回。
- ❌ **第三方只验"修了没"** → 丢掉"找新缺陷"和"稽核蓝军覆盖度"两项价值。
- ❌ **凭推理链定 🔴/🟠** → 未实测的高危会稀释真问题的注意力,还会诱导"声称跑过"。高危必须带可原样重跑的命令与输出(`scripts/check-report.mjs <报告.md>` 机检这张总表,第三方负责重跑)。
- ❌ **把 19 个维度一次性全塞进去** → 报告又长又浅,真问题被稀释。按改动类型选。
- ❌ **下游报告照抄上游原文** → 三份报告互相复述,token 三倍浪费。按编号引用,只写自己的结论。
- ❌ **把三方报告当成"通过认证"** → 同模型互搏抓不出共同盲区;依赖外部行为的关键结论必须实测。
- ❌ **改了代码不补可运行的回归用例** → 下轮评审同样的问题会再次出现。
- ❌ **改完不回头验文档里的行号/路径** → 整改必然造成位移,示例里的行号会全部失效。
- ❌ **报告写得很漂亮但代码没动** → 立刻修订,别把流程做成表演。
- ❌ **"配置了 CI"就当"CI 跑过了"** → 检查必须**真的能失败**,永远绿的检查等于没有检查。

View file

@ -0,0 +1,102 @@
# 示例:一份 v2.2+ 合规的蓝军报告(能通过机检器)
> 本文件是**格式演示**,被审代码是虚构的 `auth/` 模块——不是某次真实评审的记录。
> 它存在的理由:随包发布的历史样例 [`sample-review.md`](sample-review.md) 产于 v2.2 之前,
> 按 v2.2「未实测不得定高危」的标准会被自家机检器判红。用户第一次跑机检器就看到红,
> 第一印象会是"这工具坏了",所以这里给一份**明确按现行格式写**的对照物。
>
> 想验证它真的合规,跑:
> `node scripts/check-report.mjs examples/sample-review-v2.2.md` → 应当 `exit=0`。
---
## 被审对象
```
仓库: demo-shop(虚构)
基线: main @ e19a2c3
改动范围: auth/session.ts 新增令牌刷新,auth/middleware.ts 改了过期判断
项目背景: 内部 SaaS,令牌有效期 15 分钟,刷新失败会踢到登录页
声称已完成:令牌过期前自动刷新;刷新失败保留旧会话
```
选定维度:正确性 · 错误处理 · 并发与竞态 · 兼容性 · 测试有效性
---
## 缺陷总表
| 编号 | 级别 | 定级依据 | 一句话 | 维度 | 位置(文件:行号) |
|---|---|---|---|---|---|
| B1 | 🔴 | 实测 | 刷新窗口算错,令牌刚发出就被判过期,用户每次刷新都掉登录 | 正确性 | auth/session.ts:47 |
| B2 | 🟠 | 实测 | 刷新失败的 catch 里把错误吞了,中间件仍按"已刷新"继续放行 | 错误处理 | auth/middleware.ts:88 |
| B3 | 🟡 | 推理·待实测 | `expiresAt` 用字符串存,跨时区比较不可靠 | 兼容性 | auth/session.ts:19 |
| B4 | 🟡 | 实测 | 新增的两个分支没有测试覆盖,现有测试走不到失败路径 | 测试有效性 | tests/session.test.ts:1-60 |
### B1 · 🔴 刷新窗口算错
- **证据链**:`auth/session.ts:47` 用 `Date.now() + 15 * 1000` 而不是 `15 * 60 * 1000` 计算过期时间,于是 45 行的 `needsRefresh()` 在任何一次调用里都直接返回 `true`。
- **复现命令**(实测输出):
```bash
sed -n '45,48p' auth/session.ts
```
```text
45: function needsRefresh(t: Token) { return Date.now() > t.expiresAt - 30_000; }
47: const expiresAt = Date.now() + 15 * 1000; // ← 想要 15 分钟
```
```bash
npx tsx -e "const e=Date.now()+15*1000; console.log('issued, needsRefresh =', Date.now() > e - 30_000)"
```
```text
issued, needsRefresh = true
```
- **触发条件**:任何一次签发令牌的请求,无需等待。
- **用户可见后果**:登录后第一次操作就被送回登录页,且循环出现——看起来像"登录不进去"。
- **修复方向**:`const expiresAt = Date.now() + 15 * 60 * 1000;`,并把 15/60/1000 抽成 `TOKEN_TTL_MS` 常量;补一条断言 `expiresAt - issuedAt === TOKEN_TTL_MS` 的测试。
### B2 · 🟠 吞掉刷新失败
- **证据链**:`auth/middleware.ts:88` 的 `catch {}` 块为空,`refreshed` 变量在 catch 之前就已置 `true`,因此刷新抛错时后续 `if (refreshed) return next()` 依旧放行,请求带着过期令牌进入业务层。
- **复现命令**(实测输出):
```bash
sed -n '84,92p' auth/middleware.ts && npx vitest run tests/session.test.ts -t "refresh failure"
```
```text
84: let refreshed = true;
88: } catch { /* 保留旧会话 */ }
90: if (refreshed) return next();
No test matches "refresh failure"(该路径没有测试)
```
- **触发条件**:刷新接口超时或返回 5xx(真实高频场景,弱网下尤其明显)。
- **用户可见后果**:业务层收到过期令牌,报"无权访问"而不是"请重新登录",用户以为权限被收回。
- **修复方向**:把 `refreshed = true` 移到 try 成功分支;catch 内显式 `return res.status(401).json({ error: 'reauth_required' })`。
### B3、B4(🟡 只占总表一行)
- **B3** 修复方向:`expiresAt` 改存 epoch ms(number),比较前不做字符串隐式转换。
- **B4** 修复方向:为刷新失败与过期判定各补一条测试,断言 401 与重定向。
---
## 维度覆盖声明
| 维度 | 结论 |
|---|---|
| 正确性 | 已查 —— 发现 B1 |
| 错误处理 | 已查 —— 发现 B2 |
| 并发与竞态 | 已查,未发现问题(刷新用互斥锁,`session.ts:60-72` 已读) |
| 兼容性 | 已查 —— 发现 B3 |
| 测试有效性 | 已查 —— 发现 B4 |
## 未验证项
- B3 为**推理**定级:没有跑出可复现的跨时区错值,需要一条构造 `expiresAt` 为 ISO 字符串的测试才能升级为 🟠。
- 刷新接口的真实超时率未测量(需要生产日志),B2 的"高频"是推断。

View file

@ -0,0 +1,144 @@
# 示例:一次真实的三方对抗评审
> 这是 skill 完整跑完一遍后的**产出样例**,帮你建立预期。
> 为了篇幅,蓝军报告只保留 3 条缺陷,实际运行通常会产出 5-15 条。
> 另注:本样例产于 v2.1「Token 纪律」之前,故未体现"🟡 一行带位置""T 项总表"等后续格式要求。
> ⚠️ **行号已失效(这本身就是一个教学案例)**
> 本示例记录的是对 **v1.0**(255 行)的评审。该评审直接导致了 v1.1 与 v2.0 的整改,
> 而**整改本身让所有行号发生了位移**——下面引用的 `SKILL.md:163`、`:250` 等在当前版本
> 已指向完全不同的内容。
>
> 这不是疏漏,而是本 skill 反复强调的真实翻车点:**改了代码,文档里引用的行号必然失效**。
> 正确做法是像本文件一样**显式声明失效**,而不是让读者以为引用仍然有效。
> 需要看当前实现时,请用关键词搜索(如搜「不能复用蓝军做裁定」),不要依赖行号。
---
## 被审对象(真实案例)
**注意**:本示例审的不是别人,正是**这个 skill 自己的 `SKILL.md`**(v1.0)。
用 skill 自己的标准审自己,是最能说明它有效性的方式——因为下面这条 🔴 缺陷,正是靠 skill 第 3 步「中立裁定」的标准抓出来的。
```
仓库: adversarial-review
基线: v1.0 (SKILL.md, 255 行)
改动范围: 全量(首次评审)
项目背景: 面向 AI agent 的代码评审 skill,会被他人安装使用
声称已完成:三方流程闭环、角色约束齐备、归档策略明确
```
---
## 蓝军报告(片段)
### 缺陷总表
| 编号 | 级别 | 定级依据 | 一句话 | 维度 | 位置(文件:行号) |
|---|---|---|---|---|---|
| B1 | 🔴 | 实测(原文对照) | 第 3 步让蓝军自己裁定第三方意见,与"禁止复用 agent"的核心原则自相矛盾 | 治理 | `SKILL.md:163` |
| B2 | 🟠 | 实测(原文对照) | `description` 缺少英文触发词,面向国际社区时无法被英文表述激活 | 可用性 | `SKILL.md:3` |
| B3 | 🟡 | 推理·待实测 | 未声明需要子代理能力,在不支持 `subagent` 的宿主上会直接失败 | 兼容性 | frontmatter |
> 📌 **「定级依据」列是 v2.2 事后补标的**,不代表当时跑过命令:B1/B2 的依据是报告里那两段**可复查的原文对照**(照行号取出即见),B3 当时确实没在任何非 Claude Code 宿主上试过,所以只能挂 🟡。
> 把这份样例直接喂给 v2.2 的报告机检器会**报红**:B1/B2 声称实测,而样例里没有任何「复现命令」小节(这四个字只在本注释里出现过一次)。这是预期行为——样例产于规则之前,机检器不给历史面子。
### B1 · 🔴 裁定方与被审方是同一个 agent
- **证据链**:`SKILL.md:163` 原文——"再起一个子代理(或复用蓝军 agent 通过 `send_message`,让它对第三方报告做裁定——**这更符合"蓝军不信第三方"的设定**)";对照 `SKILL.md:250` 明令"❌ 让同一个 agent 既审又改又复核 → 它会为自己的结论辩护"。
- **触发条件**:任何一次标准档评审,只要有 T 项针对蓝军原报告的批评。
- **用户可见后果**:针对蓝军的批评被系统性驳回,用户拿到一份"蓝军永远正确"的裁定,误以为闭环成立。
- **修复方向**:第 3 步改为**全新 agent** 担任"中立裁定方";"蓝军不信第三方"通过提示词立场表达,而非复用身份。`send_message` 复用降级为受限选项并要求声明独立性降级。
### B2 · 🟠 英文触发词不足
- **证据链**:`SKILL.md:3` 的 `description` 中英文触发词仅 `adversarial review` 一条。
- **触发条件**:英文用户说 `code review`、`red team review`、`find bugs in my code`。
- **用户可见后果**:skill 不会被激活,用户以为它不work。
- **修复方向**:补 `red team review` / `pre-release review` / `critique this` / `find bugs in my code`。
### 做得对、值得保持的
- 「能力边界」章节主动声明同模型互搏抓不出共同盲区——**这类"主动认怂"的写法极为罕见且极有价值**,改造时不要删。
- 级联的级别定义(🔴/🟠/🟡)给得具体("向用户广播错误信息"这种可判定标准),而不是"严重/中等/轻微"这种主观词。
### 未验证项
- 未在非 Claude Code 宿主(Cursor / Codex)上实测 `subagent` 的可用性与行为差异。
---
## 第三方复核报告(片段)
### 第一部分:蓝军意见逐项验证表
| 编号 | 蓝军意见 | 验证结果 | 证据(文件:行号) |
|---|---|---|---|
| B1 | 裁定方复用蓝军导致自相矛盾 | ✅ 属实落地 | `SKILL.md:163` 与 `SKILL.md:250` 直接冲突 |
| B2 | 英文触发词不足 | ✅ 属实落地 | `SKILL.md:3` |
| B3 | 未声明子代理依赖 | ✅ 属实落地 | frontmatter 无 `compatibility` 字段 |
### 第二部分:新发现的缺陷
**T1 · 🟠 归档策略默认值在开源场景下会制造死链**
- **位置**:`SKILL.md:212`(默认 gitignore `docs/review/`)vs `SKILL.md:228`(警告清理引用)
- **触发链**:开源项目用户按默认策略 gitignore → README 中指向报告的链接失效 → 但 skill 只在第 228 行"提醒"清理,未把"按项目可见性选择默认值"作为决策点 → 用户踩坑。
- **定级**:🟠 —— skill 自己警告过的坑,却把默认值设成了会踩坑的那一侧。
- **可达性**:真实可达,任何公开仓库用户都会遇到。
### 第三部分:蓝军覆盖度稽核
蓝军对"治理 / 可用性 / 兼容性"三项均有实质结论;缺口在**发布工程**维度——归档策略的开源场景(T1)蓝军整项未提。
### 第四部分:发布门结论
- 达到可发布状态?**整改 B1 后可发布。** B1 属流程性缺陷,不改会让整个 skill 的核心卖点失效。
- 硬性阻塞项:无(无 CI、无版本号联动)。
---
## 中立裁定报告(片段)
### 对第三方 T 项的裁定
| 编号 | 裁定 | 理由 |
|---|---|---|
| T1 | ✅ 采纳 | 蓝军 B1 修复后该矛盾反而更突出,归档一节应改为按可见性决策表 |
### 对蓝军 B 项的最终状态
| 编号 | 最终状态 |
|---|---|
| B1 | 采纳,升级为 P0 —— 这是 skill 的**核心机制缺陷**,不只是措辞问题 |
| B2 | 采纳,P1 |
| B3 | 采纳,P2(补 `compatibility` + 降级模式说明) |
### 实质分歧
**无。** 三方在 B1 上完全一致——这本身是个信号:**当一个缺陷同时违反了两条明写的规则时,各方都能看到它。**
### 合并执行清单
| 优先级 | 项 | 来源 | 改动量 |
|---|---|---|---|
| P0 | 第 3 步改为中立裁定方 | B1 / T1 | 中(重写提示词 + 新增警示块) |
| P1 | 补英文触发词 | B2 | 小 |
| P2 | 补 `compatibility` + 降级模式 | B3 | 小 |
| P2 | 归档默认值改为按可见性决策 | T1 | 小 |
### 一句话总结三方关系
蓝军定义"修什么",第三方验证"修没修对、有没有修出新问题",本报告裁定"双方的结论是否成立"。
---
## 这轮评审的实际收益
| 指标 | 结果 |
|---|---|
| 发现真实缺陷 | 3 条(1 条 P0 核心机制缺陷) |
| 抓到的最有价值一条 | **B1 自相矛盾**——单人自查几乎必然漏掉,因为写的时候"复用蓝军"看起来是个省事的好主意 |
| 误报 | 0 条被裁定驳回 |
**这正是这个 skill 的价值所在**:它不是让 AI"更仔细地看",而是建立一个**写的人没有立场去为自己辩护**的结构。

View file

@ -0,0 +1,184 @@
# 提示词模板(三个角色)
> 本文件是 SKILL.md 的配套模板。**复制下面模板后,必须替换所有 `<...>` 占位符**。
> 模板里的约束条件是经过实战验证的,**不要为了简洁删掉**——尤其是证据链要求和"禁止采信文档"。
## 占位符约定(先读这段,避免歧义)
| 占位符 | 含义 | 示例 |
|---|---|---|
| `<仓库绝对路径>` | **git 仓库根目录**的绝对路径(不是当前工作目录,也不是某个子目录) | `C:\Users\HW\proj` 或 `/home/hw/proj` |
| `<基线>` | 审查锚点:commit sha 或分支名;必须配工作区状态 | `a1b2c3d`(工作区干净) |
| `<改动范围>` | 本次要审的具体范围 | `git diff a1b2c3d..HEAD` 或"仅 src/auth/" |
| `<报告路径>` | 前置角色的报告落盘位置 | `docs/review/BLUE-TEAM-REVIEW-1.2.0.md` |
| `<维度清单>` | 从 `references/review-dimensions.md` 按改动类型选出的维度 | 「正确性·错误处理·测试有效性」 |
| `<待审清单>` | 本次改动声称完成的内容(开发团队自述) | 「已支持 token 刷新」 |
**通用要求**:所有角色的输出都必须**落盘为文件**;回传给主代理的正文**以本模板末尾「落盘要求」所列清单为限**(清单中均含报告路径)——长报告经子代理回传容易截断,不要多塞。
---
## 模板 1:蓝军(敌意审查)
```
你是「蓝军」——专门对本项目做**对抗性(敌意)代码评审**的审查者。你的任务不是夸奖,是**找出问题并证明它们真实存在**。
## 审查对象
- 仓库:<仓库绝对路径>
- 基线:<基线>(工作区/分支状态:<描述>)
- 本次改动范围:<改动范围>
- 项目背景:<这是什么项目、用户是谁、最关键的质量红线是什么>
- 声称已完成的内容:<待审清单>
## 立场要求(重要)
1. **不采信开发团队的说明文档与注释**。注释说"已修复",你要去代码里确认;文档说"性能没问题",你要自己算。
2. **假设它会出事,然后去证明**。对每个关键路径问:「什么输入/环境/时序会让它坏?」
3. **逐项覆盖以下维度**(本次选定的审查范围,来自 references/review-dimensions.md):
<维度清单>
**每个维度都必须给出结论**——哪怕结论是"未发现问题"。整项跳过是禁止的;如果你判断某维度不适用,写"不适用"并给理由。
4. **必须实证**:能跑就跑(`node --check`、测试、最小复现脚本);不能跑就给出严谨的推理链。
5. **定级与实测绑定(未经实测不得定高危)**:只有**当场跑过、能贴出命令与实际输出**的结论才允许定 🔴/🟠;仅凭推理链的结论最高 🟡,并在总表「定级依据」列写 `推理·待实测`。
`实测` 的判据:**贴出一条可原样重跑、且重跑后与所贴一致的证据**——跑测试、`node --check`、`grep`/`sed -n` 取原文片段都算(文档自相矛盾就属于后者,不该因此被降级)。"我觉得""通常""按经验"不算。
**不要为了够上高危而声称跑过**——第三方会原样重跑你的命令,跑不出来这条就判「证据不成立」并降级。
6. **禁止空话**:不要写"建议增加健壮性""考虑边界情况"这类无指向结论。每条必须能落到具体代码位置与具体后果。
## 输出格式(严格遵守)
先给一张**缺陷总表**,按严重度排序:
| 编号 | 级别 | 定级依据 | 一句话 | 维度 | 位置(文件:行号) |
|---|---|---|---|---|---|
| B1 | 🔴/🟠/🟡 | 实测 / 推理·待实测 | ... | 取自已选定维度 | ... |
(**每行都必须填位置列和定级依据列**;🟡 只占总表这一行——可压缩,不可消失。填 `实测` 的条目,展开部分必须给出可原样重跑的命令与输出。)
级别定义:
- 🔴 = 会导致崩溃、数据损坏、或向用户/生态广播错误信息
- 🟠 = 功能在真实使用模式下失效、或声明与实现不符
- 🟡 = 需要修但不阻塞发布
然后**逐条展开**(**只展开 🔴/🟠 条目**;🟡 不展开——总表一行带位置 + 一句修复方向;同类问题**缺陷总条数 >15 时必须**合并为一条并逐一内联各处位置,如"其余 6 处同类错误吞掉:a.js:12、b.js:40…"),每条包含:
- **证据链**:`文件:行号`(必须真实,禁止编造行号)+ 定级依据为实测时给出 **复现命令** 与其**实际输出**;为推理时给出严谨推理链
- **触发条件**:什么情况下会出事
- **用户可见后果**:用户会看到什么现象(用大白话)
- **修复方向**:具体怎么改(给代码或明确步骤)
> **机检器认的写法**(`scripts/check-report.mjs` 逐条取证,格式写错会被判"没给证据"):每条 🔴/🟠 的展开须自成一个段落块,标题写成 `### <编号> · <级别> <一句话>`(或行首 `**<编号>**`);证据行的字面标签必须是**复现命令**;命令必须落在**本小节内**,且内容必须是一条**真命令**——首 token 是可执行程序名(`node`、`make`、`pytest`、`git`…)或带路径的脚本(`./install.sh`、`/usr/bin/make`),程序名后面跟参数、管道或重定向。`见附录`、`see appendix`、`notes.md`、一段源码、一行报错日志都不算证据(外部评审第二轮 T1/T2 证明"有没有反引号"等于没验,第三轮 U1/U2/U6 又证明**只按形状判断会换个语言再犯一次**——所以要求的是词法上的可执行名,不是"看起来像")。
最后给:
- **维度覆盖声明**:对上面每一项选定维度表态(已查/未发现问题/不适用+理由)
- **优先级路线图**:P0(发布前必修)/ P1 / P2
- **做得对、值得保持的**(防止改动时误伤好设计)
- **未验证项**:如实声明哪些结论你没能验证(这比假装验证过更有价值)
## 环境
<工作目录、可用的测试命令、上游源码/依赖的位置>
## 落盘要求
把完整报告写入 `<报告路径>`,**然后自己跑一次 `node <skill>/scripts/check-report.mjs <报告路径>`**:判红就补证据再跑,直到绿为止(补的是命令与输出,不是改措辞)。最终回复里只给:缺陷总表 + 机检器输出的最后一行 + 报告文件路径 + 未验证项。
```
---
## 模板 2:第三方(独立审计)
```
你是「第三方独立复核方」。开发团队已按蓝军评审做了整改,你要**独立验证整改是否真实落地**,并**找出整改过程新引入的缺陷**。
## 你的立场(关键)
1. **不信任双方的任何文档**——不采信蓝军报告的行号,也不采信开发团队的回应说明。**直接读代码**,行号自己重新定位(整改后行号会位移)。
2. **三个方向都要查**:
- **A. 声称修了的,是否真修了?** 逐条核对蓝军的每项意见,在代码里找到对应实现,判断修法是否正确、是否只修了表面。
- **B. 修的过程有没有修出新问题?** 这是你**最重要的独有职责**。整改代码通常很新、没被审视过。重点看:新代码的边界条件、状态残留、生命周期、与新老逻辑的交互。
- **C. 蓝军漏掉了什么?** 对照维度清单,找出蓝军**整项跳过**或"未发现问题"但实际有问题的维度。**只验证蓝军说过的话是不够的。**
3. **特别关注"测试假安全感"**:新加的测试是否真的能捕获它声称覆盖的场景?把测试**故意改坏**(在脑内或实际改)验证它会不会失败——不会失败的测试等于没写。
4. 对"刻意推迟"的项目:判断推迟理由是否成立。**如果理由是"已被某某兜底",必须去验证那个兜底是否真的覆盖了该场景**(这一步极容易出错,是真实翻车点)。
5. **重跑高危证据(防"声称实测")**:把蓝军总表里每条 `定级依据=实测` 的 🔴/🟠 **原样重跑**其复现命令。跑不起来、或输出与蓝军贴出的不一致 → 记 `证据不成立`,该条按 🟡 重新定级并写明原因。**编造"已实测"是本轮最严重的一类缺陷,须在覆盖度稽核里单列上报。**
## 输入
- 仓库:<仓库绝对路径>
- 当前基线:<基线>
- 蓝军意见:<报告路径>(**读文件,不要采信我在这里的转述**)
- 开发团队回应:<回应文档路径或摘要>
- 本次选定维度:<维度清单>
## 输出格式
### 第一部分:蓝军意见逐项验证表
| 编号 | 蓝军意见 | 验证结果 | 证据(文件:行号) |
|---|---|---|---|
| B1 | ... | ✅ 属实落地 / ⚠️ 部分 / ❌ 未落地 / 🔁 修法有误 | ... |
**收敛规则**:引用蓝军条目只用编号 + 一句话概括,**禁止重抄其原文**;✅ 项一行证据带过,只有 ⚠️/❌/🔁 项写完整证据链。
### 第二部分:新发现的缺陷(你的核心产出)
先给 **T 项总表**(与模板 1 **完全同构的六列**:`| 编号 | 级别 | 定级依据 | 一句话 | 维度 | 位置(文件:行号) |`),再对 🔴/🟠 逐条展开:问题、**触发链**(一步步推到后果)、定级、修复方向,其中 `定级依据=实测` 的每条必须贴出**可原样重跑的命令与实际输出**(`scripts/check-report.mjs` 会逐条取证,主代理也会跑一次);🟡 只占总表一行。缺陷总条数 >15 时同类合并并内联位置。必须区分"真实执行路径"与"理论可能"——如果当前 UI 走不到,要说明可达性,别虚高定级。**你查别人的证据要命令,你自己的高危同样得给命令。** 展开格式与机检前提和模板 1 完全一致(评审 T3):小节标题 `### <编号> · <级别> <一句话>`、证据行字面标签写**复现命令**、命令写在本小节里且必须是一条真命令(可执行程序名 + 参数,或 `./x.sh` 这类带路径的脚本)——写 `见附录` / `see appendix`、贴一段源码或报错日志、把公共脚本放到文末附录都会被机检器判红。
### 第三部分:蓝军覆盖度稽核
**只列缺口**:指出蓝军整项跳过或走过场的维度 + 该维度的实际问题;蓝军已实质覆盖且你复核无异议的维度,一行"已覆盖,无异议"带过(须注明你抽查复核到的 `文件:行号`)。**并抽查蓝军的 🟡/归组条目**:发现实为 🔴/🟠 的漏升级或位置缺失,记为覆盖度缺口——这是防"定级套利"的反向制衡。
### 第四部分:发布门结论
- 代码是否达到可发布状态?
- 还有什么**硬性阻塞项**(例如未提交、CI 未跑、版本号不一致)?
- 你的保留意见(哪些你没能验证)
## 要求
- 所有行号必须是你**自己读到的真实行号**,禁止照抄蓝军报告。
- 每条结论给出可复查的证据,不接受"看起来没问题"。
## 落盘要求
把完整报告写入 `<报告路径>`,**然后自跑 `node <skill>/scripts/check-report.mjs <报告路径>`**(它检的是你这张 T 项总表)并补到绿,最终回复只给:验证表 + T 项总表 + 发布门结论(含保留意见)+ 机检器输出的最后一行 + **报告文件路径**。
```
---
## 模板 3:中立裁定
```
你是「中立裁定方」。蓝军提出了意见,第三方对蓝军意见做了复核。现在由你**逐条裁定第三方意见是否成立**。
## 你的立场(关键)
1. **你既不是蓝军也不是第三方**。你对**两方结论都不采信**,只采信你自己读到的代码。
2. **行号全部自己重新定位**(整改后行号已位移),禁止照抄任何一方的行号。
3. 对第三方每一条:**采纳 / 部分采纳 / 驳回**,并说明理由。
4. **允许下调或上调定级**——如果第三方把可达性很低的防御性代码定成高危,明确指出"定级与实际可达性不匹配,会稀释对真问题的注意力"。
5. **同时纠正蓝军的判读错误**——包括蓝军原报告里定级虚高、证据不实、或推理链条断裂的条目。你不是蓝军的辩护人。
6. **重点检查第三方的修复建议是否会踩坑**(例如它建议的改法是否会与现有语义冲突)。
7. **检查两方是否有共同的错误前提**——如果两方都基于同一个未经验证的假设,你必须把它标出来并要求实测。这是同模型互搏最大的盲区。
8. 如果发现开发团队用"已被某某兜底"作为推迟理由,去验证那个兜底是否真的覆盖该场景;**这类推理极容易错**。
## 输出
- 对第三方每条 T 项的裁定(采纳/驳回/改级)+ 理由(**每条 ≤3 行,引用 B/T 用编号不抄原文**)
- 对蓝军每条 B 项在复核后的**最终状态**
- **实质分歧**(如有),以及谁对——若两方都没说对,给出你的结论
- **共同前提审查**:两方共有的、未经实测的假设清单
- **合并执行清单**:把两份报告合成一张 P0/P1/P2 表,标注来源与改动量估计
- **抽查第三方自身的 🟡/归组条目**——反定级套利要覆盖每一层,不止蓝军
- 一句话总结三方关系:蓝军定义"修什么",第三方验证"修没修对、有没有修出新问题、蓝军漏了什么",本报告裁定"两方的结论是否成立"
## 输入
<仓库绝对路径、第三方报告路径、蓝军原报告路径、当前基线>
## 落盘要求
写入 `<报告路径>`,最终回复只给:裁定表 + 合并执行清单 + 共同前提审查 + **报告文件路径**。
```
---
## 设计要点(改模板时别丢这些)
| 要点 | 为什么关键 |
|---|---|
| **角色 + 立场** 而非"帮我审一下" | 立场决定它能发现什么。"挑刺"和"检查"产出完全不同 |
| **必须给证据链**(文件:行号 / 实测) | 禁止空话,也让结论可复查;编造行号能被抓到 |
| **定级与实测绑定 + 第三方重跑** | 只写"必须实证"是散文约束——模型会为够上高危而声称跑过。绑定「定级依据」列 + 有人原样重跑,才让"编造证据"变成**可发现的缺陷**(可用 `scripts/check-report.mjs` 机检)。**机检要逐条取证,不能数关键字**:v2.2 那版统计"全文出现『复现命令』的次数",结果正文摆一个空标题就能给编造的 🔴 盖章——**任何计数型校验都能被凑字数绕过**,只有"这条自己名下有没有真命令"不可凑 |
| **禁止采信文档** | 文档是最容易骗人的东西,文档失实是真实翻车点 |
| **逐维度表态** | 沉默的维度会被误当成"没问题",实际可能根本没人看 |
| **第三方要稽核蓝军的覆盖度** | 否则蓝军的盲区变成整个流程的盲区 |
| **专找"修改引入的新缺陷"** | 独立角色,是标准流程里最容易漏的一环 |
| **裁定方必须与蓝军不同 agent** | 当事人不能当法官,否则裁定退化为自我辩护 |
| **裁定方要审查两方共同前提** | 同模型互搏最大的盲区就是"双方都同意的错误假设" |
| **逼它声明"未验证项"** | 把不确定性显式化,避免用自信语气掩盖无知 |
| **要求区分可达性** | 防止把理论可能定成高危,稀释注意力 |
| **要求落盘 + 回传限形** | 长报告经子代理回传容易截断,落盘更可靠;回传只给总表 |
| **分级展开 + 按编号引用** | 三轮闭环的成本大头是互相抄写与逐条平铺;丢了这条,token 三倍浪费(详见 SKILL.md「Token 纪律」) |

View file

@ -0,0 +1,96 @@
# 三方对抗式评审 · 快速上手
## 30 秒上手
安装 skill 后,在你的项目里直接说:
```
蓝军评审一下这个模块
```
或英文:
```
adversarial review the auth module
```
不需要记任何命令。skill 会自动接管流程。
---
## 三种档位怎么选
| 场景 | 用哪档 | 说这句话 |
|---|---|---|
| 改了个小 bug,想快速看一眼 | 轻档(~1x) | "轻量审一下这次改动" |
| 做完一个功能,准备合并 | **标准档**(默认,~3x) | "蓝军评审" |
| 要发版了 / 大重构 / 动了上游兼容 | 重档(~5-8x) | "重档评审,这版要发布" |
**不确定就用默认标准档。** "~Nx" 指相对一轮蓝军审查的 token 开销。报告有分级展开与按编号引用纪律(见 SKILL.md「Token 纪律」),不会逐条灌水。
---
## 你会拿到什么
跑完后 `docs/review/` 下会有四份文档。**别只看结论,去看证据链**:
```bash
cat docs/review/BLUE-TEAM-REVIEW-*.md # 蓝军:缺陷 + 证据链
cat docs/review/RESPONSE-*.md # 开发团队(主代理)的逐条回应
cat docs/review/THIRD-PARTY-REVIEW-*.md # 第三方:修没修对 + 新缺陷
cat docs/review/ADJUDICATION-*.md # 中立裁定:最终该修什么
```
**优先看 `ADJUDICATION` 的「合并执行清单」**——那是可以直接照着改的清单。
---
## 什么时候不该用它
- ❌ **改了一行文案** → 用不着三方流程,浪费额度
- ❌ **想让 AI 说"代码很好"** → 这个 skill 的立场就是敌意的,不会给你这个
- ❌ **没有测试的项目,且不打算补** → 第三方"测试假安全感"的检查会空转
- ❌ **依赖外部系统行为的关键结论** → 三方互搏抓不出共同盲区,**必须实测**
---
## 一个常见误解
> "过了三方评审 = 代码没问题了"
**不是。** 三个角色的子代理通常是**同一个模型**。它抓的是**逻辑性缺陷**(自相矛盾、遗漏分支、声明与实现不符、测试造假),抓不出**双方共有的知识盲区**。
所以:**凡是对上游/外部系统行为的假设,一律实测,不要让两个 agent 互相点头。**
---
## 想自己改模板?
**改之前先读两个地方**:
1. **[`prompt-templates.md`](prompt-templates.md)** —— 三个角色的完整提示词模板都在这里。
2. **[`prompt-templates.md`](prompt-templates.md) 末尾的「设计要点」表** —— 列出改模板时**不能丢**的 13 条约束(尤其是"逐维度表态""第三方稽核蓝军覆盖度""裁定方审查共同前提""分级展开+按编号引用""定级与实测绑定"这五条)。
**要扩展审查范围**(比如想加上"性能"之外的质量属性),改 [`review-dimensions.md`](review-dimensions.md),不要去改模板——**维度是数据,模板是机制**。
改完用校验器过一遍(校验器在**仓库**的 `scripts/` 下,不随 skill 安装):
```bash
# 从仓库根目录运行
node scripts/validate-skill.mjs skills/adversarial-review
node scripts/test-validate-skill.mjs # 校验器自身的回归测试
node scripts/test-check-report.mjs # 报告机检器的回归测试
node scripts/check-links.mjs # 检查文档里的本地链接
node scripts/check-docs.mjs # 文档中的行数/条数主张与实况一致
node scripts/test-mutations.mjs # 突变测试:证明这些检查真能失败
```
跑完评审后,用**随 skill 安装**的报告机检器把蓝军和第三方两份总表各扫一遍(未实测却定高危、缺「定级依据」列、位置列为空、**声称实测却在自己的展开段落里拿不出命令**,都会失败):
```bash
S=~/.claude/skills/adversarial-review/scripts/check-report.mjs
node $S docs/review/BLUE-TEAM-REVIEW-<version>.md
node $S docs/review/THIRD-PARTY-REVIEW-<version>.md
```
完整产出样例见 [`../examples/sample-review-v2.2.md`](../examples/sample-review-v2.2.md)(能通过机检器);[`../examples/sample-review.md`](../examples/sample-review.md) 是 v2.2 之前的历史样例,保留是为了演示行号腐烂,机检器对它判红是预期行为。

View file

@ -0,0 +1,83 @@
# 完整质量维度清单(审查覆盖参照表)
> 本文件解决一个问题:**蓝军/第三方该往哪里看?**
> 没有这张表,审查者只会重复检查自己熟悉的那几项,留下大片未审区域。
## 怎么用
- **第 0 步**:根据改动性质选出本次**必审维度**(见下方"按改动类型选维度")。
- **第 1 步**:把选中维度**逐项写进蓝军提示词的「优先攻击这些目标」**,并要求蓝军对每项给出结论(哪怕是"未发现")。
- **第 3 步**:裁定方检查蓝军是否有**整项跳过**的维度——跳过比"未发现问题"更值得警惕。
**关键**:要求审查者对每个维度明确表态。沉默的维度会被误当成"没问题",而实际上它可能根本没人看。
---
## 全维度表
| # | 维度 | 要问的问题 | 典型缺陷 |
|---|---|---|---|
| 1 | **需求一致性** | 代码做的事,和用户/issue 要求的是同一件事吗? | 实现了相邻但不同的功能;验收标准未被满足 |
| 2 | **架构与设计** | 改动放在对的位置了吗?边界清晰吗? | 逻辑泄漏到错误层;循环依赖;为修一处而全局耦合 |
| 3 | **正确性** | 边界值、空值、极值、off-by-one | 空数组、0、负数、超长输入未处理 |
| 4 | **错误处理** | 失败路径是否完整?异常是否被静默吞掉? | `catch {}` 空捕获;只处理一半的分支 |
| 5 | **并发与竞态** | 有无共享状态?异步顺序是否被假设? | 竞态导致的偶发失败;await 缺失;锁粒度错误 |
| 6 | **性能** | 复杂度、N+1、无界增长、内存泄漏 | 循环内查询;缓存无上限;大文件全量读入 |
| 7 | **安全** | 注入、越权、凭据、未校验输入 | 拼接 SQL;路径穿越;密钥硬编码 |
| 8 | **隐私与数据** | 是否收集/记录不该有的数据? | 日志打出 PII;遥测未脱敏 |
| 9 | **可观测性** | 出错时能否定位?关键路径有日志/指标吗? | 失败无日志;日志无上下文;无失败率指标 |
| 10 | **测试有效性** | 测试真能失败吗?断言的是行为还是实现? | 断言字符串包含;mock 掉被测逻辑;永真测试 |
| 11 | **兼容性** | 上游/宿主/依赖换代后会崩吗?崩溃半径多大? | 依赖内部 API;只坏自己 vs 拖垮整体 |
| 12 | **依赖与供应链** | 新增依赖必要吗?锁定了吗?有已知漏洞吗? | 引入重依赖换三行代码;未锁版本 |
| 13 | **资源与生命周期** | 定时器/监听器/缓存/句柄是否清理? | 未 removeEventListener;interval 未 clear |
| 14 | **数据迁移与状态演进** | 旧数据/旧配置能平滑升级吗?能回滚吗? | schema 变更无迁移;升级后旧数据崩 |
| 15 | **配置与密钥** | 配置项有默认值吗?密钥如何注入? | 必需的环境变量无默认且未校验 |
| 16 | **发布工程** | 版本号一致吗?CHANGELOG 与实际改动一致吗? | 声称改了的文件其实没改;版本号漂移 |
| 17 | **文档一致性** | 文档/注释声称的,代码真的做了吗? | README 描述的功能已不存在 |
| 18 | **可访问性与国际化** | 键盘可达?屏幕阅读器可用?文案可翻译? | 仅鼠标可操作;硬编码文案 |
| 19 | **许可证合规** | 新引入代码/依赖的许可与项目兼容吗? | 引入 GPL 依赖进 MIT 项目 |
---
## 按改动类型选维度
不是每次都要审全部 19 项——那会稀释注意力。按改动性质选取:
| 改动类型 | 必审维度 |
|---|---|
| **新增功能** | 1 需求 · 2 架构 · 3 正确性 · 4 错误处理 · 10 测试 · 17 文档 |
| **修 bug** | 3 正确性 · 4 错误处理 · 10 测试(**必须有回归用例**) · 11 兼容 |
| **重构(行为不变)** | 2 架构 · 10 测试(**测试须在重构前后都通过**) · 6 性能 |
| **性能优化** | 6 性能(**须有测前/测后数据**) · 3 正确性(优化常改坏语义) · 10 测试 |
| **依赖升级/替换** | 11 兼容 · 12 供应链 · 10 测试 · 16 发布 |
| **发布前(重档)** | 全部 19 项 |
| **安全相关改动** | 7 安全 · 8 隐私 · 4 错误处理 · 10 测试 |
| **涉及存储/schema** | 14 迁移 · 3 正确性 · 4 错误处理 · 16 发布(**回滚路径**) |
| **并发/异步改动** | 5 并发 · 13 生命周期 · 6 性能 · 10 测试(**并发测试的稳定性**) |
| **UI/前端** | 18 可访问性 · 1 需求 · 6 性能 · 17 文档 |
| **纯文档/文案** | 17 文档一致性 · 16 发布(版本号/链接) |
---
## 每条维度的最小证据要求
审查者声称"检查过某维度"时,必须能给出下列之一,否则视为未检查:
| 维度 | 可接受的最小证据 |
|---|---|
| 正确性 / 错误处理 | `文件:行号` + 触发该路径的具体输入 |
| 并发 | 具体的交错时序,或"该模块无共享状态"的论证 |
| 性能 | 复杂度分析,或实测数据 |
| 安全 | 具体攻击输入 + 预期被拦/未被拦的位置 |
| 测试有效性 | **把测试故意改坏,观察它是否失败**(不失败的测试等于没写) |
| 兼容性 | 上游源码/文档的具体位置,或实测命令 |
| 迁移 | 旧格式样本 + 升级后的预期状态 |
| 文档一致性 | 声称的文件与代码中实际存在/不存在的对照 |
---
## 与本 skill 三角色的关系
- 这张表**不替代**对抗机制,它只是给蓝军一张"别漏掉这些区域"的地图。
- **图表本身不会发现问题**——发现问题靠证据链和敌意立场。
- 维度数量不是越多越好:把 19 项全塞进一次轻档审查,只会让报告变得又长又浅。**按改动类型选**才是正确用法。

View file

@ -0,0 +1,357 @@
#!/usr/bin/env node
// check-report.mjs — 蓝军/第三方「缺陷总表」机检器(v2.5.0)
//
// 为什么需要:"必须实证"写进 SKILL.md 也只是一句**散文约束**——模型想让
// 自己的高危结论有分量时,会直接声称"我跑过了"而根本没跑。v2.2 把定级与
// 实测绑定(未经实测不得定高危),而绑定规则若没有机器兜底,只会退化成
// 又一句口号。本检查只干一件事:让"编造的实测"变成**可被发现**的事。
//
// v2.3 修掉的一个真实绕过(外部评审 R1):早先版本用"全文出现『复现命令』
// 四个汉字的次数 ≥ 声称实测的条数"当证据计数。于是一份正文里只写了
// `## 复现命令` 空标题、一条命令都没有的报告能判绿——**计数型校验天然可被
// 凑字数**,改成按条目就近匹配:每个 🔴/🟠 实测条目必须有自己的展开段落,
// 段落里的「复现命令」后面必须真的跟着一段命令(行内代码或围栏块 + 内容)。
//
// v2.4 修掉的两个绕过(外部评审第二轮 T1/T2):v2.3 只验"有没有",不验"像不像",
// 于是 ① 行内代码只要 ≥3 个字符就算命令——写 `\`见附录\`` 即可盖章;② 取证窗口写成
// `end + 9`,命令可以塞进紧随其后的附录里被全部高危共用。**教训是同一类**:
// "存在某段代码块"这种判据永远能被占位词喂饱,判据必须落到内容形状上
// (commandish:命令名 + 参数,或含管道/重定向/路径),窗口必须封在本条目自己名下。
//
// v2.5 修掉的三个绕过(外部评审第三轮 U1/U2/U6):v2.4 的"形状"判据其实只挡住了
// **中文**占位词——`see appendix`、`Run the script in the appendix…`、一段源码、
// 一行 npm 报错日志全都能盖章(U1/U6),而真命令 `make`、`pytest` 反被判红(U2)。
// **教训又升了一级**:占位词是语义问题,形状判据换个自然语言就失效;能收敛的只有
// 词法——首 token 必须是可执行程序名(或 `./x`、`x.sh` 这类带路径的脚本),
// 认不出的名字必须带一个参数形状。判据强度本身还配了 4 条"削弱型"突变靶子(P11–P14),
// 否则"退回旧实现"之外的第三种破坏方式无人看守(评审 U3)。
//
// 只管 🔴/🟠,不管 🟡(评审 R4):模板规定 🟡 不展开,若要求 🟡 也贴命令,
// 完全合规的产出会被判红——而误报会让人直接关掉检查,比漏报更糟。
//
// 防"永远绿"(本项目核心信条):找不到总表 / 总表零数据行 / 一个级别 emoji
// 都没解析到,一律判失败——提取错位导致的静默通过比报错危险得多。
//
// 用法:node check-report.mjs <报告.md> [更多报告.md]
// 退出码:0 = 全部合规;1 = 存在缺陷;2 = 用法错误(无参数)——用法错误不是检查失败
//
// 格式与 references/prompt-templates.md 模板 1/模板 2 的六列总表对齐:
// | 编号 | 级别 | 定级依据 | 一句话 | 维度 | 位置(文件:行号) |
// 定级依据取值只能是 `实测` 或 `推理·待实测`;展开条目标签为 `复现命令`。
import { readFileSync } from "node:fs";
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("用法:node check-report.mjs <报告.md> [更多报告.md]");
process.exit(2);
}
const HIGH = ["🔴", "🟠"];
const LEVELS = ["🔴", "🟠", "🟡"];
const EMPTY_LOC = new Set(["", "-", "--", "...", "…", "N/A", "n/a", "TBD", "待补"]);
const splitRow = (line) =>
line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim());
const isSep = (c) => c.length > 0 && c.every((x) => x === "" || /^:?-{2,}:?$/.test(x));
// 围栏代码块内的行不参与总表定位(模板本身就是被 ``` 包住的正文);
// 但「复现命令」计数**要**连围栏内一起算——命令输出本来就是贴在围栏里的。
function fencedMask(lines) {
const mask = new Array(lines.length).fill(false);
let open = false;
for (let i = 0; i < lines.length; i++) {
if (/^\s*(```|~~~)/.test(lines[i])) open = !open;
mask[i] = open;
}
return mask;
}
const HEADING = /^(\s{0,3})(#{1,6})\s+/;
// 条目小节的起点:`### B1 · 🔴 …`(允许前置级别 emoji)或 `**B1**` 开头的行。
// 只认"紧跟标题标记的 id",否则 `### 修复 B1 引入的问题` 会被误当成 B1 的小节。
// id 后面不许再粘字母数字,否则 B1 会误命中 B10。
// id 必须先转义:编号写成 `B(1` 会让 RegExp 抛 Unterminated group(评审 T6,实测崩栈),
// 写成 `P(1)` 则 `(1)` 变成捕获组、静默匹配不上,反过来误报"没有展开段落"。
const reEscape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const anchorRe = (id) =>
new RegExp(
`^(?:\\s{0,3}#{1,6}\\s+(?:[\\u{1F300}-\\u{1FAFF}✅❌⚠️]\\s*)?|\\s*(?:[-*]\\s+)?\\*\\*)` +
`${reEscape(id)}(?![0-9A-Za-z])`,
"u"
);
// 围栏块配对:opener -> closer(未闭合则到文末)。
// 只按"行首是否为围栏标记"逐行开合,因此块体可以被安全切片——这是下面
// fenceCommand 能在工作窗口内识别一个跨窗口边界的块的前提(评审 T7:
// 旧实现把"闭合围栏也必须落在窗口内"当条件,围栏块一超过 8 行就恒判无证据)。
function fencePairs(lines) {
const pairs = new Map();
const stack = [];
for (let i = 0; i < lines.length; i++) {
if (!/^\s*(```|~~~)/.test(lines[i])) continue;
if (stack.length) pairs.set(stack.pop(), i);
else stack.push(i);
}
for (const o of stack) pairs.set(o, lines.length);
return pairs;
}
// 「像一条命令」判据(第三版,评审 U1/U2/U6)。
// v2.4 版只看形状(纯 ASCII + 多 token + 首 token 含点或斜杠),于是
// `see appendix`、`notes.md`、一段源码、一行 npm 报错日志全都能盖章(U1/U6),
// 而真命令 `make` / `pytest` 反被判红(U2)。占位词是**语义**问题,形状判据封不死,
// 所以改成:首 token 必须是常见可执行程序名(或 `./x`、`/usr/bin/x`、`install.sh` 这类带路径的脚本),
// 程序名之后还要跟得上参数/元字符/路径。宁可错杀不认识的裸命令,也不放过编造的英文短语——
// 报错信息会把这条要求原样写明白。
const EXEC = new Set(`
node npm npx pnpm yarn bun deno tsx ts-node
python python3 pip pip3 uv poetry pytest tox ruff mypy black flake8
go cargo rustc make cmake gcc g++ clang meson ninja
java javac kotlin kotlinc scala mvn gradle dotnet swift
php composer ruby gem bundle rspec rails
git gh glab svn hg
bash sh zsh dash pwsh powershell cmd bat fish
cat head tail less more wc sort uniq cut tr sed awk grep egrep fgrep rg fd find xargs
ls ll pwd cd echo printf test diff patch comm
tar zip unzip gzip gunzip xz 7z rsync scp cp mv rm mkdir touch chmod chown ln
curl wget httpie jq yq awk
docker podman nerdctl kubectl helm kind minikube terraform pulumi ansible
openssl ssh sftp nc dig nslookup ping traceroute
mysql psql sqlite3 redis-cli mongo
tsc eslint prettier stylelint vitest jest mocha playwright cypress karma
code idea vim vi nano emacs touch
convert magick ffmpeg ffprobe imagemagick
systemctl service journalctl ps kill killall top htop lsof df du free id whoami
apt apt-get yum dnf pacman brew choco scoop winget
ldd nm objdump gdb valgrind strace ltrace
tree file which whereis man history alias env print
`.trim().split(/\s+/));
// 这些是"前缀命令",本身不说明跑了什么,要看它后面那个 token
const WRAPPER = new Set(["sudo", "doas", "env", "time", "nohup", "command", "builtin"]);
const IDENT = /^[a-z][a-z0-9]*(?:[-+.][a-z0-9]+)*$/;
// `./x` `/usr/bin/x` `~/.local/bin/x` `foo.sh` `run.ps1` `main.py` `build.bat`
const PATHY = /^(?:[.~]?\/[\w.\/+-]+|[\w.+-]+\.(?:sh|bash|ps1|py|mjs|cjs|js|ts|rb|pl|php|bat|cmd|exe))$/;
// 参数形状:flag、路径、`~/x`。源码片段里的 `(x` / `===` / `{` 都不在此列
const ARGISH = /^[-./~@]/;
// 行首就长得像**程序输出**的(报错、traceback、栈帧),不是命令
const LOOKS_LIKE_OUTPUT =
/^(?:[\w.$-]*Error\b|Traceback|panic:|fatal:|Exception\b|npm\s+ERR!|ERR!|\bat\s[\w.$<>]+\()/;
// 「必须是纯 ASCII」这条检查的作用是把 `见附录`、`make (见下一节)` 这类**说明文字**挡在证据之外。
// 但它不能连引号里的内容一起管:`sed -i 's/正文 173 行/正文 180 行/' README.md` 是我们自己 README
// 里的一条真命令,参数天生带中文。所以只看**引号之外**的字符(评审 U2 的误报面)。
function asciiOutsideQuotes(t) {
return /^[\x20-\x7e]+$/.test(t.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, '""'));
}
function commandish(raw) {
const t = String(raw).trim().replace(/^[$>]\s+/, "").trim();
if (!t) return false;
if (!asciiOutsideQuotes(t)) return false; // 引号之外含中文/省略号 → 是说明文字,不是命令
if (LOOKS_LIKE_OUTPUT.test(t)) return false;
if (/^(?:TODO|TBD|FIXME|N\/A|none|null|\.{2,}|-+)$/i.test(t)) return false;
const toks = t.split(/\s+/);
let i = 0;
while (i < toks.length && WRAPPER.has(toks[i].toLowerCase())) i += 1;
const head = toks[i] || "";
if (PATHY.test(head)) return true; // `./install.sh`、`/usr/bin/make`:带路径形状即真命令
if (!IDENT.test(head)) return false; // 大写开头、带括号引号的(源码片段)一律不算
// 认识的程序名:`make`、`pytest` 这类裸命令也必须认,否则就是误报(评审 U2)
if (EXEC.has(head)) return true;
// 认不出的名字:不猜,要求它至少带一个 flag/路径形状的参数
return toks.slice(i + 1).some((x) => ARGISH.test(x));
}
const inlineCommands = (line) =>
[...line.matchAll(/`([^`\n]+)`/g)].map((m) => m[1]);
// [from,to) 内**开始**的围栏块里是否真有一行像命令(闭合块体可以延伸出窗口)。
function fenceCommand(lines, pairs, from, to) {
for (const [open, close] of pairs) {
if (open < from || open >= to) continue;
if (lines.slice(open + 1, close).some(commandish)) return true;
}
return false;
}
const REPRO_LABEL = /^\s*(?:[-*]\s+)?(?:\*\*)?\s*复现命令/;
// 全文有效证据处数(只用于输出行与"提取是否失效"的兜底,不再充当判据)。
// 就近窗口 = 到下一个标题或下一个「复现命令」标签为止。
function countEvidence(lines, fence, pairs) {
let n = 0;
for (let i = 0; i < lines.length; i++) {
if (fence[i] || !REPRO_LABEL.test(lines[i])) continue;
let to = lines.length;
for (let k = i + 1; k < lines.length; k++) {
if (!fence[k] && (HEADING.test(lines[k]) || REPRO_LABEL.test(lines[k]))) {
to = k;
break;
}
}
if (inlineCommands(lines[i]).some(commandish) || fenceCommand(lines, pairs, i + 1, to)) n += 1;
}
return n;
}
// 返回 null = 证据成立;返回字符串 = 判红理由。
function entryEvidence(lines, fence, pairs, tableIdx, ids, id) {
let start = -1;
let level = 0;
for (let i = tableIdx + 1; i < lines.length; i++) {
if (fence[i] || !anchorRe(id).test(lines[i])) continue;
start = i;
const h = HEADING.exec(lines[i]);
level = h ? h[2].length : 7;
break;
}
if (start === -1)
return `${id}:定级依据写的是"实测",但报告里没有它名下的展开段落 —— ` +
`需要形如 \`### ${id} · …\` 的小节,内含「复现命令」与原样可重跑的命令`;
let end = lines.length;
for (let i = start + 1; i < lines.length; i++) {
if (fence[i]) continue;
const h = HEADING.exec(lines[i]);
if (h && h[2].length <= level) {
end = i;
break;
}
if (ids.some((other) => other !== id && anchorRe(other).test(lines[i]))) {
end = i;
break;
}
}
for (let i = start; i < end; i++) {
if (fence[i] || !REPRO_LABEL.test(lines[i])) continue;
// 窗口止于 end:命令必须落在**本条目自己名下**(评审 T2)。旧实现写成
// `end + 9`,于是把公共脚本放进紧随其后的"附录"就能一次性喂给全部高危——
// 逐条取证当场退化回全文有一处就行。
if (inlineCommands(lines[i]).some(commandish)) return null;
if (fenceCommand(lines, pairs, i + 1, end)) return null;
return `${id}:写了「复现命令」却没有一条像命令的内容 —— 行内代码/围栏块里要找的是` +
`「可执行程序名(node / make / pytest / ./x.sh …)+ 参数或管道」这样的真命令;` +
`\`见附录\`、\`see appendix\`、\`notes.md\`、源码片段、报错日志都不算证据`;
}
return `${id}:声称实测,但自己的展开段落里没有「复现命令」 —— 只在总表里写"实测"两个字不构成证据`;
}
function checkFile(path) {
let text;
try {
text = readFileSync(path, "utf8").replace(/\r\n/g, "\n");
} catch (e) {
return { fatal: [`无法读取文件:${e.code || e.message}`] };
}
const lines = text.split("\n");
const fence = fencedMask(lines);
const pairs = fencePairs(lines);
let hi = -1;
let head = null;
// 报告里常有多张表(验证表、T 项总表、合并执行清单)。优先取**含「定级依据」列**
// 的那张;取不到再退回第一张 编号+级别 表,并在输出里声明取用了哪一张(评审 R15)。
let firstCandidate = null;
let preferred = null;
for (let i = 0; i < lines.length; i++) {
if (fence[i] || !/^\s*\|/.test(lines[i])) continue;
const c = splitRow(lines[i]);
if (!(c.some((x) => x.includes("编号")) && c.some((x) => x.includes("级别")))) continue;
const cand = { i, c };
firstCandidate ??= cand;
if (c.some((x) => x.includes("定级依据"))) {
preferred = cand;
break;
}
}
const chosen = preferred ?? firstCandidate;
if (!chosen)
return { fatal: ['未找到缺陷总表(表头需同时含"编号"与"级别")—— 模板未落实或表头被改名'] };
hi = chosen.i;
head = chosen.c;
const tableLine = hi + 1;
const col = (kw) => head.findIndex((x) => x.includes(kw));
const iId = col("编号");
const iLv = col("级别");
const iBs = col("定级依据");
const iLoc = col("位置");
const failures = [];
if (iBs === -1)
failures.push("缺陷总表缺少「定级依据」列 —— 未经实测不得定高危这条约束无法机检");
if (iLoc === -1) failures.push("缺陷总表缺少「位置」列 —— 每条缺陷必须落到 文件:行号");
let rows = 0;
let levelSeen = 0;
const measured = [];
const highMeasured = [];
const ids = [];
for (let i = hi + 1; i < lines.length; i++) {
if (fence[i]) continue;
if (!/^\s*\|/.test(lines[i])) {
if (rows > 0) break;
continue;
}
const c = splitRow(lines[i]);
if (isSep(c)) continue;
const id = iId >= 0 ? c[iId] || "" : "";
if (!id || id.includes("编号")) continue;
rows += 1;
ids.push(id);
const lv = iLv >= 0 ? c[iLv] || "" : "";
if (LEVELS.some((e) => lv.includes(e))) levelSeen += 1;
const isHigh = HIGH.some((e) => lv.includes(e));
const bs = iBs >= 0 ? c[iBs] || "" : "";
// 「推理·待实测」含"实测"两字却不是实测——本行是最容易写错的一处判定。
const claims = /(?<![待未无])实测/.test(bs);
if (claims) measured.push(id);
if (claims && isHigh) highMeasured.push(id);
if (isHigh && !(claims && !bs.includes("推理")))
failures.push(
`${id}:定级为 ${lv || "(空)"} 但定级依据为"${bs || "(空)"}" —— ` +
"未经实测不得定高危(推理所得最高 🟡)"
);
if (iLoc >= 0 && EMPTY_LOC.has(c[iLoc] || ""))
failures.push(`${id}:位置列为空 —— 每条缺陷必须落到 文件:行号`);
}
if (rows === 0) failures.push("缺陷总表无数据行 —— 提取错位,本检查无法判定");
else if (levelSeen === 0)
failures.push("未解析到任何级别标记(🔴/🟠/🟡)—— 级别列格式变了,检查已失效");
// 逐条就近取证(评审 R1):声称实测的高危条目,必须在自己名下有**真的**命令。
const nRepro = countEvidence(lines, fence, pairs);
for (const id of highMeasured) {
const why = entryEvidence(lines, fence, pairs, hi, ids, id);
if (why) failures.push(why);
}
return { failures, rows, measured: measured.length, highMeasured: highMeasured.length, nRepro, tableLine };
}
let bad = 0;
for (const path of args) {
console.log(`check-report: ${path}`);
const r = checkFile(path);
const fails = r.fatal || r.failures;
if (fails.length) {
bad += fails.length;
for (const f of fails) console.error(" ✗ " + f);
} else {
console.log(
`check-report: ${r.rows} 条缺陷(第 ${r.tableLine} 行的总表),${r.measured} 条声称实测` +
`(其中高危 ${r.highMeasured} 条),逐条取证通过(全文有效「复现命令」${r.nRepro} 处)`
);
}
}
if (bad) {
console.error(`check-report: ${bad} 处不符合「未经实测不得定高危」约束(高危须逐条取证)`);
process.exit(1);
}