# Changelog 本插件锁定目标 dsh 版本:`@deepseek-ai/dsh` 0.1.x(developer preview,API 可能有破坏性变更)。 兼容性以实际安装的 profile 依赖树为准。 ## 0.8.3 — 2026-10-31 **长需求不再「贴得越详细越召不回」;项目模板允许单证据,但只认 name。** 起因是一次真实语料检验:同一后台项目连续 6 个列表页 UI 改版,6 轮全部「命中 0 份」。 拆开看是两个独立缺陷,第三个原因是根本没有对应模板(属使用习惯,不是判据)。 ### ① 查询侧被引用数据挤占(`lib/prepare.js`、`lib/fingerprint.js`) `cleanRequirement` 只剥大括号 JSON,不剥 `` / `@image#1:` / `@long-text:"` / URL / 相对路由带查询串。实测 13016 字的需求清洗后仍剩 157 字,其中 `publish/list?keyword=&pageNum=1&pageSize=10` 这类片段产出 `keyword/pagenum/pagesize` 等权重 2 的 strong token。而 `queryFingerprint` 的 strong 是**无上限全收**, 19 项指纹里 13 项是字段名,句尾的「优化」一个都没进来 —— 同一主题,短句 0.23,长句 0.02。 - `cleanRequirement` 先过一遍 `stripReferenceNoise`(新增,`effectiveRequirement` 复用同一份), JSON key 也只从「去引用后的正文」抽,不再从 quote 里的接口返回抄字段名。 - `queryFingerprint` 的 strong 加上限 8,且技术词(`TECH_SET`,权重 3)优先补位。 顺序语义不受影响:否定词判定读的是 `tokenize` 的 Map,不读这个数组。 ### ② 项目模板的证据闸分治(`lib/match.js`) 0.7.5 取消单证据通道是为了挡「日志页面 vs 日志定位」这类跨域同形词,但项目模板 只在本仓库生效、库小、同域,单证据足够指向。实测「00:00-00:00 全天时段」对项目模板 打到 0.45,仅因只命中「跨天」一个词就被挡。 - `scope !== 'global'` 时,1 个证据词即放行,但必须落在**模板 name 原文**里。 - 为什么不用 tags:按 tags 放行时「字体加载成功吗」以 0.555 命中「空态文案脱钩」 (tags 里有「加载态」),跨主题 FP 从 7.1% 翻到 13.3%,被硬门禁拦下。 - 全局模板维持 ≥2,ASCII 单证据例外不变。 ### 验收 `dev/replay-run.mjs` 默认带 `--no-record`(回放不再污染 hitCount)。 | 指标 | 0.8.2 | 0.8.3 | |---|---|---| | 真实语料命中(238 条) | 12.2%(29) | **22.3%(53)** | | 今日 6 轮列表页改版 | 0/6 | **5/6** | | 跨主题 FP · same-cross(硬门禁) | 7.1% | **6.7%** | | bench 召回 / FP | 100% / 0% | 100% / 0% | 单测 266 通过,smoke 全通过,doctor 问题 0。 > 依然未解:6 轮里「开票总览页面也优化一下」仍未命中 —— 该页业务词(开票/总览) > 不在模板 name 里。补词会挤占 name 的 8 个指纹配额,暂不为单条需求调判据。 ## 0.8.2 — 2026-10-26 **项目级重复禁区收进项目档案,模板里只留主题约束。** 0.8.1 的 `doctor` 报了 6 条「5 份模板重复同一条禁区」,但只提示不修——因为直接删掉等于把约束丢了。 真正的问题是**归属错了**:项目级的技术边界(不改接口/后端/mock)被抄进了每份模板, 而项目档案 `profile.md` 本来就是存这类约束的地方(`collectRedlines` 每次召回都会并入)。 ### 判据(`lib/hygiene.js` 的 `classifyDuplicateRedline`) - **只认特征词**:接口 / API / 后端 / 前端 / mock / 类型定义 / 校验 / 公共 / 全局 / 统一 / 调用逻辑 / 工程。 - **不按出现次数**。第一版加了「出现 ≥3 次即项目级」,结果把「不擅自改跨天快捷时长语义」也搬进档案 —— 它重复是因为那 3 份模板同属"跨天时间"主题族,收进档案等于给全仓库任务注入无关约束。 重复次数只能说明「同一主题被拆成了几份模板」,不能说明它跨主题成立。 ### 迁移(`scripts/rebuild-project-scope.mjs --hoist`) - 逐 scope 找出重复禁区 → 分类 → 项目级写入 `profile.md`(`dedupeRedlines` 近重复去重)、 从各模板正文删除;主题级原样保留。 - 本次(某前端项目 scope):**4 条入档**、5 份模板各减 3~4 条、**2 条主题级保留**。 副作用是每次召回多 4 行档案禁区(之前无档案),但模板命中时总数不增(8 行上限内)。 - 幂等:收进档案后模板里不再有这些行,重跑无改动。 ### 体检 - `doctor` 的重复禁区提示改为**只报项目级**(可操作),主题级合并成一行计数,不再逐条刷屏。 ### 验收 - `node --test`:266 通过(新增 `classifyDuplicateRedline` 用例)。 - `bench` 19/19、`audit` 87.5% / 8.3%、44 条路由 implement 41 / triage 3、`regression-gate` 全过 —— 均未变化。 - `doctor`:问题 0,提示仅剩「2 条主题级重复」。 ### 残留 `seed` / `spec_retro` 仍会把同一条项目级禁区同时写进模板与项目档案(插件侧 `persistRedlines` 一直如此)。 重复真正积累时 `doctor` 会报出来,重跑 `--hoist` 即可收敛;写入侧不做自动拆分, 是因为单条禁区的归属判断没有重复上下文时误判成本更高(例如「接口返回的时段字段为分」会被特征词误判成项目级)。 ## 0.8.1 — 2026-10-26 **模板卫生 + 输入护栏:一次性任务边界不许固化进模板,极短/仅引用的输入不再召回。** 0.8.0 的审计暴露两类残留:① 项目模板的「禁区」里写着「本次只动 lib/store.js…不碰 index.js」, 在下一次同仓库的技能审计请求上以 lex 0.48 命中,把一次性边界当硬约束注入 —— 恰好挡住那次该改的 index.js; ② 项目模板指纹混进 `回结`/`储层`/`似纯` 这类正文切片,同仓库任意任务都能靠「检查/返回/结构」凑分。 ### 模板卫生(`lib/hygiene.js` 新增) - **`isTaskScopedRedline`**:判据是「范围词 + 具体文件」同时出现。 只写「只改用户指定的页面文件」「临时脚本用完即删」这类通用纪律**保留**—— 第一版只认关键词,把两条全局模板的通用纪律误判成一次性边界,已修正。 - **写入侧拦截**:CLI `seed` 与插件 `spec_retro` 在落盘前丢弃一次性禁区(`sanitizeRedlines`); `seed` 默认校验不过就退出,`--force` 才强制写入并丢弃。 - **`seed --scope project --cwd <项目根>`**:项目模板此前根本无法通过 CLI 正规沉淀(`cmdSeed` 硬编码 global), 这也是存量项目模板指纹不受控的来源。项目模板的指纹改用 `topicOnly`(只用 name+tags), 并在 `validateSeedTemplate` 里要求 ≥2 个主题标签(否则召回率为 0)。 - **`doctor` 新增两项检查**:一次性禁区、项目模板的正文切片指纹;跨模板重复的项目级禁区列为提示。 - **存量清理**:`scripts/rebuild-project-scope.mjs`(`--apply` / `--retire`)重建 7 份项目模板指纹、丢弃 12 条一次性禁区; 退役 `tpl-8010a721e5`(一次性 purgeStale 任务模板,其 name 里的 spec-forge 会误命中同仓库审计请求)。 ### 输入护栏(`lib/prepare.js`) - **`shouldSkipMatching`**:剥掉路径/URL/代码块/引用块后算「有效字符」。 <6(「启动」)或 <15 且带**截图**引用(「[截图] 实际需求有用到这种配置」)→ 不召回模板、不计命中, 直接给「执行路径:直接实现(输入过短或仅为引用)」。 - **文件/路径引用不算续轮信号**:`@"src/api/order.ts" 这个接口报 500,帮我定位` 是正常需求, 文件本身就是锚点;只有截图才是「看这个」式的指代。 ### 使用统计(CLI `stats`) - 输出命中排行、零命中清单(含创建日期)、按 scope 的分布,`--json` 可机读。 这是 0.8.0 埋下的 `recordHit` 的第一个消费者;样本 <100 次命中前不建议按零命中剪枝(误命中同样计数)。 ### 验收 - `node --test`:265 通过(新增 `tests/hygiene.test.js` 5 组、`prepare` 护栏 4 组)。 - 回归门禁 8 项通过;`same-same` 桶因唯一模板退役而变空,门禁新增「空桶不计失败」(0/0 不是 0%)。 - `bench` 19/19、`audit` 召回 87.5% / 假阳性 8.3%、44 条真实需求路由 implement 41 / triage 3,均与 0.8.0 持平。 - 数据面:`doctor` 问题从 4 项降到 0;审计请求的误命中从 lex 0.48 降到 0。 ## 0.8.0 — 2026-10-26 **路由收口:`triage` 不再当兜底。L2 直通实现、默认值随召回内联,`triage` 只留给 L3。** 起因是在 WorkBuddy 侧对近一周真实任务做使用审计(`tools/spec-forge-usage-audit.mjs`): 23 个会话、44 条真实编程需求回放,**41 条被判 `triage`**,其中 **40 条信号是 `L2:default`**(零信号兜底)。 而体检报告的终点又写着「直接按默认动手、不调用 `ask_user_question`」——与 `implement` 等价。 净效果是每次任务多一轮往返 + 1.3K 字符,路径标签 93% 同值、失去分辨力;实测模型 18 次里 9 次直接绕过体检。 ### 路由(`lib/render.js` 新增 `decideRoute()`) - **路由合成收敛成单一实现**。此前 CLI 与插件各写一份 `fastTrack ? implement : contentGap ? confirm : triage`, 兜底即 `triage`;现在两边都调 `decideRoute()`,壳层不再有第二份。 - **L2 → `implement`**:默认值由新增的 `renderInlineDefaults()` 内联进召回正文 (字段名/UI 类型/默认值/改动文件 + 列表/校验/后端三项通用默认,共 2~3 行),不再单开一次体检。 - **L2 安全阀 → `confirm`**:它的动作一直是"问一次再动手",此前挂在 `triage` 上变成"先体检、报告再叫你去问"。 - **`triage` 只留给 `level=3`**。 ### 分级(`lib/classify.js`) - **补 `rewrite` 信号**:「全部重构一遍 / 项目重写一遍 / 整个后台全部重构 / 结构全扔掉 / 推倒重来」判 L3。 此前只认「整体重构/重写整个」,于是「把 X 项目重写一遍,现有代码结构全扔掉」判 L2; 长文本又躲过 L2 安全阀(阈值是 `<15` 字),报告直接写「按默认执行、禁止追问」—— 这是唯一会把「该追问」变成「不许问」的方向。 刻意不认裸的「重写一遍」:「把这个函数重写一遍」仍是 L2 原子改写。 - **安全阀补「具体对象 + 编辑动词」锚点**:`CONCRETE_OBJECT_HINTS` × `EDIT_ACTION_HINTS` 同时命中即算有落点。 否则「加一个角色字段」「把表格做成滚动条」这类短但明确的需求会被判"未成形"——0.8.0 之后那是白白多问一次。 ### 测量(`tools/spec-forge-usage-audit.mjs`) - 新增 WorkBuddy 侧使用审计:从会话日志解析真实调用,输出调用分布 / 路径分布 / 命中率 / 声明率 / `triage` 跟进率 / 路径违背;`--route-stats` 用当前判据重放近期真实需求,`--reqs-out` 导出需求集。 与 `dev/replay-run.mjs` 互补:前者测「用不用」,后者测「准不准」。 - CLI `recall` 命中时写 `recordHit`(插件侧一直有,壳侧遗漏),补上热度/新鲜度先验的数据来源; `--no-record` 供回放/审计使用,避免污染计数。 ### 验收 - `node --test`:256 通过(新增 `decideRoute` 路由表用例、L2 内联、L3 口语化重构、安全阀四组)。 - 真实语料回放(44 条):`triage` 41 → 3(其余 41 条 implement),`level=3` 3 条保持体检。 - `bench` 19/19、`audit` 召回 87.5% / 假阳性 8.3%、`regression-gate` 8 项全过(打分与命中判定未改动)。 ## 0.7.3 – 0.7.5 — 2026-10-20 **用真实语料回放(而非合成基准)重定判定口径:覆盖率、置信度方向、单证据误命中三件事一起收口。** 起因是把某前端项目群 239 条真实需求逐条回放,发现三类合成基准掩盖不了的问题: 长文本置信度反向(信息越多分越低)、单证据词误命中(判定正确率 1/20)、 平台标签冒充主题证据(真实语料假阳性一度 100%)。 ### 判定与指纹(`lib/match.js` / `lib/fingerprint.js` / `lib/prepare.js`) - **路径在 tokenize 层直接不产出 token**(0.7.4)。此前所有「路径降权/路径片段识别」补丁全部作废 —— 噪声源头消失后补丁也不需要。收益:长文本置信度不再反向(0.298 → 0.599)。 - **平台标签(miniprogram/wxml/vue/vant 等)不进主题证据**(0.7.3)。它们描述「在哪写」,不描述「是什么主题」。 - **取消单证据通道**(0.7.5):必须 ≥2 个证据词;唯一例外是单个 ASCII 技术标识符 且作者在 name/tags 里声明过。单证据在真实语料上判定正确率只有 1/20 (`日志`/`加载`/`插件`/`时段` 全是词面同形、语义不同)。 - **低信号词表补齐**:结构名词 + 否定副词 + 状态词(`检查`/`生效`/`业务`)。 - **覆盖率分母与证据判定统一口径**(同一个 `skipInCoverage`),避免「分母算它、证据不算它」的自相矛盾。 - **指纹 tags 配额 8 → 16**:tags 是作者显式声明的主题,优先级高于 trigger 的举例; 之前 13 个标签的模板被截掉 5 个,最典型需求与模板 cos=0。 - **新增 `stripInlineDataPairs`**:剥「标识符 + 时间戳/数字」的内联数据对,只进分母不进分子。 ### 测量与验收(`docs/VERIFIABILITY.md` 新增) - **断言可复核表**:每条断言给来源/证据/复现路径,没有复现路径的断言不算断言。 - **变异测试**(`tests/mutation.test.js`):判定规则必须「能被打脸」—— 故意改坏,确认基准会红; 无护栏覆盖时诚实报告而不是硬凑绿。 - **分层回归门禁**:四桶(global-only / same-same / same-cross / cross-project), Recall 用「基线 − 容差」软门禁,跨主题 FP / 挤占率 / 项目模板污染用硬门禁。 **不追 100% 召回 —— 100% 往往是靠把业务词当主题锚点的「特定化」换来的。** - 实测:253 项测试全绿;合成基准 19/19 零假阳性;标注审核召回 87.5%、假阳性 8.3%; 真实语料命中率 4.6% → 11.7%(按语料频次补模板后)。 ### 教训(写进文档,避免重蹈) - bench 全是短句,会系统性掩盖长文本缺陷;**判据改动必须先跑真实语料回放**。 - 标注不能拿系统输出反推 —— 至少 2 条标注是「系统误命中后被追认」的,让 100% 召回失真。 - 机制式模板命名会牺牲词面召回:trigger 的引号段只放用户最典型的原话,抽象描述放引号之后。 ## 0.7.2 — 2026-10-17 **修掉"结构名词冒充主题证据",并把指纹容量从经验值变成有基准支撑的常量。** 起因是重建指纹后做验收:7 份真实模板从 24 → 40 个 token,需要确认这个扩容没有被稀释反噬。 A/B 结果显示扩容是对的(正文词召回 3/7 → 7/7,假阳性数三档相同),但同一组无人值守的 无关需求里暴露出 **5/10 误命中** —— 这是 0.7.0 `hasTopicEvidence` 的漏网之鱼,与扩容无关。 ### 误命中的共同点:交集全是"容器名词" | 无关需求 | 误命中 | 唯一交集 | | --- | --- | --- | | 列表默认按创建时间倒序 | 3 份模板(0.174~0.209) | `列表` | | 表单校验提示文案改一下 | 2 份(0.174~0.178) | `表单` | | 详情页样式要统一 | 3 份(0.155~0.164) | `详情` / `情页` | | 卡片上显示个图标 | 1 份(0.192) | `卡片` / `显示` | | 弹窗里加个字段 | 1 份(0.546) | `modal` / `字段` | `列表` 覆盖 4/7 份模板、`表单` 覆盖 3 份 —— 这类词在多份模板里同时出现,**没有区分度**, 拿它当主题证据等于没做。0.7.0 的词表只拦了动作词(提交/新增/接口),漏了容器词。 ### `lib/match.js` - 新增 `STRUCTURAL_WORDS`(列表/页面/表单/详情页/卡片/表格/按钮/弹窗/modal/样式/文案/图标/布局/标题/模块/区域/组件/内容/数据/字段)。 - `isLowSignalToken()`:精确命中动作词表 **或** token 是某结构名词的**片段**(`情页` ⊂ `详情页`)即判低信号。 中文 2-gram 会把「详情页」切成 `详情/情页`,只拉黑整词挡不住碎片。 不做反方向(token 包含结构词):`订单列表`、`组件库` 仍带主题信息,`组件库` 正是组件库类模板的核心词。 - `breakdown` 新增 `hasTopicEvidence` 与 `topicEvidence[]`。此前只在 `hit` 内部使用、不出现在输出里, 排查时只能看到 `undefined`,**无法区分"代码没生效"和"被门槛挡住"** —— 这次就是踩了这个坑。 ### `scripts/bench-cap.js`(新增) 指纹容量基准,回答"上限 24/32/40 哪个好"这个有反作用的问题:名额越大召回越广,但余弦分母也越大。 三组指标:自我召回(索引词覆盖)、正文词召回(正文词覆盖)、无关需求(噪声是否放大)。 0.7.2 实测(7 份真实模板): | 上限 | 自我召回 | 平均分 | 正文词召回 | 无关需求最高分 | 误命中 | | --- | --- | --- | --- | --- | --- | | 24 | 7/7 | 0.565 | 3/7 | 0.256 | 0 | | 32 | 7/7 | 0.641 | 5/7 | 0.233 | 0 | | **40** | 7/7 | 0.611 | **7/7** | 0.209 | **0** | 结论:40 用 0.03 的平均相似度换回 4 条正文词召回,且最高分随名额增加而**下降**(分母归一化生效), 噪声没有放大。因此保留 40,并把这张表作为下次调参的对照基线。 ### 验收 - 单元测试 244 通过(新增 3 条:结构名词不构成证据、2-gram 碎片被挡、有主题词时不受影响)。 - 标注审核 23 条:**TP 8 / FN 0 / FP 0**,召回 100%、假阳性 0%(0.7.1 的保护词修复让此前的 唯一漏召回 A-09 转为命中)。 - 真实回放:有对应模板的 8 条 8/8,其余无关样本 0 误命中。 - **不需要跑 rebuild** —— 本版只改判定层,不涉及指纹生成。 ## 0.7.1 — 2026-10-17 **把"真实输入长什么样"这件事补进召回链路:先清洗,再算指纹。** 0.7.0 换掉了命中判据,但召回仍假设输入是"一句话需求"。真实会话回放(3 个工作区、24 条去重需求) 推翻了这条假设:**38% 的需求把接口 JSON 或错误堆栈整段粘进来**,最长 21K 字。这些内容把指纹向量 的模长撑大,任务描述被稀释 —— 不清洗时,EBUSY 崩溃、跨天时长这类本该命中的需求全部漏召回 (词汇分 0.02~0.10);清洗后回到 0.23~0.32。 ### 新增 `lib/prepare.js` - `cleanRequirement()`:剥代码块、堆栈行、`node:internal` 路径、长 hex、按**括号配对**剥离内联 JSON (不能简单取「第一个 `{` 到最后一个 `}`」,需求里常有多个片段或 JSON 被截断)。 - **JSON 不整段丢**:字段名是最强的标识符信号,只保留有辨识度的业务字段(`id/status/data` 等通用键排除), 最多 8 个。实测全收 24 个会让跨天/报错类命中从 7 掉到 5,8 个则与全删持平、不降级。 - 两条顺序规则写进注释:**先清洗完整原文再截断**(反过来 JSON 被切半、剥离失效); **字段名必须在剥离前抽**。 ### 指纹层三项修正 | 修正 | 病灶 | | --- | --- | | 同义词归一(跨日/跨夜→跨天、开关→switch、下拉→select、弹窗→modal) | 用户说的和模板写的不是同一个词,词面永不交集 | | 领域词提权(跨天/时长/时段/插件/注入/门禁/错误码等) | 不在 `TECH_DICT`,退化成权重 1 的 2-gram,被截断挤掉 | | **保护词**(不可/可用/允许/无法/不能…) | `STOP_CHARS` 里有「不」「可」「用」,逐字过滤会把整个判断词丢掉 —— 「页面提示当前不可用」清洗后只剩「当前」。这是门禁类需求长期不命中的真因 | ### 指纹生成方式 - 查询侧改 `queryFingerprint()`:2-gram **按出现顺序**取前 16 个。旧路径先按 (权重, token 字母序) 排序再截断,而中文 2-gram 的字母序就是拼音序,没有语义意义。 - 模板侧改 `fingerprintTemplate()`:**分级配额** —— trigger 里引号包裹的用户说法 → 名称 → 标签 → trigger 其余 → 正文,总长 40。此前统一按权重截断,中文业务词永远进不来(试过"索引词全收 64 个", 向量变长导致其它模板 cosine 全线下滑)。 ### 行为调整 - `confirm` 态**不注入模板与禁区,也不累计热度**。实测「登录页加个按钮」会以 0.156 命中"状态门禁"模板, 两者都含"按钮/操作",词面相似但语义无关;不注入也不记热度,避免假阳性被热度自我强化。 - `matched` 改为与实际注入内容一致(`confirm` 时为 false),避免模型去找正文里不存在的模板。 ### 新增 `scripts/rebuild.js` 0.7.1 改了指纹生成方式,**存量模板不会自动更新** —— 新需求用新指纹、老模板用旧指纹,口径不一致。 `npm run rebuild -- --dry-run` 可先看每份模板的 token 变化(实测 24 → 40,新增的是此前被截断的业务词), 去掉 `--dry-run` 写回,保留 `hitCount/created/lastUsed`。 ### 验证 | 基准 | 结果 | | --- | --- | | 合成 16 例 | 12/12,假阳性 0 | | 真实回放 24 条(有对应模板 8 条) | **8/8 命中(100%)**,假阳性 0 | | 单元测试 / 冒烟 | 227 → **241 通过** / 全过 | 其中门禁变体那条从 0.144(漏召回)升到 0.32(命中)—— 直接来自"保护词"修复。 > 采集与验证脚本不在本仓库:它们是本地诊断工具,依赖具体会话数据。方法论见 > [`docs/workbuddy-port-2026-10-17.md`](docs/workbuddy-port-2026-10-17.md)。 ## 0.7.0 — 2026-10-17 **命中判定换量:从"含先验的总分"改为"词汇分",阈值 0.35 → 0.15。** WorkBuddy 侧用 6 份通用模板 + 16 例真实形态基准(6 类需求 × 长句/短句 + 4 条无关)做了对照实验: | 方案 | 命中 | 假阳性 | 无关样本最高分 | | --- | --- | --- | --- | | 旧公式 `0.62·lexical + 先验`,阈值 0.35 | **3/12** | 0 | 0.095 | | 新公式:`hit = lexical ≥ 0.15`,先验只排序 | **12/12** | **0** | 0.098 | 根因不是阈值数字,而是**判据用错了量**:先验项(同分类 0.14 + 标签 0.08 + 同仓库 0.08 + 新鲜 0.05 + 热度 0.03)无条件加总,合计最多 0.38,阈值 0.35 意味着词汇只需贡献 0.06~0.09。 于是两条路同时坏掉: - **假阳性**:无关需求靠"同仓库 + 同分类 + 一个通用词"就能贴到 0.333(实测),差 0.017 越线; - **漏召回**:真命中的 lexical 通常只有 0.15~0.25,被 0.35 的总分门槛挡在门外。global 层没有 "同仓库"可加,命中率实测仅 **1/6** —— 这也是 global 模板库长期空置的数学原因。 改动: - `lib/match.js`:`hit = lexical >= threshold`;`score` 改为词汇匹配度(可直接与阈值比较); 新增 `rankScore = 0.9·lexical + 0.1·先验` 专用于排序。两个量分开,消除"分数 0.13 却命中"的口径矛盾。 - `lib/match.js`:**覆盖率只统计任务语义 token**,路径与文件名不计入分母。实测一条 112 字的需求带 5 个路径 token(权重合计 17),中文语义词只剩 8 个,命中一个词覆盖率也只有 0.04 —— 路径回答的是 "改哪个文件",不是"要做什么"。余弦仍用全量。 - `lib/match.js`:**命中必须有主题词证据**。低信号词(提交/新增/接口/字段/参数/实现…)重合不算证据。 真实回放里「提交,release的exe也提交」以 0.296 命中"状态门禁"模板(交集只有「提交」), 「新增分页查询接口」以 0.18 命中"插件静默失效"模板(交集只有「接口」)——都由这条拦住。 - `DEFAULT_THRESHOLD` 与 schema 默认值 0.35 → **0.15**;`breakdown` 新增 `prior` 字段。 - 同步更新 README / `docs/design.md` / `docs/operations.md` / `scripts/*` 的口径与示例。 - 测试调整 4 处:断言从"先验使 score 更大"改为"先验使 rankScore 更大、而 score 不变", 这正是新语义;测试 227 全绿,冒烟全过。 **真实会话回放验证**(3 个工作区、21 个会话 → 24 条去重需求): | 指标 | 结果 | | --- | --- | | 模板库有对应主题的 8 条 | 命中 **7 条(87.5%)** | | 其余 16 条(无对应模板) | 0 命中,**假阳性 0** | | 合成基准回归 | 16 例 12/12,未退化 | 回放过程中还暴露了三个**未在本版修复**的问题,写在 `docs/workbuddy-port-2026-10-17.md`: ① 真实需求 38% 带接口 JSON / 堆栈,查询侧需先清洗再截断; ② `focusFingerprint` 按字母序截断中文 2-gram,语义词会被丢掉("时段"根本没进指纹); ③ 模板指纹的中文 2-gram 被英文 token 挤掉,需改分级配额。 **未包含(下一版)**:指纹层的同义词归一(跨日/跨天、开关/switch)、领域词提权、模板指纹名额 24→32。这三项在同一实验里把命中从 9/12 推到 12/12,但**需要重建存量模板指纹**才能对旧库生效, 因此单独作为 0.7.1 并配套指纹重建工具,不混在本版里。 > 数据来源与完整实验记录见 [`docs/workbuddy-port-2026-10-17.md`](docs/workbuddy-port-2026-10-17.md)。 ## 0.6.6 — 2026-10-17 **会话事实读取与真实宿主契约对齐:两处常量写错,导致两个功能在真机上 100% 空转。** 真实使用诊断([`docs/diagnosis-2026-10-17.md`](docs/diagnosis-2026-10-17.md))回放 16 轮历史会话时发现: `spec_retro` 的过程摘要只有「(未知) / (未捕获)」,兜底提醒一次都没出现。根因有两处,都在 `lib/extract.js`: | 位置 | 旧实现 | 真实契约 | 后果 | | --- | --- | --- | --- | | `getEvents()` | 只读 `session.events` | `class Session { private log }`,公开的是 `ownEvents()` / `snapshotEvents()` | 事实恒为空 → 兜底提醒永不触发、digest 空壳 | | `isDirectUserMessage()` | 只认 `source.kind === 'direct'` | `{ readonly kind: 'user' }` | 真实用户消息全被过滤 → "用户说要继续"永不推翻完成判定 | **为什么 225 项测试没发现**:测试夹具是 `{ id, events }`,`USER()` 宏的 `kind` 也写 `'direct'` —— **实现与测试共享同一个错误假设**。与 0.6.4 的 `Config` 导出名事故同类:测的是函数自洽,不是"在 dsh 里能用"。 改动: - `getEvents()` 按「数组属性 → `ownEvents()` → `snapshotEvents()`」顺序探测,并保持 seq 排序; - `isDirectUserMessage()` 同时接受 `'user'` 与 `'direct'`; - 新增 2 条回归测试,**按真实 Session 形状(无 `events`、只有 `ownEvents`)构造**; - 新增诊断报告与两个只读诊断脚本(会话路由分布、召回质量)。 测试 225 → **227 通过**。 > 本版只修"读不到事实"这一层。诊断报告里的 P0-2(数据根与会话不同源)、P0-3(三库分裂)、 > P0-4(调节项白送分)尚未处理,不要因为本版发布就认为沉淀链路已通。 ## 0.6.5 — 2026-09-18 **加一道会喊出来的金丝雀:配置没走过 schema 时必须出声。** 0.6.4 的事故之所以能潜伏三个版本,是因为它**完全静默**:默认值失效、开关变 `undefined`、 整段逻辑被跳过,而插件表面一切正常(工具照样注册、日志一行不写)。这一版让这种处境不可能 再悄悄发生: ```js if (config?.preStepRouting === undefined) ctx.logger?.warn?.('[spec-forge] 配置未经过 schema 校验…') ``` 判据是精确的:cordis 只在插件导出 `Config` 时校验并填默认值,所以**"带默认值的键是 undefined" 就等价于"配置没走过 schema"**。告警之后仍按 schema 默认值继续运行 —— fail-soft,但不再 fail-silent。 **验证方法上也留一条教训**:0.6.2/0.6.3 的埋点写在"被怀疑的那段 `if` 块内部",于是"代码是旧的" 与"代码是新的但块被跳过"在埋点里长得一模一样,白绕了两轮。定位用埋点必须放在被测对象**外面**; 0.6.4 起改用**会话日志**(harness 自己写的,独立于插件)作为判据。 测试 224 → **225 通过**。 ## 0.6.4 — 2026-09-18 **真正的根因:配置 schema 导出名写错了 —— 导出的是 `schema`,而 cordis 只认 `Config`。** cordis 的 `resolveConfig()`(`@deepseek-ai/cordis` lib/index.js:955): ```js function resolveConfig(runtime, config) { if (!runtime.Config) return config; // ← 只认这个键 const result = runtime.Config["~standard"].validate(config); ... } ``` 于是这个插件**从来没被校验过、schema 里的默认值一个都没生效**:`config` 就是 profile patch 里 原样那 8 个键。profile 没写 `preStepRouting` → 它是 `undefined` → 那句 `if (config.preStepRouting && typeof ctx.on === 'function')` 直接为假 → **`agent/pre-step` 监听器从 0.5.0 起一次都没注册过**。所有内置插件导出的都是 `Config` (`export { Config, apply, name }`),只有本插件写错了约定。 **证据(全是本地复现,不是推断)**: | 验证 | 结果 | | --- | --- | | 真 cordis 加载真插件 + profile 那份 config,数注册的监听器 | 修前 **0 个**,修后 **1 个** | | 真 cordis 按 `dsh-agent-loop` 的方式真派发一次 `agent/pre-step` | 修后 decision.messages 里出现第 3 条注入消息(`source.plugin = "dsh-spec-forge"`) | | 线上现象对照 | 工具全正常(工具注册在监听器之后)、监听器零调用、埋点一次没落盘 —— 与"整段被跳过"完全吻合 | **0.6.3 的结论是错的**:那版判断"监听器被 scope 过滤丢掉",方向不对。本地实验确实证明 带外来 scope 标签的 ctx 会静默收不到事件,但本插件的 ctx 根本没标签 —— 它压根没走到注册那一步。 `{ global: true }` 作为无害的保险保留(`dsh-scope` 自己的跨切面监听器也这么注册)。 **连带修好的第二处**:`retroRequireCodeChange` 同样缺键(默认 true),旧代码直接把它传进门槛判定 → `undefined` 让"必须真改过代码"这道门槛失效,纯问答/只读诊断也会被催沉淀 ✗。现在读开关一律用 `!== false`:**缺键 = 保持默认开启**,只有显式 `false` 才关。 **改动**: - `export const Config = Schema.object({…})`(另保留 `schema` 旧名做别名,兼容既有测试与文档); - 所有开关改读 `config.xxx !== false`:`autoRecall` / `preStepRouting` / `autoRetro` / `retroRequireCodeChange`; - 拆掉 0.6.2/0.6.3 的临时诊断埋点与对照组注册; - 新增两条配置契约护栏:① 必须导出 `Config`,且 `Config({})` 要填满默认值; ② **拿 profile 那份 config(没有 `preStepRouting` 键)原样跑 `apply()`,必须照样注册注入** —— 这条正是能拦住本次事故的测试; - `docs/operations.md` 的"必踩坑"从两条升到三条,把导出名这条写进去。 测试 223 → **224 通过**。 ## 0.6.3 — 2026-09-18 **定位到了:`agent/pre-step` 监听器被 Cordis 的 scope 过滤静默丢掉。** 用**真 cordis + 真 dsh-scope** 在本地做了一次派发实验(以 `scopeTarget(agent, agent)` 当 thisArg, 与 `dsh-agent-loop` 的真实调用一致),结果: | 注册方式 | 收到事件? | | --- | --- | | 无 scope 标签的 ctx,不带选项 | ✅ 收到 | | 带"外来" scope 标签的 ctx,不带选项 | ❌ **静默收不到**(无异常、无日志) | | 带外来标签的 ctx + `{ global: true }` | ✅ 收到 | 线上现象与第二行完全一致:`apply()` 确实跑到了监听器注册那一步(工具注册在它之后、却一切正常), 监听器却一次都没被调用 —— 连 0.6.2 的埋点都没落盘。 **这一版用"同 ctx 同 handler、只差选项"的方式注册两份**: - `global-ctx`:`ctx.on('agent/pre-step', handler, { global: true })` —— Cordis 的 dispatch 看到 `hook.global` 直接放行、绕过 scope 过滤;`dsh-scope` 自己的跨切面不变式监听器就是这么注册的。 - `plain-ctx`:不带选项的对照组 —— 若两个落点都收到,说明另有其因,届时按埋点数据再收窄。 任一落点触发都会在 `<数据目录>/prestep-trace.log` 落一行(含 label),同一轮仍只注入一次。 真机验证通过后,埋点与对照组一并删除。 ## 0.6.2 — 2026-09-18 **诊断版:`agent/pre-step` 注入在真实会话里一次都没触发 —— 这一版用来把它钉死。** 现象(真实观察,不是推断):装进 profile → 重启 DSH → 发一条**必定触发**的消息 (`直接做:只回我一句「OK」,别改任何文件`,用插件自己的函数在本机复算:`level:1`、`fastTrack:true`、 notice 245 字符),会话日志里**仍然没有**任何 `{"kind":"plugin","plugin":"dsh-spec-forge","form":"notice"}`; 而同格式的其他插件注入(runtime-context / user-approval / compact)都在 —— 说明日志确实记录这类消息。 **已逐项排除**: | 假设 | 结论 | | --- | --- | | 没装好 | 否。profile 依赖 / `dsh.profile.bundles` / 锁文件 sha / 装进去的 `index.js`(含 `ctx.on('agent/pre-step')`)全部核对过 | | 配置把开关关了 | 否。`--dump-config` 显示该层无 `preStepRouting` 覆盖 → 取 schema 默认 `true` | | 注册方式不对 | 否。与内置 `dsh-tool-skill` / `dsh-repeat-tool-reminder` **逐行同构**(`await next()` → 改 `messages` 返回) | | 事件没在发 | 否。技能目录注入用的就是同一事件,本会话正常出现 | | 插件没被 apply | 否。运行中的进程里 `spec_triage` 可调用且返回正常 | | 消息形状不符 | 否。真实用户消息带 `role:"user"` + `source.kind:"user"`(38/38 条都有) | **这一版做两件事**: 1. **修两处可疑点**(无论是否为根因都该修): - 判定池改为**优先读 `payload.messages`**(内置插件都读它),没有再退回 `decision.messages`; - 兜底挑选**排除一切插件注入** —— 此前会把 runtime-context(`role:user`、且不含 ``) 当成"用户需求"拿去分级,后果是**静默判定为非 L1 而不注入**。 2. **加临时诊断埋点**:每次 pre-step 判定往 `<数据目录>/prestep-trace.log` 落一行 JSON,记录 `claimed/entering` 条数、挑中哪条、级别,以及走的是 `inject` / `no-requirement` / `empty-notice` / `duplicate-skip` / `error` 哪条分支。**验证完即删**(连同它的测试)。 另补两条**按真实发射端形状**的回归测试:mock 与真实形状不一致,正是这个问题能溜过 220 条测试的原因。 测试 220 → **223 通过**。 ## 0.6.1 — 2026-09-18 **修一个"克隆下来装不上"的边角:`peerDependencies` 让仓库自己的 `pnpm install` 直接失败。** 现象(用户实操复现): ``` [ERR_PNPM_NO_MATCHING_VERSION] No matching version found for @deepseek-ai/dsh-tools@>=0.1.0 while fetching it from https://registry.npmmirror.com/ ``` 根因:pnpm 8+ 默认 `auto-install-peers=true`,于是这三个 peer 被当成**要真去装的依赖**,而 `@deepseek-ai/dsh-tools` 在公共镜像上最新只到 `0.0.1-rc.1`(`0.1.x` 只挂在 `alpha` / `next` 两个 tag 下), 区间 `>=0.1.0` 无解。 修复:加 `peerDependenciesMeta`(三个都 `optional: true`)。它们本来就**由宿主在运行时提供** —— 插件只是 `import` 它们,从不自己安装;声明它们的价值是当文档,不是当依赖。 | | 之前 | 之后 | | --- | --- | --- | | 装进 profile(`qbot-dsh plugin add …`) | 正常 | 正常(profile 的 `pnpm-workspace.yaml` 本来就有 `autoInstallPeers: false`,这条路从没炸过) | | 克隆仓库后 `pnpm install` | **ERR_PNPM_NO_MATCHING_VERSION** | 跳过可选 peer,正常结束 | 只动 `package.json` 元数据,**零代码变化**:已装进 profile 的 0.6.0 不需要重装。 ## 0.6.0 — 2026-09-18 **把 ponytail 从"审计视角"真的用成"执行标准":本轮是净删除。** 触发是用户的一句追问:「你现在是按照 ponytail 给我重构了插件吗?验证了没」。 诚实的回答是"没有完全按,而且最关键的一环没验证过" —— 于是这一版按 ponytail 的七级阶梯重新走了一遍, 并把此前漏掉的三条补上(删除优先、`ponytail:` 标记、不做未请求的抽象)。 ### 删掉的东西(每一条都有数据依据,不是"看着没用") | 删掉的 | 依据 | | --- | --- | | **`spec_library` 整个工具**(list / info / migrate 三个动作) | 15 个真实会话里被调用 **0 次**,却占 **481 token/请求** | | **`runStoreAction` + `copyTree` + `isCrossDrive`**(迁移机械) | 迁移=复制一个普通 markdown 目录,**文件系统自带**(ponytail 阶梯第 3/4 级);真实库只有 7 份模板 | | **`purgeStale` + `staleTemplates` + `usedDateOf`** | ">90 天没命中就删"是纯推测需求;真要删,删掉那个 `.md` 即可 | | **`describeStore`** | 唯一消费者就是被删的 `spec_library` | | **档案的「约定」「备注」两节** | 6 份真实 `profile.md` 里这两节全是 `(暂无)` 占位符,且**没有任何读取路径**(既不注入上下文也不进报告) | 合计 **−489 行**(+17 / −506):`index.js` 984 → 768、`lib/store.js` 748 → 620、`tests/store.test.js` −129。 **这是本插件第一个净删除的版本。** 做法上吸取了 0.4.7 的教训:切除用锚点断言完成(起点锚点必须唯一 + 切除范围必须含预期符号,否则不写盘), 而不是"读一遍觉得对就删"。 ### 收益(`npm run token-audit` 实测) | 项 | 0.5.0 | 0.6.0 | | --- | --- | --- | | 工具数 | 5 | **4** | | 工具定义 | 2740 token | **2237** | | **固定税/请求** | 3229 token | **2726(−15.6%)** | | 测试 | 230 | 220(删掉的 10 条全是被删对象的测试) | ### 刻意没删的(以及为什么) - **`spec_distill`**:真实调用 5 次而不是 0 —— 缺替代路径的证据就不删。 - **`liftLegacyNesting`**:它是**启动时自动**修的布局事故(历史模板曾全部读不到),不是可选功能。 - **`ROUTING_CONTRACT`**:这是我自己引入的抽象、只有一个渲染消费者,**本该被质疑**;保留的理由是它让 常驻段 / 文档 / 测试同源,而 0.4.7 的 SKILL.md 漂移正是"多处手写"造成的。现在它带 `ponytail:` 注释写明上限。 ### 补上 ponytail 明确要求、此前漏掉的三条 1. **`ponytail:` 标记(6 处)** —— `classify.js` 关键词判据 / `fingerprint.js` 不接 embedding / `match.js` 词面打分 / `extract.js` 事件流提取 / pre-step 幂等按原文哈希 / 档案只写禁区一节。 每条都写"上限 + 升级触发条件",而不只是"为什么这么做"。 2. **删除优先** —— 见上表。 3. **护栏跟着工具面走** —— 契约护栏从「SKILL.md + README」扩展到 `docs/*.md`:删 `spec_library` 时, 文档里正留着"调 `spec_library({ action: 'info' })` 看路径"这种**指着一扇已封的门**的说明, 而旧护栏扫不到 `docs/`。 ### 补记 0.5.0 的验证状态(仍未闭环,别当成已验证) 0.5.0 的 `agent/pre-step` 注入**仍未在真实会话里跑过**(装插件与重启都需要审批,本会话审批已关闭)。 这一版补的是**源码级契约核实**,逐条对照通过: - `dsh-tool-cordis` 的权威签名:`payload = { agent, messages, turn, step, signal }`(`agent` 由 dsh-scope 注入) - `PreStepDecision = { kind:'reject' } | { kind:'enter'; messages: UserMessage[] }` - `dsh-agent-loop.preStep()`:默认实现即 `{ kind:'enter', messages: claimed }`,**`decision.messages` 就是进模型的上下文** - `MessageId` 是恒等函数("no validation is performed")→ 自造消息与官方 `createUserMessage` 等价 - 已安装运行时里另有 **14 个插件**在用同一个事件 **这仍然是推断,不是观察。** 要看它真的生效,需要装一次 ≥0.5.0 并跑一条真实需求。 ## 0.5.0 — 2026-09-18 **新增一层:在请求发出之前,由插件自己把本轮路由算好并注入。** 0.4.7 修的是"模型读到的说明书与代码不一致";这一版修的是更根本的一点 —— **常驻段与工具返回值都是软约束:模型得先读到、再自觉执行。** 判据来自真实数据(15 个会话逐事件解压统计):20 次 `spec_recall` 里有 17 次紧接着调了 `spec_triage`, 也就是召回已经判过的结论,模型又走了一遍流程;而 0.4.6 记录的那次事故,是模型自己替用户挑了按钮用途。 ### 机制 dsh 提供 `agent/pre-step` 瀑布事件(等价于 Claude Code 的 `UserPromptSubmit`):它在每个 step 的请求**发出之前** 调用,返回值里的 `messages` 就是本轮进入模型的上下文。内置插件正是这么用的 —— `dsh-tool-skill` 拿它注入 skill 目录,`dsh-repeat-tool-reminder` 拿它手搓用户消息。本插件照同一范式实现,**不新增任何依赖** (`createUserMessage` 所需的 `{id, role, content, source}` 自己构造,与内置插件同形)。 命中两态才注入,其余一分 token 不花: | 命中 | 注入正文 | | --- | --- | | L1 一步直达 | 先调一次 `spec_recall` 拿模板与禁区,然后**直接实现**;**不要**调 `spec_triage` / `spec_distill` | | 需求缺内容 | 缺少 **X**(原样点名);先 `spec_recall`,再用一次 `ask_user_question` 问清 X,再动手 | `triage` 与普通对话**不注入**:常驻段已写明"先 `spec_recall` → 再 `spec_triage`",重复一遍只是重复计费。 注意注入**不替代召回**:`spec_recall` 仍在流程里,模板与项目禁区都靠它拿。 ### 三条安全设计(这是它敢下硬指令的前提) 1. **同源判据** —— 注入用的就是 `spec_recall` 的同一个 `classifyComplexity`、同一份需求原文, 所以注入结论与随后召回的结论不可能自相矛盾。 2. **幂等** —— 注入消息带 `source.plugin` / `source.digest` / `source.turn`,同一轮同一需求只注入一次, 多 step、会话重放、断线恢复都安全。 3. **异常一律放行** —— 判定整体 `try/catch`,出错只写一条 warn 并原样返回下游 decision,绝不拖垮本轮。 ### 实测(`node scripts/verify-load.js` 末段会打印真实注入正文) | 需求原文 | 结果 | | --- | --- | | `@src/views/login/index.vue 登录页加个忘记密码按钮` | 注入「L1 一步直达」 | | `登录页加个按钮` | 注入「缺少关键内容:按钮的文案与用途」→ 先问一次 | | `把首页标题文案改成「资产预警总览」` | 注入「L1 一步直达」 | | `你好,今天几号?` | **不注入**(不是需要短路的情形) | 测试 **222 → 230**:新增 6 条 pre-step 用例(判定与正文、闲聊不注入、同轮幂等、跨轮重判、 异常放行/垃圾载荷、开关关闭后不注册)与 2 条契约护栏(注入与常驻段对同一态的口径必须一致; README 与运维手册必须写明这一层)。开关 `preStepRouting` 默认开,关掉即完全回到 0.4.7 行为。 注入单价实测:L1 一步直达 245 字符 ≈ **136 token**,需求缺内容 279 字符 ≈ **150 token**, 不命中为 0(`npm run token-audit` 已把它做成常设的两行,避免这里只写一个拍脑袋的估值)。 常驻段与固定税不变(489 / 3229 token)。 ### 下一步(这一层换来的空间,本轮刻意没动) 常驻段那 489 token 里相当一部分在讲"三态各该怎么做"。现在每轮会按实际需求单独注入这一条, 常驻段理论上可以瘦身成"什么时候必须用这个插件 + 大文件纪律"。**但先用量数据确认注入确实生效,再动它** —— 这正是 0.4.4「为压缩而压缩、静默删掉 4 条约束」的教训。 ## 0.4.7 — 2026-09-18 **本次不加功能:修已坏的、删多余的、补上让它们不会再坏的护栏。** 起因是一次外部视角审计(用 ponytail 的 YAGNI 阶梯过一遍代码),并**改用真实会话数据做判据** —— 把本机 15 个真实会话、11944 个事件逐帧解压(dsh 的会话日志是多帧 zstd,Node 单次解压只出第一帧), 统计 `spec_*` 的真实调用: | 工具 | 真实调用 | 常驻税/请求 | | --- | --- | --- | | `spec_recall` | 20 | 568 | | `spec_triage` | 19 | 405 | | `spec_retro` | 15 | 778 | | `spec_distill` | 5 | 313 | | `spec_library` | **0** | 481 | 三条读数: 1. **「先召回」这条常驻指令是有效的**:5 个真正改过代码的会话,5 个都调了 `spec_recall`(激活率 100%)。 常驻段不是白花的。 2. **「别多走流程」基本无效**:20 次召回里 17 次紧接着就调 `spec_triage` —— 但绝大多数样本落在 0.4.3 之前,`confirm` 三态与 fastTrack 在真实会话里**几乎没有样本**(只有 2 次召回发生在 0.4.3 之后)。 3. **`spec_library` 从未被真实调用过**,而它占 481 token/请求。本轮**不删**(`migrate` 是升级路径的一部分), 留给下一轮用真实数据裁决。 ### 一、四处手写的契约,已经漂移(本次最严重的问题) `nextStep` 三态原先手写在四处:常驻系统提示段、5 个工具描述、`skills/spec-forge/SKILL.md`、`docs/*.md`。 `docs/operations.md` 把「改契约必须同步四处」写成了流程要求 —— 而 SKILL.md 实际上漏了: | SKILL.md(模型真正读的) | 代码(0.4.6 的修复) | | --- | --- | | `:69 / :85 / :174` 把「**加按钮 / 加列 / 加路由**」写成 L1 直通信号 | 容器型原子改动缺内容 → `confirm`,**必须先问一次** | 也就是说:**0.4.6 花一整版修好的 `confirm`,在模型读的说明书里被反向撤销,而当时 204 条测试全绿** (`tests/routing.test.js` 只护常驻段,不读 SKILL.md)。 修法是把重复的那份**消掉**,而不是再加一个校验: `lib/render.js` 新增 `ROUTING_CONTRACT` + `renderRoutingLines()`,常驻段由它渲染(不再是手写副本), `tests/contract.test.js` 对着同一张表校验 SKILL.md 与文档。要改路由,只改这一处 + SKILL.md,两边不一致就会失败。 ### 二、修掉的真实缺陷(每条都有回归测试) | # | 缺陷 | 后果 | 位置 | | --- | --- | --- | --- | | 1 | 验收标准每次覆盖更新多叠一层 `- [ ] [ ] x` | 模板越沉淀越脏 | `index.js` / `render.js` / `store.js` 三份条目提取实现都不剥复选框前缀 | | 2 | ddl 正则裸写 `加列` | 「表格增加列宽自适应」这类**样式**需求被判 L3(完整 Grill-me + 提问前禁止读文件,代价最高) | `lib/classify.js` | | 3 | 写盘失败仍 `state.retroDone.add()` | 兜底提醒**永久失效**,而返回值却说"下次重试" | `index.js` spec_retro | | 4 | `sessionComplete` 从未接线(恒为默认 `true`) | 「上一轮已完成」这条门槛从未被评估,任务做一半也催沉淀 | `index.js` buildRecallNotice | | 5 | `strictDistill` 的「空则报错」是空承诺 | `missingConstraints` 模型根本看不到,SKILL.md 却叫它据此补问;且关掉开关警告照旧 | `index.js` spec_distill | | 6 | home 模式下「旧路径」就是当前数据目录 | `info` 把现用库报成旧路径;`migrate` 把目录复制进自己并 `bumpWriteEpoch()` | `index.js` runStoreAction | | 7 | `readTemplate` / `readProfile` 裸 `readFileSync` | 一个损坏/被占用/手工命名的文件就让**整次召回**失败(实测:写盘失败本该 `saved:false`,结果异常直接穿出工具边界) | `lib/store.js` | | 8 | `profilePath` 里 `ensureDir` | **读操作有写副作用**,数据目录不可写时抛 ENOTDIR 穿透工具 | `lib/store.js` | | 9 | 渲染层两处与权威表相反 | L1 徽标写死「组件/默认值明确」(原子小改路径也这么写);L2 报告写「列表展示默认展示」,而 `L1_DEFAULTS.listDisplay` 是「不展示」 | `lib/render.js` | | 10 | `hash !== 'global'` 恒真、`buildPreview` 不可达 | 看着在防护,什么也没挡;一段永不执行的代码 | `index.js` | 第 7、8 条是**新写的工具层测试当场挖出来的**(`tests/plugin.test.js`)—— 也就是说这一层此前不是"缺覆盖率",是真的没测过。 ### 三、删掉的死物(零行为变化) - `renderTriageReport` 的 `templateHints` 死参数及其两处永不执行的分支(唯一构造方从不传) - 三份「条目提取」实现合一为 `store.bulletLines()`(这正是缺陷 1 的根因) - 死导出:`SECTION_ORDER`、`L2_MAX_QUESTIONS`、`sep` 的转发导出 - 恒真守卫 `hash !== 'global'`、不可达的 `buildPreview` ### 四、成本与护栏 - 常驻提示段 **813 字符 ≈ 489 token**(0.4.6 为 823 ≈ 501):改由契约渲染后**更短**,内容等价。 五个工具定义 ≈ 2740 token,固定税合计 ≈ 3229 token/请求(`npm run token-audit` 实测)。 `token-audit.js` 的提取逻辑同步改成"按顺序重组 + 展开契约渲染",否则它会漏掉契约里的三态路由 (实测会把这 813 字符量成 238 token)。 - 测试 **204 → 222**:新增 `tests/contract.test.js`(契约一致性)、`tests/plugin.test.js`(工具层)、 `tests/regression.test.js`(上述缺陷的触发条件)。契约护栏上线即抓到 README 配置示例只剩 2 个键的回归。 ### 五、本轮明确未动(留给下一轮,需要数据或更高风险) - **`spec_library` 与 `spec_distill` 的去留**:真实调用 0 次 / 5 次,但没有替代路径的证据,不凭一次审计删。 - **平台级注入**:dsh 已提供 `agent/pre-step`(≈ UserPromptSubmit,可在本轮请求前注入消息)与 `tools/post-execute`(`additionalContexts`)。把路由指令从"常驻喊话"改成"硬注入",才是 敢大幅削常驻段的前提 —— 需要先解决加载与回退路径。 - **布局迁移机械**(`liftLegacyNesting` / `copyTree` / `isCrossDrive` / `migrate`):为一次历史布局 bug 保留, 但用户真实数据仍在旧路径上,删除有风险。 - 档案里 `conventions` / `notes` 两个字段**写了不读**(既不注入也不展示):接线还是删除,需要产品决策。 ## 0.4.6 — 2026-09-14 **用户质疑:「登录页加个按钮」这个需求没写清加什么按钮、要做什么,直接让它通过开始编写是不是有问题。** 结论:**有问题,而且性质比"忘了问"更严重 —— 是插件明文禁止了问。** ### 那次会话到底发生了什么(session-d200e527) 需求 `@example-admin/src/views/login/index.vue 登录页加个按钮`,9 次工具调用: 1. `spec_recall` 一次 → `fastTrack=true` 2. 模型明确说「fastTrack=true,直接实现。先看登录页现状。」 3. 读文件 → grep → 读 `api/login.js` → **自己替用户挑了「忘记密码?」这个用途** → 3 次 edit 4. 在代码里留了 `// TODO: [待确认] 需求仅说「加个按钮」,未指明用途…请告知按钮文案与行为` 5. 回复里主动列了备选,并声明没跑 dev server **模型没有违规。** 当时注入给模型的原文是: ``` > **执行路径:1 步直达。** fastTrack=true(L1 原子改动 / 自包含新建 / 用户已要求不追问)。 > **下一步就是实现**:禁止追问;**不要调用 `spec_triage`,也不要调用 `spec_distill`**(本条已替代它们的产出)。 > 直接改代码,疑虑写 `// TODO: [待确认] <内容>`,最终报告里点出。 ``` `index.js:112` 还把提问权限定为「仅 L3 与 L2 安全阀可 `ask_user_question`」——**L1 被明文剥夺**。 所以插件对"需求内容缺失"的处方是**猜 + 标 TODO**,而 TODO 兜底完全靠模型自觉、无任何强制。 这次是模型自己较真才没出事;换个不较真的模型,按钮就直接落地,没人知道它猜过。 ### 根因:`classifyComplexity` 只查「范围」,没查「信息完备」 三道闸全是范围闸(长度 ≤48 / 无多任务连接词 / 无大范围限定词)。而同一文件第 4 行注释写着 L1 的定义是「单文件 CRUD + **组件/默认值明确**」,第 5 条通路(0.4.3 的 atomic-edit)把这半个条件 丢掉了,还在注释里当优点写「无字段名也能一轮做完」——**同一文件里 L1 有两个互相矛盾的定义**。 这是 0.4.3「标题写必做、子条目写可跳过」之后第二次犯同一类错。 ### 修法:拆开「范围」与「内容」,新增 `nextStep=confirm` 判据一句话: > **缺的是「新增物的身份/用途」→ 必须问;缺的是「已有物的属性取值」→ 自己定。** 因此把原子小改分成两族: | 族 | 例子 | 缺的 | 路由 | | --- | --- | --- | --- | | 取值型(copy / style / constant / rename / tiny / button-attr) | 改文案、调间距、改按钮样式、超时改成30s | 取值(可逆、可见) | 仍 `implement` | | 容器型(button / column / route) | 加个按钮、加个路由、加一列、加个菜单项 | 新增物**是什么/干什么** | 无内容则 `confirm` | `nextStep` 新增第三态: - `confirm` —— 用一次 `ask_user_question` 问清 `contentGap` 再动手,**不走** `spec_triage` (既不该直接实现,也不必付一次完整四维体检)。提问权列举同步加上 `confirm`。 ### 实测数据(两处关键,都推翻了"想当然") **① 只降级到 L2 并不够,而且是静默的。** L2 的追问安全阀是 `unactionable = tooShort && missing≥2 && !hasAnchor`: | 需求 | tooShort | missing | hasAnchor | 若降到 L2 会问吗 | | --- | --- | --- | --- | --- | | `登录页加个按钮` | true | 3 | false | 会 | | `@…/login/index.vue 登录页加个按钮`(**真实那条**) | **false** | 2 | **true**(识别出文件) | **不会** | 带路径前缀 → 变长 + 拿到 file 锚点 → 安全阀根本不触发。 **所以 `contentGap` 必须独立于长度与锚点**,不能复用 `unactionable`。 (顺带发现:内容闸门原挂在长度闸门内部,而那条需求恰好 48 字 —— 路径再长一个字符就整个漏掉。 已改为内容检查不受长度约束,只排除多任务/大范围信号。) **② 特异性倒挂。** button 正则原来要求"个"紧邻"按钮",中间有修饰语就不认: | 需求 | 修前 | 修后 | | --- | --- | --- | | `登录页加个按钮`(更糊) | L1 fastTrack,不问 | **L2 → confirm,问** | | `登录页加个忘记密码按钮`(更清楚) | L2,反而去问 | **L1 fastTrack** | **越具体的需求反而越被降级去问** —— 同一处缺陷的另一面,一并修掉。 ### 顺带修掉的自相矛盾 `renderTriageReport` 的"信息不足"段无条件写「这条需求没有任何可动手的锚点(无文件、无字段…)」, 而 contentGap 场景往往**是有锚点的**(识别出了目标文件)。现在按成因分开措辞: `unactionable` → 原话;`contentGap` → 新增「需求缺内容:先确认要加的是什么」,并列出必问项。 ### 刻意不拦的 - `改文案 / 调间距 / 改按钮样式 / 超时改成30s` → 缺的是取值,可逆可见,自决即可。 - `加个导出按钮 / 加个按钮点击跳转注册页 / 加一列状态 / 加个报表路由` → 身份已给,照旧 `implement`。 - `直接做,加个按钮` → `skip-trigger` 优先于内容闸门:用户显式弃权提问权,无条件 `implement`。 - 多任务/大范围需求 → `contentGap` 恒为空,仍走 `triage` 常规路径(避免绕过体检)。 ### 验证 - 单测 **204/204**(197 → 204,新增 7 条:分类器 5 条含"带路径前缀不得被当成内容已给"的实测反例、 路由 2 条 confirm / 不误伤)。 - 常驻提示段 **823 字符 ≈ 501 tokens**(0.4.5 是 426)。 **超审计脚本参考线约 51 token,这是有意的**:`SYS_BUDGET=450` 是自设参考线而非 dsh 硬限制 (`renderPrompt` 无截断),而 confirm 分支是行为必需。**请勿为了压线再删约束**(0.4.3/0.4.4 已踩过)。 - 端到端(安装产物 + 真实模板库): | 需求 | 结果 | | --- | --- | | `@…/some-page/index.vue 登录页加个按钮` | **`confirm`** · contentGap `按钮的文案与用途` · 931 字符 | | `加一列` | **`confirm`** · contentGap `新增列对应的字段` | | `改一下登录页的文案` | `implement` · L1 · 921 字符 | | `登录页加个忘记密码按钮,点击跳转注册页` | `implement` · L1(不再倒挂) | | `调一下间距` | `implement` · L1 | ## 0.4.5 — 2026-09-14 **0.4.4 首次真实会话验证暴露的三个缺陷。** fastTrack 短路本身成立(3 次调用 → 1 次),但同一次实跑 也暴露出:一个假阳性召回、注入内容与 fastTrack 指令自相矛盾、禁区档案近重复累积。 ### 🔴 修复 1:假阳性召回 —— 先验分脱离词汇证据 **症状**(真实会话 `session-d200e527`):「登录页加个按钮」命中了完全不相关的 `tpl-4a81289f10`「把设计稿 HTML 落成 admin 展示+导出页面」,匹配度 **0.372**(阈值 0.35), 还把该模板 2549 字符的澄清清单与标准改法注入了上下文。 **根因有两层**: 1. **先验分是无条件的**。`sameCategory(0.14) + sameRepo(0.08) + recency(0.05) + popularity(0.03) = 0.30`, 而默认阈值只有 0.35 —— 铁证:同库里 `tpl-59c3f8cf8f` **词汇交集为 0**(cosine=0、coverage=0) 却拿到 **0.298**,即一份与需求零词面重叠的模板能拿到阈值的 86%。 2. **通用基名吃满路径权重**。`lib/fingerprint.js` 给路径基名(含扩展名)赋**满路径权重 4**, 于是 `…/login/index.vue` 与 `…/third-party-integration/index.vue` 共享一个 `w=4` 的 `index.vue`, 在词汇上"看起来命中了同一条路径"。 **修法(两处,都用实测数据定标)**: - `fingerprint.js`:新增 `GENERIC_BASENAMES`(`index.vue` / `main.js` / `app.vue` / `package.json` …), tokenize 时这类基名降权到最低档(保留 token,避免误伤「改 index.vue」这类真实需求)。 另暴露 `downweightGenericBasenames()`,打分前对**已落盘的老指纹**同样降权 —— 否则同一份修复对不同年代写入的模板效果不一致;不改磁盘历史数据。 - `match.js`:命中必须有词汇证据 —— `hit = score >= threshold && lexical > 0`。 先验分仍计入 score(排序用),但不再能单独构成"命中"。 **为什么不设更高的 lexical 门槛**:实测真命中的 lexical 跨度很大 (0.142~0.635 —— 中文描述对上以路径为主的指纹时天然偏低), 例如「把设计稿…」对自身模板只有 0.142,抬高门槛会连真命中一起杀掉。所以只卡"零证据"这条硬边界。 **效果(真实库 7 模板)**: | 用例 | 改前 | 改后 | | --- | --- | --- | | 「登录页加个按钮」(假阳性) | 0.372 **命中** | 0.311 未命中 | | 「管理页表单加开关字段」(真命中) | 0.558 命中 | 0.558 命中 | | 「把设计稿…」(真命中) | 0.373 命中 | 0.374 命中 | 冒烟基线分数不变(同类 0.499 / 异类 0.136)。 ### 🔴 修复 2:fastTrack 时注入内容与指令自相矛盾 **症状**:同一次返回里,首行写「**不要再调 `spec_triage` 或 `spec_distill`**」, 而同一个模板块注入的「标准改法」第一条写着 `spec_recall → spec_triage(此类需求通常 L2,问上面 3 条)→ spec_distill`; 模板的「澄清清单」还列着 3 个待问问题,与 fastTrack「禁止追问」冲突。 **修法**(`lib/render.js`):`renderInjection` 新增 `fastTrack` 参数,为 true 时 - 省略「澄清清单」; - 滤掉「标准改法」里引用 `spec_*` 工具的**流程行**(正则 `spec_(recall|triage|distill|retro|library)`), 保留改法的实质内容("抄某参考页的结构""导出走 Blob"); - 保留区块禁区与验收标准(这些不会误导); - 加一行「(本次已判定 1 步直达,该模板的澄清问题与体检流程已省略。)」说明原因。 `index.js` 里把 `fastTrack` 的计算提前到渲染注入之前。 顺带解决体积问题:实测模板块占返回量 **72%(2549/3527 字符)**,而 fastTrack 正是最该省 token 的场景。 ### 🟡 修复 3:禁区档案近重复累积 **症状**:某真实项目 `profile.md` 禁区 11 条里只有 9 条唯一 —— `package.json 不新增依赖(…不引 jszip)` 与 `package.json 不新增依赖(项目已登记禁区)`; `dist 是 git 跟踪的目录…` 与 `验证构建必须显式 --outDir…`(同一规则、改写语序)。 旧版只用 `[...new Set(...)]` 做字符串级去重,换个括号说明或改个语序就漏过去了。 **修法**(`lib/store.js`):新增 `normalizeRedline`(去括号补充、去标点空白)/ `isNearDuplicateRedline` (字面相等或字符 bigram 包含度 ≥ 0.7,后者用来抓改写语序)/ `dedupeRedlines`(合并时保留信息量更大的那条)。 放在 `parseProfile` 里 —— 读路径同时服务注入与写盘,**读时收敛、下次沉淀写回即自愈**,无需一次性迁移脚本。 `collectRedlines` 与 `spec_retro` 的两处合并也改用该函数。 **ratio=0.7 的标定依据**(真实项目档案 11 条两两实测): 真实重复对 = 1.000 / 0.897,最高的非重复对 = 0.571(其余 ≤ 0.222)→ 安全窗口 (0.571, 0.897)。 已写进代码注释,改阈值前请重跑分布。 **效果**:真实档案 11 条 → 9 条,恰好合并预期的两组,其余 9 条无误伤。 ### 验证 - `node --test` → **197 passed / 0 failed**(182 → 197,新增 15 条回归)。 - `node scripts/smoke.js` / `verify-load.js` / `token-audit.js` 全部通过。 - 三条修复都在**真实库 + 真实档案**上实测过(不是只跑合成单测)。 ## 0.4.4 — 2026-09-10 **修正 0.4.3 一次基于错误前提的提示段压缩:补齐被静默删掉的 4 条约束。** ### 🔴 病灶:把自设的"参考线"当成硬限制,压缩换来内容丢失 起因是用户点破两件事,逐一查清: **① 前提本身是错的。** 0.4.3 压缩的动因是"常驻段 447 token 超了 token-audit 的 450 预算线"。 实际 **447 < 450**,当时的审计输出根本没有打印告警——这是对自己输出的一次误读。 更进一步,读 harness 源码确认:`packages/core/system-prompt/src/index.ts` 的 `renderPrompt()` 只做 `assembly.sections.map(展开).filter(text => text.length > 0).join('\n\n')`, **没有截断、没有长度上限**。`SYS_BUDGET = 450` 是 `scripts/token-audit.js` 里**自己设的参考线,不是 dsh 的限制**。 所以从来不存在"超预算"这件事。 **② 压缩不会影响工具调用——架构上不可能。** dsh 的 `SystemPrompt.assemble()` 里 **sections 与 tools 是两条独立通道**(tools 另按 `orderTools()` 排序,占 order 1000–2900;提示段占 150)。 缩短提示段文本动不到工具注册。实证:`spec_recall` / `spec_triage` / `spec_library` 的注册在各版本完全一致, 安装产物的 15 条行为断言照过。 **③ 但压缩确实静默删掉了 4 条真实约束。** 且恰好打脸 0.4.3 自己的目标: | 被删/被弱化的约束 | 后果 | 冗余兜底 | | --- | --- | --- | | 「仅 L3 与 L2 安全阀可 `ask_user_question`」被弱化成非排他表述 | L1 理论上也能追问,直接抵消 fastTrack「禁止追问」 | 无 | | 触发词里的「极速模式」掉了 | 用户自己的口令不再触发 fastTrack | 无 | | 免沉淀清单里的「报错排查」掉了 | 报错排查类需求可能被误沉淀(正是 0.4.3 要治的病) | 有:`spec_retro` 的工具描述里仍写着 | | 点名工具 `read/grep/glob/bash/ls` 退化成「文件类工具」 | 约束更含糊,更易误读 | 无 | ### 修复 - `index.js` 4 条全部补回。常驻段 538 → 679 字符(估算 338 → **426** token)。 相对 0.4.2 基线 358 的 **+68 是 0.4.3 的新增能力**(`nextStep` 路由指令行、L1 原子小改、单字段 CRUD), 属新增内容,不是膨胀。 - `scripts/token-audit.js`:`SYS_BUDGET` 明确标注「参考线,非硬限制」;超出时不再打 ⚠️, 改述为「超参考线约 N token;无截断风险,按内容需要取舍」。 - `tests/routing.test.js` 新增回归护栏 **「常驻提示段:关键约束不得在精简中丢失」**: 从 mock 的 `systemPrompt.section` 注册里抽出该段真文本,断言 `order === 150` 且包含 16 条必需子串 (含本次丢的 4 条)。**只比 token 数字不够——压缩类改动必须配"内容清单"回归。** ### 成本结论:压缩要挑对杠杆 同一时刻的审计数字: | 每轮请求固定注入 | 估算 token | 占比 | | --- | --- | --- | | ① 常驻提示段 `spec-forge:routing` | 426 | 15% | | ② 五个工具定义合计 | **2381** | **85%** | | 合计 | 2807 | 100% | 盯着压的那 20 token 只占本插件每轮开销的 **0.7%**,而工具定义占 **85%**。 **在占 0.7% 的地方抠字、同时让 2381 token 的工具定义原样每轮重发——这就是"为了压缩而压缩"。** 方向应该是压工具定义(0.4.0 合并工具 6→5、省 480 token/请求,才是对的路)。 ### 验证 - `node --test` → **182 passed / 0 failed**(181 → 182)。 - `node scripts/token-audit.js`:常驻段 426 token,无告警。 - 新增诊断脚本 `.local/section-compare.mjs`:三版提示段的**体积 + 20 条约束逐条在否**双维度对照 (正是它把「报错排查」等 4 条揪出来的)。 ## 0.4.3 — 2026-09-10 **「简单需求被过度加工」的专项治理。** 逐条解 dsh 会话日志做成本归因时发现:一条本该一轮做完的简单需求,插件走了 4 次工具往返。根因不是分级器判错,而是**行为契约自相矛盾**——分级器判 L1、模型照样把六步走完。 ### 🔴 修复:`SKILL.md` 第 2 步「必做」与 L1「跳过体检」互相矛盾,导致快速通道形同虚设 - **实测症状**:会话 `session-dd99e8c7` 中,需求「参考第三方对接文档页面…制作一个线下物料页面」的调用链是 `spec_recall(57字) → spec_triage(622字) → spec_distill(1897字) → spec_retro(1009字)`,同一需求两轮共 8 次调用、7064 字。 另一条会话 `session-8e5cbfed` 里,最简单的「加一个 isMainAdmin 开关字段」也走成 `recall → triage`,随后一轮 `distill → retro`,并额外触发了一次 5 问澄清。 - **根因**:`skills/spec-forge/SKILL.md` 写着「### 第 2 步:体检 + 复杂度分级(**必做**)」, 而 L1 的跳过约定藏在第 2 步的子条目和第 4 步里。模型读到的是标题上的"必做",于是无条件调用 `spec_triage`。 **分级再准,也拦不住一个写着"必做"的流程。** - **修复(三层同时说清,不再依赖隐含约定)**: 1. **工具输出**:`spec_recall` 新增 `nextStep` 字段(`'implement'` / `'triage'`),并在召回正文首行改成指令式 「**执行路径:1 步直达**」/「**执行路径:先体检**」,fastTrack 时明文写「**不要调用 `spec_triage`,也不要调用 `spec_distill`**」。 2. **常驻提示词**:路由段改成「严格按 `nextStep` 执行,不自作主张加步骤」,并给出两条分支的具体动作。 3. **Skill 文档**:第 2 步标题改为「(**仅当 `nextStep=triage` 时执行**)」并前置闸门;新增「执行路径速查」表,把 6 步压成 3 条真实路径。 > **证据强度说明(重要)**:逐个解压 `~/.dsh/sessions` 下 14 份会话日志核对后确认, > 上述两次会话运行的是 **v0.3.2 / v0.3.3**(工具集含 `spec_store`、`spec_recall` 返回里**没有任何分级行**), > 而 `fastTrack` 与分级行是 **0.4.0 才引入**、0.4.2 于今日 17:24 才装进 profile 的。 > 也就是说,**fastTrack 这条快速通道从上线到 0.4.2 从未被任何一次真实会话验证过**—— > 它是否真能短路,此前无据可依。本次修复因此同时补上 `tests/routing.test.js` 与 > `.local/chain-compare.mjs`,把"短路"从"文档约定"变成"可回归验证的行为"。 > 上面列出的三处矛盾在 0.4.0~0.4.2 代码里都实际存在,是**潜伏缺陷**,不是事后归因。 ### 🟡 移除:`spec_triage` 重复执行 `spec_recall` 已做过的模板扫描 - **现象**:`spec_triage.execute` 为给报告追加「历史模板澄清清单」,又完整跑了一遍 `listTemplates(home, scope)` + `rankTemplates(...)`——即「全量模板扫描 + 指纹 + 余弦打分」。 这正是 `spec_recall` 上一次调用刚算过的东西。 - **修复**:删掉这段重复计算。该清单早已随 `spec_recall` 的注入正文进入上下文; 受影响面只有 L3 与 L2 安全阀两条路径的「历史模板建议追加确认」段落,内容与召回注入重复,无信息损失。 - **顺带**:`spec_triage` 不再需要 `cwd` 参数(唯一的用途就是那次扫描),已从参数表移除。 ### 🟢 增强:原子小改纳入快速通道(此前全部落 L2,每次白跑一趟体检) - 0.4.0~0.4.2 只有两条 L1 通路(单字段 CRUD、自包含新建 + 参考物)。 实测中「加按钮 / 改文案 / 调样式 / 加一列 / 加搜索项 / 加路由 / 改默认值 / 字段改名」这类同样一轮能做完的小需求全部落 L2。 - 新增第三条通路 `L1:atomic-edit`(12 个族:copy / style / button / column / route / constant / rename / tiny)。 - **三道闸门**(同时满足才升,宁可不升也不误升):长度 ≤ 48 字、不含多任务连接词(以及/同时/顺便…)、不含大范围限定词(整体/全局/所有/批量…)。 - **同时覆盖中文语序**:动词前置(「改一下登录页的文案」)与名词前置(「标题换成…」「超时时间改成 30s」「默认值改成草稿」)两类写法。 - **复合词防误伤**(与 0.4.0 的 DDL 误判同源):「列」用 `列(?!表|页|格)` 排除「列表/列页/列格」,「改列表页」不会被当成「改一列」。 ### 🟡 修复:召回未命中时无条件要求沉淀,是「过度沉淀」的直接诱因 - **现象**:`renderInjection` 未命中分支写着「会话结束时请调用 `spec_retro` 沉淀为新模板」。 实测会话里 `spec_recall` 返回的 57 字正是这句——它无条件推着模型去沉淀,与第 6 步的复用价值三问闸门冲突。 - **修复**:改为「收尾时先过复用价值三问,确有复用价值才调用 `spec_retro`;原子小改、一次性任务跳过沉淀即可」。 - `SKILL.md` 第 6 步的「必须跳过的情形」补充 L1 原子小改。 ### 量化效果(对真实模板库回放,非线上采集) 用 `.local/chain-compare.mjs` 装载真实库(`/.dsh-spec-forge`,9 份模板)并指定会话 cwd=`<真实项目目录>`, 对一批代表性需求跑一遍分级,再按「旧契约(六步字面读法)」与「新契约(按 nextStep)」分别数调用次数: | 需求 | 分级 | nextStep | 旧契约调用 | 新契约调用 | | --- | --- | --- | --- | --- | | 参考物料页做线下物料页面(实测踩坑用例) | L1 FT | implement | 3 | **1** | | 登录页加一个按钮 | L1 FT | implement | 3 | **1** | | 改一下登录页的文案 | L1 FT | implement | 3 | **1** | | 列表加一列显示手机号 | L1 FT | implement | 3 | **1** | | 在 UserController 新增一个分页查询接口 | L2 | triage | 3 | 3 | | 帮我改一下那个查询 | L2 安全阀 | triage | 3 | 3 | | 把用户模块重构拆分为独立服务 | L3 | triage | 4 | 4 | - 7 条样本合计:**22 次 → 14 次(−36%)**;其中 4 条简单需求全部 **3 次 → 1 次(−67%)**。 - 常驻系统提示段改写为指令式路由后为 338 tokens(0.4.2 基线 358)。 ⚠️ **此条已于 0.4.4 修正**:当时"压回预算内"的前提是错的(338 相对 447 的对比建立在误读之上), 且这次压缩**静默删掉了 4 条真实约束**。详见 0.4.4 一节。 - L2/L3 路径**未做任何削减**——那次体检与先问后查是必要的,不做"为省而省"。 ### 验证 - `node --test` → **181 passed / 0 failed**(168 → 181)。新增 `tests/routing.test.js`(7 条)把行为契约钉死: `nextStep` 必填、fastTrack 正文必须禁止调用 triage/distill、L2/L3 必须给出正向指令、 `spec_triage` 不得再接收 `cwd`、不得出现无条件沉淀指令。 - `classify.test.js` 新增 6 条(14 个原子小改正例 + 多任务连接词/大范围限定词/长度闸门/复合词防误伤反例)。 - `node scripts/smoke.js`、`node scripts/verify-load.js`、`node scripts/token-audit.js` 全部通过。 ## 0.4.2 — 2026-09-10 **核查「本地装的是不是最新版、能否直接启动」时发现的两处收尾问题。** 0.4.1 的核心修复均正常,这版只收尾,无行为风险。 ### 🟡 修复:上移归位后留下空的 `/spec-forge/` 空壳 - **现象**:0.4.1 的 `liftLegacyNesting()` 把子项上移后不清理源目录,实测在 `/.dsh-spec-forge/` 下永久留下一个空的 `spec-forge/`。 - **更隐蔽的一种**:若「新布局已存在」的守卫提前返回(走正常路径),空壳**再也不会**被清理——守卫在归位逻辑之前就退出了。 - **修复**:上移后 `rmdirSync` 删空目录(非空时自然失败,属预期);「新布局已在用」分支里也补一次空壳清理。非空旧目录(存在被跳过项)**不删**,旧数据不覆盖。 - **测试**:+3 条(归位后无空壳 / 新布局在用时清空壳 / 非空旧目录保留且不覆盖)。 ### 🟡 修复:README 的「手动覆盖配置」示例是错的,照抄无效 - **现象**:README 给的是缩进块 ```yaml spec-forge: storageRoot: workspace ``` 这既不是 `cordis.patch.yml` 的语法,也误导人以为配置是「合并」进默认值的。 - **真相**(依据 `apps/cli/src/profile-boot.ts` 的 `PatchOptions = { id, disabled, config? }`,以及 base bundle patch 的官方注释):`cordis.patch.yml` 是**顶层 YAML 数组**,条目用 `id` 定位到已有行;且 **id 定向补丁会「整体替换」该行的 `config`,而不是合并进它**。照 README 抄不仅静默无效,即使 id 写对、只给一个字段,也会把其余字段压回插件默认值。 - **修复**:README 新增「手动覆盖配置:改的是 `cordis.patch.yml`」小节,给出可直接用的顶层数组示例(完整重述 11 个配置项)+ `--dump-config` 校验方法;「配置」一节的表述同步纠正。 - **影响面**:纯文档,无代码变更。 ### 验证 - `node --test` → **168 passed / 0 failed**(165 → 168)。 - `npm run smoke` / `npm run verify` / `npm run token-audit` 全部通过。 - 装载自检(`dsh-plugin-release-verify`)对安装产物复验通过。 ## 0.4.1 — 2026-09-10 **从线上仓库全新安装 0.4.0 做实机验证时发现的三个缺陷修复。** 安装本身没问题(`dsh plugin add github:…` 3.7 秒通过、bundle 自动挂载、dump-config 正常、5 工具 + 提示段 + 技能均注册、各工具真执行可用),但核心「沉淀 → 复用」闭环实际是断的。 ### 🔴 修复:`spec_retro` 写出的指纹是垃圾,沉淀的模板永远无法被召回 - **现象**:模板 frontmatter 的 `fingerprint` 被写成 `'[object Object]'` × N。 - **根因**:`spec_retro` 把 `fingerprint()` 返回的 `{token, weight}` 对象数组直接交给 `writeTemplate`,而 `stringifyFrontmatter` 用 `String(value)` 序列化对象 → `'[object Object]'`。读回时 `normalizeFingerprint` 得到 `{token: '[object Object]', weight: 1}`,导致 `cosine=coverage=lexical=0`、`score` 只有 0.136,**永远无法达到 0.35 阈值**。 - **连带风险**:`recordHit` 会用 `readTemplate` 读出的对象指纹再写回,**首次命中就会把原本健康的旧模板一起写坏**(用户库里 7 份真实模板此前 `hitCount` 全为 0,才侥幸未被破坏)。 - **修复**:`writeTemplate` 统一走新增的 `serializeFingerprint()`,把 `{token, weight}` 与 `'token|weight'` 两种输入都规范成 `token|weight` 字符串落盘;非法项丢弃而不是写坏。 - **为什么测试没拦住**:`smoke.js` 与单测都自己手工拼了 `` `${token}|${weight}` ``,绕过了 `spec_retro` 这条真实路径。冒烟已改为传对象数组,并新增「指纹可解析」「落盘无 `[object Object]`」「命中必须来自词汇相似度(`lexical > 0`)」三条断言。 ### 🔴 修复:存储根多嵌套一层 `spec-forge`,历史模板全部失联、迁移无效 - **现象**:数据实际落在 `/spec-forge/{global,projects}`,而 `spec_library({action:'migrate'})` 却写到 `/{global,projects}` —— 读写路径不一致。 - **根因**:0.3.3 把 `home` 从 `$DSH_HOME` 改成 `resolveStorageRoot().path`(**本身已是数据根**),但 `scopeDir()` 与 `describeStore()` 仍按「home 目录」处理,继续调用 `dataRoot()` 追加一层 `spec-forge`。 - **实证后果**: - `listTemplates('~/.dsh/spec-forge', '')` 返回 **0 份**,而磁盘上明明有 5 份 → 升级到 0.3.3+ 后**历史模板再也不会被召回**(这解释了一段时间以来「召回质量不高」的观感)。 - `home` 模式(`storageRoot: 'home'`)同样失效,且与 0.3.2 及更早的数据位置不兼容。 - `info` 的「旧路径数据概览」经 `listTemplates` 统计,恒报「旧路径无数据」。 - `migrate` 报告「已复制 N 个文件」但插件读不到 —— 迁移形同虚设。 - **修复**:`scopeDir()` / `describeStore()` 不再重复调用 `dataRoot()`,**`home` 即数据根**。三种模式恢复到文档描述的语义:`workspace` → `/.dsh-spec-forge/`、`home` → `$DSH_HOME/spec-forge/`、`storageHome` → 你给的绝对路径。 - **一次性归位**:新增 `liftLegacyNesting()`,插件启动时若发现 0.3.3/0.4.0 遗留的 `/spec-forge/…` 且新位置为空,就把子项 `rename` 上移一层(目标已存在则跳过,不覆盖)。失败只告警,不影响其余功能。 - **验证**:真实库 `~/.dsh/spec-forge` 从「模板数 0」恢复为 5 / 1 / 1,相关需求重新命中(0.441 / 0.51)。 ### 🟡 修复:README 的验证命令在 PowerShell 下不可用 - 原文 `dsh --profile web --dump-config | grep spec-forge`:`grep` 是 Unix 命令,PowerShell 需 `Select-String`;且 `dsh` 通常不在 PATH,应经 `pnpm dsh`。已改为同时给出 Bash 与 PowerShell 两种写法。 ### 测试与文档 - 测试:158 → **165**。新增指纹序列化(对象/字符串/脏数据)、`recordHit` 不破坏指纹、`home` 即数据根、旧布局归位、新位置有数据时不归位 共 7 个用例;3 个构造磁盘布局的旧用例改到新语义。 - `scripts/smoke.js`:改用真实调用形态传指纹 + 4 条新断言。 - `scripts/token-audit.js`:真实库路径构造对齐「home 即数据根」。 ### 兼容性 - **`home` 模式(以及从 ≤0.3.2 升级的用户)**:本版起数据位置回到 `$DSH_HOME/spec-forge/`,历史模板重新可见。**这是恢复而非破坏。** - **0.3.3/0.4.0 期间在 `workspace` 模式下写入的数据**:位于 `/.dsh-spec-forge/spec-forge/`,启动时会被一次性上移到 `/.dsh-spec-forge/`,无需手工干预。 - **已被 `[object Object]` 写坏的模板指纹无法自动恢复**,需重新沉淀一次(同名覆盖)或手工把该字段改回 `token|weight` 形式。检测方法:搜索模板文件里的 `[object Object]`。 --- ## 0.4.0 — 2026-09-10 成本归因驱动的「少追问、少烧 token」版本。起因是一次真实会话(620K 日志、482 事件)被逐事件解码后,发现一次简单的需求花了 1.41 元调度费且需返工。归因结论:**插件只占约 4% 成本,整读大文件才是大头(≈50%)**;但插件确实存在「简单需求被反复追问」的体验问题。本版把两类问题一起治。 ### P0-1 大文件纪律(治成本大头) - 系统提示段新增第 3 条:`>20K` 字符的文件禁止整文件 read,先 `grep` 定位再分段读;确需整读先落要点摘要。 - `SKILL.md` 新增「大文件纪律」章节(5 条:禁用整读 / 读后落摘要 / 不预扫工作区 / grep 优先 / 长 HTML 只读目标区段),并写入硬规则第 4 条。 ### P0-2 L1 快速通道扩容(治「简单需求被追问」) - `classifyComplexity` 新增 `fastTrack` 字段(`level===1` 时为 `true`),所有返回分支都带上。 - 新增「带参考物的自包含新建」识别:需求含 `新建/做一个/实现…页面|组件|弹窗|表单|列表页|详情页|卡片|视图` **且**含 `参考/参照/仿照/按照/对标/类似/依据/基于` 时 → L1 fastTrack。典型:「参考现有列表页新建一个订单页」。 - `spec_recall` 返回值新增 `level` 与 `fastTrack`,命中快速通道时在注入文本顶部插入「禁止追问;跳过 spec_triage 与 spec_distill」的显式指令。 - 修复一处会**吃掉快速通道收益**的 DDL 误判:原 `建[…]{0,15}表` 会把「新建订单**列表页**」「搜索**表单**组件」「**报表**页面」里的"表"当成建表信号判成 L3。改为带前后视断言 `(?/.dsh-spec-forge/`)/ `'home'`(兼容 0.3.2 默认)/ 留空 → 旧行为。`storageHome` 绝对路径仍保留作为显式覆盖(优先级最高)。 - **新增工具 `spec_store`**:查询当前模式 / 跨盘判定 / 旧路径数据量;`migrate` 操作可把 `$DSH_HOME/spec-forge/{global, projects/}` 一次性复制(同名文件跳过不覆盖,可选 `move: true` 删源)到当前数据目录。 - **新工具 helper**:`lib/store.js` 新增 `resolveStorageRoot()` 解析函数、`isCrossDrive()` 跨盘判定、`copyTree()` 递归复制(同名跳过)、`bumpWriteEpoch()` 让 `spec_store` 拷贝文件后强制让进程内读缓存失效。 - **向后兼容**:旧用户升级后 `storageRoot` 默认改为 `workspace`,**旧 `$DSH_HOME/spec-forge` 数据**留在原处不自动迁移;调 `spec_store({ action: 'info' })` 看一眼,调 `spec_store({ action: 'migrate' })` 一次性拷过去。 - 测试:138 → 148。新增 `resolveStorageRoot` / `STORAGE_MODES` / `isCrossDrive` / `copyTree` / `bumpWriteEpoch` 共 10 个用例。 ### 行为变更 - 旧默认路径 `$DSH_HOME/spec-forge` 不再是默认**新装**用户的目标(仅当用户显式设置 `storageRoot: 'home'` 时使用)。 - system prompt 路由段描述不变(该段不涉及存储路径)。 --- ## 0.3.2 — 2026-09-05 代码审查驱动的健壮性 & 性能修复(配合 README 去公式化重写),并回应 token 消耗验证结论。 ### 新增:查询侧关键词聚焦(提高命中余量) - **症状**:真实用户需求往往很长(带路径、叙述、寒暄),`fingerprint()` 默认 24 个 token 里一多半是权重 1 的中文 2-gram,压低余弦与覆盖率;跨仓库(无同仓库加分)时容易跌破 0.35 阈值。同一条「管理页加 isMainAdmin 开关字段」需求,浓缩表述命中而原话(85 字)只在阈值边缘。 - **修复**:新增 `focusFingerprint()`(lib/fingerprint.js):查询指纹削掉低信号 2-gram 尾巴(权重≥2 的强 token 全保留 + 最多 8 个 2-gram 兜底中文表述),只作用于查询侧、不碰落盘模板指纹。`buildQueryFp`(spec_recall/spec_triage 共用)统一应用。 - **实证(真实模板库,含仓库/标签/分类全加分)**:原话长需求总分 0.462 → 0.510(gram 上限 8);无关需求 0.105 不变,无误召回。审计脚本按真实接线测量后,3 个仓库中 2 个默认阈值命中。 ### 精简:常驻系统提示段与工具描述 - 常驻系统提示段 `spec-forge:routing`:873 字符 ≈603 tok → **615 字符 ≈407 tok**(删冗余句式,硬规则全部保留)。 - `spec_triage` / `spec_retro` 工具描述精简(完整判据仍在 SKILL.md 与报告正文,不依赖描述里的长文)。 - 五个工具定义合计 ≈2364 → ≈2250 tok/完整请求;固定税 ≈2967 → ≈2657 tok/完整请求。 ### 修复(spec_retro:假成功、坏指纹、摘要没接线) - **`saved: true` 硬编码**:模板写盘失败会静默返回"已保存"。现在写盘包 try/catch,失败返回 `saved: false` + 错误信息 + 重试指引,不再向上冒泡崩溃 dsh。 - **指纹引用了参数表里不存在的 `args.requirement`**(恒 undefined):指纹只有 name/trigger/tags 三个来源,丢掉了真实做过的 approach。现改为 `name + trigger + tags + approach + digest前300字` 五源拼接。 - **`buildRetroDigest` 只 import 没调用**:描述里承诺的"digest 留空自动提取"从未实现。现接入 `exec.agent.session` 事件流自动提取摘要,`prompt` 为空时回退到 digest。 ### 修复(召回打分一致性 & 失真) - `spec_triage` 的模板排序仍用裸 `fingerprint()`,tags/category 加分没接上(0.3.1 只修了 `spec_recall`)。现抽出 `buildQueryFp()` 统一两处查询指纹。 - `spec_recall` 记命中前只排 top-N:热度排序在小库上失真。改为全量排序后取 top 再注入。 - 标签重叠从 jaccard 改为**覆盖率**(交集/模板标签数):查询侧标签上限 8 个会撑大 jaccard 分母, 把重叠率稀释到无意义。新增组件别名归一(`a-switch`→`switch`、`el-switch`→`switch`、 `element-plus`→`elementplus`、`vue3`→`vue`)与中英技术词映射("开关"→`switch`、"分页"→`pagination`), 解决模板标签与需求原文跨语言对不上。 ### 性能 & 死代码 - `listTemplates` 每次召回都全量读盘解析全部模板。现加进程内读缓存:任何写盘(写版本号)或 文件名集合变化(外部增删)即失效,返回副本防调用方原地排序污染。 - `purgeStale` 无人调用(死代码)。拆出 `staleTemplates()`(只统计不删除),`spec_library` 报告 ≥90 天未命中的过期模板;物理删除只在用户显式要求时通过 `purge: true` 执行。 ### 验证 - 单元测试 129 → **138 通过**(新增:staleTemplates 统计、缓存三态失效、调用方排序不污染、focusFingerprint 4 例) - smoke / verify 全绿;`npm test` 全量通过 - 新增 `npm run token-audit`:静态 token 预算审计(可复现,无外部依赖) ### 修复:打分里 22% 权重从未生效(查询侧 tags / category 没接线) - **症状**:`scoreTemplate` 设计的「同分类 +0.14」「标签重叠 +0.08」在真实调用中恒为 0。 - **根因**:`spec_recall` 传给打分器的 `queryFp` 是 `fingerprint()` 返回的**纯指纹数组**,不带 `category`/`tags`;而模板侧这两个字段在沉淀时都存了。天平两侧只接了一边。 - **修复**: - 新增 `inferQueryTags(requirement)`(lib/fingerprint.js):从需求原文推断候选标签,**保留带连字符的复合词整体**(`a-switch` / `ant-design-vue`,否则会被切碎成 ant/design/vue 与模板标签对不上),并抽取路径片段、技术词、中文技术词、驼峰标识符;结果按权重截断到 8 个(数量过大会稀释 jaccard 分母,反而降低重叠率) - 新增 `inferQueryCategory(requirement)`(lib/classify.js):只推断**一级**分类(`bugfix`/`refactor`/`frontend`/`feature`),判不出来返回 undefined(宁可不加分,也不误加分 0.14) - `scoreTemplate` 的分类比较放宽为**一级相同即同类**,匹配模板侧自由填写的二级名 - `spec_recall` execute 中把两者附加到 `queryFp` 上再打分 ### 实证(真实模板,非构造数据) | 需求 | 修复前 | 修复后 | 命中 | | --- | --- | --- | --- | | 同类:Vue 管理页加 isMainAdmin 开关字段(a-switch) | 0.487 | **0.633**(tag 重叠 0.077 + 同分类 1) | ✅ 命中,区分度更大 | | 无关:node_modules 加 .gitignore + 写 README | 0.105 | 0.105(tag/category 均为 0) | ❌ 仍不命中,无误召回 | ### 验证 - 单元测试 117 → **129 通过**(新增 12 用例:标签推断 5、类别推断 5、接线加分 2) ## 0.3.0 — 2026-09-05 ### 新功能:沉淀时机判据(三层漏斗 + 合并沉淀) 按用户反馈治理"沉淀时机":不该每轮对话都沉淀,也不该让该沉的漏掉。引入三层判定: 1. **第一层 硬门槛(插件代码判定,不依赖模型自觉)** - 新增 `evaluateRetroEligibility()`:要求会话**真实改过代码**(工具名命中写工具集合 edit/write/...,只读诊断、纯问答不计)且工具调用达到 `retroMinToolCalls`,才认为"有资格沉淀" - 新增配置 `retroRequireCodeChange`(默认 `true`):纯问答 / 只读诊断 / 一次性任务被自动拦下,不再提示沉淀 2. **第二层 复用价值三问(模型在调用 spec_retro 前自检)**:下次是否还这么干 / 结论是否跨项目成立 / 用户是否会反复提;任一为否则跳过 3. **第三层 合并沉淀(任务链收尾一次)**:一条任务链(两次 spec_recall 之间)只沉淀一次,链内小修(编译错、警告修复)合并进最终那份;`spec_retro` 描述与 SKILL.md 同步改写 配套规则:跳过沉淀时必须回一句话说明("本次为只读诊断/无复用价值,已跳过;需要记录说一声");用户说"沉淀/总结/记到模板库"时无条件调用 `spec_retro`;纯问答/只读诊断禁止沉淀。 ### 修复:兜底提醒从未真正生效(关键 Bug) - **症状**:模型漏调 `spec_retro` 时没有任何提醒,沉淀完全靠模型自觉。 - **根因**:兜底逻辑挂在 `ctx.on('turn/end')`,但 dsh 的 turn/end 事件载荷**不含 session 事件流**(`data` 只有 `{turn, reason}`),`extractSessionFacts(turn.session)` 恒为 null、`toolCalls` 恒为 0,低于 `retroMinToolCalls=2` → `pendingRetro` **从未被设置**,`spec_recall` 的 notice 从不出现。 - **修复**:删除失效的 turn/end 判定与 `pendingRetro` 状态;`buildRecallNotice()` 改为在 `spec_recall` execute 内用 `exec.agent.session`(确定可得)实时提取会话事实并跑门槛,未沉淀且真实改码时随召回结果附带提示。 ### 验证 - 单元测试 109 → **117 通过**(新增门槛判定 8 用例) - 硬门槛分离度验证:真实 dsh 会话(含多步任务链)中 spec_retro 2 次沉淀均可被门槛放行;只读问答会话被 `no-code-change` 拦下 ## 0.2.1 — 2026-09-05 ### 修复(阻断性 Bug) - **修复 `dsh web` / harness 启动崩溃**:`spec_triage` 工具 output schema 中的 `classification` 字段写成了 `{ type: 'object' }`,缺少 `additionalProperties: true`,违反 dsh schema 编译器要求(object 类型必须显式声明 true/false),导致启动时抛 `UNSUPPORTED_SCHEMA: schema.properties.classification.additionalProperties must be explicitly true or false`,整个插件树加载失败。 - 修复位置:`index.js` `spec_triage` output schema(顶层 schema 及所有嵌套 object 字段均需显式 `additionalProperties`,v0.1.0 已知坑在 0.2.0 引入新字段时复发)。 - 已全量排查:其余 5 个工具的顶层 schema 均带 `additionalProperties: true`,无其他 object 类型隐患。 ### 验证 - 单元测试 109/109 通过 - `pnpm dsh web`(deepseek-harness 开发模式)成功启动,web 服务在 `http://127.0.0.1:3080/` 正常监听,schema 错误消失 ## 0.2.0 — 2026-09-05 按用户反馈治理"教条式追问",新增 **3 级复杂度分级响应**。 ### 痛点 - 简单 CRUD(加一个 `isMainAdmin` 开关字段)也被当成架构变更处理,连发 5 个问题 - 即便每个问题都给了默认值,仍然强迫用户回答,对模板填充型需求过度严谨 - 缺乏"用户明确说不要问"的快捷通道 ### 新功能 - **3 级复杂度分级**:每条需求会被分成 L1 原子操作 / L2 模块变更 / L3 架构重构 - **L1 原子操作**(单文件 CRUD + 组件/默认值明确):**禁止追问**,报告输出"直接执行清单 + 风格自举要求 + 保守默认表 + 待办标注规则"。模型扫描目标文件最近 50 行表单代码模仿现有风格,疑虑用 `// TODO: [待确认]` 标注 - **L2 模块变更**(模块级新增/调整):最多 3 个追问,每题附分类器推断出的默认值;被截断的维度按当前信息推断执行 - **L3 架构重构**(架构/重构/迁移/升级/DDL/跨文件):完整 Grill-me,问题数无上限 - **用户跳过词**:消息中含 `直接做/速做/不用问/别问/不要问/极速模式` 任意一个 → 无条件 L1 - **保守默认表**(L1 用):列表展示默认不展示、校验默认不加、后端默认已支持;用户主动追加可改写 - **代码风格自举要求**(L1 用):先 grep 目标文件最近 50 行表单代码,沿用 el-form-item 写法、value 绑定方式、列表列展示约定 - **分类器推断**:自动从需求中抽取字段名(`isMainAdmin`)、组件类型(`el-switch`)、默认值(`否`)、目标文件路径 ### 工具协议变更 `spec_triage` 输出新增字段: - `mode: 'fast-track' | 'clarify' | 'ready'` - `level: 1 | 2 | 3` - `classification: { level, signals, inferredComponent, inferredField, inferredDefaultValue, inferredFile, skipTrigger }` L2 不再附带"历史模板建议追加确认"——分类器的推断值已覆盖该信息(L3 仍保留模板提示)。 ### 已知限制 - 复杂度分级是启发式判定:写得很短的需求(如"加个字段")会被判 L2,而不会默认 L1 - 字段名/组件/默认值推断基于关键词匹配,遇到生僻表述可能漏检——漏检时回退到 L2 ## 0.1.0 — 2026-09-04 首个社区试用版。 ### 功能 - **五工具闭环**:`spec_recall`(历史模板召回)/ `spec_triage`(四维需求体检)/ `spec_distill`(提示词提炼)/ `spec_retro`(复盘沉淀)/ `spec_library`(模板库状态) - **三层注入**:常驻系统提示词(约 200 token 路由规则)+ 运行时 Skill(按需加载流程说明书)+ 工具层 - **双层模板库**:全局层 + 按仓库哈希隔离的项目层,明文 Markdown + JSON,原子写 - **禁区持久化**:每次会话识别到的"不能改"写入项目档案,后续同类需求自动注入 - **幂等沉淀**:同名需求覆盖更新,不堆积 ### 修复 - **先问后查急停规则**:需求不完整时,模型此前可能先扫一遍工作区再提问(浪费 token)。 现已在体检报告、常驻系统提示、Skill 硬规则、工具描述四层钉死: `spec_triage` 判定缺失后,唯一动作是向用户提问,提问前禁止调用任何文件类工具。 ### 已知限制 - 匹配基于加权关键词指纹(非向量检索),对"说法完全不同但语义相同"的需求召回有限 - 自动复盘依赖模型调用 `spec_retro`(插件有三层保障 + 漏调提醒,但理论上仍可能遗漏) - 详见 README「已知限制与风险」