# agent-clinic 项目方案(v0.2 修订版) - 修订日期:2026-10-15 - 修订依据:**《agent-clinic 独立评估报告(实测版)》**(第三方,下载 Node 22.20.0 + 合成 DSH 语料实跑 6 组测试) - 修订性质:**不改变项目定位与诊断层设计**,只针对评估暴露的质量保障层缺陷与判定健壮性缺陷做调整 - 适用版本:v0.1.x → v0.2.0(发布门槛见第 5 节) --- ## 0. 修订摘要 评估报告的总评:诊断准确性 **A-**、证据可复核性 **A**、自检可信度 **C+**、环境可移植性 **C**、判定健壮性 **B**、工程纪律 **B+**。 也就是说,**风险不在诊断层,在质量保障层**:诊断核心链路(扫描 → 判定 → 证据 → 报告)经第三方实测零误报零漏报、数字分毫不差;但 CI 全红未修、徽章 249/249 与实测 247/2/退出码 1 不符、自检依赖作者本机数据形态。一个把「每条结论都可复核」写进产品理念的工具,它自己的绿灯不可复核 —— 这是本次修订要消除的首要问题。 本次修订四条主线(严格对应评估报告 §三/§四/§八): 1. **把绿灯变真**:修 smoke 降级语义 + CI 造合成会话 + 徽章由机器校验(对应 §3.1、§3.2、§4.2) 2. **处方层数据流贯通**:名额轮转修复 + 现搜条目端到端进处方(对应 §4.1) 3. **判定健壮化**:N3 outcome 三分类、C1 基线排除自身(对应 §4.3、§5.1) 4. **发布门槛前置**:以上全绿才谈上线与收录(对应 §八) --- ## 1. 项目定位与边界(保持不变) **定位**:只读扫描 DSH 本地会话,用可复核证据诊断 Agent 使用问题,并开出可执行处方。 | 维度 | 约定 | 本次是否变更 | |---|---|---| | 数据来源 | 仅本地 DSH 会话目录(`~/.dsh/sessions`),默认零外发 | 不变 | | 写权限 | 全流程不写会话文件(评估报告 §六.3 实测确认) | 不变 | | 无数据行为 | 优雅降级,逐维度说明"为什么没有数据"(§六.4 实测确认) | 不变(但**退出码语义要改**,见 4.2) | | 口径 | 实测 vs 估算分开标注(§六.5 实测确认) | 不变 | | 依赖 | 零第三方运行依赖、离线可用(§六.7 实测确认) | 不变 | | 差异化核心 | 证据可复核(report 给出的定位符与 show 展开严格一致,§六.2) | 不变,且新增护栏守护 | 评估报告证实成立的内容**一律保留**:10 项指标判定逻辑、证据层、只读承诺、降级说明文案、口径标注、零依赖离线运行、三旋钮(maxSessions / minRepeatCount / contextLines)、插件目录三来源。 --- ## 2. 指标体系与判定健壮性(修订) 指标总数与类别不变:**19 项**(C1 C2 C3 C5 C6 C7 · E1–E5 · N1–N4 · A2 A3 A4 A5),A1(MCP) 排期,K1/K2 因隐私取舍不实现。 ### 2.1 N3「被拒」判定:由二分类改为三分类 **现状(缺陷)**:`src/diagnostics/index.mjs:831` ```js if (!/^allow/i.test(outcome)) denied += 1; ``` 任何非 `allow*` 开头的 outcome 都被计为"拒绝"。评估报告实测:5 个 `approved` 被报成"被拒 5 次";若 DSH 改用 `approved` 措辞,所有 N3 报告的"被拒"字段将系统性全错,而用户会据此调整权限配置。 **调整后**:显式匹配拒绝语义,其余归入"未知结果",绝不猜。 ```js const outcome = String(d.outcome ?? '').toLowerCase(); if (/^(deny|denied|reject|rejected|decline|declined|refus|forbid)/.test(outcome)) denied += 1; else if (!/^(allow|approv|accept|grant|ok|yes|permit)/.test(outcome)) unknownOutcome += 1; ``` - 分母口径同时写清:`审批请求 N 次 · 被拒 X 次 · 未知结果 Y 次 · 无决定记录 Z 次`(`unknown` 现有语义保留为"无对应决定记录",与"未知结果"分列) - 报告文案不再把非 allow 一律说成拒绝 - 文档:`docs/diagnostics.md` 的 N3 条目同步改写(该文件是"指标 = 代码镜像",含断言) ### 2.2 C1 基线排除被判定者自身 **现状(缺陷)**:`src/diagnostics/index.mjs:442` ```js const baselinePool = stats.filter((x) => !x.subagent && x.output > 0).map((x) => x.perRequest); const base = median(baselinePool); ``` 基线池含被判会话自己,评估报告 §5.1 指出(中位数对离群值稳健,本次结果仍对,但严格实现应排除自身)。 **调整后**:逐会话计算排除自身后的基线。 ```js const poolEntries = stats.filter((x) => !x.subagent && x.output > 0); // 逐个会话排除自身后再取中位数:自己不该是自己的对照 ``` **必须同步的连带影响(口径更正:本方案 v0.2 初稿此处写反了,现更正)**:排除自身后基线**更低** → 倍率**更高** → 判定**更敏感**,而不是"更宽松、降低误报"。以评估报告 §5.1 的例子 `[100, 250, 2000]`(每会话 200 条 assistant 事件,perRequest = 单位请求 output)为例: | 口径 | 中位数基线 | 2000 的倍率 | C1 是否命中 | |---|---|---|---| | 旧:含自身 | 250 | 8.0× | 命中 | | 新:不含自身 | 175 = median(100, 250) | 11.4× | 命中(阈值 2 倍) | 即该修正会让**历史报告里的倍率整体变大**,可能让原本刚好低于阈值的会话转为命中(宁可多报,不少报)。 处理方式: - `docs/diagnostics.md` 明确写"基线 = 排除被判会话后的其余主会话中位单位请求 output(对照样本不足时退回全体主会话并在摘要标注)" - `CHANGELOG.md` 标注"判定口径变更:C1 基线排除自身,历史报告倍率可能上升" - smoke 新增护栏断言:3 会话 `[100, 250, 2000]` 下摘要必须含 `是主会话基线 175 的 11.4 倍` 且 C1 命中(防回退到含自身口径,也防有人以为这会让误报变少) --- ## 3. 处方层数据流(修订重点) ### 3.1 名额轮转修复:第 2 个名额此前永远发不出去 **实测复现(本项目自查,非评估报告原文,但正是 §4.1 现象的成因)**: ``` $ node .tmp/repro-live-rx.mjs C1 单点处方: config: | plugin:demo-ctx-compress | behavior:behavior-split-session 全局处方( 7 类问题): config … | command … | behavior … | config … | config … | behavior … 现搜条目在全局处方里吗: false ``` **根因**:`src/prescriber/index.mjs:520-548`,`matchPrescriptions` 的轮转被"第 0 轮吃光名额"破坏: ```js for (let round = 0; round < Math.max(perFinding, 1) && out.length < limit; round++) { for (const f of findings) { if (out.length >= limit) break; … pick = all[round] … } } ``` `perFinding=2`、`limit=6`:当 finding 类数 ≥ 6 时,第 0 轮就把 6 个名额填满,`round=1` 永不执行 —— **每类问题的第 2 个名额(插件类处方所处位次)结构性丢失**。 **调整后**:给每一轮保留配额,保证后续轮次一定有位置。 ```js // 每轮配额 = 总名额 / 轮数(上取整),避免第 0 轮吃光全部名额: // perFinding=2、limit=6 时每轮 3 个 —— 第 1 轮必定执行,插件类处方才有机会进入全局列表。 const roundCap = Math.max(1, Math.ceil(limit / Math.max(perFinding, 1))); ``` 未入选的候选不静默丢弃:报告与面板对"因名额未展示"的处方给出计数(沿用既有"不静默降级"原则)。 ### 3.2 现搜条目端到端进处方(评估 §4.1) 评估报告的最小复现:注入结构合规的现搜条目 `demo-ctx-compress`(能力标签对上 C 类)后,`live 进推荐了吗: true`,但 `插件类处方: [ '@shengsheng/dsh-taskboard' ]` —— 现搜只进推荐、不进处方,用户点完"联网现搜"看不到期望结果。 **修正后的准确描述**(本项目复核):现搜条目**已经能进入 C1 的单点/聚合卡处方**(`src/report.mjs:311`、`src/report.mjs:328` 走的是 `prescribeOne`),但进不了全局 `prescriptions` 列表(`src/report.mjs:297` → `matchPrescriptions`)。也就是说: - 属实缺陷:受 3.1 的名额轮转 bug 影响,全局处方段(报告末尾"处方"清单、CLI 输出)里现搜条目缺位 - 断言缺陷:`scripts/smoke.mjs:1511-1515` 断言的正是全局列表,于是**该断言能否通过取决于本机扫描出几个 finding 类**(类数 < 6 时通过,≥ 6 时必然失败)—— 与 §4.2 同属"自检不自包含" **调整后(两条一起改)**: 1. 实现侧:3.1 的轮转修复,保证 ≥6 类问题时插件类处方仍能获得名额 2. 断言侧:改为**不依赖本机数据形态**的双重断言 - 针对性断言:构造 finding 集与现搜条目,断言 `prescribeOne(C1 finding, { ctx })` 结果里含该条目(per-finding 口径) - 名额护栏断言:构造 7 类 finding + 1 条现搜条目,断言其出现在 `matchPrescriptions(..., { ctx })` 输出中(防 3.1 回退) - 现有"全局列表含该条目"的断言保留,但输入改为上述构造数据,不再读本机 `scan` --- ## 4. 质量保障体系(重写) 评估报告的核心批评:**「249/249」当前只能靠作者自觉**。本节把绿灯变成机器可验证的。 ### 4.1 自检语料固定装置:`scripts/fixtures/synth-dsh.mjs`(新增) 评估报告 §4.2 指出:「合并确实压掉了重复(类数 < 处数)」要求每个指标命中 ≥2 处,干净合成数据(每指标 1 处)下必挂 —— **248 项自检是针对作者本机数据校准的,不是自包含的**。 **调整后**:把评估报告测试 3/测试 4 用到的合成语料固化为仓库内生成器,并写死不变式: - 输出一个完整可用的合成 `DSH_HOME`(`--out `),多帧 zstd(覆盖 `src/adapters/zstd-frames.mjs` 的按 magic 切帧路径) - 覆盖 13 种以上 DSH 事件类型:正常对话、死循环、冗余调用、大结果、审批(含 `approved` / `deny` / 未知 outcome 三类)、压缩(`shadowedTokenCount`)、注入(`request/header`)、大 tool schema、高消耗、skill 触发(**必须走 skill 工具 + `arguments.name`**,A2 才不会误报) - **不变式一:每个指标至少命中 2 处**(尽力而为 —— 原意是"合并类断言要有得比",但第三方评估 §4.2 之后那些断言已改为**构造输入**,≥2 不再是正确性前提) - **不变式二:每个指标至少命中 1 处**(硬门槛:保证各诊断分支都被触达,命中 0 即非零退出) - 生成器自带自校验:跑一遍诊断,逐指标打印命中处数(`--verify`);不足 1 处直接 exit 1 - 生成器为唯一语料来源:CI 与本地"无数据环境复现"都指向它(评估报告测试 1/测试 3 的语料与生成脚本可直接复用) - **实施期新增(方案初稿未预见)**:A2「skill 闲置」的一半输入是"这台机器装了哪些 skill"(`src/diagnostics/index.mjs:1243` 的 `listInstalledSkills()` 读 `$HOME/.agents/skills`、`$HOME/.workbuddy/skills`,与 `DSH_HOME` 无关)—— 全新的 CI runner 上必然 0 个已装 skill → A2 命中 0 → 语料自检假红,正是 §4.2 那类"断言绑在宿主机上"的病。**修法**:新增 `AGENT_CLINIC_SKILL_DIRS`(路径分隔符分隔,未设时行为不变),语料自带一个必然闲置的 demo skill 目录,CI 与 `--verify` 都指向它;smoke 增一条断言守住该覆盖(`A2:已装 skill 名单可由 AGENT_CLINIC_SKILL_DIRS 给定`) - **实施期实测命中数(`--verify` 实跑)**:C1/C2/C3/C5/C6/E1/E2/E3/E4/E5/N1/A3 = 2,C7/N2/N3/N4/A2/A4/A5 = 1 → 19/19 命中,`DSH_SMOKE_REQUIRE_DATA=1` 下 smoke 263 通过 · 0 失败 · 0 跳过 ### 4.2 smoke 降级语义:skip ≠ fail **现状(缺陷)**:`scripts/smoke.mjs:62` `check('会话根目录存在', fs.existsSync(root), root)` 与 `:65` 在无数据环境计为失败,`:68-71` 虽打印"无会话数据,跳过剩余检查",但**跳过前的断言已计为失败**,降级逻辑形同虚设 → `:2273 process.exit(fail === 0 ? 0 : 1)` 退出码 1。这正是 CI 全红的根因。 **调整后**: - 新增 `skipCheck(name, reason)`:不计入 `fail`、不计入 `pass`,单独计数 `skip`,输出 `⊘ name —— reason` - `:62`/`:65` 改为:无会话根目录 / 扫不到文件时走 `skipCheck`,其余检查照旧跳过 - 新增环境变量 `DSH_SMOKE_REQUIRE_DATA=1`:设置后 **skip 直接升级为 fail**(CI 必设)。这样"无数据静默通过"在 CI 里不可能发生 - 汇总行改为 `通过 N · 失败 M · 跳过 K`,末尾第 18 段新增断言:`DSH_SMOKE_REQUIRE_DATA=1 时跳过数必须为 0` - 退出码语义:`fail > 0 → 1`;`REQUIRE_DATA=1 且 skip > 0 → 1`;纯降级(无数据、未要求数据)→ 0,并在输出中写明"本次为降级运行,未验证的维度:…" ### 4.3 CI 重写(`.github/workflows/ci.yml`) **现状(缺陷)**:`ci.yml:32-33` 的 smoke 步骤未设 `DSH_HOME`、未造合成数据 —— runner 上不存在 `~/.dsh/sessions`,退出码必为 1;两次 CI 运行 `conclusion: failure`,**当前所有提交都未经自动化验证**。 **调整后步骤序列**: ```yaml - name: 生成合成会话语料 run: node scripts/fixtures/synth-dsh.mjs --out "${{ runner.temp }}/dsh-home" env: { DSH_HOME: "${{ runner.temp }}/dsh-home" } - name: 自检(合成数据,3 平台 × node 22.15/24) run: node scripts/smoke.mjs env: DSH_HOME: "${{ runner.temp }}/dsh-home" DSH_SMOKE_REQUIRE_DATA: '1' - name: 自检(空数据降级,必须退出码 0) run: node scripts/smoke.mjs env: { DSH_HOME: "${{ runner.temp }}/agent-clinic-empty-home" } - name: 徽章与实际通过数一致 run: node scripts/badge.mjs --check - name: 空数据下跑一次报告 # 保留 - name: 目录命令自检(不联网) # 保留 ``` - `privacy` job 保留不变(它守的是"仓库里不得出现会话数据/本地路径",与本次修订无关) - CI 状态徽章进入 README 顶部(见 4.4) ### 4.4 徽章治理(评估 §3.2:项目自身价值主张被直接违背) **现状(缺陷)**:`README.md:21` 静态徽章 `smoke-249%2F249-brightgreen`;`README.md:110`、`README.md:344` 同样声称 249。实测 247 通过 / 2 失败 / 退出码 1。项目自检第 18 段(`scripts/smoke.mjs:2257`)断言了"声明的断言全部真的执行过",**但没有断言守护徽章数字本身** —— 评估报告点名这是发布纪律缺陷。 **调整后**: 1. 新增 `scripts/badge.mjs`: - `--check`:跑 smoke(或读 `--from-file `,规避宿主沙箱内 pipe 捕获 EPERM),解析汇总行 `通过 N · 失败 M`,对比 `README.md` 徽章数字与两处文字声称;不一致 → 非零退出并打印期望/实际 - `--write`:本地维护者用,把徽章与文字改为实际值(**只在 CI 全绿后使用**) 2. README 徽章改造: - 静态数字徽章替换为 **CI 实际状态徽章**(GitHub Actions 工作流徽章) - `smoke N/N` 徽章保留,但由 `--write` 生成,并由 `--check` 在 CI 守护 - `README.md:110`、`README.md:344` 的"249 项/249/249"改为由 `--write` 同步的数字 3. 新增自检断言(照抄项目已有的"声明的断言全部真的执行过"同款写法):**README 声称的通过数 == smoke 实际通过数**,作为第 18 段第 3 条 **实施偏差(已落地,与初稿不同,必须如实记录)**: - **没有新建 `scripts/badge.mjs`**,核对逻辑**内建进 `scripts/smoke.mjs` 的收尾段**: ①最终条数只有跑完才知道,独立脚本要么重跑一遍 smoke、要么解析日志,两者都更脆; ②沙箱内 `spawn` 捕获子进程输出会 EPERM(本项目踩过),"跑 smoke 再解析汇总行"这条路在受限期跑不通。 因此 `--write-badge` 改为 smoke 自己的开关(仅 `fail===0 且 skip===0` 时才改写)。 - **守护范围比初稿多一个文件**:除 `README.md` 的三处(徽章、`冒烟测试 N/N 通过`、`自检(N 项)`)外, 还核对 `SKILL.md` 的 `自检(N 项)` —— 它当时停在 233 项,是同一类漂移。 - **徽章策略**:静态 `smoke N/N` 徽章保留(由 `--write-badge` 同步 + 收尾段守护), 另外新增一枚 **GitHub Actions CI 实况徽章**(就是工作流的真状态,改不了), 两者并列并加一行说明,读者不必猜哪个可信。 ### 4.5 断言自包含规则(硬约定,写入 CONTRIBUTING 段与 SKILL.md) 评估报告 §4.2 的教训固化为规则,防止同类缺陷再犯: 1. **禁止**断言依赖"本机恰好有对应数据"或"每指标恰好命中 N 处";需要数据形态时用 `scripts/fixtures/synth-dsh.mjs` 或就地构造 scan 对象 2. **禁止**条件分支静默吞掉断言(沿用第 18 段既有机制,新断言一律无条件执行) 3. 新增/修改指标时,必须同步:`docs/diagnostics.md` 镜像条目 + 一条护栏断言 + 合成语料覆盖 4. 断言涉及"全局上限/名额/条数"时,必须用**由本测试构造的输入**(本次 §4.1 的成因) --- ## 5. 发布门槛与排期 **发布门槛(v0.2.0 生效)**:评估报告 §八 顺序即本项目的准入顺序,任一项不满足不得发布 / 不得收录。 | 序 | 准入门槛 | 验收方式 | |---|---|---| | 1 | smoke 降级语义修复 + CI 造合成会话 | CI 6 job 全绿;空数据运行退出码 0;`REQUIRE_DATA=1` 时跳过数为 0 | | 2 | 现搜条目进处方层 | 构造数据断言 + 名额护栏断言全绿(≥6 类问题时插件处方仍在全局列表) | | 3 | N3 被拒判定修复 | 断言:`approved/accept/granted/allow` 不计被拒;`deny/rejected` 计被拒;其余计未知 | | 4 | 徽章 == 实际通过数 | `node scripts/smoke.mjs`(收尾段自动核对 README 三处 + SKILL.md,不一致即退出码 1;`--write-badge` 回写);README 另有 CI 实况徽章 | | 5 | 以上全绿 | 才谈上线与收录(npm 发布另需凭据与发布决策) | **排期**: - **P0(本次修订,v0.2.0 前必须完成)**:4.1 语料装置、4.2 降级语义、4.3 CI、4.4 徽章、3.1 轮转、3.2 断言、2.1 N3、2.2 C1 - **P1**:A1(MCP 指标)、A3 的 `GENERIC_TOOLS` 自动学习(见第 6 节) - **P2**:npm 发布、plugin/client.js 浏览器端测试、定位符解析合并为一份 --- ## 6. 已知缺陷登记(保留项 + 本次更新) | 编号 | 缺陷 | 状态 | 说明 | |---|---|---|---| | D1 | npm 未发布 | 保留 | 需凭据 + 发布决策;受第 5 节门槛约束 | | D2 | A3 的 `GENERIC_TOOLS` 硬编码 | 保留 | `request/header` 无法区分内置与对症候选,自动学习会误排真实候选 | | D3 | 定位符解析仍两份 | 保留 | `src/adapters/base.mjs:parseLocator` 不支持区间 / `src/evidence/index.mjs:parseRange` 支持;`show` 区间展开有 200 行上限 | | D4 | `plugin/client.js` 无浏览器端测试 | 保留 | 仅 Node 侧功能测试(面板 `state.live→组件 live` 已有源码级护栏断言) | | D5 | K1/K2 未实现 | 保留 | 隐私取舍,非平台做不到 | | **D6** | **CI 全红未修 / 徽章失实 / smoke 降级失效** | **本次修复** | 评估 §3.1、§3.2、§4.2 | | **D7** | **现搜条目进不了全局处方(名额轮转 bug)** | **本次修复** | 评估 §4.1 + 本项目实测复现 | | **D8** | **N3 非 allow 一律计"被拒"** | **本次修复** | 评估 §4.3 | | **D9** | **C1 基线含被判会话自身** | **本次修复** | 评估 §5.1(口径变更,见 2.2) | --- ## 7. 常用命令(更新) ```powershell $node = "$env:USERPROFILE\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe" & $node scripts\fixtures\synth-dsh.mjs --out .tmp\synth-home # 新增:造合成语料 $env:DSH_HOME = ".tmp\synth-home"; $env:DSH_SMOKE_REQUIRE_DATA = "1" & $node scripts\smoke.mjs # 自检(合成数据,跳过数必须为 0) & $node scripts\badge.mjs --check # 新增:徽章与实际通过数一致性 & $node src\cli.mjs report # 体检(默认零外发) & $node src\cli.mjs report --live # 联网现搜 & $node src\cli.mjs show "#L100-L110" --context 2 & $node scripts\catalog-sync.mjs --pool 900 --limit 150 ``` --- ## 8. 文档同步清单 | 文档 | 变更 | |---|---| | `README.md` | 徽章改 CI 状态 + 数字由 `badge.mjs --write` 生成;"当前状态/已知缺陷"段同步第 6 节 | | `CHANGELOG.md` | v0.2.0 条目:质量保障层重建(CI/合成语料/徽章守护)、N3 三分类、C1 基线口径变更、处方名额轮转修复 | | `docs/diagnostics.md` | N3 outcome 分类;C1 基线排除自身(该文件是"指标 = 代码镜像",改动必须带断言) | | `docs/report-format.md` | 全局处方名额轮转语义;"因名额未展示"的处方计数 | | `docs/data-formats.md` | 合成语料装置说明(依赖它做 CI 与离线复现) | | `SKILL.md` | 4.5 断言自包含规则 | | `docs/plan-2026-10-15.md`(本文) | 新增,作为 v0.2 修订基准 | --- # 【修改对照表】评估问题 → 项目调整措施 ## 高严重度 | # | 评估发现 | 项目调整措施 | 改动点 | 验收 | |---|---|---|---|---| | 1 | **§3.1 CI 全红且根因未修**:6 个 smoke job 全失败;smoke 步骤未设 `DSH_HOME`、未造合成数据;`check('会话根目录存在')`/`check('扫到会话文件')` 在无数据时计入 fail,`exit(fail===0?0:1)` 退出码 1 —— "跳过前的断言已计为失败,降级逻辑形同虚设" | ① 新增 `skipCheck()`,无数据走跳过不计失败;② 新增 `DSH_SMOKE_REQUIRE_DATA=1`,CI 下设了就必须有数据(skip 升级为 fail);③ CI 增"生成合成会话语料"步骤并给 smoke 步骤设 `DSH_HOME`;④ 保留"空数据降级必须退出码 0"为独立步骤 | `scripts/smoke.mjs:37-47`(check 体系)、`:62`、`:65`、`:68-71`、`:2268-2273`(汇总与退出码);`.github/workflows/ci.yml:22-33` | CI 6 job(3 平台 × node 22.15/24)全绿;无数据运行退出码 0 且输出"降级运行,未验证维度";`REQUIRE_DATA=1` 时跳过数 = 0 | | 2 | **§3.2 徽章与实际状态不符**:README 声称 `smoke 249/249`,实测 247 通过 / 2 失败 / 退出码 1;第 18 段断言了"声明全部执行"却**没有断言守护徽章数字本身** | ① 新增 `scripts/badge.mjs --check/--write`;② CI 增"徽章与实际通过数一致"步骤,不一致即红;③ 静态数字徽章替换为 CI 实际状态徽章,`smoke N/N` 由 `--write` 生成;④ 第 18 段新增断言「README 声称的通过数 == 实际通过数」 | `README.md:19-21`、`:110`、`:344`;新增 `scripts/badge.mjs`;`scripts/smoke.mjs:2244-2266` | `badge.mjs --check` 退出码 0;README 三处数字与实跑一致;CI 徽章反映真实运行状态 | ## 中严重度 | # | 评估发现 | 项目调整措施 | 改动点 | 验收 | |---|---|---|---|---| | 3 | **§4.1 现搜条目"进推荐不进处方"**:注入合规现搜条目后 `live 进推荐=true`、插件类处方仍只有快照条目;"该断言在仓库当前代码上必然失败" | ① **根因修复**:`matchPrescriptions` 第 0 轮吃光全部名额(`perFinding=2`/`limit=6`,finding ≥6 类时 `round=1` 永不执行),改按轮配额 `roundCap = ceil(limit/perFinding)`;② 断言从"全局列表"改为构造输入的双重断言(per-finding 口径 + 名额护栏);③ 未入选处方不静默丢弃,给出计数 | `src/prescriber/index.mjs:520-548`;`scripts/smoke.mjs:1487-1515`;`src/report.mjs:297/311/328`;`docs/report-format.md` | 构造 7 类 finding + 1 条现搜条目,条目出现在 `matchPrescriptions` 输出中;`prescribeOne(C1)` 亦含该条目;两条断言不读本机 `scan` | | 4 | **§4.2 断言对数据形态有隐式依赖**:「合并确实压掉了重复(类数 < 处数)」要求每指标命中 ≥2 处,干净合成数据必挂;"248 项自检是针对作者本机数据校准的,不是自包含的" | ① 新增合成语料生成器 `scripts/fixtures/synth-dsh.mjs`,写死不变式"每指标 ≥2 处"并自带命中数自校验;② 合并类断言改用就地构造的 finding 集(同一 id × 3 处 → 断言 `groups.length===1 < findings.length===3`),不再依赖本机 `scan`;③ 固化"断言自包含规则"到 SKILL.md 与新断言清单 | 新增 `scripts/fixtures/synth-dsh.mjs`;`scripts/smoke.mjs:893-924`(12c 合并段)、`:1518+`(第 15 节护栏);`SKILL.md`;`docs/data-formats.md` | 干净环境(无本机会话)跑 smoke 全绿;生成器在指标命中 < 2 处时 exit 1;新增断言在两种环境(合成/本机)下结论一致 | | 5 | **§4.3 N3"被拒"判定脆弱**:`!/^allow/i` 使任何非 `allow*` 的 outcome(`approved`/`accept`/`granted`)都被计为拒绝;实测 5 个 `approved` 报成"被拒 5 次";若 DSH 改用 `approved` 措辞,被拒字段将系统性全错 | outcome 判定由二分类改三分类:显式匹配 `deny/denied/reject/rejected/decline/refus/forbid` 计"被拒";`allow/approv/accept/grant/ok/yes/permit` 计通过;**其余计"未知结果"**,绝不猜;分母口径分列"被拒 / 未知结果 / 无决定记录" | `src/diagnostics/index.mjs:818-844`;`docs/diagnostics.md`(N3 条目) | 新增断言:5×`approved` → 被拒 0;`deny`/`rejected` → 被拒计数正确;`weird` → 未知结果计数,不进被拒 | ## 低严重度 | # | 评估发现 | 项目调整措施 | 改动点 | 验收 | |---|---|---|---|---| | 6 | **§5.1 C1 基线池含被判定者自身**(`[100,250,2000]` → 基线 250,含被判会话的 2000);"中位数对离群值稳健,结果本次仍正确,但更严格的实现应排除自身" | 逐会话排除自身后取中位数;同步改写 `docs/diagnostics.md` 基线定义、`CHANGELOG.md` 标注口径变更(示例:同一组数据 8.0× → **11.4×**,倍率上升、更敏感,口径更正见 §2.2),并加护栏断言防回退到含自身口径 | `src/diagnostics/index.mjs:441-450`;`docs/diagnostics.md`;`CHANGELOG.md` | 断言:`[100,250,2000]` 三会话下摘要含 `是主会话基线 175 的 11.4 倍` 且 C1 命中;A/B 各 600 的既有合成样本结论不变 | | 7 | **§5.2 A2 窗口说明诚实**("还差约 26 天本项才会开始生效") | 保留,作为可预期性设计的样板;同类指标(样本不足)沿用同一"还差多少"表述 | 不改代码;写入 `docs/diagnostics.md` 措辞约定 | 现有断言保留 | | 8 | **§5.3 `evidence` 已支持 `#L-L`** | 保留;D3(定位符解析两份)仍登记为待办,不因本次实测通过而扩大承诺 | 不改代码 | 现有断言保留 | ## 值得肯定项(评估 §六)的固化措施 | 评估肯定项 | 固化措施 | |---|---| | 诊断核心零误报零漏报(10 项指标) | 由合成语料装置 + 每指标 ≥1 处命中不变式长期守护(4.1) | | 证据层 `show` 行号与 `report` 严格一致 | 保留现有断言,不再依赖本机数据形态(4.5 规则 1) | | 只读承诺兑现 | CI `privacy` job 保留不变 | | 无数据时逐一说明原因 | 保留文案,并把"降级"与"失败"在退出码上彻底分开(4.2) | | 口径自觉(实测 vs 估算) | 保留;N3 新增"未知结果"归口即同一原则的延伸 | | 第 18 段"自检自身完整性" | 保留并扩展第 3 条:徽章数字 == 实际通过数 | | 零依赖、离线可用、catalog 无快照如实提示 | 保留;`badge.mjs` 亦不引入第三方依赖 | --- ## 附:本方案不做什么(避免偏离评估结论) 1. 不重构诊断层判定逻辑(评估实测全中,动它只有风险没有收益) 2. 不改动证据层与只读承诺 3. 不因"CI 要绿"而删除断言 —— 断言红就修实现或修断言的输入构造,禁止调低阈值掩盖 4. 不在门槛未全绿前做 npm 发布与目录收录 --- ## 附二:2026-10-15 用户决策台账(D1–D11) 用户逐条拍板后落地。**这张表是"谁决定、决定什么、现在什么状态"的唯一台账** —— 后续改动若要推翻某条决策,请先在这里改状态并写明原因。 | 编号 | 议题 | 用户选择 | 状态 | |---|---|---|---| | D1 | npm 发布方式 | 先发 **0.2.0-rc.1 预发布**(`--tag next`) | ⏳ 待凭据(本机无 npm / 无 `.npmrc`) | | D2 | 版本号 | **0.2.0**(预发布为 0.2.0-rc.1) | ✅ `package.json` 已改 | | D3 | 全局处方名额 | **6 → 8**(覆盖前 4 类各 2 条) | ✅ 代码 + 文档 + 断言 | | D4 | K1/K2 隐私取舍 | **指纹写入本地快照** | ✅ 已实现(19 → 21 项指标) | | D5 | A3 通用工具清单 | **半自动学习 + 基础集保底** | ✅ 代码 + 文档 + 断言 | | D6 | A1 MCP 指标 | **当时决定继续排期**(等真有 MCP 环境,不用合成数据自证) | ✅ **已完成**:2026-10-15 接入本地 filesystem MCP server 后实现(配置驱动:读 profile patch 的 MCP 行 + 按 `mcp____*` 计数),指标 21 → **22 全部实现** | | D7 | 定位符解析两份 | **合并为一份**(以 evidence 版为准) | ✅ base 层支持区间,evidence 转发 | | D8 | `show` 200 行上限 | **增加 `--offset/--limit` 显式分页** | ✅ CLI + 提示给可复制命令 | | D9 | 插件浏览器端测试 | **自写零依赖 DOM 桩** | ✅ 65 条;顺带登记 1 处真实缺陷(第六轮已修,条数补到 76) | | D10 | 示例与实测数字 | **重生成示例 + 实测数字加守护** | ✅ `docs/measured.json` + 断言 | | D11 | 语料边界覆盖 | **不补语料,改列对照表** | ✅ `docs/diagnostics.md` 边界分支一节 | **实施中发现、原方案未预见的三个事实**(都已写进代码注释与文档): 1. **K2 的判据只能是代理信号** —— 日志里没有"这件事已解决"的显式信号;不加"最早那次之后不能有失败" 这层收紧时,合成语料里 K2 一次报 8 条(同一段文本在很多会话里都出现)。 2. **"内置包白名单"在会话数据上不可实现** —— 只有工具名,没有工具→包的映射。改用可复核的广度判据。 3. **零依赖 DOM 桩顺带抓到真缺陷** —— `/report` 失败时 `liveErr` 被设置但错误分支不渲染它, 点击"联网现搜"在错误态下无可见反馈(`plugin/client.js` 的 liveErr 渲染点落在 OK 分支内)。 本轮**未改产品代码**,以"待修缺陷"断言登记。 **实施期踩到的坑**:smoke 里新建目录若用 `new URL('../.tmp/x', import.meta.url)`(结尾无 `/`), URL 解析会把最后一段当文件名,`./sub/` 被拼成兄弟目录 —— 断言读到 0 个文件而假红。 正确写法:`fileURLToPath(new URL('../.tmp/x/', import.meta.url))` 再 `path.join`。 **实施期踩到的坑(二):import 有副作用 → 间歇性假红**。 `scripts/fixtures/synth-dsh.mjs` 原本末尾是裸 `await main()`,于是 smoke 里 `import { writeCorpus }` 会**顺带执行 CLI**,以 `DEFAULT_OUT`(`.tmp/synth-home`)为目标 重新生成语料 —— 而那正是开发者本地跑 smoke 时 `DSH_HOME` 常指向的目录。 后果:smoke 跑到一半语料被删了又写回,表现为**间歇性**假红,且症状会骗人: - `扫描没有新增/删除任何会话文件 1 → 0`(改到一半被抓到) - `ENOENT: 打不开…/session.v4.jsonl.zstd`(文件正好在那一刻不存在) - 断言总数在 288 / 303 之间跳(中途抛错提前结束) - 单独跑一次是绿的,所以最容易被当成"偶发环境问题"放过 **修法**:fixture 末尾加直接执行守卫(`path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))`), 并在 smoke 里加一条"import 前后 `DEFAULT_OUT/sessions` 的 mtime 不变"的断言。 教训写进这里是因为它比 bug 本身值钱:**"偶发"几乎总是有确定性根因,只是被并发/时序藏起来了**。