sillyspec 3.28.0 → 3.28.3

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 (60) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -93
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -47
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -214
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -270
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -87
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -70
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -64
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -175
  11. package/package.json +1 -1
  12. package/src/archive-delta.js +462 -0
  13. package/src/constants.js +6 -0
  14. package/src/design-facts.js +418 -0
  15. package/src/dispatch/backends/sillyhub-mcp.js +17 -5
  16. package/src/docs-check.js +197 -34
  17. package/src/docs-migrate.js +124 -0
  18. package/src/doctor-diagnostics.js +11 -2
  19. package/src/endpoint-baseline.js +109 -0
  20. package/src/git-helper.js +31 -0
  21. package/src/index.js +381 -22
  22. package/src/modules.js +41 -12
  23. package/src/progress/change-registry.js +39 -0
  24. package/src/progress/consistency-doctor.js +124 -5
  25. package/src/progress/stage-machine.js +13 -2
  26. package/src/progress.js +5 -0
  27. package/src/quicklog.js +51 -0
  28. package/src/run/command.js +199 -5
  29. package/src/run/complete-handlers.js +60 -4
  30. package/src/run/complete.js +105 -6
  31. package/src/run/gates.js +353 -2
  32. package/src/run/progress-confirm.js +66 -0
  33. package/src/run/prompt.js +193 -15
  34. package/src/run/quick-audit.js +12 -5
  35. package/src/run/scan-profile.js +117 -7
  36. package/src/run/shared.js +22 -6
  37. package/src/run/stage.js +14 -1
  38. package/src/scan-diff.js +54 -1
  39. package/src/scan-facts.js +173 -0
  40. package/src/scan-postcheck.js +114 -3
  41. package/src/spec-sync.js +19 -2
  42. package/src/stage-review.js +62 -3
  43. package/src/stages/archive.js +5 -5
  44. package/src/stages/brainstorm-auto.js +2 -1
  45. package/src/stages/brainstorm.js +12 -20
  46. package/src/stages/execute.js +19 -21
  47. package/src/stages/plan-postcheck.js +194 -1
  48. package/src/stages/plan.js +8 -3
  49. package/src/stages/quick.js +4 -2
  50. package/src/stages/scan.js +44 -102
  51. package/src/stages/verify.js +18 -18
  52. package/src/task-review.js +200 -0
  53. package/src/taskcard.js +8 -1
  54. package/src/verify-postcheck.js +771 -29
  55. package/src/verify-probes.js +89 -10
  56. package/src/workflow.js +30 -0
  57. package/templates/agents-instruction.md +1 -1
  58. package/templates/prompts/taskcard-rules.md +5 -0
  59. package/templates/workflows/scan-docs.yaml +6 -0
  60. package/.claude/skills/verify-per-user/SKILL.md +0 -6
package/src/git-helper.js CHANGED
@@ -13,6 +13,8 @@
13
13
  * safeGit 额外支持 opts.retryOnTimeout:ETIMEDOUT(机器忙瞬时抖动)时用 2× timeout 重试一次。
14
14
  */
15
15
  import { execFileSync } from 'node:child_process'
16
+ import { existsSync, readdirSync } from 'node:fs'
17
+ import { join } from 'node:path'
16
18
 
17
19
  /**
18
20
  * git C 风格引号路径解码:`\\` `\"` `\n` `\t` 与 `\NNN` 八进制**原始字节**。
@@ -148,3 +150,32 @@ export function gitQuiet(cwd, args, opts = {}) {
148
150
  return null
149
151
  }
150
152
  }
153
+
154
+ /**
155
+ * 归档产物机械暂存(2026-09-05 全流程审计:原 archive 步骤 6 的 3 条 git add agent 指令 CLI 化)。
156
+ * 固定路径:changes/archive/(归档产物)、knowledge/decisions/(决策提炼)、docs/<project>/modules/
157
+ * (本次同步的模块文档——按已存在目录枚举,未变的目录 git add 是无害 no-op)。
158
+ * 只 add 不 commit(统一提交工具处理);平台模式由调用方跳过(specRoot 在源码仓外)。
159
+ * @param {string} cwd 源码项目根
160
+ * @returns {{ staged: string[], skipped: string[] }}
161
+ */
162
+ export function stageArchiveArtifacts(cwd) {
163
+ const specBase = join(cwd, '.sillyspec')
164
+ const candidates = [join(specBase, 'changes', 'archive'), join(specBase, 'knowledge', 'decisions')]
165
+ const docsRoot = join(specBase, 'docs')
166
+ try {
167
+ for (const e of readdirSync(docsRoot, { withFileTypes: true })) {
168
+ if (e.isDirectory()) candidates.push(join(docsRoot, e.name, 'modules'))
169
+ }
170
+ } catch { /* 无 docs 目录 → 跳过 */ }
171
+ const staged = []
172
+ const skipped = []
173
+ const existing = candidates.filter(p => existsSync(p))
174
+ if (existing.length === 0) return { staged, skipped }
175
+ const r = safeGit(cwd, ['add', ...existing])
176
+ if (r && r.error) {
177
+ console.warn(` ⚠️ 归档产物 git add 失败(可手动处理):${String(r.error).slice(0, 120)}`)
178
+ return { staged, skipped: existing }
179
+ }
180
+ return { staged: existing, skipped }
181
+ }
package/src/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
10
10
  import { writeAtomicSync } from './fs-atomic.js';
11
- import { basename, dirname, extname, join, resolve, isAbsolute } from 'path';
11
+ import { basename, dirname, extname, join, resolve, isAbsolute, sep } from 'path';
12
12
  import { safeGit, git } from './git-helper.js';
13
13
  import { getVersion } from './version.js';
14
14
 
@@ -93,12 +93,18 @@ SillySpec CLI — 规范驱动开发工具包
93
93
  sillyspec gate <stage> --change <name> [--json] 机器门控:阶段能否标记完成(只读)
94
94
  sillyspec derive <facet> --change <name> [--json] 单项事实核验(facet: execute-evidence|verify-test|task-reviews|artifacts)
95
95
  sillyspec backfill-reviews --change <name> [--adopt] [--json] 缺 review.json 的 task 生成草稿;--adopt 重算代填已有 review 的 base/head 等机械字段(verdict 保留)
96
+ sillyspec review write --change <name> --task task-NN --spec <verdict> --quality <verdict>
97
+ [--notes ...] [--evidence ...] [--changed-files a,b] [--force]
98
+ 单 task review.json 命令式写入:verdict/notes 你给,base/head/changedFiles/diffPaths CLI 代算
96
99
  sillyspec symbol-impact --change <name> 生成 symbol-impact.md 逐 task <!--TODO--> 骨架(gate 拒绝未替换占位,防骨架直接过门)
100
+ sillyspec design-init --change <name> [--force] 从 decisions.md 生成 design.md 十三章节骨架(决策追踪表预填;已存在不覆盖)
101
+ sillyspec delta --change <name> [--json] 生成变更 delta.md(Before/Delta/After 三段式聚合;幂等覆盖重跑即刷新)
97
102
  sillyspec next 项目状态探测:输出当前状态 + 下一步命令 + 依据(吸收 continue/resume 手工探测表)
98
103
  sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message(只建议不执行)
99
104
  sillyspec verify-probes --change <name> [--init] verify 机械探针(TODO 标记/测试覆盖/API 对账/删除对账);--init 生成 verify-result.md 骨架
100
105
  sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
101
106
  sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <dir>|--files <a.py,b.js>] 静态扫描路由装饰器生成 endpoints.json
107
+ sillyspec endpoints baseline --change <name> [--spec-dir <path>] [--json] 拍变更前端点基线(幂等不覆盖;worktree 内跑自动锚主仓;归档 delta 端点增删 before 侧)
102
108
  sillyspec scan-fix-headers [--project <名>] scan 文档补 author/created_at header(幂等)
103
109
  sillyspec plan-adopt-waves --change <名> [--dry-run] plan.md Wave 段一键重排为 depends_on 拓扑分组(同步任务总表 W 列,幂等)
104
110
  sillyspec workspace add <名> <路径> [--role] [--repo] | remove <名> | status 多项目工作区登记与状态探测
@@ -562,6 +568,72 @@ async function main() {
562
568
  process.exitCode = exitCode;
563
569
  break;
564
570
  }
571
+ case 'review': {
572
+ // 单 task review.json 命令式写入(2026-09-07 noAI 主流程批次 T2-4):agent 只给判断字段
573
+ // (verdict/notes/evidence),mechanics(executeRunId/base/head/changedFiles/diffPaths)全由
574
+ // CLI 代算——消灭「手拼 JSON→gate 拒→重写」循环与手算 hash 错误面。
575
+ // sillyspec review write --change <名> --task task-NN --spec pass --quality pass
576
+ // [--notes "..."] [--evidence "..."](cannot_verify 必填)
577
+ // [--changed-files a,b](归属切片为空或纯验证任务时显式给出;""=空覆盖)
578
+ // [--base <rev> --head <rev>](无 worktree meta 时显式锡点)[--force]
579
+ const rwSub = filteredArgs[1];
580
+ if (rwSub !== 'write') {
581
+ console.error('用法: sillyspec review write --change <名> --task task-NN --spec <pass|fail|cannot_verify> --quality <verdict>\n [--notes "评审备注"] [--evidence "cannot_verify 证据说明"] [--changed-files a,b] [--base <rev> --head <rev>] [--force] [--json]\n 单 task review.json 命令式写入:verdict/notes 由你给,executeRunId/base/head/changedFiles/diffPaths 由 CLI 从 git + task 卡代算;已存在默认拒覆盖(--force 越过)');
582
+ process.exit(2);
583
+ }
584
+ const rwVal = (flag) => {
585
+ const i = filteredArgs.indexOf(flag);
586
+ return i >= 0 && filteredArgs[i + 1] ? filteredArgs[i + 1] : null;
587
+ };
588
+ const rwChange = rwVal('--change');
589
+ const rwTask = rwVal('--task');
590
+ const rwSpec = rwVal('--spec');
591
+ const rwQuality = rwVal('--quality');
592
+ const rwNotes = rwVal('--notes');
593
+ const rwEvidence = rwVal('--evidence');
594
+ const rwBase = rwVal('--base');
595
+ const rwHead = rwVal('--head');
596
+ const rwFilesIdx = filteredArgs.indexOf('--changed-files');
597
+ // --changed-files 存在即视为显式覆盖(含空串=纯验证任务零 diff 声明);不存在传 null 走归属切片
598
+ const rwFilesOverride = rwFilesIdx >= 0
599
+ ? (filteredArgs[rwFilesIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean)
600
+ : null;
601
+ if (!rwChange || !rwTask || !rwSpec || !rwQuality) {
602
+ console.error('用法: sillyspec review write --change <名> --task task-NN --spec <verdict> --quality <verdict> [--notes ...] [--evidence ...] [--changed-files a,b] [--base <rev> --head <rev>] [--force]');
603
+ process.exit(2);
604
+ }
605
+ assertSafeChangeName(rwChange, '--change 变更名');
606
+ const { writeTaskReview } = await import('./task-review.js');
607
+ const rwPlatformOpts = {};
608
+ const rwResolved = resolvePlatformOpts(dir, specDir);
609
+ if (rwResolved) {
610
+ rwPlatformOpts.specRoot = rwResolved.specRoot;
611
+ if (rwResolved.runtimeRoot) rwPlatformOpts.runtimeRoot = rwResolved.runtimeRoot;
612
+ }
613
+ const rwResult = await writeTaskReview({
614
+ changeName: rwChange, cwd: dir, taskId: rwTask,
615
+ specVerdict: rwSpec, qualityVerdict: rwQuality,
616
+ reviewerNotes: rwNotes || '',
617
+ requiredEvidence: rwEvidence ? [rwEvidence] : [],
618
+ baseOverride: rwBase, headOverride: rwHead,
619
+ changedFilesOverride: rwFilesOverride,
620
+ force: filteredArgs.includes('--force'),
621
+ platformOpts: rwPlatformOpts,
622
+ });
623
+ if (json) {
624
+ console.log(JSON.stringify({ ok: rwResult.ok, ...rwResult }, null, 2));
625
+ } else if (rwResult.ok) {
626
+ console.log(`✅ review.json 已写入:${rwResult.reviewPath}`);
627
+ console.log(` executeRunId: ${rwResult.executeRunId}`);
628
+ for (const w of rwResult.warnings) console.log(`⚠️ ${w}`);
629
+ } else {
630
+ console.error('❌ review write 失败:');
631
+ for (const e of rwResult.errors) console.error(` - ${e}`);
632
+ for (const w of rwResult.warnings) console.error(`⚠️ ${w}`);
633
+ process.exit(1);
634
+ }
635
+ break;
636
+ }
565
637
  case 'backfill-reviews': {
566
638
  // 手动补 task(reopen execute / 直接实现)缺 review.json 的官方入口(坑 verify-archive-flow-pitfalls 坑2)。
567
639
  // 复用 generateTaskReviewDrafts(execute --done 同源兜底,幂等、fail-open):据 git diff base..head + working-tree
@@ -833,7 +905,7 @@ async function main() {
833
905
  process.exit(2);
834
906
  }
835
907
  assertSafeChangeName(vpChange, '--change 变更名');
836
- const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase } = await import('./verify-probes.js');
908
+ const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts } = await import('./verify-probes.js');
837
909
  // spec 根统一走漂移锚定(坑 worktree-spec-artifact-misplace):在 worktree 内跑时锚回主仓,
838
910
  // 探针读取与 --init 骨架都落主仓——与 plan/execute/verify/archive 的 command.js 守卫同口径。
839
911
  // resolvePlatformSpecDir 仍先调(保留平台接管 fail-closed 检查副作用),但仅 pointer 存在时
@@ -851,11 +923,26 @@ async function main() {
851
923
  if (vpInit) {
852
924
  const vpReportPath = join(vpSpecBase, 'changes', vpChange, 'verify-result.md');
853
925
  if (existsSync(vpReportPath)) {
854
- console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}`);
926
+ const vpExisting = readFileSync(vpReportPath, 'utf8');
927
+ // 检测正则与提取侧(parseProbePrefillAnchors 的 /^#### 探针 (\d+)[::]/)对齐要求冒号——
928
+ // 对抗复审 BUG-1(2026-09-07):agent 手写无冒号走样标题 `#### 探针 1` 会被宽松检测误判
929
+ // 「已有子节」跳过注入,而提取侧不认 → 死路复活(--init no-op + gate error 无解)。
930
+ if (!/^#### 探针 \d+[::]/m.test(vpExisting)) {
931
+ // P1 修复(2026-09-07):存量旧格式(九章节)报告 + --init 曾是死路——facts 无条件刷新
932
+ // 而正文不补,checkProbeConsistency 判别子判「疑似 agent 删除预填段」error 阻断,而修复
933
+ // 指引「重跑 --init」对已存在报告是 no-op。修法:探针段是 CLI 拥有的机械段,检测缺失时
934
+ // 尾部补注入(正文其余不动),判别子自此有子节可对账。
935
+ const vpSection = `\n## 探针结果(CLI 机械预填,--init 补注入) [层:可复跑探针——gate 抽查防篡改]\n${renderVerifyProbesReport(vpResult)}\n`;
936
+ writeFileSync(vpReportPath, vpExisting.replace(/\s*$/, '\n') + vpSection);
937
+ console.log(`\n📄 存量旧格式 verify-result.md 已补注入探针预填段: ${vpReportPath}(正文其余未动;补注后按新预填段如实核对结论)`);
938
+ } else {
939
+ console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}`);
940
+ }
855
941
  } else {
856
942
  writeFileSync(vpReportPath, generateVerifyResultSkeleton(vpResult));
857
943
  console.log(`\n📄 已生成 verify-result.md 骨架: ${vpReportPath}(探针已预填;结论必须写明 PASS/FAIL,留待填会被 gate 判不过)`);
858
944
  }
945
+ writeVerifyFacts(join(vpSpecBase, 'changes', vpChange), vpResult, vpChange); // P3b:facts 底稿随 --init 刷新(骨架已存在也刷新,最近快照语义)
859
946
  }
860
947
  break;
861
948
  }
@@ -945,8 +1032,78 @@ async function main() {
945
1032
  // 产生大量假 missing):聚合模式——逐 task 卡提取,各自落 contract-artifacts/<change>/<task>/,
946
1033
  // 与 verifyApiParity 的聚合读侧(contract-artifacts/<change>/*/endpoints.json 全量并集)对齐。
947
1034
  const epSub = filteredArgs[1];
1035
+ // 子分发(变更 2026-09-07-endpoint-baseline task-02):baseline(变更前基线)+ extract(原样)。
1036
+ if (epSub === 'baseline') {
1037
+ // 端点基线(D-002 gap-1):变更起点全仓现扫端点集快照 → .runtime/endpoint-baselines/
1038
+ // <change>.json(幂等,已拍过不覆盖——首次跑即变更前状态,重跑保持原快照)。归档 delta
1039
+ // 端点增删的 before 侧;扫描/落盘在 endpoint-baseline.js 纯函数,此处只做参数解析 +
1040
+ // worktree 主仓锚定 + 输出。
1041
+ const ebChangeIdx = args.indexOf('--change');
1042
+ const ebChange = ebChangeIdx >= 0 && args[ebChangeIdx + 1] ? args[ebChangeIdx + 1] : null;
1043
+ if (!ebChange) {
1044
+ console.error('用法: sillyspec endpoints baseline --change <name> [--spec-dir <path>] [--json]\n 拍变更前端点基线(全仓现扫,幂等已拍过不覆盖)→ .runtime/endpoint-baselines/<change>.json;归档 delta 端点增删的 before 侧。\n execute worktree 内跑:扫描根与落点自动锚定主仓(变更前代码在主仓;worktree 内是交付态且其 .runtime 随清理丢失)');
1045
+ process.exit(2);
1046
+ }
1047
+ assertSafeChangeName(ebChange, '--change 变更名');
1048
+ // worktree 主仓锚定(D-002 gap-1 关键):cwd 在 execute worktree 内时——
1049
+ // ① 扫描 cwd 若保持 worktree:扫到的是 baseline 检出 + 本变更改动(非「变更前」端点集);
1050
+ // ② runtimeRoot 若随 cwd 落 worktree 副本 .runtime:基线随 worktree 清理静默丢失。
1051
+ // 判据同 resolveEffectiveDir(P1-1)/ worktree.js _resolveMainRepoRoot:--git-dir ≠
1052
+ // --git-common-dir 且非 submodule = linked worktree,common-dir(可能是相对路径,须相对
1053
+ // dir 绝对化)父目录即主仓根 → 扫描根与 spec 解析都锚过去(--spec-dir 显式指定仍优先)。
1054
+ let ebScanCwd = dir;
1055
+ let ebSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
1056
+ // --json 纪律(design §8):机器输出模式下人类提示走 stderr,stdout 只留最终 JSON
1057
+ const ebNote = (msg) => { (json ? console.error : console.log)(msg); };
1058
+ try {
1059
+ const gd = git(dir, ['rev-parse', '--git-dir']);
1060
+ const gcd = git(dir, ['rev-parse', '--git-common-dir']);
1061
+ const superProj = git(dir, ['rev-parse', '--show-superproject-working-tree']);
1062
+ if (gd && gcd && gd !== gcd && !superProj) {
1063
+ const ebMainRoot = dirname(resolve(dir, gcd));
1064
+ if (ebMainRoot !== dir && existsSync(ebMainRoot)) {
1065
+ ebNote(`⚠️ cwd 在 linked worktree 内,基线扫描根与落点已锚定主仓: ${ebMainRoot}(变更前代码在主仓;worktree 内是交付态且其 .runtime 随清理丢失,防基线静默失效)`);
1066
+ ebScanCwd = ebMainRoot;
1067
+ ebSpecBase = resolvePlatformSpecDir(ebMainRoot, specDir) || join(ebMainRoot, '.sillyspec');
1068
+ // 显式 --spec-dir 指向 worktree 副本(任意形态——手建目录/检出副本;下方 drift 只认
1069
+ // .runtime/worktrees 检出形态,覆盖不了手建副本)→ 重算结果仍在 worktree 内时锚回主仓
1070
+ if (!resolve(ebSpecBase).toLowerCase().startsWith(resolve(ebMainRoot).toLowerCase() + sep)) {
1071
+ ebNote(`⚠️ 显式 spec 命中 worktree 副本(${ebSpecBase}),基线落点一并锚定主仓: ${join(ebMainRoot, '.sillyspec')}`);
1072
+ ebSpecBase = join(ebMainRoot, '.sillyspec');
1073
+ }
1074
+ }
1075
+ }
1076
+ } catch { /* git 不可用/非仓库 → 不锚定,按原 cwd 继续 */ }
1077
+ // 终局兜底(同下方 extract 分支 detectWorktreeSpecDrift 用法,无论上面是否已锚定都跑:
1078
+ // --spec-dir 显式指向 worktree 副本 spec 时,上面重算 resolvePlatformSpecDir 仍原样
1079
+ // 返回该显式路径)——spec 命中副本 → 锚回主仓,防 runtimeRoot 落副本 .runtime。
1080
+ const ebDrift = detectWorktreeSpecDrift(ebSpecBase);
1081
+ if (ebDrift) {
1082
+ ebNote(`⚠️ 已自动锚定主仓 spec: ${ebDrift.mainSpecBase}(基线落主仓;当前 spec 命中 worktree 副本 ${ebDrift.changeName},流程继续)`);
1083
+ ebSpecBase = ebDrift.mainSpecBase;
1084
+ }
1085
+ const ebRuntimeRoot = (await import('./run/shared.js')).resolveRuntimeRoot({ specRoot: specDir }, ebSpecBase);
1086
+ const { captureEndpointBaseline } = await import('./endpoint-baseline.js');
1087
+ const ebResult = captureEndpointBaseline({ cwd: ebScanCwd, changeName: ebChange, runtimeRoot: ebRuntimeRoot });
1088
+ if (json) {
1089
+ console.log(JSON.stringify({ command: 'endpoints-baseline', change: ebChange, ok: !ebResult.error, ...ebResult }, null, 2));
1090
+ if (ebResult.error) process.exit(1);
1091
+ break;
1092
+ }
1093
+ if (ebResult.error) {
1094
+ console.error(`❌ 端点基线拍摄失败: ${ebResult.error}`);
1095
+ process.exit(1);
1096
+ }
1097
+ if (!ebResult.written) {
1098
+ console.log(`ℹ️ 端点基线已存在,跳过(幂等不覆盖): ${join(ebRuntimeRoot, 'endpoint-baselines', `${ebChange}.json`)}`);
1099
+ break;
1100
+ }
1101
+ console.log(`✅ 已拍变更前端点基线(${ebResult.count} 个端点)→ ${ebResult.path}`);
1102
+ console.log(' 幂等:已拍过不覆盖(重跑保持首次快照);归档 delta 用它计算端点增删(before 侧)。');
1103
+ break;
1104
+ }
948
1105
  if (epSub !== 'extract') {
949
- console.error('用法: sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <backendRoot> | --files <a.py,b.js...>] [--spec-dir <path>]\n 静态扫描后端路由装饰器生成 endpoints.json(FastAPI/Express/Spring);verify 探针 5 消费。\n --all-tasks:聚合逐 task 卡 allowed_paths 各自提取(对账口径与探针 5 聚合读侧一致,推荐)。\n execute worktree 模式:allowed_paths 自动按 worktree 根解析,产物恒落主仓 spec(坑 endpoints-extract-worktree-pitfalls)');
1106
+ console.error('用法: sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <backendRoot> | --files <a.py,b.js...>] [--spec-dir <path>]\n 静态扫描后端路由装饰器生成 endpoints.json(FastAPI/Express/Spring);verify 探针 5 消费。\n --all-tasks:聚合逐 task 卡 allowed_paths 各自提取(对账口径与探针 5 聚合读侧一致,推荐)。\n execute worktree 模式:allowed_paths 自动按 worktree 根解析,产物恒落主仓 spec(坑 endpoints-extract-worktree-pitfalls)\nsillyspec endpoints baseline --change <name> [--spec-dir <path>] [--json]\n 拍变更前端点基线(全仓现扫,幂等已拍过不覆盖)→ .runtime/endpoint-baselines/<change>.json;归档 delta 端点增删的 before 侧。worktree 内跑自动锚定主仓。');
950
1107
  process.exit(2);
951
1108
  }
952
1109
  const epChangeIdx = args.indexOf('--change');
@@ -1156,6 +1313,121 @@ async function main() {
1156
1313
  console.log(' 逐行替换 <!--TODO--> 为结论(无签名级变更也显式写「无」);gate 拒绝未替换的占位行。');
1157
1314
  break;
1158
1315
  }
1316
+ case 'design-init': {
1317
+ // design.md 十三章节骨架生成(change: 2026-09-07-ir-stage-p3c,task-02,FR-03):agent 此前
1318
+ // 从零手写整份设计文档,章节漏项到 stage-contract/plan 门禁才暴露。本命令从 decisions.md
1319
+ // 当前版本 D 条目预填决策追踪表,章节标题逐字取自 brainstorm Step 6 模板(R-03,渲染本体在
1320
+ // design-facts.js generateDesignSkeleton——本 case 只负责读盘/幂等保护/落盘)。已存在不覆盖
1321
+ // (幂等,agent 产出优先),--force 覆盖重生成。
1322
+ const diChangeIdx = args.indexOf('--change');
1323
+ const diChange = diChangeIdx >= 0 && args[diChangeIdx + 1] ? args[diChangeIdx + 1] : null;
1324
+ const diForce = args.includes('--force');
1325
+ if (!diChange) {
1326
+ console.error('用法: sillyspec design-init --change <name> [--force] [--json] [--spec-dir <path>]\n 从变更 decisions.md 生成 design.md 十三章节骨架(决策追踪表按当前版本 D 条目预填;已存在不覆盖,--force 覆盖)');
1327
+ process.exit(2);
1328
+ }
1329
+ assertSafeChangeName(diChange, '--change 变更名');
1330
+ const diSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
1331
+ const diChangeDir = join(diSpecBase, 'changes', diChange);
1332
+ const diDecisionsPath = join(diChangeDir, 'decisions.md');
1333
+ if (!existsSync(diDecisionsPath)) {
1334
+ console.error(`❌ decisions.md 不存在: ${diDecisionsPath}`);
1335
+ console.error(' 先跑 sillyspec run brainstorm 推进决策环节(形成技术决策落盘 decisions.md),再生成 design 骨架。');
1336
+ if (json) console.log(JSON.stringify({ command: 'design-init', change: diChange, ok: false, error: `decisions.md 不存在: ${diDecisionsPath}` }, null, 2));
1337
+ process.exit(1);
1338
+ }
1339
+ const { generateDesignSkeleton } = await import('./design-facts.js');
1340
+ // author/created_at 与 taskcard/scan-postcheck 同口径:git user.name(回退 unknown)+ 本地墙钟
1341
+ let diAuthor = 'unknown';
1342
+ try { diAuthor = git(dir, ['config', 'user.name']) || 'unknown' } catch { /* git 不可用 → unknown */ }
1343
+ const { nowWallClock } = await import('./datetime.js');
1344
+ const diSkeleton = generateDesignSkeleton({
1345
+ changeName: diChange,
1346
+ decisionsText: readFileSync(diDecisionsPath, 'utf8'),
1347
+ author: diAuthor,
1348
+ now: nowWallClock(),
1349
+ });
1350
+ const diDesignPath = join(diChangeDir, 'design.md');
1351
+ const diExisted = existsSync(diDesignPath);
1352
+ if (diExisted && !diForce) {
1353
+ if (json) {
1354
+ console.log(JSON.stringify({ command: 'design-init', change: diChange, ok: true, path: diDesignPath, written: false, reason: 'exists' }, null, 2));
1355
+ } else {
1356
+ console.log(`ℹ️ design.md 已存在,不覆盖: ${diDesignPath}(确要重新生成骨架加 --force)`);
1357
+ }
1358
+ break;
1359
+ }
1360
+ writeFileSync(diDesignPath, diSkeleton);
1361
+ if (json) {
1362
+ console.log(JSON.stringify({ command: 'design-init', change: diChange, ok: true, path: diDesignPath, written: true, overwritten: diExisted }, null, 2));
1363
+ break;
1364
+ }
1365
+ console.log(`✅ 已生成 design.md 骨架: ${diDesignPath}${diExisted ? '(--force 覆盖既有文件)' : ''}`);
1366
+ console.log(' 决策追踪表已按 decisions.md 当前版本 D 条目预填;回到 brainstorm Step 6(写设计文档并自审)逐节填散文,填毕删除生成注释。');
1367
+ break;
1368
+ }
1369
+ case 'delta': {
1370
+ // delta.md 生成(change: 2026-09-07-ir-stage-p3d,task-02):task-01 buildDeltaReport
1371
+ // 四源 fail-soft 聚合(reconcile/verify-facts/module-map/decisions)的 CLI 落盘入口——
1372
+ // 归档确认步(handleArchiveConfirmStep)自动生成失败时的手动补跑通道。幂等覆盖
1373
+ // (重跑即刷新):与 design-init「agent 产出优先不覆盖」语义相反——delta 是源数据
1374
+ // (verify-runs/module-map)时点快照,旧报告天然过期,重新生成即对齐。
1375
+ const dlChangeIdx = args.indexOf('--change');
1376
+ const dlChange = dlChangeIdx >= 0 && args[dlChangeIdx + 1] ? args[dlChangeIdx + 1] : null;
1377
+ if (!dlChange) {
1378
+ console.error('用法: sillyspec delta --change <name> [--spec-dir <path>] [--json]\n 生成变更 delta.md(Before/Delta/After 三段式;四源 fail-soft,缺源逐段降级注记;幂等覆盖重跑即刷新)');
1379
+ process.exit(2);
1380
+ }
1381
+ assertSafeChangeName(dlChange, '--change 变更名');
1382
+ const dlSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
1383
+ const dlChangeDir = join(dlSpecBase, 'changes', dlChange);
1384
+ if (!existsSync(dlChangeDir)) {
1385
+ console.error(`❌ 变更目录不存在: ${dlChangeDir}`);
1386
+ if (json) console.log(JSON.stringify({ command: 'delta', change: dlChange, ok: false, error: `变更目录不存在: ${dlChangeDir}` }, null, 2));
1387
+ process.exit(1);
1388
+ }
1389
+ // runtimeRoot 解析同 register-stage-review 先例(resolvePlatformOpts > resolveRuntimeRoot):
1390
+ // reconcile(verify-runs/<ts>/reconcile-result.json)与 apply-pathspec 兜底清单都在 runtime 下。
1391
+ const dlPlatformOpts = {};
1392
+ const dlResolved = resolvePlatformOpts(dir, specDir);
1393
+ if (dlResolved) {
1394
+ dlPlatformOpts.specRoot = dlResolved.specRoot;
1395
+ if (dlResolved.runtimeRoot) dlPlatformOpts.runtimeRoot = dlResolved.runtimeRoot;
1396
+ }
1397
+ const { resolveRuntimeRoot } = await import('./run/shared.js');
1398
+ const dlRuntimeRoot = resolveRuntimeRoot(dlPlatformOpts, dlSpecBase);
1399
+ const { buildDeltaReport } = await import('./archive-delta.js');
1400
+ // project 同口径修复(change: 2026-09-07-ir-hardening D-005@v1):此前硬编码 null →
1401
+ // 手动补跑恒走 loadModuleMap 降级(报告恒「无 module-map」注记)。改从 progress.project
1402
+ // 取值(与归档确认步自动生成同源),null 兜底降级不变。
1403
+ let dlProject = null;
1404
+ try {
1405
+ const { ProgressManager } = await import('./progress.js');
1406
+ const dlPm = new ProgressManager({ specDir: dlSpecBase });
1407
+ const dlProgress = dlPm.read(cwd, dlChange);
1408
+ dlProject = dlProgress?.project || null;
1409
+ } catch { /* 读 progress 失败 → null 兜底降级(fail-soft) */ }
1410
+ const dlResult = buildDeltaReport({
1411
+ changeDir: dlChangeDir,
1412
+ specRoot: dlSpecBase,
1413
+ project: dlProject,
1414
+ runtimeRoot: dlRuntimeRoot,
1415
+ withSummary: true,
1416
+ });
1417
+ const dlMarkdown = dlResult.markdown;
1418
+ const dlPath = join(dlChangeDir, 'delta.md');
1419
+ writeFileSync(dlPath, dlMarkdown);
1420
+ // 回灌 sidecar(2026-09-07-ir-hardening D-006):fail-soft,写失败不阻断 delta 输出
1421
+ const { writeLastDeltaSidecar } = await import('./archive-delta.js');
1422
+ writeLastDeltaSidecar(dlRuntimeRoot, dlResult);
1423
+ if (json) {
1424
+ console.log(JSON.stringify({ command: 'delta', change: dlChange, ok: true, path: dlPath, written: true }, null, 2));
1425
+ break;
1426
+ }
1427
+ console.log(`✅ 已生成 delta.md: ${dlPath}(幂等覆盖,重跑即刷新)`);
1428
+ console.log(' Before/Delta/After 三段式快照;归档确认步自动生成失败时可用本命令手动补。');
1429
+ break;
1430
+ }
1159
1431
  case 'register-stage-review': {
1160
1432
  // 手动注册 stage 级 review(brainstorm/plan/execute-acceptance)的确定性 writer ——
1161
1433
  // 生成 run 目录 + review.json 骨架(cannot_verify,待独立审查子代理填 verdict)或 adopt
@@ -1229,8 +1501,40 @@ async function main() {
1229
1501
  case 'docs': {
1230
1502
  const docsSubCmd = filteredArgs[1];
1231
1503
  if (docsSubCmd === 'migrate') {
1232
- const { migrateDocs } = await import('./migrate.js');
1233
- migrateDocs(dir);
1504
+ // docs migrate 双形态(2026-09-08-docs-fix-capability / D-002+D-004):
1505
+ // 有 --from/--to → 新语义(路径引用前缀迁移,薄模块 docs-migrate.js)
1506
+ // 无 flag → 旧语义(.sillyspec 结构一次性迁移 migrateDocs,兼容保留)
1507
+ const rawMigrateArgs = filteredArgs.slice(2);
1508
+ let mFrom = null, mTo = null, mApply = false;
1509
+ for (let i = 0; i < rawMigrateArgs.length; i++) {
1510
+ const a = rawMigrateArgs[i];
1511
+ if (a === '--from' && rawMigrateArgs[i + 1] !== undefined) { mFrom = rawMigrateArgs[i + 1]; i++; }
1512
+ else if (a === '--to' && rawMigrateArgs[i + 1] !== undefined) { mTo = rawMigrateArgs[i + 1]; i++; }
1513
+ else if (a === '--apply') { mApply = true; }
1514
+ else if (a === '--from' || a === '--to') {
1515
+ console.error(`❌ docs migrate: ${a} 缺值(如 --from "modules/" --to "backend/app/modules/")`);
1516
+ process.exit(2);
1517
+ } else if (a.startsWith('--')) {
1518
+ console.error(`❌ docs migrate: 未知 flag「${a}」。已知 flag:--from --to --apply`);
1519
+ process.exit(2);
1520
+ }
1521
+ }
1522
+ if (mFrom !== null || mTo !== null) {
1523
+ if (mFrom === null || mTo === null) {
1524
+ console.error('❌ docs migrate: --from 与 --to 必须成对提供');
1525
+ process.exit(2);
1526
+ }
1527
+ const { runDocsMigrate } = await import('./docs-migrate.js');
1528
+ const result = runDocsMigrate({ projectRoot: dir, from: mFrom, to: mTo, apply: mApply });
1529
+ // exit code 契约:0=dry-run 零计划或 --apply 后 postCheck 全绿;1=--apply 后仍失效或
1530
+ // unverified>0;dry-run 有计划恒 0(预览不判成败)
1531
+ if (!mApply) { process.exit(0); }
1532
+ const fail = result.unverified > 0 || (result.postCheck && result.postCheck.invalid > 0);
1533
+ process.exit(fail ? 1 : 0);
1534
+ } else {
1535
+ const { migrateDocs } = await import('./migrate.js');
1536
+ migrateDocs(dir);
1537
+ }
1234
1538
  } else if (docsSubCmd === 'check') {
1235
1539
  // docs check(2026-08-15 docs-check-productize D-6):文档 file:line 引用校验。
1236
1540
  // exit code 三档(D-003):0 全绿 / 1 存在无效引用 / 2 配置/IO 错误(DocsCheckConfigError)。
@@ -1243,14 +1547,14 @@ async function main() {
1243
1547
  // 写回);--dry-run 预览不写盘,单独传(无 --fix)也走预览路径——design §5.2 行为矩阵
1244
1548
  // --dry-run 列独立存在(「报告修复预览 + exit 1」),即 dryRun 置位 = fix 语义自动生效
1245
1549
  // 但零写盘(本口径记录于此,防歧义)。
1246
- const BARE_FLAGS = ['--suggest', '--fix', '--dry-run'];
1550
+ const BARE_FLAGS = ['--fix', '--dry-run', '--no-exempt'];
1247
1551
  const PAIRED_FLAGS = ['--paths'];
1248
1552
  const rawDocsArgs = filteredArgs.slice(2);
1249
1553
  const docsCheckFlags = [];
1250
1554
  let cliPaths = null;
1251
- let suggest = false;
1252
1555
  let fix = false;
1253
1556
  let dryRun = false;
1557
+ let noExempt = false;
1254
1558
  for (let i = 0; i < rawDocsArgs.length; i++) {
1255
1559
  const a = rawDocsArgs[i];
1256
1560
  if (a === '--paths' && rawDocsArgs[i + 1] !== undefined) {
@@ -1260,9 +1564,9 @@ async function main() {
1260
1564
  console.error('❌ docs check: --paths 缺值(逗号分隔 glob,如 --paths "docs/**/*.md")');
1261
1565
  process.exit(2);
1262
1566
  } else if (BARE_FLAGS.includes(a)) {
1263
- if (a === '--suggest') suggest = true;
1264
- else if (a === '--fix') fix = true;
1567
+ if (a === '--fix') fix = true;
1265
1568
  else if (a === '--dry-run') dryRun = true;
1569
+ else if (a === '--no-exempt') noExempt = true;
1266
1570
  } else if (a.startsWith('--')) {
1267
1571
  console.error(`❌ docs check: 未知 flag「${a}」。已知 flag:${[...BARE_FLAGS, ...PAIRED_FLAGS].join(' ')}(--json 为全局 flag)`);
1268
1572
  process.exit(2);
@@ -1281,6 +1585,7 @@ async function main() {
1281
1585
  skip: cfg.skip,
1282
1586
  keywordAssert: cfg.keywordAssert,
1283
1587
  crossRepoRoots: cfg.crossRepoRoots,
1588
+ exempt: !noExempt,
1284
1589
  });
1285
1590
  // task-03 修复链路(FR-04,platform-map-auto-anchors):fixActive 才构造 fixes——
1286
1591
  // 仅 fix.fixable===true 且 newLine 为整数的条目可重锚;newRef = ref 的行号部分替换为
@@ -1301,6 +1606,31 @@ async function main() {
1301
1606
  }
1302
1607
  }
1303
1608
  const fixResult = fixActive ? applyFixes(dir, fixes, dryRun ? { dryRun: true } : {}) : null;
1609
+ // 修复回执(change: 2026-09-07-ir-hardening D-007@v1,FR-04):--fix 落盘后用与首查
1610
+ // 完全相同的口径重跑一次失效收集,得 after 计数——「前 N → 重锚 M → 后 K(消除 N-K)」
1611
+ // 即修复建议被机器证明消除了多少诊断(acceptsFix 最小件)。仅 --fix(写盘)路径;
1612
+ // --dry-run 不写盘重跑无意义,receipt 为 null;非 fix 路径零变化。
1613
+ let fixReceipt = null;
1614
+ if (fix && !dryRun && fixResult && fixResult.applied > 0) {
1615
+ try {
1616
+ const after = runDocsCheck({
1617
+ projectRoot: dir,
1618
+ docs: docsCheckFlags.length > 0 ? docsCheckFlags : null,
1619
+ paths: cliPaths || cfg.paths,
1620
+ skip: cfg.skip,
1621
+ keywordAssert: cfg.keywordAssert,
1622
+ crossRepoRoots: cfg.crossRepoRoots,
1623
+ });
1624
+ fixReceipt = {
1625
+ invalidBefore: result.invalid.length,
1626
+ reAnchored: fixResult.applied,
1627
+ invalidAfter: after.invalid.length,
1628
+ eliminated: result.invalid.length - after.invalid.length,
1629
+ };
1630
+ } catch (e) {
1631
+ console.warn(`⚠️ 修复回执重算失败(不影响修复本身):${e && e.message ? e.message : e}`);
1632
+ }
1633
+ }
1304
1634
  // 变更名提名 advisory(2026-08-31 变更关联审计 P2):铁律同决策规则族——只 warn,
1305
1635
  // 不进 invalid、不影响下方 exit code 与 docs gate。降级容错:advisory 自身异常
1306
1636
  // 不拖垮主检查(try-catch 吞并提示)。
@@ -1313,7 +1643,7 @@ async function main() {
1313
1643
  if (json) {
1314
1644
  // --fix 与 --json 组合保持 stdout 纯 JSON 可解析(constraints):修复统计作 result.fixReport
1315
1645
  // 附加后整体 stringify,主体仍是 runDocsCheck 返回(invalid[].fix 是 task-01 设计内增量)
1316
- if (fixResult) result.fixReport = { applied: fixResult.applied, skipped: fixResult.skipped.length, dryRun };
1646
+ if (fixResult) result.fixReport = { applied: fixResult.applied, skipped: fixResult.skipped.length, dryRun, ...(fixReceipt ? { receipt: fixReceipt } : {}) };
1317
1647
  if (changeNameReport) result.changeNameReport = changeNameReport;
1318
1648
  console.log(JSON.stringify(result, null, 2));
1319
1649
  } else {
@@ -1324,7 +1654,10 @@ async function main() {
1324
1654
  if (result.ok) {
1325
1655
  console.log(`✅ docs check: ${result.total} 处引用全通过(其中 ${result.kwChecked} 处带关键词断言)`);
1326
1656
  } else {
1327
- console.error(`\n❌ docs check: ${result.invalid.length}/${result.total} 处引用失效:`);
1657
+ // FR-5(D-005,2026-09-08-docs-fix-capability):报告内容统一 stdout(脚本 capture_output
1658
+ // 只读 stdout 可捕获);stderr 仅留 ⚠️ 诊断(console.warn)与配置错误(exit 2 路径)。
1659
+ // 文本逐字不变,只改通道。
1660
+ console.log(`\n❌ docs check: ${result.invalid.length}/${result.total} 处引用失效:`);
1328
1661
  let appliedLeft = fixResult ? fixResult.applied : 0;
1329
1662
  const skippedQueue = fixResult ? [...fixResult.skipped] : [];
1330
1663
  for (const inv of result.invalid) {
@@ -1332,26 +1665,31 @@ async function main() {
1332
1665
  if (newRef !== undefined && appliedLeft > 0) {
1333
1666
  // 已应用(dry-run 为将应用):归因按构造顺序消耗 applied 计数——写回 skipped 仅
1334
1667
  // 防御路径(文档消失/行内失配)触发,CLI 正常流 fixable 必然落位
1335
- console.error(` ✅ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${newRef}${dryRun ? '(dry-run 未写盘)' : ''}`);
1668
+ console.log(` ✅ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${newRef}${dryRun ? '(dry-run 未写盘)' : ''}`);
1336
1669
  appliedLeft--;
1337
1670
  } else if (newRef !== undefined) {
1338
1671
  const s = skippedQueue.shift();
1339
- console.error(` ⚠️ [${inv.doc}:L${inv.docLine}] ${inv.ref} → 写回跳过:${s ? s.reason : '未知原因'}`);
1672
+ console.log(` ⚠️ [${inv.doc}:L${inv.docLine}] ${inv.ref} → 写回跳过:${s ? s.reason : '未知原因'}`);
1340
1673
  } else if (fixResult) {
1341
1674
  // needs-manual:多命中歧义/零命中/无 token——fix.reason 含候选行号,交人工
1342
- console.error(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${(inv.fix && inv.fix.reason) || inv.reason}(待人工)`);
1675
+ console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${(inv.fix && inv.fix.reason) || inv.reason}(待人工)`);
1343
1676
  } else {
1344
- console.error(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${inv.reason}`);
1677
+ console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${inv.reason}`);
1345
1678
  }
1346
- if (suggest && inv.suggest && inv.suggest.length > 0) {
1347
- console.error(` 💡 候选行号: ${inv.suggest.join(', ')}(token 命中行,人工确认后更新文档锚)`);
1679
+ if (inv.suggest && inv.suggest.length > 0) {
1680
+ // 2026-09-07-ir-hardening task-08:原 --suggest 旗标删除(no-op 首选路径退役),
1681
+ // 候选行号提示改为 needs-manual 默认输出——旗标唯一用途,默认开更有信息量
1682
+ console.log(` 💡 候选行号: ${inv.suggest.join(', ')}(token 命中行,人工确认后更新文档锚)`);
1348
1683
  }
1349
1684
  }
1350
1685
  if (fixResult) {
1351
- console.error(`\n🔧 重锚报告:${fixResult.applied} 处已${dryRun ? '预览' : '改写'}${dryRun ? '(dry-run 未写盘)' : ''}、${result.invalid.length - fixes.length} 处待人工${fixResult.skipped.length > 0 ? `、${fixResult.skipped.length} 处写回跳过` : ''}。`);
1686
+ console.log(`\n🔧 重锚报告:${fixResult.applied} 处已${dryRun ? '预览' : '改写'}${dryRun ? '(dry-run 未写盘)' : ''}、${result.invalid.length - fixes.length} 处待人工${fixResult.skipped.length > 0 ? `、${fixResult.skipped.length} 处写回跳过` : ''}。`);
1687
+ if (fixReceipt) {
1688
+ console.log(`🔁 修复回执(机器复跑证明):修复前 ${fixReceipt.invalidBefore} 处失效 → 重锚 ${fixReceipt.reAnchored} 处 → 修复后 ${fixReceipt.invalidAfter} 处(消除 ${fixReceipt.eliminated} 处)。`);
1352
1689
  }
1353
- console.error(`\n修复指引:行号漂移 → 更新文档行号到当前源码;文件删改名 → 更新引用路径;`);
1354
- console.error(`关键词缺失但行号正确 → 确认符号是否改名,改文档 token 或行号。`);
1690
+ }
1691
+ console.log(`\n修复指引:行号漂移 → 更新文档行号到当前源码;文件删改名 → 更新引用路径;`);
1692
+ console.log(`关键词缺失但行号正确 → 确认符号是否改名,改文档 token 或行号。`);
1355
1693
  }
1356
1694
  // 变更名提名 advisory 段(主结果之后、exit 之前;不参与 ok/invalid 与 exit code)。
1357
1695
  // 零输出原则(docs-check-fix 契约:无新 flag 时 stdout 逐字节一致;决策规则族同款
@@ -1432,7 +1770,9 @@ async function main() {
1432
1770
  }
1433
1771
  process.exit(g.exitCode);
1434
1772
  } else {
1435
- console.log('用法: sillyspec docs migrate | sillyspec docs check [--paths <glob,...>] [--json] [--suggest] [--fix] [--dry-run] | sillyspec docs gate [--init-baseline] [--json]');
1773
+ console.log('用法: sillyspec docs migrate [--from <旧前缀> --to <新前缀> [--apply]] | sillyspec docs check [--paths <glob,...>] [--json] [--fix] [--dry-run] [--no-exempt] | sillyspec docs gate [--init-baseline] [--json]');
1774
+ console.log(' docs migrate:--from/--to 路径前缀迁移(默认 dry-run,--apply 写盘+自动复核);无 flag = 旧 .sillyspec 结构迁移');
1775
+ console.log(' docs check --no-exempt:archive/finished 与 doc_type: snapshot 文档照常校验(缺省豁免)');
1436
1776
  process.exit(2);
1437
1777
  }
1438
1778
  break;
@@ -1610,6 +1950,25 @@ async function main() {
1610
1950
  break;
1611
1951
  }
1612
1952
  case 'scan': {
1953
+ // scan facts 子命令(archify 借鉴:机械事实预咀嚼,2026-09-05)。
1954
+ // CLI 确定性抽取端点/依赖/规模/git 基线 → docs/<project>/scan/_facts.md,
1955
+ // 供 scan 步骤 3/5 的子代理共享(禁止各自 grep 重复发现)。与 diff 同为纯只读旁路。
1956
+ if (filteredArgs[1] === 'facts') {
1957
+ const { buildAndWriteScanFacts } = await import('./scan-facts.js')
1958
+ const factsEffectiveDir = specDir ? dir : resolveEffectiveDir(dir)
1959
+ const projIdx = filteredArgs.indexOf('--project')
1960
+ const pathIdx = filteredArgs.indexOf('--path')
1961
+ const r = buildAndWriteScanFacts({
1962
+ cwd: factsEffectiveDir,
1963
+ specDir: resolvePlatformSpecDir(dir, specDir) || join(factsEffectiveDir, '.sillyspec'),
1964
+ projectName: projIdx >= 0 && filteredArgs[projIdx + 1] ? filteredArgs[projIdx + 1] : basename(factsEffectiveDir),
1965
+ projectPath: pathIdx >= 0 && filteredArgs[pathIdx + 1] ? filteredArgs[pathIdx + 1] : null,
1966
+ })
1967
+ const f = r.facts
1968
+ console.log(`scan facts 已写入: ${r.outPath}`)
1969
+ console.log(` 类型 ${f.type} · 依赖 ${f.deps.length} · 端点 ${f.endpoints.backendTotal}(后端)/ ${f.endpoints.frontendTotal}(前端调用)· 源文件 ${f.size.fileCount}`)
1970
+ break
1971
+ }
1613
1972
  // scan diff 子命令(D-001@v1:接线唯一入口 index.js,非 command.js 裸 token 静默吞)。
1614
1973
  // 纯只读比较命令:跳过下方共享块的 triggerPullActiveChange(不触发网络 pull),
1615
1974
  // 也不走 scan 主流程(--standard/--deep 分支不动)。解析 --base/--full/--report 后转发 scan-diff。