sillyspec 3.27.1 → 3.27.5

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 (224) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +8 -0
  2. package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
  3. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  4. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
  5. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  6. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  7. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
  8. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  9. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
  10. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
  11. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
  12. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
  13. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  14. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  15. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  16. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
  17. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  18. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
  19. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
  20. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  21. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
  22. package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
  23. package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
  24. package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
  25. package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
  26. package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
  27. package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
  28. package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
  29. package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
  30. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
  31. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  32. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
  33. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
  34. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
  35. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
  36. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
  37. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
  38. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
  39. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
  40. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
  41. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
  42. package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
  43. package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
  44. package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
  45. package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
  46. package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
  47. package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
  48. package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
  49. package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
  50. package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
  51. package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
  52. package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
  53. package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
  54. package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
  55. package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
  56. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
  57. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
  58. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
  59. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
  60. package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
  61. package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
  62. package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
  63. package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
  64. package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
  65. package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
  66. package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
  67. package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
  68. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
  69. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
  70. package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
  71. package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
  72. package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
  73. package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
  74. package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
  75. package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
  76. package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
  77. package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
  78. package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
  79. package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
  80. package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
  81. package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
  82. package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
  83. package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
  84. package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
  85. package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
  86. package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
  87. package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
  88. package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
  89. package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
  90. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
  91. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
  92. package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
  93. package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
  94. package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
  95. package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
  96. package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
  97. package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
  98. package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
  99. package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
  100. package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
  101. package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
  102. package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
  103. package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
  104. package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
  105. package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
  106. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
  107. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
  108. package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
  109. package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
  110. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
  111. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
  112. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
  113. package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
  114. package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
  115. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
  116. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
  117. package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
  118. package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
  119. package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
  120. package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
  121. package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
  122. package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
  123. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
  124. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
  125. package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
  126. package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
  127. package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
  128. package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
  129. package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
  130. package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
  131. package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
  132. package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
  133. package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
  134. package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
  135. package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
  136. package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
  137. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
  138. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
  139. package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
  140. package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
  141. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
  142. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
  143. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
  144. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
  145. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
  146. package/README.md +1 -1
  147. package/eval/README.md +86 -0
  148. package/eval/eval.config.json +28 -0
  149. package/eval/lib/adapters/cli.mjs +63 -0
  150. package/eval/lib/adapters/dryrun.mjs +27 -0
  151. package/eval/lib/config.mjs +82 -0
  152. package/eval/lib/db.mjs +76 -0
  153. package/eval/lib/report.mjs +120 -0
  154. package/eval/lib/runner.mjs +157 -0
  155. package/eval/lib/tasks.mjs +73 -0
  156. package/eval/lib/util.mjs +107 -0
  157. package/eval/lib/workdir.mjs +30 -0
  158. package/eval/run.mjs +83 -0
  159. package/eval/tasks/001-slugify/files/package.json +6 -0
  160. package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  161. package/eval/tasks/001-slugify/task.json +6 -0
  162. package/eval/tasks/001-slugify/verify.mjs +28 -0
  163. package/eval/tasks/002-each-day/files/package.json +6 -0
  164. package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  165. package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  166. package/eval/tasks/002-each-day/task.json +6 -0
  167. package/eval/tasks/002-each-day/verify.mjs +31 -0
  168. package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  169. package/eval/tasks/003-word-report/files/package.json +6 -0
  170. package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  171. package/eval/tasks/003-word-report/task.json +6 -0
  172. package/eval/tasks/003-word-report/verify.mjs +38 -0
  173. package/package.json +4 -2
  174. package/src/agent-session-log.js +803 -0
  175. package/src/change-list.js +22 -2
  176. package/src/change-risk-profile.js +83 -14
  177. package/src/config-schema.js +18 -3
  178. package/src/contract-matrix.js +153 -8
  179. package/src/datetime.js +20 -0
  180. package/src/decision-distill.js +517 -0
  181. package/src/dispatch/backends/local-agent.js +4 -4
  182. package/src/docs-check.js +227 -0
  183. package/src/docs-debt.js +187 -24
  184. package/src/doctor-diagnostics.js +102 -2
  185. package/src/foreign-declared.js +78 -0
  186. package/src/index.js +99 -10
  187. package/src/init.js +9 -7
  188. package/src/knowledge-match.js +107 -6
  189. package/src/local-register.js +7 -1
  190. package/src/module-resolve.js +12 -1
  191. package/src/modules.js +2 -1
  192. package/src/plan-adopt-waves.js +126 -0
  193. package/src/quicklog.js +61 -7
  194. package/src/run/command.js +139 -20
  195. package/src/run/complete-handlers.js +48 -19
  196. package/src/run/complete.js +43 -3
  197. package/src/run/gates.js +87 -22
  198. package/src/run/prompt.js +84 -42
  199. package/src/run/quick-audit.js +28 -0
  200. package/src/run/shared.js +176 -11
  201. package/src/run/stage.js +23 -0
  202. package/src/run.js +2 -2
  203. package/src/scan-postcheck.js +30 -17
  204. package/src/stage-contract-spec.js +2 -1
  205. package/src/stage-contract.js +68 -4
  206. package/src/stages/archive.js +53 -12
  207. package/src/stages/brainstorm-auto.js +7 -0
  208. package/src/stages/brainstorm.js +11 -0
  209. package/src/stages/doctor.js +157 -1
  210. package/src/stages/execute.js +33 -6
  211. package/src/stages/plan-postcheck.js +79 -18
  212. package/src/stages/plan.js +4 -0
  213. package/src/stages/quick.js +6 -1
  214. package/src/stages/verify.js +31 -14
  215. package/src/sync.js +94 -2
  216. package/src/task-review.js +2 -2
  217. package/src/taskcard-placeholders.js +21 -0
  218. package/src/taskcard.js +10 -1
  219. package/src/verify-postcheck.js +315 -13
  220. package/src/verify-probes.js +46 -4
  221. package/src/worktree-apply.js +184 -7
  222. package/src/worktree-deps.js +69 -0
  223. package/src/worktree.js +201 -58
  224. package/templates/prompts/taskcard-rules.md +1 -0
@@ -0,0 +1,388 @@
1
+ /**
2
+ * scan-diff — scan 文档 vs 源码漂移清单(change: 2026-08-16-scan-diff-command, task-01)
3
+ *
4
+ * 定位:scan 文档是架构快照(基线 = 生成时源码 commit,frontmatter source_commit),源码持续演进。
5
+ * scan-diff 算出「基线之后源码变了什么、哪些模块的 scan 文档需要核对/刷新」,按四分类输出:
6
+ * A 新增文件 → scan 文档缺记(缺文档)
7
+ * D 已删除文件 → scan 文档多记(多文档)
8
+ * M / R / C 修改 / 重命名 / 复制 → scan 文档过时
9
+ * 其余(T 等)→ unknown(不判漂移,只展示)
10
+ * 扫描范围缺省 = module-map paths 覆盖集(parseModuleMapSimple 收集全部 paths 去重),
11
+ * 非 src/-only——范围外的源码变更与模块文档无涉,不计入漂移。
12
+ *
13
+ * 分层:
14
+ * computeScanDiff —— 计算层(git 只读 + frontmatter/module-map 读取;无渲染无落盘)。
15
+ * 硬错误返回 { ok:false, error }(无效 base / git 失败 / 无 source_commit);
16
+ * advisory 问题进 warnings(非祖先 base / rev-list 降级 / module-map 缺失)。
17
+ * runScanDiff —— IO 面(终端聚合渲染 + --report 落盘 scan-diff-report.md),返回退出码。
18
+ *
19
+ * 复用(先读源码确认签名后使用):parseSourceCommit(scan-staleness)、
20
+ * matchFilesToModules(docs-debt,含裸名兜底)、safeGit(git-helper)、
21
+ * parseModuleMapSimple(modules.js);零新依赖。
22
+ */
23
+ import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs'
24
+ import { join } from 'node:path'
25
+ import { safeGit } from './git-helper.js'
26
+ import { parseSourceCommit } from './scan-staleness.js'
27
+ import { matchFilesToModules } from './docs-debt.js'
28
+ import { parseModuleMapSimple } from './modules.js'
29
+
30
+ /** safeGit 超时(仿 docs-debt 的 GIT_TIMEOUT_MS 模式:大 diff 慢时降级不挂起) */
31
+ const GIT_TIMEOUT_MS = 10000
32
+
33
+ /** 每模块终端渲染默认上限(--full 展开全部) */
34
+ const DEFAULT_LIST_CAP = 5
35
+
36
+ /**
37
+ * 计算层:scan 文档 vs 源码漂移事实。
38
+ * @param {{ projectRoot: string, specBase: string, projectName: string, base?: string|null }} opts
39
+ * projectRoot:git 锚(diff/rev-list 的 -C 目录)
40
+ * specBase / projectName:scan 文档与 module-map 定位(specBase/docs/<projectName>/...)
41
+ * base:基线 commit(短/长哈希、引用均可);缺省读 scan 文档 frontmatter 的 source_commit
42
+ * @returns {{ ok: true, base, baseSource, head, behindCommits, daysSinceScan, scope,
43
+ * added, deleted, modified, unknown, unmapped, outOfScope, renameMap, byModule, warnings, driftCount }
44
+ * | { ok: false, error: string }}
45
+ * ok:false = 硬错误(参数缺失 / 无 source_commit / 无效 base / git diff 失败)
46
+ * added/deleted/modified/unknown:范围内变更文件(POSIX 相对路径;R/C 记新路径)
47
+ * unmapped:范围内未匹配到模块的文件;outOfScope:范围外变更(不计入漂移)
48
+ * renameMap:newPath → oldPath(R 归 modified 且保留 rename 细节)
49
+ * byModule:{ moduleId: { doc, added, deleted, modified, files } }(A/D/M 计数 + 文件清单)
50
+ * driftCount = added + deleted + modified
51
+ */
52
+ export function computeScanDiff({ projectRoot, specBase, projectName, base } = {}) {
53
+ if (!projectRoot || !specBase || !projectName) {
54
+ return { ok: false, error: 'projectRoot / specBase / projectName 必填,缺一不可' }
55
+ }
56
+ const warnings = []
57
+ let baseSource = '--base 显式指定'
58
+
59
+ // ── 基线:缺省读 specBase/docs/<projectName>/scan/*.md frontmatter 的 source_commit ──
60
+ if (!base) {
61
+ const sourceCommit = readSourceCommit(specBase, projectName)
62
+ if (!sourceCommit) {
63
+ return {
64
+ ok: false, error:
65
+ 'scan 文档无 source_commit 字段(绿地项目 / 旧版生成 / 无 scan 文档),无法定基线——请用 --base <commit> 显式指定基线',
66
+ }
67
+ }
68
+ base = sourceCommit
69
+ baseSource = 'scan 文档 frontmatter source_commit'
70
+ }
71
+ const baseDisplay = displayBase(base)
72
+
73
+ // ── base 有效性守卫(仿 computeScanStaleness 的 isAncestor 守卫)──
74
+ // merge-base --is-ancestor 对「无效 commit」与「有效但非祖先」都返回非 0(safeGit 记为 error),
75
+ // 无法直接区分——先用 rev-parse 验有效性:无效 → 报错;有效但非祖先 → 警告(diff 按两快照对比仍可算)。
76
+ const revRes = safeGit(projectRoot, ['rev-parse', '--verify', `${base}^{commit}`])
77
+ if (revRes.error || !revRes.value) {
78
+ return { ok: false, error: `基线 ${baseDisplay} 不是有效 commit(${revRes.error || '无输出'}),跳过 scan-diff` }
79
+ }
80
+ const resolvedBase = revRes.value.trim()
81
+ const headRes = safeGit(projectRoot, ['rev-parse', 'HEAD'])
82
+ const headShort = headRes.error ? 'HEAD' : headRes.value.trim().slice(0, 7)
83
+ const isAncestor = safeGit(projectRoot, ['merge-base', '--is-ancestor', resolvedBase, 'HEAD'])
84
+ if (isAncestor.error) {
85
+ warnings.push(`基线 ${baseDisplay} 不是 HEAD(${headShort})祖先(rebase / 分支切换)——diff 按两快照对比,删除后重建可能呈假新增`)
86
+ }
87
+
88
+ // ── 落后计数(advisory:超时/失败降级 null,不抛)──
89
+ const countRes = safeGit(projectRoot, ['rev-list', '--count', `${resolvedBase}..HEAD`], { timeout: GIT_TIMEOUT_MS })
90
+ let behindCommits = countRes.error ? null : parseInt(countRes.value, 10)
91
+ if (behindCommits === null || Number.isNaN(behindCommits)) {
92
+ behindCommits = null
93
+ warnings.push('rev-list 计数失败(git 降级),behindCommits=null')
94
+ }
95
+ const dateRes = safeGit(projectRoot, ['log', '-1', '--format=%cI', resolvedBase])
96
+ let daysSinceScan = null
97
+ if (!dateRes.error && dateRes.value) {
98
+ daysSinceScan = Math.floor((Date.now() - new Date(dateRes.value.trim()).getTime()) / 86400000)
99
+ }
100
+
101
+ // ── 扫描范围:module-map paths 覆盖集(默认范围,非 src/-only)──
102
+ const cardsDir = join(specBase, 'docs', projectName, 'modules')
103
+ const mapPath = join(cardsDir, '_module-map.yaml')
104
+ let moduleIndex = null
105
+ if (existsSync(mapPath)) {
106
+ try { moduleIndex = parseModuleMapSimple(readFileSync(mapPath, 'utf8')) } catch { moduleIndex = null }
107
+ }
108
+ const scope = collectScope(moduleIndex)
109
+ if (scope.length === 0) {
110
+ warnings.push('module-map 缺失 / 解析为空(跑 sillyspec modules rebuild 生成)——扫描范围退化=全部变更文件,无法归模块')
111
+ }
112
+
113
+ // ── diff:只算路径(--find-renames 检出重命名;超时降级为错误返回)──
114
+ const diffRes = safeGit(projectRoot, ['diff', '--name-status', '--find-renames', `${resolvedBase}..HEAD`], { timeout: GIT_TIMEOUT_MS })
115
+ if (diffRes.error) {
116
+ return { ok: false, error: `git diff ${baseDisplay}..HEAD 失败(${diffRes.error}),跳过 scan-diff` }
117
+ }
118
+
119
+ // ── 四分类 + 范围过滤 ──
120
+ const added = [], deleted = [], modified = [], unknown = [], outOfScope = []
121
+ const renameMap = {} // newPath → oldPath(R 归 modified 且保留 rename 细节)
122
+ const scopeFilter = scope.length > 0 ? scope : null
123
+ for (const item of parseNameStatus(diffRes.value)) {
124
+ if (scopeFilter && !inScope(item.path, scopeFilter)) { outOfScope.push(item.path); continue }
125
+ switch (item.status) {
126
+ case 'A': added.push(item.path); break
127
+ case 'D': deleted.push(item.path); break
128
+ case 'M': modified.push(item.path); break
129
+ case 'R': modified.push(item.path); renameMap[item.path] = item.oldPath; break
130
+ case 'C': modified.push(item.path); break // 复制目标(新路径)视同过时
131
+ default: unknown.push(item.path); break // T / B 等
132
+ }
133
+ }
134
+
135
+ // ── 归模块(matchFilesToModules,含裸名兜底;结果 = 同文件清单直接调用一致)──
136
+ const inScopeFiles = [...added, ...deleted, ...modified, ...unknown]
137
+ let byModuleMap = new Map()
138
+ let unmapped = inScopeFiles
139
+ if (inScopeFiles.length > 0) {
140
+ try {
141
+ const matched = matchFilesToModules(inScopeFiles, moduleIndex || {}, { cardsDir })
142
+ byModuleMap = matched.byModule
143
+ unmapped = matched.unmapped
144
+ } catch {
145
+ warnings.push('matchFilesToModules 归模块失败(降级为全部未归模块)')
146
+ }
147
+ } else {
148
+ unmapped = []
149
+ }
150
+
151
+ // ── byModule 聚合 A/D/M 计数 ──
152
+ const addedSet = new Set(added)
153
+ const deletedSet = new Set(deleted)
154
+ const modifiedSet = new Set(modified)
155
+ const byModule = {}
156
+ for (const [id, entry] of byModuleMap) {
157
+ let a = 0, d = 0, m = 0
158
+ for (const f of entry.files) {
159
+ if (addedSet.has(f)) a++
160
+ else if (deletedSet.has(f)) d++
161
+ else if (modifiedSet.has(f)) m++
162
+ }
163
+ byModule[id] = { doc: entry.doc || '', added: a, deleted: d, modified: m, files: entry.files }
164
+ }
165
+
166
+ return {
167
+ ok: true,
168
+ base: baseDisplay,
169
+ baseSource,
170
+ head: headShort,
171
+ behindCommits,
172
+ daysSinceScan,
173
+ scope,
174
+ added, deleted, modified, unknown, unmapped, outOfScope,
175
+ renameMap,
176
+ byModule,
177
+ warnings,
178
+ driftCount: added.length + deleted.length + modified.length,
179
+ }
180
+ }
181
+
182
+ /**
183
+ * IO 面:终端渲染 + 可选落盘报告。
184
+ * @param {{ projectRoot: string, specBase: string, projectName: string, base?: string|null,
185
+ * full?: boolean, report?: boolean }} opts
186
+ * full:终端展开每模块全部文件(缺省每模块 ≤5 条);report:落盘 scan-diff-report.md
187
+ * @returns {number} 退出码:0 无漂移 / 1 有漂移 / 2 错误(无效 base、git 失败、无 source_commit)
188
+ */
189
+ export function runScanDiff(opts = {}) {
190
+ const { projectRoot, specBase, projectName, base = null, full = false, report = false } = opts
191
+ const result = computeScanDiff({ projectRoot, specBase, projectName, base })
192
+ if (!result.ok) {
193
+ console.error(`scan-diff:${result.error}`)
194
+ return 2
195
+ }
196
+ for (const w of result.warnings) console.warn(` ⚠️ ${w}`)
197
+
198
+ console.log(`scan diff: ${result.base}..HEAD(${fmtNum(result.behindCommits)} commit / ${fmtNum(result.daysSinceScan)} 天)`)
199
+ if (result.driftCount === 0) {
200
+ console.log('scan 文档与源码一致(0 漂移)')
201
+ if (result.unknown.length > 0) {
202
+ console.log(` 无法归类(unknown)${result.unknown.length} 条:${result.unknown.join(', ')}`)
203
+ }
204
+ } else {
205
+ renderTerminal(result, { full })
206
+ }
207
+
208
+ if (report) {
209
+ const p = writeReport(result, { specBase, projectName })
210
+ if (p) console.log(`scan-diff 报告已落盘:${p}`)
211
+ else console.warn(' ⚠️ scan-diff-report.md 写入失败')
212
+ }
213
+ return result.driftCount > 0 ? 1 : 0
214
+ }
215
+
216
+ /** 终端聚合渲染:每模块计数 + 文件清单(缺省 ≤5 条,--full 展开全部) */
217
+ function renderTerminal(r, { full }) {
218
+ const cap = full ? Number.MAX_SAFE_INTEGER : DEFAULT_LIST_CAP
219
+ console.log(`扫描范围:module-map paths 覆盖集(${r.scope.length} 条路径)——范围外的变更不计入漂移`)
220
+ const moduleIds = Object.keys(r.byModule).sort()
221
+ if (moduleIds.length > 0) {
222
+ console.log('按模块聚合漂移(A=缺文档 D=多文档 M=过时,--full 展开全部):')
223
+ for (const id of moduleIds) {
224
+ const m = r.byModule[id]
225
+ const doc = m.doc ? `(${m.doc})` : ''
226
+ console.log(` [${id}] A${m.added} D${m.deleted} M${m.modified}${doc}`)
227
+ for (const f of m.files.slice(0, cap)) {
228
+ console.log(` - ${statusLetter(r, f)} ${f}${r.renameMap[f] ? `(原 ${r.renameMap[f]})` : ''}`)
229
+ }
230
+ if (m.files.length > cap) console.log(` ... 其余 ${m.files.length - cap} 条,--full 展开`)
231
+ }
232
+ }
233
+ if (r.unmapped.length > 0) {
234
+ console.log(`未归模块(unmapped,${r.unmapped.length} 条):`)
235
+ for (const f of r.unmapped.slice(0, cap)) {
236
+ console.log(` - ${statusLetter(r, f)} ${f}`)
237
+ }
238
+ if (r.unmapped.length > cap) console.log(` ... 其余 ${r.unmapped.length - cap} 条,--full 展开`)
239
+ }
240
+ if (r.unknown.length > 0) {
241
+ console.log(`无法归类(unknown,${r.unknown.length} 条):${r.unknown.slice(0, cap).join(', ')}`)
242
+ }
243
+ console.log(`漂移合计:${r.driftCount} 条(A${r.added.length} / D${r.deleted.length} / M${r.modified.length})`)
244
+ }
245
+
246
+ /** 文件归属状态字母(渲染用):R(renameMap 命中)> A > D > M */
247
+ function statusLetter(r, f) {
248
+ if (r.renameMap[f]) return 'R'
249
+ if (r.added.includes(f)) return 'A'
250
+ if (r.deleted.includes(f)) return 'D'
251
+ return 'M'
252
+ }
253
+
254
+ /** 落盘 scan-diff-report.md(markdown 完整清单,不截断) */
255
+ function writeReport(r, { specBase, projectName }) {
256
+ const dir = join(specBase, 'docs', projectName, 'scan')
257
+ try { mkdirSync(dir, { recursive: true }) } catch { return null }
258
+ const lines = [
259
+ '# scan-diff 报告',
260
+ '',
261
+ `- 生成时间:${new Date().toISOString()}`,
262
+ `- 项目:${projectName}`,
263
+ `- 基线:${r.base}(${r.baseSource})`,
264
+ `- 对比:\`${r.base}..HEAD\``,
265
+ `- 落后:${fmtNum(r.behindCommits)} commit / ${fmtNum(r.daysSinceScan)} 天`,
266
+ `- 扫描范围:module-map paths 覆盖集(${r.scope.length} 条路径)`,
267
+ '',
268
+ '## 漂移总览',
269
+ '',
270
+ '| 分类 | 含义 | 数量 |',
271
+ '| --- | --- | --- |',
272
+ `| added | 源码新增,scan 文档缺记 | ${r.added.length} |`,
273
+ `| deleted | 源码已删,scan 文档多记 | ${r.deleted.length} |`,
274
+ `| modified | 源码已变,scan 文档过时 | ${r.modified.length} |`,
275
+ `| unknown | 其他状态 | ${r.unknown.length} |`,
276
+ `| unmapped | 范围内未匹配模块 | ${r.unmapped.length} |`,
277
+ '',
278
+ '## 按模块聚合',
279
+ '',
280
+ ]
281
+ if (Object.keys(r.byModule).length === 0) {
282
+ lines.push('(无模块命中)', '')
283
+ } else {
284
+ for (const id of Object.keys(r.byModule).sort()) {
285
+ const m = r.byModule[id]
286
+ lines.push(`### ${id}(${m.doc || '无 doc'})`, '')
287
+ lines.push(`- added:${m.added}`, `- deleted:${m.deleted}`, `- modified:${m.modified}`, '- 文件:')
288
+ for (const f of m.files) {
289
+ const ren = r.renameMap[f] ? `,原 ${r.renameMap[f]}` : ''
290
+ lines.push(` - \`${f}\`(${statusLetter(r, f)}${ren})`)
291
+ }
292
+ lines.push('')
293
+ }
294
+ }
295
+ if (r.unmapped.length > 0) {
296
+ lines.push('## 未归模块(unmapped)', '')
297
+ for (const f of r.unmapped) lines.push(`- \`${f}\``)
298
+ lines.push('')
299
+ }
300
+ if (r.outOfScope.length > 0) {
301
+ lines.push('## 范围外变更(不计入漂移)', '')
302
+ for (const f of r.outOfScope) lines.push(`- \`${f}\``)
303
+ lines.push('')
304
+ }
305
+ if (r.warnings.length > 0) {
306
+ lines.push('## 提示', '')
307
+ for (const w of r.warnings) lines.push(`- ${w}`)
308
+ lines.push('')
309
+ }
310
+ lines.push('## 结论', '')
311
+ lines.push(`- 漂移合计:${r.driftCount} 条(A${r.added.length} / D${r.deleted.length} / M${r.modified.length})`)
312
+ if (r.driftCount === 0) {
313
+ lines.push('- scan 文档与源码一致(0 漂移)')
314
+ } else {
315
+ lines.push('- 存在漂移:请按需核对并刷新对应 scan 文档(sillyspec run scan)')
316
+ }
317
+ lines.push('')
318
+ lines.push('> 由 sillyspec scan-diff 自动生成;改动源码后请按需核对 scan 文档。')
319
+ const outPath = join(dir, 'scan-diff-report.md')
320
+ try {
321
+ writeFileSync(outPath, lines.join('\n') + '\n', 'utf8')
322
+ return outPath
323
+ } catch {
324
+ return null
325
+ }
326
+ }
327
+
328
+ /** 读 scan 目录任一 .md 的 source_commit(同 modules.js 写入同值,取首个命中;全失败 → null) */
329
+ function readSourceCommit(specBase, projectName) {
330
+ const scanDir = join(specBase, 'docs', projectName, 'scan')
331
+ if (!existsSync(scanDir)) return null
332
+ try {
333
+ for (const f of readdirSync(scanDir)) {
334
+ if (!f.endsWith('.md')) continue
335
+ const c = parseSourceCommit(readFileSync(join(scanDir, f), 'utf8'))
336
+ if (c) return c
337
+ }
338
+ } catch { /* 读失败 → null */ }
339
+ return null
340
+ }
341
+
342
+ /** 收集 module-map 全部 paths 去重(归一 Windows 反斜杠 + 去尾部斜杠;无 paths → 空数组) */
343
+ function collectScope(moduleIndex) {
344
+ const set = new Set()
345
+ for (const m of Object.values(moduleIndex || {})) {
346
+ if (!Array.isArray(m.paths)) continue
347
+ for (const raw of m.paths) {
348
+ if (typeof raw !== 'string' || !raw) continue
349
+ set.add(raw.replace(/\\/g, '/').replace(/\/+$/, ''))
350
+ }
351
+ }
352
+ return [...set]
353
+ }
354
+
355
+ /** 路径是否落在范围(p 可为目录或文件:精确相等或目录前缀) */
356
+ function inScope(filePath, scope) {
357
+ return scope.some((p) => p && (filePath === p || filePath.startsWith(p + '/')))
358
+ }
359
+
360
+ /** 解析 `git diff --name-status --find-renames` 输出 → [{ status, path, oldPath? }](R/C 双路径) */
361
+ function parseNameStatus(out) {
362
+ const items = []
363
+ if (!out) return items
364
+ for (const line of out.split('\n')) {
365
+ const l = line.replace(/\r$/, '')
366
+ if (!l) continue
367
+ const tab = l.indexOf('\t')
368
+ if (tab === -1) continue
369
+ const m = l.slice(0, tab).match(/^([A-Z])(\d*)$/)
370
+ if (!m) continue
371
+ const status = m[1]
372
+ const parts = l.slice(tab + 1).split('\t')
373
+ const item = { status, path: parts[parts.length - 1] }
374
+ if ((status === 'R' || status === 'C') && parts.length >= 2) item.oldPath = parts[0]
375
+ items.push(item)
376
+ }
377
+ return items
378
+ }
379
+
380
+ /** 40 位全哈希缩成 7 位显示;引用/短哈希原样 */
381
+ function displayBase(b) {
382
+ return /^[0-9a-f]{40}$/i.test(b) ? b.slice(0, 7) : b
383
+ }
384
+
385
+ /** null/NaN → '?'(降级显示) */
386
+ function fmtNum(n) {
387
+ return n === null || Number.isNaN(n) ? '?' : String(n)
388
+ }