# spec-forge 判定链:断言可复核表 > 写法借鉴自内部评测套件的可复核表: > 原则(research 验收):每条断言给出**来源**、**证据**、**复现路径**(可执行)。 > 没有复现路径的断言不算断言。 > > 注:复现路径里 `dev/` 下的测量脚本(真实语料回放、分桶门禁)属本机工具,**不入库** > (`.gitignore` 的 `dev/` 同源);`node --test` 与 `npm run bench` 类断言任何人可复跑。 ## 判定链总览 ``` 需求原文 └─ cleanRequirement (lib/prepare.js) 剥 JSON/堆栈/hex/内联数据对;路径不进指纹 └─ queryFingerprint (lib/fingerprint.js) 强 token 全留 + 2-gram 按出现顺序取 16 └─ semanticOnly (lib/match.js) 剔细节 token(时间戳切片、纯数字) └─ scoreTemplate lexical = 0.6·cos + 0.4·cov └─ 主题证据闸(hasEvidence) └─ rankTemplates 证据优先 → 证据质量 → 分数 ``` --- ## 回归口径(2026-10-20 定稿) ### 目标不是 100% 召回 要求 100% 会逼出「特定化」:候选集被迫放大、低 IDF 泛用词被迫保留、 中性示例名被当锚点,结果是同项目跨主题互相误匹配、FP 上升。 **通用性 = 跨项目、跨主题分布上不污染、不挤占、不误匹配。** 它对应的是四个可测指标,不是单一 Recall: | 指标 | 判据 | 门禁类型 | |---|---|---| | global 查询里项目模板被 hit | 接近 0 | 硬门禁(不得上升) | | 后过滤候选挤占率 | 0 | 硬门禁(不得上升) | | 同项目跨主题 FP 率 | 可控且清洗后显著下降 | 硬门禁(不得上升) | | 各桶 Recall@k | ≥ 基线 − 容差(默认 10pp) | 软门禁(允许波动) | ### 评估本身有抽样误差 回归集不是全量分布,Recall 是估计值。样本少时几个 query 波动就能让指标跳几个点。 所以: - 门禁用「基线 − 容差」,不用绝对值; - **分层四桶**,不看总均值; - `n < 10` 的指标标注 ⚠小样本,数值不可当结论。 四桶定义: | 桶 | 含义 | |---|---| | `global-only` | 通用查询(与具体项目主题无关) | | `same-same` | 同项目同主题(期望命中该项目自己的模板) | | `same-cross` | 同项目跨主题(期望命中 global 模板,项目模板不该抢) | | `cross-project` | 跨项目查询 | ### 运行方式 ```bash cd dsh-spec-forge node dev/regression-gate.mjs --baseline # 生成/覆盖基线 node dev/regression-gate.mjs # 按基线判门禁(容差 10pp) node dev/regression-gate.mjs --tolerance 0.15 ``` --- ## 断言表 | # | 断言 | 来源 | 证据 | 复现路径 | |---|---|---|---|---| | V1 | 路径不产出任何 token | `lib/fingerprint.js` `tokenize()` | `tokenize('修改 src/main/java/XxxController.java')` 无路径相关 key | `node --test tests/fingerprint.test.js` | | V2 | 覆盖率分母排除低信号/细节 token | `lib/match.js` `skipInCoverage()` | 同一主题长文本分数不随描述变长而下降 | `node --test tests/match.test.js` | | V3 | 证据闸:≥2 个证据词才放行;单 ASCII 标识符需在 name/tags 声明过 | `lib/match.js` `hasEvidence` | 单证据 `日志`/`加载`/`插件` 一律不命中;单证据 `switch`(在 tags 里)命中 | `node --test tests/match.test.js` | | V4 | 平台标签不作主题证据 | `lib/match.js` `STRUCTURAL_WORDS` | 「小程序端加个提示」不进证据 | `node tools/spec-forge-cli.mjs bench` | | V5 | 排序按证据质量优先 | `lib/match.js` `compareByRank` | 项目模板占 same-cross top1 从 78.6% → 25%(清洗后) | `node dev/measure-buckets.mjs` | | V6 | 长文本置信度不反向 | 覆盖率分母口径 | `跨天时长` 与「+贴接口原文」分数不降 | `node tools/spec-forge-cli.mjs recall "<原文>" --cwd …` | | V7 | 模板指纹里 tags 优先 | `lib/fingerprint.js` `fingerprintTemplate` | tags 配额 16(门禁模板 13 个标签全进指纹) | `node --test tests/fingerprint.test.js` | | V8 | 项目模板指纹只用 name+tags | `topicOnly` 模式 | 项目模板指纹不含 `spot`/`wxml`/业务症状 | `node dev/check-behavior-dupes.mjs` | | V9 | 数据根只有一处 | `doctor` 孤儿检查 | 「模板 18 份(global 11 + 项目 7)」+ 无另一根告警 | `node tools/spec-forge-cli.mjs doctor` | | V10 | 分层回归门禁 | `dev/regression-gate.mjs` | 8 项指标全过,小样本标注 | `node dev/regression-gate.mjs` | | V11 | 规则可证伪性 | `tests/mutation.test.js` | 变异后指标有可观测变化;无变化时诚实报「无护栏覆盖」 | `node --test tests/mutation.test.js` | | V12 | 真实语料回放可复算 | `dev/replay-run.mjs` + `dev/replay-hits.mjs` | 某前端项目群 239 条真实需求:命中 28 条(11.7%),命中条目 35,逐条可查证据词 | `node dev/replay-hits.mjs ` | --- ## 指标基线(2026-10-20 晚 · 0.7.5) | 指标 | 值 | 样本 | 判据 | |---|---|---|---| | 插件测试 | 253/253 | — | `node --test` | | bench 召回 | 100%(19/19) | 19 | `node tools/spec-forge-cli.mjs bench` | | bench 假阳性 | 0%(0/17) | 17 | 同上 | | audit 召回 | 87.5%(7/8) | 8 | `node tools/spec-forge-cli.mjs audit` | | audit 假阳性 | 8.3%(1/12) | 12 | 同上(B-02 边缘案例,已记录) | | **真实语料命中率** | **11.7%(28/239)** | 239 | `node dev/replay-hits.mjs ` | | **真实语料判定正确率** | **约 60%(新增 17 条命中里 11 真 / 4 假 / 2 存疑)** | 17 | 逐条人工判定 | | 四桶 Recall | global-only 0%(n=2) / same-same 100%(n=1) / same-cross 100%(n=4) | ⚠小样本 | `node dev/regression-gate.mjs` | | 跨主题 FP(same-cross) | 7.1%(1/14,长文档粘贴类) | 14 | 同上 | | 项目模板被 hit(global-only) | 0% | 8 | 同上 | | 后过滤挤占率 | 0% | 8 | 同上 | ### 按真实语料补模板(2026-10-20 晚,覆盖率 4.6% → 11.7%) 先量化再动手:239 条真实需求里,「接口文档 / 契约对齐」类 88 条、「角色·权限·菜单」26 条、 「自定义输入控件」17 条、「上传」24 条 —— 而库里一条对应模板都没有。据此补 4 份 global 模板 (seed 源在本机 `tools/` 下,不入库): | 模板 | 来源主题 | 新增命中 | |---|---|---| | 接口契约与前端实现漂移:字段名、类型、必填、前缀 | 88 条 | 7 条,判定全对 | | 导航双份维护:接口菜单数据 vs 前端硬编码路由 | 菜单 16 条 | 5 条,4 对 1 疑 | | 可见入口与角色判定不一致 | 角色 14 条 | 4 条,2 对 2 假 | | 自定义输入控件与容器状态不同步 | 17 条 | 0 条(证据闸未过,见边界) | **A/B(同一语料、同一口径)**:命中查询 11 → 28(4.6% → 11.7%),命中条目 13 → 35, bench 19/19 与 audit 87.5%/8.3% 均未回退。 ### 两条标注修正(不是调参换分) | 样本 | 原标注 | 改为 | 理由 | |---|---|---|---| | 样本 A(截图提问) | 门禁模板 | `none` | 「这是页面截图,你看有加载成功吗」问的是字体加载,与门禁无关;旧标注是系统误命中后追认的 | | 样本 B(文档更新) | 门禁模板 | 接口契约模板 | 「接口文档有更新,参照文档检查页面」是契约对齐主题;旧标注同样是误命中追认 | 修正后 same-cross 的跨主题 FP 从 0% 变成 7.1% —— **不是变差,是原本被错误标注遮住的假阳性露出来了** (门禁模板被一整段粘贴的预约接口文档命中)。这条已列入已知边界。 ### 试过但回退的方案(负结果,别再走一遍) | 方案 | 动机 | 结果 | |---|---|---| | 证据里必须有一个「真词」(模板侧权重 ≥3) | 中文 2-gram 切出的 `牌组`/`件调` 也能算证据,太水 | 真实语料 28→24 条,但 **bench 19/19 → 16/19**,代价太大,回退 | | 把「可见性」塞进门禁模板的 trigger | 少建一份模板 | 指纹 40 格已被占满,新词进不去,命中数纹丝不动,回退 | | 按 query 长度提高证据门槛(长文本要 ≥3 证据) | 粘贴长文档易误命中 | 被误命中的那条 query 只有 156 字,规则打不到它,未实施 | ### 0.7.5 的规则变更(取消单证据通道) 用某前端项目 212 条真实需求回放(而非 23 条人工标注集)发现:31 条命中里 **20 条是单证据**, 而单证据的判定正确率只有 **1/20**。错法全是「词面同形、语义不同」: | 需求里的词 | 需求说的是 | 模板说的是 | |---|---|---| | `日志` | 审计日志**页面** | 用宿主日志**定位**静默失效 | | `加载` | 字体文件**加载**了却没生效 | 空态文案与**加载态** | | `插件` | 如何**发布**一个插件 | 插件看着正常却没**生效** | | `时段` | 时段颜色没标注 | 跨天**时段行**的时长计算 | 变更后:命中条目 31 → 12,判定正确率 16% → 40%,代价是漏掉 1 条单证据真命中(`优惠`)。 同时把 `检查`/`生效`/`业务` 三个状态词加入低信号表(真实语料贡献 3 条假阳性、0 条真命中)。 `不可` 试过拉黑但会让 bench 的「提示不可预约但按钮还能点」漏召回,已回退。 --- ## 诚实边界 以下**尚未验证**或**已知不足**,不要当结论用: 1. **样本量严重不足**。四桶正例只有 2/1/5 条,⚠小样本。100% 不可当结论 —— 1~2 条样本变化就能让指标跳 20pp。要拿到可信曲线,每桶至少需要 20~30 条。 2. **`hasEvidence` 闸在 bench 上无护栏覆盖**。17 条护栏样本的 lexical 全部 < 0.15, 根本没走到证据闸;变异测试诚实报告「该规则当前无样本能暴露」。 它在真实语料上有作用(audit 拦住 11/12 条无关需求),但缺专门的窄样本集。 3. **`docFreq` 的 1/4 专有阈值与 1/3 普遍阈值是经验值**,未做敏感性分析。 4. **`inferQueryTags` 的 limit=8 硬截断**在长文本下仍会挤掉低权重主题词,未修。 5. **项目模板的通用性只是「指纹层」解决**:它们的 name/tags 已改为可复用主题, 但正文仍保留业务细节(这是对的 —— 那是它们的价值)。 6. **B-02 那条假阳性未根治**:原文「字数按中文字符算,允许 ±20%」里的「允许」 与门禁主题无关,但因同字面被当证据。属词义歧义,纯词面方法有上限。 7. **`same-same` 桶只有 1 条正例**,几乎无统计意义。 8. **覆盖率仍低但已从 4.6% 提到 11.7%**。239 条真实需求里 28 条有模板可用。 剩余缺口集中在「上传链路」(24 条,多为「加个上传字段」而非复用教训)、 「列表分页/导出」(3 条)、「UI 美化」(无机制可沉淀,不适合做模板)。 **补模板要按语料频次来,不能凭印象。** 9. **「自定义输入控件」模板没被命中**:它的 trigger 用了「输入面板/容器/回显」等抽象词, 而真实需求说的是「小键盘把输入框挡住了」,两边词面交集只有 `输入`/`组件`(低信号), 凑不够 2 个证据。**抽象到机制层的代价是牺牲词面召回** —— 这类模板要在引号段补用户原话。 10. **粘贴长文档会带来假阳性**:门禁模板被「接口文档 spot 有更新…」命中, 因为粘贴的文档里同时出现 `不可` 与 `预约`。纯词面方法分不清「用户的话」与「文档内容」。 11. **标注集存在循环标注污染**。本机标注文件(`dev/` 下,不入库)里至少 2 条 (上表的样本 A、B)是当年系统误命中后被追认的,已修正。 标注必须**先读原文再判**,不能拿系统输出反推。 --- ## 方法论备忘 - **别追 100%**。100% 召回往往是靠「特定化」换来的:把项目业务词当主题锚点, 确实能把特定 query 拉回来,但通用查询会被这些锚点带偏。 - **别只看总均值**。分层看桶,每桶单独判。 - **指标全绿 ≠ 生效**。必须用真实输入形态(长文本、贴报错、贴 JSON)回放, bench 的短句定制语料会系统性掩盖缺陷。 - **变异测试无变化时要诚实报「无覆盖」**,不要硬造样本凑绿。