sillyspec 3.31.0 → 3.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +1 -1
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
  3. package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-flow/SKILL.md +10 -6
  5. package/.claude/skills/sillyspec-quick/SKILL.md +105 -7
  6. package/.zcodeignore +66 -0
  7. package/AGENTS.md +16 -16
  8. package/assets/command-cards/flow.md +9 -3
  9. package/assets/command-cards/run-quick.md +4 -1
  10. package/package.json +2 -2
  11. package/src/agent-session-log.js +27 -5
  12. package/src/config-schema.js +21 -0
  13. package/src/datetime.js +51 -0
  14. package/src/db.js +10 -3
  15. package/src/decision-distill.js +21 -12
  16. package/src/design-facts.js +46 -6
  17. package/src/flow-draft.js +468 -95
  18. package/src/flow-parity.js +199 -5
  19. package/src/flow-review.js +136 -6
  20. package/src/flow.js +786 -145
  21. package/src/foreign-declared.js +75 -3
  22. package/src/fr-index.js +148 -162
  23. package/src/git-helper.js +16 -3
  24. package/src/hooks/worktree-guard.js +16 -5
  25. package/src/hunk-attribution.js +28 -5
  26. package/src/index.js +186 -19
  27. package/src/knowledge-classify.js +3 -1
  28. package/src/knowledge-digest.js +55 -26
  29. package/src/knowledge-match.js +135 -7
  30. package/src/knowledge-stats.js +105 -4
  31. package/src/knowledge-vector.js +160 -0
  32. package/src/machine-interface.js +90 -2
  33. package/src/module-impact.js +22 -1
  34. package/src/modules.js +9 -0
  35. package/src/progress/change-registry.js +20 -0
  36. package/src/progress/stage-machine.js +4 -7
  37. package/src/progress.js +8 -1
  38. package/src/quicklog.js +20 -0
  39. package/src/redomain.js +23 -7
  40. package/src/route-hindsight.js +276 -0
  41. package/src/run/archive-distill.js +3 -1
  42. package/src/run/command.js +65 -12
  43. package/src/run/complete-handlers.js +219 -5
  44. package/src/run/complete.js +99 -22
  45. package/src/run/gate-snapshot.js +15 -0
  46. package/src/run/gates.js +60 -4
  47. package/src/run/prompt.js +159 -31
  48. package/src/run/quick-audit.js +66 -7
  49. package/src/run/shared.js +6 -11
  50. package/src/run/stage.js +5 -1
  51. package/src/run/verify-quality-scan.js +84 -0
  52. package/src/scan-facts.js +2 -1
  53. package/src/scope-audit.js +138 -5
  54. package/src/sentinel-assertions.js +67 -16
  55. package/src/stage-contract-engine.js +4 -1
  56. package/src/stage-contract-spec.js +103 -4
  57. package/src/stage-contract.js +55 -7
  58. package/src/stages/brainstorm.js +27 -14
  59. package/src/stages/knowledge.js +4 -3
  60. package/src/stages/plan-postcheck.js +98 -3
  61. package/src/stages/plan.js +22 -2
  62. package/src/stages/quick.js +1 -1
  63. package/src/stages/verify.js +2 -1
  64. package/src/task-review.js +1 -1
  65. package/src/task-tick.js +121 -0
  66. package/src/taskcard-frontmatter.js +31 -0
  67. package/src/ui-visual.js +43 -8
  68. package/src/verify-postcheck.js +518 -38
  69. package/src/verify-probes.js +216 -5
  70. package/src/watcher.js +1067 -1031
  71. package/src/worktree-apply.js +9 -1
  72. package/src/worktree.js +21 -6
  73. package/templates/agents-instruction.md +15 -15
  74. package/templates/prompts/taskcard-rules.md +1 -0
@@ -9,6 +9,8 @@
9
9
  * 三类信号(诚实口径:机器无法判定「哪行是谁的」,门的目标是把静默混合变成显式中断):
10
10
  * ① 未归因文件:提交面文件不在本变更声明面(design 清单 ∪ requirements 测试绑定,
11
11
  * 复用 parseFileChangeList / extractRequirementBindings / testAnchorFile 同源解析);
12
+ * 其中「窗口内全部提交均属他侧变更名」的文件改判他侧归因(提交事实,2026-10-05-
13
+ * hunk-gate-commit-attribution:归档他侧变更声明面不可见致误报——信息行不计数 ok 面);
12
14
  * ② 跨变更竞争:其他活跃变更的声明面与提交面相交——同文件无法按行归属,显式列出
13
15
  * 竞争文件、对方变更名与 hunk 数,收口前人工核对;
14
16
  * ③ 在途残留:提交面文件当前工作树仍有未提交 diff——活跃并发 WIP 信号。
@@ -115,7 +117,7 @@ function collectWorkingTreeModified(gitFn, cwd) {
115
117
  * contended: Array, residue: Array, notes: string[] }}
116
118
  */
117
119
  export async function runHunkAttributionGate({ cwd, specBase, changeName, baselineCommit, committedFiles, gitFn, gate = 'warn' }) {
118
- const result = { ok: true, degraded: false, gate, faceCount: 0, unattributed: [], contended: [], residue: [], notes: [] }
120
+ const result = { ok: true, degraded: false, gate, faceCount: 0, unattributed: [], foreignByCommit: [], contended: [], residue: [], notes: [] }
119
121
  if (gate === 'off') {
120
122
  result.notes.push('hunk_gate=off——归属门关闭')
121
123
  return result
@@ -134,8 +136,26 @@ export async function runHunkAttributionGate({ cwd, specBase, changeName, baseli
134
136
 
135
137
  // ① 未归因文件(声明面全空时降为提示,不指认夹带——advisory 同口径)
136
138
  if (declared.size > 0) {
137
- for (const f of deliverables) {
138
- if (!declared.has(f)) {
139
+ const undeclared = deliverables.filter((f) => !declared.has(f))
140
+ // 他侧归因改判(提交事实,2026-10-05-hunk-gate-commit-attribution):collectForeignDeclarations
141
+ // 只扫活跃变更(排 archive/)——归档他侧交付落本窗口时报「未归因」,指引「pathspec 隔离/
142
+ // 补 design 自声明」双误导(F-4 实证:他侧 546720ea 已归档交付被指为在途)。按提交 message
143
+ // 变更名归属(commitAttributionForChange+isForeignByCommit,与 patch 冻结 filterCommittedFace
144
+ // 同源):窗口内全部提交均属他侧变更名 → 他侧归因(ℹ️ 信息行、不计 ok 阻断面);
145
+ // 裸提交/本变更名提交触碰的文件保持未归因(fail-closed 原口径)。切分不可得(非 git/
146
+ // git 失败 → null)保持全量未归因,不放宽。
147
+ let attr = null
148
+ let isForeign = null
149
+ try {
150
+ const m = await import('./foreign-declared.js')
151
+ attr = m.commitAttributionForChange(cwd, specBase, changeName, baselineCommit)
152
+ isForeign = m.isForeignByCommit
153
+ } catch { /* 同源切分不可得 → 原口径 */ }
154
+ for (const f of undeclared) {
155
+ const entry = attr ? attr.get(f) : null
156
+ if (isForeign && isForeign(entry, changeName)) {
157
+ result.foreignByCommit.push({ file: f, owners: [...entry.owners] })
158
+ } else {
139
159
  result.unattributed.push({ file: f, hunks: countHunks(gitFn, cwd, baselineCommit, f) })
140
160
  }
141
161
  }
@@ -172,8 +192,11 @@ export function renderHunkAttributionLines(r) {
172
192
  for (const n of (r && r.notes) || []) L.push(`- ℹ️ ${n}`)
173
193
  return L
174
194
  }
175
- const attributed = r.faceCount - r.unattributed.length
176
- L.push(`- ℹ️ 提交面 ${r.faceCount} 个交付文件对账:声明面归因 ${attributed}、未归因 ${r.unattributed.length}、跨变更竞争 ${r.contended.length}、在途残留 ${r.residue.length}`)
195
+ const attributed = r.faceCount - r.unattributed.length - (r.foreignByCommit || []).length
196
+ L.push(`- ℹ️ 提交面 ${r.faceCount} 个交付文件对账:声明面归因 ${attributed}、他侧归因(提交事实)${(r.foreignByCommit || []).length}、未归因 ${r.unattributed.length}、跨变更竞争 ${r.contended.length}、在途残留 ${r.residue.length}`)
197
+ for (const fb of (r.foreignByCommit || [])) {
198
+ L.push(`- ℹ️ 他侧归因(提交事实):\`${fb.file}\` 窗口提交全部属 ${fb.owners.join('、')}——已归档他侧交付非本变更未归因,patch 冻结面同口径剔除`)
199
+ }
177
200
  for (const u of r.unattributed) {
178
201
  L.push(`- ⚠️ 未归因:\`${u.file}\`(${u.hunks >= 0 ? `${u.hunks} hunk` : 'hunk 数不可得'})不在 design 清单与 requirements 测试绑定——属并行会话在途交付则须显式 pathspec 隔离并协调归属;属本变更则补 design 自声明`)
179
202
  }
package/src/index.js CHANGED
@@ -53,7 +53,7 @@ SillySpec CLI — 规范驱动开发工具包
53
53
  --json 输出 JSON(程序化读取)
54
54
 
55
55
  阶段特有参数:
56
- quick: (通道已退役 v3.31.0——仅收尾升级前在途会话:--done/--cancel/--change <会话ID>;新工作走 flow start)
56
+ quick: (通道已退役 v3.30.0——仅收尾升级前在途会话:--done/--cancel/--change <会话ID>;新工作走 flow start)
57
57
  scan: --quick | --standard | --deep 显式选择 profile(优先于规模自动判定;三档互斥)
58
58
  --force-rescan 覆盖已有 scan 文档保护
59
59
  --diff [--base <commit>] [--full] [--report] scan 文档 vs 源码漂移清单(纯只读)
@@ -168,7 +168,7 @@ SillySpec CLI — 规范驱动开发工具包
168
168
  示例:
169
169
  sillyspec init
170
170
  sillyspec run brainstorm --change 2026-07-03-add-login
171
- sillyspec flow start --change my-fix --input "<动机与背景+独立一行『成功标准:』+每行一条『- 可验证标准』>" # 轻量变更(默认快道,2 次调用)
171
+ sillyspec flow start --change 2026-07-03-my-fix --input "<动机与背景+独立一行『成功标准:』+每行一条『- 可验证标准』>" # 轻量变更(默认快道,2 次调用;变更名须 YYYY-MM-DD-<简短描述> 日期前缀)
172
172
  sillyspec run verify --done --output "验证通过,测试全绿"
173
173
  sillyspec run archive --done --confirm --output "归档完成"
174
174
  sillyspec run plan --reopen --from-step 2 # 修订 plan,从第 2 步重做
@@ -220,8 +220,13 @@ function refuseRetiredQuickFreshStart(stageArgs) {
220
220
  return i !== -1 && /^quick-[0-9a-f]{8}$/.test(String(stageArgs[i + 1] || ''))
221
221
  })()
222
222
  if (passThrough) return false
223
- console.error('❌ quick 通道已退役,不再接受新会话(v3.31.0 起)。')
224
- console.error(' 新工作请走轻量变更:sillyspec flow start --change <名> --input "<动机与背景;随后独立一行『成功标准:』;再每行一条『- <可验证标准>』>"')
223
+ console.error('❌ quick 通道已退役,不再接受新会话(v3.30.0 起)。')
224
+ // 可照抄实例(2026-10-05-input-teach-copyable):引号内换行合法,行级 trim 容忍缩进
225
+ console.error(' 新工作请走轻量变更(--input 引号内换行合法,下例可照抄):')
226
+ console.error(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
227
+ console.error('')
228
+ console.error(' 成功标准:')
229
+ console.error(' - <可验证标准>"')
225
230
  console.error(' 升级前进行中的 quick 会话仍可收尾:sillyspec run quick --change <会话ID> 续跑 / --done 收口 / --cancel 取消。')
226
231
  process.exit(1)
227
232
  }
@@ -559,7 +564,7 @@ async function main() {
559
564
  const gateChangeIdx = args.indexOf('--change');
560
565
  const gateChange = gateChangeIdx >= 0 && args[gateChangeIdx + 1] && !String(args[gateChangeIdx + 1]).startsWith("--") ? args[gateChangeIdx + 1] : null;
561
566
  if (!gateStage || gateStage.startsWith('-') || !gateChange) {
562
- console.error('用法: sillyspec gate <stage> --change <name> [--json]\n stage: brainstorm | plan | execute | verify | archive | ...');
567
+ console.error('用法: sillyspec gate <stage|last> --change <name> [--json] [--full|--docs-only]\n stage: brainstorm | plan | execute | verify | archive | ...\n --full 覆盖 --done 独有贵检查(reconcile/stage review 预检);--docs-only 轻量档(verify)跳过 test/lint 执行只核文档契约面;两者互斥\n gate last --change <name>:读上轮 verify --done 阻断稳定锚点(gate-last-<change>.json)并打印 blocked 明细,不重跑检查');
563
568
  process.exit(2);
564
569
  }
565
570
  // 与 run 入口同源消毒(防路径穿越;gate 下游拼 marker/changes 路径)
@@ -573,6 +578,16 @@ async function main() {
573
578
  // P1 --full 只读预检档(batch3 task-03):覆盖 --done 独有检查面(reconcile/stage review)——
574
579
  // 预检过=必过(同因),省一轮 gate 失败重跑的整 agent 回合
575
580
  if (args.includes('--full')) gateOpts.full = true;
581
+ // --docs-only 轻量预检档(2026-10-06-verify-docs-prefill task-01):跳过 verify-test/lint
582
+ // 执行只核文档契约面(毫秒级)——纯文档缺项(visual-evidence/结论槽/移交项)一次列清再
583
+ // --done,免逐轮撞门。与 --full 互斥(一个减执行检查一个加检查面,并存语义矛盾)。
584
+ if (args.includes('--docs-only')) {
585
+ if (args.includes('--full')) {
586
+ console.error('❌ --docs-only 与 --full 互斥——docs-only 跳过 test/lint 执行只核文档契约面,full 追加 --done 独有的贵检查;两者并存语义矛盾,按需择一。');
587
+ process.exit(2);
588
+ }
589
+ gateOpts.docsOnly = true;
590
+ }
576
591
  // A4:平台指针 / 显式 --spec-dir / 本地 fallback 三合一解析 specBase——平台模式(指针指向外部
577
592
  // specDir)下 gate 也能核验(原只传 cwd 致 runGate 的 resolveSpecDir 指向本地孤儿库,恒
578
593
  // 「无法核验」exit 2)。resolvePlatformSpecDir 指针失效时抛 PointerUnreachableError,由顶层 catch 优雅 fail-closed。
@@ -597,6 +612,41 @@ async function main() {
597
612
  } catch (e) {
598
613
  console.warn(`⚠️ gate ctx 构造失败,降级单仓核验(${e.message})`);
599
614
  }
615
+ // ── gate last(2026-10-06-verify-friction-fix task-02)── 读 verify-runs 稳定锚点:
616
+ // writeVerifyGatePointer(quick-D)只写不读,看上轮 blocked 原因只能重跑全量 gate
617
+ // (postmortem 实证 --json 超时转后台 + task id 抄错绕 3 轮)或手翻两层 JSON。此处
618
+ // 直读指针 + 其指向取证目录的 reconcile/probe-consistency 明细;exit code 反映是否存在
619
+ // 阻断(无记录/未阻断=0,blocked=1),只读不写不重跑。
620
+ if (gateStage === 'last') {
621
+ const { summarizeVerifyGatePointer } = await import('./machine-interface.js');
622
+ const summary = summarizeVerifyGatePointer({ specBase: gateSpecBase || gateOpts.specBase || dir, changeName: gateChange });
623
+ if (json) {
624
+ console.log(JSON.stringify(summary, null, 2));
625
+ } else if (!summary.found) {
626
+ console.log(`📭 无 gate-last 记录(${summary.pointerPath})——该变更尚未发生 verify --done 阻断落锚。`);
627
+ } else if (summary.unreadable) {
628
+ console.error(`❌ gate-last 指针不可读: ${summary.pointerPath}(${summary.error || 'JSON 损坏'})——删除该文件后重跑一次 verify --done 重建锚点。`);
629
+ } else {
630
+ console.log(`📌 gate-last [${gateChange}] blocked=${summary.blocked}${summary.note ? `(${summary.note})` : ''} written_at=${summary.written_at || '?'}`);
631
+ console.log(` 最新取证目录: ${summary.run_dir_resolved || summary.latest_run_dir || '?'}`);
632
+ if (summary.reconcile) {
633
+ const rc = summary.reconcile;
634
+ console.log(` target_files 对账: status=${rc.status} missing=${(rc.missing || []).length} undeclared=${(rc.undeclared || []).length} matched=${(rc.matched || []).length}`);
635
+ for (const m of (rc.missing || []).slice(0, 10)) console.log(` ✗ missing: ${typeof m === 'string' ? m : JSON.stringify(m)}`);
636
+ for (const u of (rc.undeclared || []).slice(0, 10)) console.log(` ⚠ undeclared: ${typeof u === 'string' ? u : JSON.stringify(u)}`);
637
+ if ((rc.missing || []).length > 10) console.log(` …另有 ${rc.missing.length - 10} 条 missing,直读 ${summary.run_dir_resolved || summary.latest_run_dir}`);
638
+ }
639
+ if (summary.probeConsistency) {
640
+ const pc = summary.probeConsistency;
641
+ const mm = pc.mismatches || [];
642
+ console.log(` 探针一致性抽查: status=${pc.status} severity=${pc.result_severity || pc.severity || '?'} mismatches=${mm.length}`);
643
+ for (const m of mm.slice(0, 10)) console.log(` ✗ 探针${m && m.probe ? m.probe : '?'}: ${(m && (m.note || m.expected)) ? String(m.note || m.expected) : JSON.stringify(m)}`);
644
+ if (pc.skip_reason) console.log(` skip: ${pc.skip_reason}`);
645
+ }
646
+ }
647
+ process.exitCode = summary.found && !summary.unreadable && summary.blocked ? 1 : 0;
648
+ break;
649
+ }
600
650
  const { envelope, exitCode } = await withJsonOutput(json, () => runGate(gateStage, gateChange, gateOpts));
601
651
  if (json) {
602
652
  process.stdout.write(JSON.stringify(envelope));
@@ -877,6 +927,11 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
877
927
  }
878
928
  }
879
929
  } catch { /* 注入 best-effort */ }
930
+ } else if (taskSub === 'tick') {
931
+ // ── 轻量勾选动词(2026-10-03-voluntary-task-tick):翻格+进度回显+下一任务指针——
932
+ // 纯反馈闭环(watcher task-done 事件自动上平台),自愿使用;幂等,无 review/verdict 仪式。──
933
+ const { runTaskTick } = await import('./task-tick.js')
934
+ await runTaskTick({ changeName: tChange, cwd: dir, taskId: tTask, specBase: tSpecBase })
880
935
  } else if (taskSub === 'done') {
881
936
  // ── task done 四合一(r5l-forensic-verdict 方案 1 / 评审护栏#3):13 任务 × 4 连收尾
882
937
  // 往返(review write+勾选 / finish / wt-commit)合并为单进程串行 + 合并输出——每次省
@@ -1262,16 +1317,24 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
1262
1317
  const vpForce = args.includes('--force');
1263
1318
  const vpDraft = args.includes('--draft');
1264
1319
  const vpAmendDraft = args.includes('--amend-draft');
1320
+ // --refresh-probes(2026-10-06-verify-friction-fix task-01):定向刷新通道——只刷新仍含
1321
+ // 待填占位的探针机械段(表格已填行按行携载),已手填段保留;与 --force 互斥(force 是
1322
+ // 显式全量重置通道,refresh 是保手填通道,并存语义矛盾拒跑)。
1323
+ const vpRefresh = args.includes('--refresh-probes');
1265
1324
  if (vpDraft && !vpInit) {
1266
1325
  console.error('❌ --draft 需与 --init 同用(draft 是骨架生成的机器起草模式)——sillyspec verify-probes --change <名> --init --draft');
1267
1326
  process.exit(2);
1268
1327
  }
1328
+ if (vpRefresh && vpForce) {
1329
+ console.error('❌ --refresh-probes 与 --force 互斥——refresh 只刷新未手填的探针段(保手填),force 是全量重置(自动备份后覆盖);两者并存语义矛盾。');
1330
+ process.exit(2);
1331
+ }
1269
1332
  if (!vpChange) {
1270
- console.error('用法: sillyspec verify-probes --change <name> [--init [--force]] [--draft] [--amend-draft] [--json] [--spec-dir <path>]\n 跑机械探针(TODO 标记/测试覆盖/API 契约/删除对账)输出 markdown;--init 生成 verify-result.md 骨架(探针预填,已存在不覆盖;--force 覆盖重生成全骨架——手填内容会重置,先备份);\n --init --draft 填槽模式(r5l 方案3):任务完成度/风险等级/测试结果/决策追踪四节机器预填完整句子+指纹防篡改,agent 只填三处 <!--AGENT:--> 槽(结论枚举/移交项/审查叙述);\n --amend-draft 留痕重锚:确要修改机器预填段时重算指纹并记 sidecar 审计(--done 篡改门禁的显式例外通道)');
1333
+ console.error('用法: sillyspec verify-probes --change <name> [--init [--force]] [--refresh-probes] [--draft] [--amend-draft] [--json] [--spec-dir <path>]\n 跑机械探针(TODO 标记/测试覆盖/API 契约/删除对账)输出 markdown;--init 生成 verify-result.md 骨架(探针预填,已存在不覆盖);--force 覆盖重生成全骨架(覆盖前自动备份到 .runtime/verify-runs/ 并打印路径);\n --refresh-probes 定向刷新:只替换仍含待填占位的探针预填段(表格已填行携载保留),已手填段不动——预填段过期但手填内容舍不得时的安全刷新通道(同样先自动备份);\n --init --draft 填槽模式(r5l 方案3):任务完成度/风险等级/测试结果/决策追踪四节机器预填完整句子+指纹防篡改,agent 只填三处 <!--AGENT:--> 槽(结论枚举/移交项/审查叙述);\n --amend-draft 留痕重锚:确要修改机器预填段时重算指纹并记 sidecar 审计(--done 篡改门禁的显式例外通道)');
1271
1334
  process.exit(2);
1272
1335
  }
1273
1336
  assertSafeChangeName(vpChange, '--change 变更名');
1274
- const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts, formatPlatformPathNote } = await import('./verify-probes.js');
1337
+ const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts, formatPlatformPathNote, backupVerifyResult, refreshProbeSections } = await import('./verify-probes.js');
1275
1338
  // spec 根统一走漂移锚定(坑 worktree-spec-artifact-misplace):在 worktree 内跑时锚回主仓,
1276
1339
  // 探针读取与 --init 骨架都落主仓——与 plan/execute/verify/archive 的 command.js 守卫同口径。
1277
1340
  // resolvePlatformSpecDir 仍先调(保留平台接管 fail-closed 检查副作用),但仅 pointer 存在时
@@ -1317,6 +1380,37 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
1317
1380
  break;
1318
1381
  }
1319
1382
  console.log(renderVerifyProbesReport(vpResult));
1383
+ // ── --refresh-probes 定向刷新(2026-10-06-verify-friction-fix task-01)── 独立于 --init:
1384
+ // 预填段过期但手填内容舍不得时的安全刷新通道。先自动备份,再只替换仍含待填占位的探针
1385
+ // 机械段(表格已填行按行携载),已手填段原样保留并逐段报告。刷新后 enricher 不在此跑
1386
+ // (那些是 --init 的幂等补段面,与探针段刷新正交;需要时再跑 --init,已存在 no-op 安全)。
1387
+ if (vpRefresh) {
1388
+ const vpReportPath = join(vpSpecBase, 'changes', vpChange, 'verify-result.md');
1389
+ if (!existsSync(vpReportPath)) {
1390
+ console.error(`❌ 无 verify-result.md 可刷新: ${vpReportPath}——先 sillyspec verify-probes --change ${vpChange} --init 生成骨架`);
1391
+ process.exit(2);
1392
+ }
1393
+ const { resolveRuntimeRoot: vpRefreshRTR } = await import('./run/shared.js');
1394
+ const vpBackupPath = backupVerifyResult({ mdPath: vpReportPath, runtimeRoot: vpRefreshRTR({}, vpSpecBase), changeName: vpChange });
1395
+ const vpOld = readFileSync(vpReportPath, 'utf8');
1396
+ const vpRef = refreshProbeSections(vpOld, renderVerifyProbesReport(vpResult));
1397
+ if (vpRef.text !== vpOld) {
1398
+ writeFileSync(vpReportPath, vpRef.text);
1399
+ await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpRef.text);
1400
+ }
1401
+ console.log(`\n🔄 --refresh-probes 定向刷新完成: ${vpReportPath}${vpPlatformNote}`);
1402
+ if (vpBackupPath) console.log(`🗄️ 刷新前已自动备份: ${vpBackupPath}`);
1403
+ const fmt = (arr) => arr.length > 0 ? arr.map(n => `探针${n}`).join('、') : '无';
1404
+ console.log(` 替换(仍含待填占位的机械段): ${fmt(vpRef.replaced)};保留(已手填/无占位,原样不动): ${fmt(vpRef.kept)};新注入: ${fmt(vpRef.appended)};表格已填行携载 ${vpRef.carriedRows} 行`);
1405
+ if (vpRef.kept.length > 0) {
1406
+ console.log(` 保留段如确需强刷(预填过期且确认无手填内容),用 --init --force(自动备份后全量重生成)。`);
1407
+ }
1408
+ try {
1409
+ const { triggerSync } = await import('./run/shared.js');
1410
+ triggerSync(dir, vpChange, { specRoot: vpSpecBase });
1411
+ } catch { /* 同步触发失败不阻断探针命令 */ }
1412
+ break;
1413
+ }
1320
1414
  if (vpInit) {
1321
1415
  const vpReportPath = join(vpSpecBase, 'changes', vpChange, 'verify-result.md');
1322
1416
  if (existsSync(vpReportPath)) {
@@ -1335,12 +1429,21 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
1335
1429
  await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpInjected);
1336
1430
  console.log(`\n📄 存量旧格式 verify-result.md 已补注入探针预填段: ${vpReportPath}(正文其余未动;补注后按新预填段如实核对结论)${vpPlatformNote}`);
1337
1431
  } else if (vpForce) {
1338
- // quick-B:--force 覆盖重生成全骨架(含接口矩阵等 CLI 预填段)——手填内容会重置,
1339
- // 醒目警告 + 备份指引(预填段过期/骨架仍是「无接口面」注记时的刷新通道)
1432
+ // quick-B:--force 覆盖重生成全骨架(含接口矩阵等 CLI 预填段)——手填内容会重置。
1433
+ // 2026-10-06-verify-friction-fix:覆盖前自动备份(postmortem 实证 38 格手填复核成果
1434
+ // 被清后只能靠对话记录重建——「从 git 或备份找回」的话术备份此前并不存在);备份
1435
+ // 失败时醒目提示手动复制再重跑(fail-soft 但不静默)。
1436
+ const { resolveRuntimeRoot: vpForceRTR } = await import('./run/shared.js');
1437
+ const vpBackupPath = backupVerifyResult({ mdPath: vpReportPath, runtimeRoot: vpForceRTR({}, vpSpecBase), changeName: vpChange });
1340
1438
  const vpSkeleton = generateVerifyResultSkeleton(vpResult);
1341
1439
  writeFileSync(vpReportPath, vpSkeleton);
1342
1440
  await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpSkeleton);
1343
- console.log(`\n📄 --force 已重生成 verify-result.md 骨架: ${vpReportPath}(探针与预填段已刷新;⚠️ 手填的结论/移交项等已被重置——需保留的内容从 git 或备份找回后回填)${vpPlatformNote}`);
1441
+ if (vpBackupPath) {
1442
+ console.log(`\n🗄️ --force 覆盖前已自动备份: ${vpBackupPath}(手填内容被重置时从这里找回回填)`);
1443
+ } else {
1444
+ console.error(`\n⚠️ 自动备份失败——若 verify-result.md 含手填内容,请立即手动复制保存后重跑本命令。`);
1445
+ }
1446
+ console.log(`\n📄 --force 已重生成 verify-result.md 骨架: ${vpReportPath}(探针与预填段已刷新;⚠️ 手填的结论/移交项等已被重置——需保留的内容从上方备份找回后回填)${vpPlatformNote}`);
1344
1447
  } else {
1345
1448
  console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}${vpPlatformNote}(预填段过期需刷新时用 --init --force)`);
1346
1449
  }
@@ -1620,13 +1723,35 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
1620
1723
  case 'module-impact': {
1621
1724
  // archive 模块影响矩阵骨架(2026-08-21 审计第三批 D3):文件×模块归属按 _module-map.yaml
1622
1725
  // paths 前缀匹配预填(机械),影响类型/review 标记留 <!--TODO-->(语义)。已存在不覆盖。
1726
+ // --fill-skipped(2026-10-07-flow-friction-batch3 task-04):pending 死信一键回填——verify
1727
+ // 硬拦「更新结果」pending/待办行后 agent 实测写一次性脚本批量回填,CLI 化(与门禁
1728
+ // extractPendingDocSyncRows 同源解析口径)。
1623
1729
  const miChangeIdx = args.indexOf('--change');
1624
1730
  const miChange = miChangeIdx >= 0 && args[miChangeIdx + 1] && !String(args[miChangeIdx + 1]).startsWith("--") ? args[miChangeIdx + 1] : null;
1625
1731
  if (!miChange) {
1626
- console.error('用法: sillyspec module-impact --change <name> [--json] [--spec-dir <path>]\n 生成 module-impact.md 骨架(模块影响矩阵按 module-map 预填 + 未匹配文件清单;已存在不覆盖)');
1732
+ console.error('用法: sillyspec module-impact --change <name> [--json] [--spec-dir <path>]\n 生成 module-impact.md 骨架(模块影响矩阵按 module-map 预填 + 未匹配文件清单;已存在不覆盖)\n sillyspec module-impact --change <name> --fill-skipped [--reason "<一句话原因>"]\n 「更新结果」表 pending/待办行一键回填 skipped(--reason 追加进操作列——对齐文件内规则「确定不同步的行改 skipped 并在操作列写明原因」);done 行与其余内容逐字不动,幂等');
1627
1733
  process.exit(2);
1628
1734
  }
1629
1735
  assertSafeChangeName(miChange, '--change 变更名');
1736
+ if (args.includes('--fill-skipped')) {
1737
+ const miSpecBase0 = resolvePlatformSpecDir(dir, specDir) || specDir || join(dir, '.sillyspec');
1738
+ const miPath0 = join(miSpecBase0, 'changes', miChange, 'module-impact.md');
1739
+ const miReasonIdx = args.indexOf('--reason');
1740
+ const miReason = miReasonIdx >= 0 && args[miReasonIdx + 1] && !String(args[miReasonIdx + 1]).startsWith("--") ? args[miReasonIdx + 1] : '';
1741
+ const { fillModuleImpactSkipped } = await import('./run/complete-handlers.js');
1742
+ const miRes = fillModuleImpactSkipped(miPath0, { reason: miReason });
1743
+ if (!miRes.ok) {
1744
+ console.error(`❌ ${miRes.error}`);
1745
+ process.exit(2);
1746
+ }
1747
+ if (miRes.filled === 0) {
1748
+ console.log(`ℹ️ 无 pending/待办行可回填(幂等零改动): ${miPath0}`);
1749
+ } else {
1750
+ console.log(`✅ 已回填 ${miRes.filled} 行 pending/待办 → skipped${miReason ? `(操作列追加——skipped:${miReason})` : '(未给 --reason 只翻状态——建议补真实原因)'}: ${miPath0}`);
1751
+ console.log(` verify 门禁(extractPendingDocSyncRows 同源口径)将不再拦这些行;reason 是审计面,请确认真实。`);
1752
+ }
1753
+ break;
1754
+ }
1630
1755
  const { generateModuleImpactSkeleton } = await import('./module-impact.js');
1631
1756
  const miResult = generateModuleImpactSkeleton({ cwd: dir, changeName: miChange, specDir });
1632
1757
  if (!miResult) {
@@ -3088,7 +3213,7 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
3088
3213
  const reason = flag('--reason') || 'spec'
3089
3214
  const fail = (msg) => { console.error(`❌ ${msg}`); process.exitCode = 1 }
3090
3215
  // repair-paths/--redomain 自带用法门,不走 anchor/change 前置守卫
3091
- if (!anchor && !change && !has('--repair-paths') && !filteredArgs.slice(1).includes('repair-paths') && !has('--redomain')) { fail('用法: sillyspec tests --anchor <FR-…|ql-…> | --change <名> [--bind|--unbind] [--tests <p1,p2>] [--row-id <id>] [--reason spec|capability|regression] | repair-paths | --redomain --from <域> --to <域>'); break }
3216
+ if (!anchor && !change && !has('--repair-paths') && !filteredArgs.slice(1).includes('repair-paths') && !has('--redomain')) { fail('用法: sillyspec tests --anchor <FR-…|ql-…> | --change <名> [--bind|--unbind] [--tests <p1,p2>] [--row-id <id>] [--reason spec|capability|regression] | repair-paths | --redomain --from <域> --to <域>\n 语义:--bind 追加一条新绑定行(不替换旧行;row_id 缺省自动生成 manual:*);--unbind 按 --row-id <id> 删行(或 --tests 路径反查);不带 bind/unbind 只读展示'); break }
3092
3217
  // 变更期局部锚(FR-NN 未铸全局)只读拒绝修改——修理工仅面向提升后行
3093
3218
  const isLocalAnchor = anchor && /^FR-\d+$/.test(anchor)
3094
3219
  if ((has('--bind') || has('--unbind')) && isLocalAnchor) { fail(`变更期局部锚「${anchor}」只读——修理工仅面向提升后的全局锚(FR-<域>-NNN / ql-…)`); break }
@@ -3162,22 +3287,28 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
3162
3287
  if (has('--redomain')) {
3163
3288
  const fromD = flag('--from')
3164
3289
  const toD = flag('--to')
3165
- if (!fromD || !toD) { fail('用法: sillyspec tests --redomain --from <源域> --to <目标域> [--anchor <FR-id>] [--write]'); break }
3290
+ // --by-change(坑 fr-domain-suggest-typo-and-no-split-migration 缺陷2):unmapped 等
3291
+ // 混合池按「变更:<名>」字段分批迁移——整域迁移会错置大部分条目,来源变更才是分批键。
3292
+ const byChangeD = flag('--by-change')
3293
+ if (!fromD || !toD) { fail('用法: sillyspec tests --redomain --from <源域> --to <目标域> [--anchor <FR-id>] [--by-change <变更名>] [--write]'); break }
3166
3294
  // 多 --anchor 收集
3167
3295
  const anchorList = []
3168
3296
  for (let i = 0; i < filteredArgs.length - 1; i++) if (filteredArgs[i] === '--anchor') anchorList.push(filteredArgs[i + 1])
3169
3297
  try {
3170
3298
  const { planRedomain, redomainFrEntries } = await import('./redomain.js')
3171
3299
  if (!has('--write')) {
3172
- const plan = planRedomain({ knowledgeRoot, from: fromD, to: toD, anchors: anchorList.length ? anchorList : null })
3173
- console.log(`🔧 域迁移预览(--write 落盘):fr/${fromD}.md → fr/${toD}.md,${plan.entries.length} 条`)
3300
+ // 预览与落盘同口径透传 byChange(坑 redomain-plan-preview-by-change-ignored,
3301
+ // 2026-10-02 实证:76338b4d 收编在途工作时此行漏透传——预览列整域、落盘只迁
3302
+ // 分批子集,用户按预览理解会误判整域迁移)。planRedomain 侧过滤本就在场。
3303
+ const plan = planRedomain({ knowledgeRoot, from: fromD, to: toD, anchors: anchorList.length ? anchorList : null, byChange: byChangeD || null })
3304
+ console.log(`🔧 域迁移预览(--write 落盘):fr/${fromD}.md → fr/${toD}.md,${plan.entries.length} 条${byChangeD ? `(仅「变更:${byChangeD}」)` : ''}`)
3174
3305
  for (const e of plan.entries.slice(0, 10)) console.log(` ${e.id} ${e.title}`)
3175
3306
  if (plan.entries.length > 10) console.log(` … 共 ${plan.entries.length} 条`)
3176
3307
  console.log(` 目标域文件${plan.targetExists ? '在场' : '将新建'};源域${plan.sourceWillDelete ? '将删空壳' : '保留余条'};ID 不变(身份保持)`)
3177
3308
  break
3178
3309
  }
3179
- const r = redomainFrEntries({ knowledgeRoot, from: fromD, to: toD, anchors: anchorList.length ? anchorList : null })
3180
- console.log(`✅ 域迁移完成:${r.moved.length} 条 fr/${fromD}.md → fr/${toD}.md(ID 不变)`)
3310
+ const r = redomainFrEntries({ knowledgeRoot, from: fromD, to: toD, anchors: anchorList.length ? anchorList : null, byChange: byChangeD || null })
3311
+ console.log(`✅ 域迁移完成:${r.moved.length} 条 fr/${fromD}.md → fr/${toD}.md(ID 不变${byChangeD ? `,仅「变更:${byChangeD}」` : ''})`)
3181
3312
  for (const m of r.moved.slice(0, 10)) console.log(` ${m.id} ${m.title}`)
3182
3313
  if (r.sourceDeleted) console.log(` 源域空壳已删:fr/${fromD}.md`)
3183
3314
  if (r.targetCreated) console.log(` 目标域文件新建:fr/${toD}.md`)
@@ -4355,6 +4486,9 @@ SillySpec taskcard — 生成 Windows 安全的 TaskCard 骨架
4355
4486
  用法:
4356
4487
  sillyspec taskcard <change-name> --task task-01[,task-02...] [--title <t>] [--title-zh <t>] [--set key=value]... [--force]
4357
4488
  sillyspec taskcard <change-name> --all [--set key=value]... [--force]
4489
+ sillyspec taskcard <change-name> --validate
4490
+ 校验全部任务卡(plan 门禁同源规则:frontmatter YAML 合法性含分诊、必要字段、占位符、
4491
+ target_files 严格形态),失败 exit 1——填卡后先自检,免 plan Step 4 门禁批量报错返工。
4358
4492
 
4359
4493
  骨架由 CLI 直写(LF 行尾 + frontmatter 闭合 + 硬校验 9 字段齐全),标题自动取自 tasks.md
4360
4494
  checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";--set 批量注入 design
@@ -4394,6 +4528,35 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
4394
4528
  console.error('❌ --all 与 --task 互斥(--all 取 plan.md 全部任务,--task 显式指定)');
4395
4529
  process.exit(1);
4396
4530
  }
4531
+ // ── --validate(2026-10-06-verify-friction-fix task-03)── plan 门禁同源规则的零成本自检:
4532
+ // 填卡后、plan Step 4 门禁前先跑,把「门禁时一次撞一批」前移成「提交前自检」(厚流程
4533
+ // 实证 6 轮 YAML 门禁试错)。规则同源 plan-postcheck(frontmatter 合法性/必要字段/占位符
4534
+ // /target_files 严格形态),失败 exit 1——此处报的错 --done 门禁同样会拦。
4535
+ if (filteredArgs.includes('--validate')) {
4536
+ if (all || taskVal) {
4537
+ console.error('❌ --validate 与 --all/--task 互斥(--validate 校验变更下全部任务卡)');
4538
+ process.exit(2);
4539
+ }
4540
+ const tcValSpecDir = resolvePlatformSpecDir(dir, specDir) || specDir;
4541
+ const tcValChangeDir = join(tcValSpecDir, 'changes', tcName);
4542
+ if (!existsSync(tcValChangeDir)) {
4543
+ console.error(`❌ 变更目录不存在: ${tcValChangeDir}`);
4544
+ process.exit(2);
4545
+ }
4546
+ const { validateTaskcardsCli } = await import('./stages/plan-postcheck.js');
4547
+ const tcVal = validateTaskcardsCli({ changeDir: tcValChangeDir, projectRoot: dir });
4548
+ if (tcVal.ok) {
4549
+ console.log(`✅ 任务卡校验通过(${tcVal.files} 张卡${tcVal.files === 0 ? '——无 tasks/ 目录(thin 变更无可校验卡)' : tcVal.warnings.length > 0 ? `,${tcVal.warnings.length} 条 warning(不阻断,供审查线索)` : ''})`);
4550
+ for (const w of tcVal.warnings) console.log(` ⚠ ${w}`);
4551
+ } else {
4552
+ console.error(`❌ 任务卡校验失败(${tcVal.files} 张卡,${tcVal.errors.length} 项 error):`);
4553
+ for (const e of tcVal.errors) console.error(` ✗ ${e}`);
4554
+ for (const w of tcVal.warnings) console.error(` ⚠ ${w}`);
4555
+ console.error(`修复后重跑: sillyspec taskcard ${tcName} --validate(这些错误在 plan Step 4 门禁同样会拦——同源规则)。`);
4556
+ process.exitCode = 1;
4557
+ }
4558
+ break;
4559
+ }
4397
4560
  if (!all && !taskVal) {
4398
4561
  console.error('❌ 用法: sillyspec taskcard <change-name> --task task-01[,task-02...] | --all [--force]');
4399
4562
  process.exit(1);
@@ -4552,9 +4715,12 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
4552
4715
  break;
4553
4716
  }
4554
4717
  case 'flow': {
4555
- // R7 切片二:2-调用轻量协议命令族(start/done/amend-draft)——协议必需交互=2
4718
+ // R7 切片二:2-调用轻量协议命令族(start/done/amend-draft)——协议必需交互=2。
4719
+ // --json 全局透传(2026-10-06-flow-status-json):该 flag 在上方全局解析段被剥出
4720
+ // filteredArgs,flow 族经此收到 {json} 才在真实入口下生效(status 机器可读输出 /
4721
+ // start JSON 信封同门,与 gate/derive 族「index 消费全局 flag、显式传子命令」同款形状)
4556
4722
  const flowMod = await import('./flow.js');
4557
- await flowMod.cmdFlow(filteredArgs.slice(1), dir, specDir);
4723
+ await flowMod.cmdFlow(filteredArgs.slice(1), dir, specDir, { json });
4558
4724
  break;
4559
4725
  }
4560
4726
  case 'workflow': {
@@ -4975,6 +5141,7 @@ SillySpec config — local.yaml 配置键速查
4975
5141
  'scan-runs': '平台模式 scan/workflow 取证目录',
4976
5142
  'artifacts': '派生产物目录(init 预建)',
4977
5143
  'contract-artifacts': 'execute endpoint 契约产物(verify 阶段读取)',
5144
+ 'verify-runs': 'verify --done 门禁取证目录(reconcile-result.json / probe-consistency-result.json 明细 + gate-last-<change>.json 稳定锚点;sillyspec gate last --change <名> 直读锚点与 blocked 明细,verify-result-backup-*.md 是 --force/--refresh-probes 的自动备份)',
4978
5145
  'history': '历史记录目录(init 预建)',
4979
5146
  'logs': '日志目录(init 预建)',
4980
5147
  'templates': '工作流模板目录(init 预建)',
@@ -391,7 +391,9 @@ export async function cmdKnowledgeClassify(dir, args, opts = {}) {
391
391
  export async function cmdKnowledgeInbox(dir, args, opts = {}) {
392
392
  const base = opts.specDir || join(dir, '.sillyspec')
393
393
  const knowledgeDir = join(base, 'knowledge')
394
- const wantJson = args.includes('--json')
394
+ // --json 双路判定(对齐 stages/knowledge.js digest 惯例):全局旗标被 index.js 顶层吞进
395
+ // opts.json(filteredArgs 不含 --json),args.includes 仅兜底直调/测试场景
396
+ const wantJson = args.includes('--json') || opts.json === true
395
397
  const uncPath = join(knowledgeDir, 'uncategorized.md')
396
398
  let entries = []
397
399
  if (existsSync(uncPath)) {
@@ -2,15 +2,15 @@
2
2
  * knowledge-digest.js — 知识资产治理信号收集器(2026-09-27-knowledge-digest)
3
3
  *
4
4
  * 现状:通用知识走 propose→平台人工合并闭环,规格资产(FR 索引/绑定)全自动入库零人审——
5
- * 四类信号沉睡库内无人工出口(R23-full 载体仓收件箱积压 40 条实证)。本模块把沉睡数据
5
+ * 信号沉睡库内无人工出口(R23-full 载体仓收件箱积压 40 条实证)。本模块把沉睡数据
6
6
  * 变结构化摘要:`sillyspec knowledge digest`(人读文本)/ `--json`(平台 RPC 消费)。
7
7
  *
8
- * 四信号(阈值内静默、超阈才进摘要——安静即健康态,防仪式化):
9
- * ① rot 待复核标记:knowledge/fr/*.md 条目内「待复核:」行,按域计数(阈值 100)
10
- * ② 收件箱积压:knowledge/uncategorized.md 的 `## <qlId> | <标题>` 条目(阈值 20)
11
- * ③ 伪域落库:auto-* 域文件条目数 + unmapped 池条目数(阈值 0——伪域本不该增长)
12
- * ④ 绑定路径解析失败:fr/*.md 绑定行经 resolveTestFileRel(与 repair-paths/门禁读侧
8
+ * 三信号(阈值内静默、超阈才进摘要——安静即健康态,防仪式化):
9
+ * ① 收件箱积压:knowledge/uncategorized.md 的 `## <qlId> | <标题>` 条目(阈值 20)
10
+ * ② 伪域落库:auto-* 域文件条目数 + unmapped 池条目数(阈值 0——伪域本不该增长)
11
+ * ③ 绑定路径解析失败:fr/*.md 绑定行经 resolveTestFileRel(与 repair-paths/门禁读侧
13
12
  * 同口径单源)无法自仓根解析(阈值 0)
13
+ * (rot 待复核标记信号已随标记层拆除——2026-09-29-rot-retire-inject-cap:471 条零消费实证)
14
14
  *
15
15
  * 只读扫描,零写入——动作(迁移/清账/repair)由消费方另行执行。
16
16
  */
@@ -21,8 +21,50 @@ import { readFrBindings, resolveTestFileRel, testAnchorFile } from './test-bindi
21
21
  /** 伪域判定:auto- 前缀(scaffold 时代机器蒸馏域)或 unmapped 池 */
22
22
  const isPseudoDomain = (domain) => /^auto[-_]/.test(domain) || domain === 'unmapped'
23
23
 
24
- /** 交付路径 → 建议域(落域机械改进的建议器,与 indexRequirements 的告警共用单源) */
25
- export function suggestDomainFromFiles(files) {
24
+ /** 编辑距离 ≤1 判定(短域串专用,单遍扫描) */
25
+ function levenshteinAtMost1(a, b) {
26
+ if (a === b) return true
27
+ const [s, l] = a.length <= b.length ? [a, b] : [b, a]
28
+ if (l.length - s.length > 1) return false
29
+ let i = 0, j = 0, diff = 0
30
+ while (i < s.length && j < l.length) {
31
+ if (s[i] === l[j]) { i++; j++; continue }
32
+ if (++diff > 1) return false
33
+ if (s.length === l.length) { i++; j++ } else { j++ } // 等长=替换;差一=长串跳一格
34
+ }
35
+ return true // 尾部剩余 ≤1 字符差
36
+ }
37
+
38
+ /**
39
+ * 域词典守卫(坑 fr-domain-suggest-typo-and-no-split-migration 缺陷1,2026-09-28 生产实证
40
+ * auto-rontend):候选域与既有域(fr/*.md 文件名 = 真域+伪域全集)编辑距离 ≤1 → 判拼写
41
+ * 漂移,吸附返回既有域名;已在典内 / 远距离(绿地新模块)/ 无词典 → null(不干预)。
42
+ * 域路由与建议器共用(pseudoDomainFromPaths / suggestDomainFromFiles)——任一上游路径
43
+ * 形态再出 mangle,伪域也不会凭空铸造拼写错域。
44
+ */
45
+ export function snapDomainToDictionary(candidate, { knowledgeRoot } = {}) {
46
+ if (!candidate || !knowledgeRoot) return null
47
+ let names = []
48
+ try {
49
+ names = readdirSync(join(knowledgeRoot, 'fr')).filter((f) => f.endsWith('.md')).map((f) => f.slice(0, -3))
50
+ } catch { /* fr 目录缺席=无词典,不干预 */ }
51
+ for (const n of names) {
52
+ if (n === candidate) return null
53
+ if (Math.abs(n.length - candidate.length) > 1) continue
54
+ if (levenshteinAtMost1(n, candidate)) return n
55
+ }
56
+ return null
57
+ }
58
+
59
+ /** 交付路径 → 建议域(落域机械改进的建议器,与 indexRequirements 的告警共用单源)。
60
+ * 产出过词典守卫(拼写漂移吸附,坑 fr-domain-suggest 缺陷1)。 */
61
+ export function suggestDomainFromFiles(files, { knowledgeRoot } = {}) {
62
+ const raw = suggestDomainFromFilesRaw(files)
63
+ if (raw == null) return null
64
+ return snapDomainToDictionary(raw, { knowledgeRoot }) || raw
65
+ }
66
+
67
+ function suggestDomainFromFilesRaw(files) {
26
68
  for (const raw of files || []) {
27
69
  const p = String(raw || '').replace(/\\/g, '/')
28
70
  let m
@@ -36,17 +78,12 @@ export function suggestDomainFromFiles(files) {
36
78
  return null
37
79
  }
38
80
 
39
- /** 条目级解析:fr/<域>.md 的 `## <id> <标题>` 段头与「待复核:」行 */
81
+ /** 条目级解析:fr/<域>.md 的 `## <id> <标题>` 段头(rot 待复核行解析已随标记层拆除,2026-09-29-rot-retire-inject-cap) */
40
82
  function scanFrFile(text) {
41
83
  const entries = []
42
- let cur = null
43
84
  for (const line of text.split(/\r?\n/)) {
44
85
  const h = /^## (FR-[\w.-]+-\d+|FR-\d+)\s*(.*)$/.exec(line)
45
- if (h) { cur = { id: h[1], needsReview: null }; entries.push(cur); continue }
46
- if (cur) {
47
- const r = /^待复核:(.+)$/.exec(line)
48
- if (r) cur.needsReview = r[1].trim()
49
- }
86
+ if (h) entries.push({ id: h[1] })
50
87
  }
51
88
  return entries
52
89
  }
@@ -59,9 +96,8 @@ export function collectKnowledgeDigest({ specBase, projectRoot }) {
59
96
  const knowledgeRoot = join(specBase, 'knowledge')
60
97
  const frDir = join(knowledgeRoot, 'fr')
61
98
  const signals = []
62
- const totals = { rot: 0, inbox: 0, pseudo: 0, unresolvedBindings: 0 }
99
+ const totals = { inbox: 0, pseudo: 0, unresolvedBindings: 0 }
63
100
 
64
- const rotByDomain = new Map()
65
101
  const pseudoByDomain = new Map()
66
102
  const unresolved = []
67
103
  const frFiles = existsSync(frDir) ? readdirSync(frDir).filter(f => f.endsWith('.md')).sort() : []
@@ -77,8 +113,6 @@ export function collectKnowledgeDigest({ specBase, projectRoot }) {
77
113
  const domain = f.replace(/\.md$/, '')
78
114
  const text = readFileSync(join(frDir, f), 'utf8')
79
115
  const entries = scanFrFile(text)
80
- const rot = entries.filter(e => e.needsReview).length
81
- if (rot > 0) { rotByDomain.set(domain, rot); totals.rot += rot }
82
116
  if (isPseudoDomain(domain) && entries.length > 0) {
83
117
  // 基线内的 unmapped 池不计(历史跨仓基线,非新增堆积);auto-* 伪域无基线语义恒计
84
118
  const count = domain === 'unmapped' ? Math.max(0, entries.length - unmappedBaseline) : entries.length
@@ -114,11 +148,6 @@ export function collectKnowledgeDigest({ specBase, projectRoot }) {
114
148
  const mk = (kind, level, title, count, detail, suggestion) =>
115
149
  signals.push({ kind, level, title, count, detail, ...(suggestion ? { suggestion } : {}) })
116
150
 
117
- if (totals.rot > 100) {
118
- mk('rot', 'warn', `rot 待复核批量标记(${totals.rot} 条 > 100)`, totals.rot,
119
- [...rotByDomain.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([d, n]) => `${d} ${n}`).join('、'),
120
- '批量复核:文案漂移类可批量承接,行为类逐条对账')
121
- }
122
151
  if (totals.inbox > 20) {
123
152
  mk('inbox', 'warn', `知识收件箱积压(${totals.inbox} 条 > 20)`, totals.inbox,
124
153
  inboxTitles.slice(0, 5).join(';') + (inboxTitles.length > 5 ? ' 等' : ''),
@@ -148,7 +177,7 @@ export function renderKnowledgeDigestText(d) {
148
177
  const lines = []
149
178
  lines.push(`📬 知识资产治理摘要(${d.generated_at.slice(0, 16).replace('T', ' ')})`)
150
179
  if (d.healthy) {
151
- lines.push('✅ 四类信号全部在阈内(rot/inbox/伪域/坏绑定)——安静即健康态,无需动作')
180
+ lines.push('✅ 三类信号全部在阈内(inbox/伪域/坏绑定)——安静即健康态,无需动作')
152
181
  } else {
153
182
  lines.push(`⚠️ ${d.signals.length} 类信号超阈:`)
154
183
  for (const s of d.signals) {
@@ -157,6 +186,6 @@ export function renderKnowledgeDigestText(d) {
157
186
  if (s.suggestion) lines.push(` 处置:${s.suggestion}`)
158
187
  }
159
188
  }
160
- lines.push(`底数:rot 待复核 ${d.totals.rot}|收件箱 ${d.totals.inbox}|伪域条目 ${d.totals.pseudo}|坏绑定 ${d.totals.unresolvedBindings}`)
189
+ lines.push(`底数:收件箱 ${d.totals.inbox}|伪域条目 ${d.totals.pseudo}|坏绑定 ${d.totals.unresolvedBindings}`)
161
190
  return lines.join('\n')
162
191
  }