# Changelog 本插件锁定目标 dsh 版本:`@deepseek-ai/dsh` 0.1.x(developer preview,API 可能有破坏性变更)。 兼容性以实际安装的 profile 依赖树为准。 ## 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) 需求 `@youting-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 已踩过)。 - 端到端(安装产物 + 真实 youting 库): | 需求 | 结果 | | --- | --- | | `@…/login/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`「把 newShare 设计稿 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 —— 中文描述对上以路径为主的指纹时天然偏低), 例如「把 newShare 设计稿…」对自身模板只有 0.142,抬高门槛会连真命中一起杀掉。所以只卡"零证据"这条硬边界。 **效果(真实库 7 模板)**: | 用例 | 改前 | 改后 | | --- | --- | --- | | 「登录页加个按钮」(假阳性) | 0.372 **命中** | 0.311 未命中 | | 「管理页表单加开关字段」(真命中) | 0.558 命中 | 0.558 命中 | | 「把 newShare 设计稿…」(真命中) | 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:禁区档案近重复累积 **症状**:youting 项目 `profile.md` 禁区 11 条里只有 9 条唯一 —— `package.json 不新增依赖(…不引 jszip)` 与 `package.json 不新增依赖(项目已登记禁区)`; `youting-admin/dist 是 git 跟踪的目录…` 与 `验证构建必须显式 --outDir…`(同一规则、改写语序)。 旧版只用 `[...new Set(...)]` 做字符串级去重,换个括号说明或改个语序就漏过去了。 **修法**(`lib/store.js`):新增 `normalizeRedline`(去括号补充、去标点空白)/ `isNearDuplicateRedline` (字面相等或字符 bigram 包含度 ≥ 0.7,后者用来抓改写语序)/ `dedupeRedlines`(合并时保留信息量更大的那条)。 放在 `parseProfile` 里 —— 读路径同时服务注入与写盘,**读时收敛、下次沉淀写回即自愈**,无需一次性迁移脚本。 `collectRedlines` 与 `spec_retro` 的两处合并也改用该函数。 **ratio=0.7 的标定依据**(youting 真实档案 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=`D:/HBuilderProjects/youting`, 对一批代表性需求跑一遍分级,再按「旧契约(六步字面读法)」与「新契约(按 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()` 把子项上移后不清理源目录,实测在 `D:\HBuilderProjects\DSH\deepseek-harness\.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」版本。起因是一次真实会话(youting,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 会话(youting propertyStaff 任务链)中 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「已知限制与风险」