sillyspec 3.27.2 → 3.27.6

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 (398) 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/.worktrees/8d2d0392/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  147. package/.worktrees/8d2d0392/.claude/skills/sillyspec-auto/SKILL.md +93 -0
  148. package/.worktrees/8d2d0392/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  149. package/.worktrees/8d2d0392/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  150. package/.worktrees/8d2d0392/.claude/skills/sillyspec-continue/SKILL.md +47 -0
  151. package/.worktrees/8d2d0392/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  152. package/.worktrees/8d2d0392/.claude/skills/sillyspec-execute/SKILL.md +214 -0
  153. package/.worktrees/8d2d0392/.claude/skills/sillyspec-explore/SKILL.md +102 -0
  154. package/.worktrees/8d2d0392/.claude/skills/sillyspec-export/SKILL.md +57 -0
  155. package/.worktrees/8d2d0392/.claude/skills/sillyspec-init/SKILL.md +177 -0
  156. package/.worktrees/8d2d0392/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  157. package/.worktrees/8d2d0392/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  158. package/.worktrees/8d2d0392/.claude/skills/sillyspec-propose/SKILL.md +87 -0
  159. package/.worktrees/8d2d0392/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  160. package/.worktrees/8d2d0392/.claude/skills/sillyspec-resume/SKILL.md +70 -0
  161. package/.worktrees/8d2d0392/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  162. package/.worktrees/8d2d0392/.claude/skills/sillyspec-state/SKILL.md +64 -0
  163. package/.worktrees/8d2d0392/.claude/skills/sillyspec-status/SKILL.md +54 -0
  164. package/.worktrees/8d2d0392/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  165. package/.worktrees/8d2d0392/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
  166. package/.worktrees/8d2d0392/.claude/skills/verify-per-user/SKILL.md +6 -0
  167. package/.worktrees/8d2d0392/.gitattributes +2 -0
  168. package/.worktrees/8d2d0392/.husky/pre-push +20 -0
  169. package/.worktrees/8d2d0392/README.md +203 -0
  170. package/.worktrees/8d2d0392/SKILL.md +91 -0
  171. package/.worktrees/8d2d0392/architecture-analysis.md +448 -0
  172. package/.worktrees/8d2d0392/bin/sillyspec.js +2 -0
  173. package/.worktrees/8d2d0392/eval/README.md +86 -0
  174. package/.worktrees/8d2d0392/eval/eval.config.json +28 -0
  175. package/.worktrees/8d2d0392/eval/lib/adapters/cli.mjs +63 -0
  176. package/.worktrees/8d2d0392/eval/lib/adapters/dryrun.mjs +27 -0
  177. package/.worktrees/8d2d0392/eval/lib/config.mjs +82 -0
  178. package/.worktrees/8d2d0392/eval/lib/db.mjs +76 -0
  179. package/.worktrees/8d2d0392/eval/lib/report.mjs +120 -0
  180. package/.worktrees/8d2d0392/eval/lib/runner.mjs +157 -0
  181. package/.worktrees/8d2d0392/eval/lib/tasks.mjs +73 -0
  182. package/.worktrees/8d2d0392/eval/lib/util.mjs +107 -0
  183. package/.worktrees/8d2d0392/eval/lib/workdir.mjs +30 -0
  184. package/.worktrees/8d2d0392/eval/run.mjs +83 -0
  185. package/.worktrees/8d2d0392/eval/tasks/001-slugify/files/package.json +6 -0
  186. package/.worktrees/8d2d0392/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  187. package/.worktrees/8d2d0392/eval/tasks/001-slugify/task.json +6 -0
  188. package/.worktrees/8d2d0392/eval/tasks/001-slugify/verify.mjs +28 -0
  189. package/.worktrees/8d2d0392/eval/tasks/002-each-day/files/package.json +6 -0
  190. package/.worktrees/8d2d0392/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  191. package/.worktrees/8d2d0392/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  192. package/.worktrees/8d2d0392/eval/tasks/002-each-day/task.json +6 -0
  193. package/.worktrees/8d2d0392/eval/tasks/002-each-day/verify.mjs +31 -0
  194. package/.worktrees/8d2d0392/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  195. package/.worktrees/8d2d0392/eval/tasks/003-word-report/files/package.json +6 -0
  196. package/.worktrees/8d2d0392/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  197. package/.worktrees/8d2d0392/eval/tasks/003-word-report/task.json +6 -0
  198. package/.worktrees/8d2d0392/eval/tasks/003-word-report/verify.mjs +38 -0
  199. package/.worktrees/8d2d0392/logo.jpg +0 -0
  200. package/.worktrees/8d2d0392/package-lock.json +977 -0
  201. package/.worktrees/8d2d0392/package.json +41 -0
  202. package/.worktrees/8d2d0392/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
  203. package/.worktrees/8d2d0392/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  204. package/.worktrees/8d2d0392/packages/dashboard/dist/favicon.jpg +0 -0
  205. package/.worktrees/8d2d0392/packages/dashboard/dist/index.html +16 -0
  206. package/.worktrees/8d2d0392/packages/dashboard/dist/logo.jpg +0 -0
  207. package/.worktrees/8d2d0392/packages/dashboard/dist/prototype-dashboard.html +836 -0
  208. package/.worktrees/8d2d0392/packages/dashboard/dist/prototype-overview.html +256 -0
  209. package/.worktrees/8d2d0392/packages/dashboard/package.json +25 -0
  210. package/.worktrees/8d2d0392/packages/dashboard/server/executor.js +95 -0
  211. package/.worktrees/8d2d0392/packages/dashboard/server/index.js +679 -0
  212. package/.worktrees/8d2d0392/packages/dashboard/server/parser.js +526 -0
  213. package/.worktrees/8d2d0392/packages/dashboard/server/watcher.js +357 -0
  214. package/.worktrees/8d2d0392/src/agent-session-log.js +803 -0
  215. package/.worktrees/8d2d0392/src/change-list.js +290 -0
  216. package/.worktrees/8d2d0392/src/change-risk-profile.js +339 -0
  217. package/.worktrees/8d2d0392/src/check-primitives.js +65 -0
  218. package/.worktrees/8d2d0392/src/classify-change.js +109 -0
  219. package/.worktrees/8d2d0392/src/commit-suggest.js +202 -0
  220. package/.worktrees/8d2d0392/src/config-cat.js +88 -0
  221. package/.worktrees/8d2d0392/src/config-schema.js +328 -0
  222. package/.worktrees/8d2d0392/src/constants.js +97 -0
  223. package/.worktrees/8d2d0392/src/contract-matrix.js +533 -0
  224. package/.worktrees/8d2d0392/src/datetime.js +20 -0
  225. package/.worktrees/8d2d0392/src/db-engine.js +55 -0
  226. package/.worktrees/8d2d0392/src/db.js +381 -0
  227. package/.worktrees/8d2d0392/src/decision-distill.js +517 -0
  228. package/.worktrees/8d2d0392/src/dispatch/backends/local-agent.js +155 -0
  229. package/.worktrees/8d2d0392/src/dispatch/backends/sillyhub-mcp.js +245 -0
  230. package/.worktrees/8d2d0392/src/dispatch/probe.js +246 -0
  231. package/.worktrees/8d2d0392/src/dispatch/strategy.js +121 -0
  232. package/.worktrees/8d2d0392/src/docs-check.js +825 -0
  233. package/.worktrees/8d2d0392/src/docs-debt.js +393 -0
  234. package/.worktrees/8d2d0392/src/docs-gate.js +107 -0
  235. package/.worktrees/8d2d0392/src/doctor-diagnostics.js +1099 -0
  236. package/.worktrees/8d2d0392/src/endpoint-extractor.js +409 -0
  237. package/.worktrees/8d2d0392/src/foreign-declared.js +78 -0
  238. package/.worktrees/8d2d0392/src/fs-atomic.js +73 -0
  239. package/.worktrees/8d2d0392/src/git-helper.js +150 -0
  240. package/.worktrees/8d2d0392/src/hooks/claude-pre-tool-use.cjs +130 -0
  241. package/.worktrees/8d2d0392/src/hooks/worktree-guard.js +892 -0
  242. package/.worktrees/8d2d0392/src/index.js +2854 -0
  243. package/.worktrees/8d2d0392/src/init.js +626 -0
  244. package/.worktrees/8d2d0392/src/knowledge-match.js +231 -0
  245. package/.worktrees/8d2d0392/src/local-detect.js +128 -0
  246. package/.worktrees/8d2d0392/src/local-register.js +104 -0
  247. package/.worktrees/8d2d0392/src/machine-interface.js +543 -0
  248. package/.worktrees/8d2d0392/src/migrate.js +127 -0
  249. package/.worktrees/8d2d0392/src/module-impact.js +166 -0
  250. package/.worktrees/8d2d0392/src/module-resolve.js +222 -0
  251. package/.worktrees/8d2d0392/src/modules.js +580 -0
  252. package/.worktrees/8d2d0392/src/plan-adopt-waves.js +126 -0
  253. package/.worktrees/8d2d0392/src/progress/change-registry.js +336 -0
  254. package/.worktrees/8d2d0392/src/progress/consistency-doctor.js +355 -0
  255. package/.worktrees/8d2d0392/src/progress/shared.js +36 -0
  256. package/.worktrees/8d2d0392/src/progress/stage-machine.js +594 -0
  257. package/.worktrees/8d2d0392/src/progress/step-store.js +265 -0
  258. package/.worktrees/8d2d0392/src/progress.js +1304 -0
  259. package/.worktrees/8d2d0392/src/quick-recommend.js +115 -0
  260. package/.worktrees/8d2d0392/src/quicklog.js +883 -0
  261. package/.worktrees/8d2d0392/src/review-tier.js +77 -0
  262. package/.worktrees/8d2d0392/src/run/command.js +1641 -0
  263. package/.worktrees/8d2d0392/src/run/complete-handlers.js +1552 -0
  264. package/.worktrees/8d2d0392/src/run/complete.js +1259 -0
  265. package/.worktrees/8d2d0392/src/run/concurrent-detect.js +156 -0
  266. package/.worktrees/8d2d0392/src/run/gates.js +1094 -0
  267. package/.worktrees/8d2d0392/src/run/multi-repo-context.js +208 -0
  268. package/.worktrees/8d2d0392/src/run/next.js +128 -0
  269. package/.worktrees/8d2d0392/src/run/prompt.js +843 -0
  270. package/.worktrees/8d2d0392/src/run/quick-audit.js +186 -0
  271. package/.worktrees/8d2d0392/src/run/scan-profile.js +269 -0
  272. package/.worktrees/8d2d0392/src/run/shared.js +1562 -0
  273. package/.worktrees/8d2d0392/src/run/stage.js +587 -0
  274. package/.worktrees/8d2d0392/src/run.js +23 -0
  275. package/.worktrees/8d2d0392/src/scan-diff.js +388 -0
  276. package/.worktrees/8d2d0392/src/scan-postcheck.js +524 -0
  277. package/.worktrees/8d2d0392/src/scan-staleness.js +95 -0
  278. package/.worktrees/8d2d0392/src/setup.js +414 -0
  279. package/.worktrees/8d2d0392/src/sillyhub-mcp/client.js +597 -0
  280. package/.worktrees/8d2d0392/src/sillyhub-mcp/config.js +62 -0
  281. package/.worktrees/8d2d0392/src/spec-dir-typo.js +50 -0
  282. package/.worktrees/8d2d0392/src/spec-sync.js +358 -0
  283. package/.worktrees/8d2d0392/src/stage-contract-engine.js +171 -0
  284. package/.worktrees/8d2d0392/src/stage-contract-spec.js +551 -0
  285. package/.worktrees/8d2d0392/src/stage-contract.js +948 -0
  286. package/.worktrees/8d2d0392/src/stage-review.js +597 -0
  287. package/.worktrees/8d2d0392/src/stages/archive.js +199 -0
  288. package/.worktrees/8d2d0392/src/stages/brainstorm-auto.js +285 -0
  289. package/.worktrees/8d2d0392/src/stages/brainstorm.js +624 -0
  290. package/.worktrees/8d2d0392/src/stages/cmd-existence.js +145 -0
  291. package/.worktrees/8d2d0392/src/stages/doctor.js +540 -0
  292. package/.worktrees/8d2d0392/src/stages/execute.js +1277 -0
  293. package/.worktrees/8d2d0392/src/stages/explore.js +43 -0
  294. package/.worktrees/8d2d0392/src/stages/index.js +29 -0
  295. package/.worktrees/8d2d0392/src/stages/knowledge.js +514 -0
  296. package/.worktrees/8d2d0392/src/stages/plan-postcheck.js +1398 -0
  297. package/.worktrees/8d2d0392/src/stages/plan.js +651 -0
  298. package/.worktrees/8d2d0392/src/stages/quick.js +167 -0
  299. package/.worktrees/8d2d0392/src/stages/scan.js +608 -0
  300. package/.worktrees/8d2d0392/src/stages/status.js +64 -0
  301. package/.worktrees/8d2d0392/src/stages/verify.js +246 -0
  302. package/.worktrees/8d2d0392/src/sync.js +1470 -0
  303. package/.worktrees/8d2d0392/src/task-review.js +1498 -0
  304. package/.worktrees/8d2d0392/src/taskcard-placeholders.js +21 -0
  305. package/.worktrees/8d2d0392/src/taskcard.js +217 -0
  306. package/.worktrees/8d2d0392/src/verify-postcheck.js +1718 -0
  307. package/.worktrees/8d2d0392/src/verify-probes.js +327 -0
  308. package/.worktrees/8d2d0392/src/version.js +19 -0
  309. package/.worktrees/8d2d0392/src/workflow.js +600 -0
  310. package/.worktrees/8d2d0392/src/workspace.js +125 -0
  311. package/.worktrees/8d2d0392/src/worktree-apply.js +1611 -0
  312. package/.worktrees/8d2d0392/src/worktree-deps.js +576 -0
  313. package/.worktrees/8d2d0392/src/worktree.js +1838 -0
  314. package/.worktrees/8d2d0392/templates/claude-instruction.md +23 -0
  315. package/.worktrees/8d2d0392/templates/prompts/taskcard-rules.md +29 -0
  316. package/.worktrees/8d2d0392/templates/prompts/testcase-design.md +12 -0
  317. package/.worktrees/8d2d0392/templates/prompts/verify-probes.md +32 -0
  318. package/.worktrees/8d2d0392/templates/workflows/archive-impact.yaml +79 -0
  319. package/.worktrees/8d2d0392/templates/workflows/scan-docs.yaml +132 -0
  320. package/architecture-analysis.md +448 -0
  321. package/eval/README.md +86 -0
  322. package/eval/eval.config.json +28 -0
  323. package/eval/lib/adapters/cli.mjs +63 -0
  324. package/eval/lib/adapters/dryrun.mjs +27 -0
  325. package/eval/lib/config.mjs +82 -0
  326. package/eval/lib/db.mjs +76 -0
  327. package/eval/lib/report.mjs +120 -0
  328. package/eval/lib/runner.mjs +157 -0
  329. package/eval/lib/tasks.mjs +73 -0
  330. package/eval/lib/util.mjs +107 -0
  331. package/eval/lib/workdir.mjs +30 -0
  332. package/eval/run.mjs +83 -0
  333. package/eval/tasks/001-slugify/files/package.json +6 -0
  334. package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  335. package/eval/tasks/001-slugify/task.json +6 -0
  336. package/eval/tasks/001-slugify/verify.mjs +28 -0
  337. package/eval/tasks/002-each-day/files/package.json +6 -0
  338. package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  339. package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  340. package/eval/tasks/002-each-day/task.json +6 -0
  341. package/eval/tasks/002-each-day/verify.mjs +31 -0
  342. package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  343. package/eval/tasks/003-word-report/files/package.json +6 -0
  344. package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  345. package/eval/tasks/003-word-report/task.json +6 -0
  346. package/eval/tasks/003-word-report/verify.mjs +38 -0
  347. package/package.json +4 -2
  348. package/src/agent-session-log.js +45 -5
  349. package/src/change-risk-profile.js +83 -14
  350. package/src/config-schema.js +18 -3
  351. package/src/contract-matrix.js +90 -29
  352. package/src/datetime.js +20 -0
  353. package/src/decision-distill.js +517 -0
  354. package/src/dispatch/backends/local-agent.js +4 -4
  355. package/src/docs-check.js +227 -0
  356. package/src/docs-debt.js +187 -24
  357. package/src/doctor-diagnostics.js +102 -2
  358. package/src/endpoint-extractor.js +41 -52
  359. package/src/foreign-declared.js +155 -0
  360. package/src/index.js +95 -13
  361. package/src/init.js +9 -7
  362. package/src/knowledge-match.js +107 -6
  363. package/src/local-register.js +7 -1
  364. package/src/module-resolve.js +12 -1
  365. package/src/modules.js +2 -1
  366. package/src/plan-adopt-waves.js +126 -0
  367. package/src/quicklog.js +50 -4
  368. package/src/run/command.js +124 -44
  369. package/src/run/complete-handlers.js +48 -19
  370. package/src/run/complete.js +5 -2
  371. package/src/run/gates.js +104 -22
  372. package/src/run/prompt.js +84 -42
  373. package/src/run/quick-audit.js +11 -0
  374. package/src/run/shared.js +152 -10
  375. package/src/run/stage.js +8 -5
  376. package/src/run.js +2 -2
  377. package/src/scan-postcheck.js +30 -17
  378. package/src/stage-contract-spec.js +3 -2
  379. package/src/stage-contract.js +68 -4
  380. package/src/stages/archive.js +53 -12
  381. package/src/stages/brainstorm-auto.js +50 -1
  382. package/src/stages/brainstorm.js +13 -0
  383. package/src/stages/doctor.js +157 -1
  384. package/src/stages/execute.js +32 -6
  385. package/src/stages/plan-postcheck.js +76 -17
  386. package/src/stages/plan.js +9 -4
  387. package/src/stages/quick.js +6 -1
  388. package/src/stages/verify.js +32 -15
  389. package/src/sync.js +41 -3
  390. package/src/task-review.js +2 -2
  391. package/src/taskcard-placeholders.js +21 -0
  392. package/src/taskcard.js +43 -1
  393. package/src/verify-postcheck.js +315 -13
  394. package/src/verify-probes.js +73 -10
  395. package/src/worktree-apply.js +569 -227
  396. package/src/worktree-deps.js +150 -1
  397. package/src/worktree.js +285 -77
  398. package/templates/prompts/taskcard-rules.md +2 -1
@@ -0,0 +1,948 @@
1
+ /**
2
+ * StageContract — 阶段协议
3
+ *
4
+ * 每个阶段声明:允许的前置阶段、必须的产出、校验器、后续阶段。
5
+ * CLI 不再相信 prompt 完成,completeStep 后必须过 validator。
6
+ */
7
+
8
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs'
9
+ import { join, basename } from 'path'
10
+ import { safeGit } from './git-helper.js'
11
+ import { nowWallClock } from './datetime.js'
12
+ import { detectChangeRisk, checkIntegrationEvidence, VERIFICATION_NEEDS, RISK_LEVEL_CAUSES } from './change-risk-profile.js'
13
+ import { SCAN_REQUIRED_DOCS, AUXILIARY_STAGES } from './constants.js'
14
+ import { evaluateRules } from './stage-contract-engine.js'
15
+ import { getRule } from './stage-contract-spec.js'
16
+ import { backfillFrontmatter } from './scan-postcheck.js'
17
+ import { parseAllowedPaths } from './stages/plan-postcheck.js'
18
+
19
+ /**
20
+ * 校验结果
21
+ * @typedef {{ ok: boolean, errors: string[], warnings: string[] }} ValidationResult
22
+ */
23
+
24
+ /**
25
+ * 阶段合约
26
+ * @typedef {{
27
+ * stage: string,
28
+ * description: string,
29
+ * allowedFrom: string[],
30
+ * allowedTo: string[],
31
+ * validators: Function[],
32
+ * }} StageContract
33
+ */
34
+
35
+ // ============ Validators ============
36
+
37
+ function resolveChangeDir(cwd, changeName, specRoot = null) {
38
+ const changesRoot = specRoot ? join(specRoot, 'changes') : join(cwd, '.sillyspec', 'changes')
39
+ return join(changesRoot, changeName)
40
+ }
41
+
42
+ /**
43
+ * --change 变更名存在性校验(治 cwd 漂移误匹配,缺陷 execute-in-place-windows-pitfalls 坑5)。
44
+ *
45
+ * 多项目 monorepo 下,cwd 在子项目目录会让 resolveSpecDir 命中子项目 spec,
46
+ * 此时 --change 传根项目变更名,旧逻辑(resolveChangeDir 纯拼路径)不报错而静默
47
+ * fallback 误启动子项目流程。本校验对「操作已有变更」的阶段强制 changes/<changeName>
48
+ * 存在,不存在则 fail-fast,把 cwd 漂移暴露给用户。
49
+ *
50
+ * 豁免:
51
+ * - 非 plan/execute/verify/archive 阶段(scan/brainstorm/quick/explore 不校验:
52
+ * brainstorm 可新建变更、quick 用 sessionId、scan/explore 无 change 语义)
53
+ * - quick-<8hex> sessionId(quick 会话 changeName 是 sessionId,不在 changes/)
54
+ *
55
+ * @param {string} specBase - 规范根目录(.sillyspec 或平台 specRoot)
56
+ * @param {string} stageName - 阶段名
57
+ * @param {string} changeName - 变更名(--change 或 auto)
58
+ * @returns {{ changeName: string, specBase: string, message: string } | null} null=通过,对象=失败
59
+ */
60
+ export function validateChangeExists(specBase, stageName, changeName) {
61
+ const STAGES_REQUIRE_EXISTING_CHANGE = new Set(['plan', 'execute', 'verify', 'archive'])
62
+ if (!changeName || !STAGES_REQUIRE_EXISTING_CHANGE.has(stageName)) return null
63
+ if (/^quick-[0-9a-f]{8}$/.test(changeName)) return null
64
+ if (existsSync(join(specBase, 'changes', changeName))) return null
65
+
66
+ // archive 特例:step4 --confirm 已把变更从 changes/<name>/ 移到 changes/archive/<name>/。
67
+ // step5(更新路线图/提交)--change <name> 不应因 changes/<name>/ 已移走而被前置校验误拦。
68
+ if (stageName === 'archive') {
69
+ const archiveRoot = join(specBase, 'changes', 'archive')
70
+ if (existsSync(archiveRoot) && existsSync(join(archiveRoot, changeName))) {
71
+ return null
72
+ }
73
+ }
74
+ return {
75
+ changeName,
76
+ specBase,
77
+ message: `变更 "${changeName}" 在当前 spec 下不存在:${join(specBase, 'changes', changeName)}\n` +
78
+ ` 排查方向(三选一):① 变更名是否打错(sillyspec status 或 progress show 查看活跃变更名);` +
79
+ `② cwd 是否漂到了子项目目录(回到项目根再跑,或加 --spec-dir <根>/.sillyspec);` +
80
+ `③ 是否该先跑 sillyspec run brainstorm 新建该变更。`,
81
+ }
82
+ }
83
+
84
+ function collectIdsFromLine(line, re, ids) {
85
+ for (const match of line.matchAll(re)) {
86
+ ids.add(match[0].toUpperCase())
87
+ }
88
+ }
89
+
90
+ function extractIds(content, prefix) {
91
+ if (!content) return []
92
+ const ids = new Set()
93
+ const idRe = new RegExp(`\\b${prefix}-\\d+(?:@v\\d+)?\\b`, 'gi')
94
+ const headingLineRe = /^\s{0,3}#{1,6}\s+/i
95
+ const fieldLineRe = /^\s*(?:[-*]\s*)?(?:id|decision[-_ ]?ids?|requirement[-_ ]?ids?|covers?|coverage|references?|impacts?|覆盖(?:来源|决策|需求)?)\s*[::]/i
96
+ const tableLineRe = /^\s*\|/
97
+ const listStartsWithIdRe = new RegExp(`^\\s*(?:[-*]|\\d+\\.)\\s*(?:\\[[ xX]\\]\\s*)?${prefix}-\\d+(?:@v\\d+)?\\b`, 'i')
98
+
99
+ for (const line of content.split(/\r?\n/)) {
100
+ if (!headingLineRe.test(line) && !fieldLineRe.test(line) && !tableLineRe.test(line) && !listStartsWithIdRe.test(line)) continue
101
+ collectIdsFromLine(line, idRe, ids)
102
+ }
103
+ return [...ids].sort()
104
+ }
105
+
106
+ function readDecisionField(body, fieldPattern, fallback = '') {
107
+ const re = new RegExp(`^\\s*(?:[-*]\\s*)?(?:${fieldPattern})\\s*[::]\\s*([^\\n]+)`, 'im')
108
+ return (body.match(re)?.[1] || fallback).trim()
109
+ }
110
+
111
+ function buildDecisionRecord(id, body) {
112
+ const status = readDecisionField(body, 'status', 'accepted').toLowerCase()
113
+ const blockerValue = readDecisionField(body, 'blocker', 'false').toLowerCase()
114
+ const blocker = ['true', 'yes', '1'].includes(blockerValue)
115
+ const priorityValue = readDecisionField(body, 'priority|level|severity')
116
+ const priorityMissing = priorityValue.length === 0
117
+ const fallbackPriority = (['unresolved', 'blocking'].includes(status) || blocker) ? 'P1' : 'P2'
118
+ const priority = (priorityValue.match(/P[0-2]/i)?.[0] || fallbackPriority).toUpperCase()
119
+ // 解析 supersedes 字段:记录本条决策取代了哪个旧版本
120
+ const supersedesRaw = readDecisionField(body, 'supersedes', '')
121
+ const supersedes = supersedesRaw
122
+ ? supersedesRaw.split(',').map(s => s.trim().toUpperCase().replace(/['"]/g, '')).filter(Boolean)
123
+ : []
124
+ return { id: id.toUpperCase(), body, status, priority, blocker, priorityMissing, supersedes }
125
+ }
126
+
127
+ function findNextDecisionBoundary(content, startIndex) {
128
+ const boundaryRe = /^(\s{0,3}#{2,6}\s+D-\d+(?:@v\d+)?\b|\s*(?:[-*]\s*)?(?:id|decision[-_ ]?id|decision)\s*[::]\s*D-\d+(?:@v\d+)?\b)/gmi
129
+ boundaryRe.lastIndex = startIndex
130
+ const next = boundaryRe.exec(content)
131
+ return next ? next.index : content.length
132
+ }
133
+
134
+ function isInsideRange(index, ranges) {
135
+ return ranges.some(range => index >= range.start && index < range.end)
136
+ }
137
+
138
+ function parseDecisionRecords(content) {
139
+ if (!content) return []
140
+ const records = []
141
+ const ranges = []
142
+ const headingRe = /^\s{0,3}#{2,6}\s+(D-\d+(?:@v\d+)?)(?:\b|:)[^\n]*$/gmi
143
+ const headings = []
144
+ let match
145
+ while ((match = headingRe.exec(content)) !== null) {
146
+ headings.push({ id: match[1].toUpperCase(), index: match.index, end: headingRe.lastIndex })
147
+ }
148
+ for (let i = 0; i < headings.length; i++) {
149
+ const current = headings[i]
150
+ const next = headings[i + 1]
151
+ const body = content.slice(current.end, next ? next.index : content.length)
152
+ const end = next ? next.index : content.length
153
+ ranges.push({ start: current.index, end })
154
+ records.push(buildDecisionRecord(current.id, body))
155
+ }
156
+
157
+ const idLineRe = /^\s*(?:[-*]\s*)?(?:id|decision[-_ ]?id|decision)\s*[::]\s*(D-\d+(?:@v\d+)?)(?:\b|$)/gmi
158
+ while ((match = idLineRe.exec(content)) !== null) {
159
+ if (isInsideRange(match.index, ranges)) continue
160
+ const bodyEnd = findNextDecisionBoundary(content, idLineRe.lastIndex)
161
+ const body = content.slice(match.index, bodyEnd)
162
+ records.push(buildDecisionRecord(match[1], body))
163
+ }
164
+
165
+ return records
166
+ }
167
+
168
+ /**
169
+ * decisions.md 缺 author/created_at header 时自动补齐(坑 decisions-header-late-warning,
170
+ * 2026-08-24 用户反馈二期:brainstorm step8 模板曾自带无 frontmatter 的 decisions 样例——
171
+ * agent 照抄生成必缺 header,拖到后续环节才提示。模板已修;本函数兜存量:gate 前幂等补
172
+ * 机械字段(author=git user / created_at=now),与 scan-fix-headers 同一 backfillFrontmatter 实现。
173
+ * @param {string} changeDir - changes/<name> 目录
174
+ * @returns {boolean} true = 本次补齐过(false = 无 decisions.md / header 已齐 / 写失败)
175
+ */
176
+ export function ensureDecisionDocHeader(changeDir) {
177
+ try {
178
+ const decisionPath = join(changeDir, 'decisions.md')
179
+ if (!existsSync(decisionPath)) return false
180
+ const content = readFileSync(decisionPath, 'utf8')
181
+ let author = 'unknown'
182
+ const gitUser = safeGit(changeDir, ['config', 'user.name'])
183
+ if (gitUser && gitUser.value) author = String(gitUser.value).trim() || 'unknown'
184
+ const r = backfillFrontmatter(content, { author, createdAt: nowWallClock() })
185
+ if (!r.changed) return false
186
+ writeFileSync(decisionPath, r.content)
187
+ console.log(`ℹ️ 已自动补齐 decisions.md 缺失的 author/created_at header: ${decisionPath}`)
188
+ return true
189
+ } catch { /* 补齐失败不阻断 gate——decisions 内容校验照常进行 */ }
190
+ return false
191
+ }
192
+
193
+ function extractCurrentDecisionIds(content) {
194
+ const records = parseDecisionRecords(content)
195
+ if (records.length === 0) return extractIds(content, 'D')
196
+ // 收集所有被 supersedes 声明取代的旧版本 ID
197
+ const supersededIds = new Set()
198
+ for (const r of records) {
199
+ for (const oldId of r.supersedes) {
200
+ supersededIds.add(oldId)
201
+ }
202
+ }
203
+ return records
204
+ .filter(r => !['superseded', 'rejected'].includes(r.status))
205
+ .filter(r => !supersededIds.has(r.id)) // 被新版本显式取代的旧版本不再校验
206
+ .map(r => r.id)
207
+ .sort()
208
+ }
209
+
210
+ function findBlockingDecisionIssues(content) {
211
+ return parseDecisionRecords(content)
212
+ .filter(r => (r.blocker || ['unresolved', 'blocking'].includes(r.status)) && ['P0', 'P1'].includes(r.priority))
213
+ .map(r => `${r.id} (${r.priority}${r.priorityMissing ? ', priority=missing->P1' : ''}, status=${r.status})`)
214
+ }
215
+
216
+ function readIfExists(file) {
217
+ return existsSync(file) ? readFileSync(file, 'utf8') : ''
218
+ }
219
+
220
+ function warnMissingIds(warnings, ids, targetContent, targetName, sourceName) {
221
+ // 剥版本后缀(@vN)按基号词边界匹配:target 里写裸号 D-001 即视为引用了 D-001@V1
222
+ // 的当前版本(prompt 常裸号引用,旧版字面 includes("D-001@V1") 会批量误报)。
223
+ // 大小写不敏感:target 整体大写后比对。
224
+ const targetUpper = targetContent.toUpperCase()
225
+ for (const id of ids) {
226
+ const base = id.replace(/@V\d+$/, '')
227
+ const re = new RegExp(`\\b${base}\\b`)
228
+ if (!re.test(targetUpper)) {
229
+ warnings.push(getRule('shared.id-traceability').failMessage.replaceAll('${target}', targetName).replaceAll('${source}', sourceName).replaceAll('${id}', id))
230
+ }
231
+ }
232
+ }
233
+
234
+ /**
235
+ * scan 完成校验:检查 7 份 scan 文档 + manifest
236
+ */
237
+ function validateScanOutputs(cwd, changeName, context = {}) {
238
+ const { projectName, specRoot } = context
239
+ // 平台模式:specRoot 直接是规范目录(含 docs/)
240
+ // 本地模式:规范目录是 cwd/.sillyspec
241
+ // 不用 isSpecDir 启发式猜测——很多项目根目录有自己的 docs/,会误判
242
+ const specBase = specRoot || join(cwd, '.sillyspec')
243
+ const docsRoot = projectName
244
+ ? join(specBase, 'docs', projectName, 'scan')
245
+ : join(specBase, 'docs', 'scan')
246
+
247
+ // 7 份 scan 文档存在性由引擎消费 manifest(与 SCAN_REQUIRED_DOCS 单源,文案/事前契约同源)
248
+ const engineResult = evaluateRules('scan', { docsRoot })
249
+ const errors = [...engineResult.errors]
250
+ const warnings = [...engineResult.warnings]
251
+
252
+ // 检查 modules 目录
253
+ const modulesRoot = projectName
254
+ ? join(specBase, 'docs', projectName, 'modules')
255
+ : join(specBase, 'docs', 'modules')
256
+ if (!existsSync(modulesRoot)) {
257
+ warnings.push('modules 目录不存在')
258
+ } else {
259
+ const modules = readdirSync(modulesRoot).filter(f => f.endsWith('.md'))
260
+ if (modules.length === 0) {
261
+ warnings.push('modules 目录为空')
262
+ }
263
+ }
264
+
265
+ return { ok: errors.length === 0, errors, warnings }
266
+ }
267
+
268
+ /**
269
+ * brainstorm 完成校验:检查四件套规范文件是否生成
270
+ */
271
+ function validateBrainstormOutputs(cwd, changeName, context = {}) {
272
+ const { specRoot } = context
273
+ const changesRoot = specRoot ? join(specRoot, 'changes') : join(cwd, '.sillyspec', 'changes')
274
+ if (specRoot && !existsSync(changesRoot)) {
275
+ return { ok: false, errors: [`平台模式 specRoot 缺少 changes 目录: ${changesRoot}`], warnings: [] }
276
+ }
277
+ const changeDir = resolveChangeDir(cwd, changeName, specRoot)
278
+ // 读 design.md frontmatter scale(若有)传入引擎 ctx,供 BRAINSTORM_RULES 的 condition 判定:
279
+ // scale=small → proposal/requirements/tasks 三规则跳过(小变更只产 design.md,与末步 prompt 一致,
280
+ // 避免"照 Step8 small 指引只写 design.md 后 --done 必撞四件套 error墙")。
281
+ // fail-safe:design.md 不存在或无 scale → null → condition(ne 'small')成立 → 四件套全要求(保守走重流程)。
282
+ const designPath = join(changeDir, 'design.md')
283
+ let scale = null
284
+ let designContent = null
285
+ if (existsSync(designPath)) {
286
+ designContent = readFileSync(designPath, 'utf8')
287
+ const fm = designContent.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
288
+ const sm = fm && fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
289
+ if (sm) scale = sm[1]
290
+ }
291
+ // 纯 kind 规则(四件套存在性 + proposal/requirements/design 章节 + tasks 列表)由引擎消费
292
+ // stage-contract-spec.js manifest,与报错文案 + prompt 事前契约严格同源(事前给的 == 事后查的)。
293
+ // custom kind(lifecycle / decisions)保留下方旧逻辑,数据/文案在 Batch 3 迁入 manifest。
294
+ const engineResult = evaluateRules('brainstorm', { changeDir, scale })
295
+ const errors = [...engineResult.errors]
296
+ const warnings = [...engineResult.warnings]
297
+
298
+ if (designContent) {
299
+ const content = designContent
300
+
301
+ // lifecycle-exemption(custom kind):判定算法保留(trigger/exemption/table 三段短路),
302
+ // data + failMessage/exemptionPassedMessage 从 stage-contract-spec.js manifest 同源。
303
+ const lcRule = getRule('brainstorm.design.lifecycle-table')
304
+ const { trigger: lcTrigger, exemptions: lcExempts, table: lcTable } = lcRule.data
305
+ if (new RegExp(lcTrigger.pattern, lcTrigger.flags).test(content)) {
306
+ const declaresNotApplicable = lcExempts.some(e => new RegExp(e.pattern, e.flags).test(content))
307
+ if (declaresNotApplicable) {
308
+ warnings.push(lcRule.exemptionPassedMessage)
309
+ } else {
310
+ const hasLifecycleTable = lcTable.some(t => new RegExp(t.pattern, t.flags).test(content))
311
+ if (!hasLifecycleTable) {
312
+ errors.push(lcRule.failMessage)
313
+ }
314
+ }
315
+ }
316
+
317
+ // ── 风险判级提前提示(坑 risk-first-use-opaque,2026-08-24 用户实证)──
318
+ // verify --done 的 risk gate 既往只在流程末段暴露,首次使用者撞墙后才知道 frontmatter
319
+ // risk_level 覆盖通道。design --done 即按同款 detectChangeRisk 预判一次(此时 plan.md 尚未
320
+ // 产出,verify 时会连 plan 一起重判、级别可能上浮——本提示 advisory 不作为定论):
321
+ // 高危关键词命中 / 存在否定抑制 → 透出判级结果 + 覆盖指引,把机制可见性提前到 design 阶段。
322
+ {
323
+ const profile = detectChangeRisk({ designContent: content })
324
+ if (['integration-critical', 'deployment-critical'].includes(profile.level) && !profile.explicit) {
325
+ warnings.push(
326
+ `[risk] 本次 design 判级 ${profile.level}(命中关键词:${profile.triggers.join('、')})——verify --done 将强制真实集成证据门控。` +
327
+ `若属关键词误伤(实际未触碰 daemon/session/启动入口),现在就在 design.md frontmatter 加 risk_level: <真实等级>(如 unit-sufficient)显式覆盖,免得到 verify 阶段才发现而返工。`
328
+ )
329
+ }
330
+ if ((profile.suppressedTriggers || []).length > 0) {
331
+ warnings.push(
332
+ `[risk] 关键词命中 ${profile.suppressedTriggers.join('、')} 已被同句否定语境抑制,当前判级 ${profile.level}。` +
333
+ `若实际涉及对应改动,请在 design.md frontmatter 声明高危级(抑制可审计,防静默降级逃证据门控)。`
334
+ )
335
+ }
336
+ }
337
+ }
338
+
339
+ const decisionsFile = join(changeDir, 'decisions.md')
340
+ if (existsSync(decisionsFile)) {
341
+ const decisions = readFileSync(decisionsFile, 'utf8')
342
+ const blockers = findBlockingDecisionIssues(decisions)
343
+ for (const issue of blockers) {
344
+ errors.push(getRule('shared.decision-blocker').failMessage.replace('${issue}', issue))
345
+ }
346
+ const decisionIds = extractCurrentDecisionIds(decisions)
347
+ if (decisionIds.length === 0) {
348
+ warnings.push('decisions.md 存在但没有当前版本 D-xxx@vN 决策 ID')
349
+ } else {
350
+ const design = readIfExists(join(changeDir, 'design.md'))
351
+ const requirements = readIfExists(join(changeDir, 'requirements.md'))
352
+ const tasks = readIfExists(join(changeDir, 'tasks.md'))
353
+ // decision 的天然引用落点是 design.md;requirements(需求按 FR 组织)与
354
+ // tasks(骨架,待 plan 展开)不强求逐条引用每个架构决策,否则批量误报。
355
+ warnMissingIds(warnings, decisionIds, design, 'design.md', 'decisions.md')
356
+ }
357
+ }
358
+
359
+ return { ok: errors.length === 0, errors, warnings }
360
+ }
361
+
362
+ /**
363
+ * plan 完成校验:检查 plan.md 生成
364
+ */
365
+ function validatePlanOutputs(cwd, changeName, context = {}) {
366
+ const { specRoot } = context
367
+ const changeDir = resolveChangeDir(cwd, changeName, specRoot)
368
+ const planFile = join(changeDir, 'plan.md')
369
+
370
+ // 读 design.md frontmatter scale(若有)传入引擎 ctx,供 plan.module-impact.exists 的 condition
371
+ // (scale≠small)判定——large 要求 module-impact 首版,small 豁免(走 quick)。fail-safe:无 scale→null→
372
+ // condition(ne 'small')成立→保守要求 module-impact(同 brainstorm scale 读取模式,line 264-272)。
373
+ const designPathForScale = join(changeDir, 'design.md')
374
+ let scale = null
375
+ if (existsSync(designPathForScale)) {
376
+ const fm = readFileSync(designPathForScale, 'utf8').match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
377
+ const sm = fm && fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
378
+ if (sm) scale = sm[1]
379
+ }
380
+ // plan.md 存在性由引擎消费 manifest。entryPoint/id-trace/decisions 为 custom kind(下方保留)。
381
+ const engineResult = evaluateRules('plan', { changeDir, scale })
382
+ const errors = [...engineResult.errors]
383
+ const warnings = [...engineResult.warnings]
384
+
385
+ if (existsSync(planFile)) {
386
+ const plan = readFileSync(planFile, 'utf8')
387
+ const requirements = readIfExists(join(changeDir, 'requirements.md'))
388
+ const requirementIds = extractIds(requirements, 'FR')
389
+ warnMissingIds(warnings, requirementIds, plan, 'plan.md', 'requirements.md')
390
+
391
+ const decisions = readIfExists(join(changeDir, 'decisions.md'))
392
+ const blockers = findBlockingDecisionIssues(decisions)
393
+ for (const issue of blockers) {
394
+ errors.push(getRule('shared.decision-blocker').failMessage.replace('${issue}', issue))
395
+ }
396
+ const decisionIds = extractCurrentDecisionIds(decisions)
397
+ warnMissingIds(warnings, decisionIds, plan, 'plan.md', 'decisions.md')
398
+ }
399
+ // ── P0: 生产接线路径检查:design 提到入口但 task 的 allowed_paths 不含入口文件 ──
400
+ // entry-point-wiring(custom):trigger/file 抽取/exemption/failMessage 从 manifest 同源
401
+ // (stage-contract-spec.js),保留多源 allowed_paths 收集 + 逐文件对账 + 豁免算法。
402
+ const designContent = readIfExists(join(changeDir, 'design.md'))
403
+ if (designContent) {
404
+ const epRule = getRule('plan.entry-point-wiring')
405
+ const { entryPointPatterns, fileExtractionPattern, exemptionPattern } = epRule.data
406
+ const fileExtractRe = new RegExp(fileExtractionPattern.pattern, fileExtractionPattern.flags)
407
+ const mentionedFiles = new Set()
408
+ for (const ep of entryPointPatterns) {
409
+ const pattern = new RegExp(ep.pattern, ep.flags)
410
+ pattern.lastIndex = 0
411
+ for (const match of designContent.matchAll(pattern)) {
412
+ const fileMatch = match[0].match(fileExtractRe)
413
+ if (fileMatch) mentionedFiles.add(fileMatch[1].toLowerCase())
414
+ }
415
+ }
416
+ if (mentionedFiles.size > 0) {
417
+ const tasksDir = join(changeDir, 'tasks')
418
+ const allAllowedPaths = new Set()
419
+ if (existsSync(tasksDir)) {
420
+ const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/i.test(f))
421
+ for (const taskFile of taskFiles) {
422
+ const taskContent = readFileSync(join(tasksDir, taskFile), 'utf8')
423
+ // 复用 parseAllowedPaths(坑6①:认 inline 数组/缩进块/顶格块 + 剥反引号 + CRLF 归一,
424
+ // 消除此处内联正则与 plan-postcheck 的解析漂移)。卡片无 frontmatter 时退全文扫描
425
+ // (包裹伪 frontmatter 复用同一解析器)——原内联正则是全文匹配语义,entry-point-wiring
426
+ // 只关心路径覆盖,卡片格式合法性归 plan-postcheck 把关,此处不收紧。
427
+ const paths = parseAllowedPaths(taskContent)
428
+ const effective = paths.length > 0 ? paths : parseAllowedPaths(`---\n${taskContent}\n---`)
429
+ for (const p of effective) allAllowedPaths.add(p.toLowerCase())
430
+ }
431
+ }
432
+ // 也从 plan.md 文件变更清单中收集
433
+ if (existsSync(planFile)) {
434
+ const planContent = readFileSync(planFile, 'utf8')
435
+ const planFileChanges = planContent.match(/\|\s*(?:新增|修改|new|modify|update)\s*\|\s*`?([^`|]+)`?\s*\|/gi) || []
436
+ for (const line of planFileChanges) {
437
+ const file = line.match(/\|\s*(?:新增|修改|new|modify|update)\s*\|\s*`?([^`|]+)`?\s*\|/i)
438
+ if (file) allAllowedPaths.add(file[1].trim().toLowerCase())
439
+ }
440
+ }
441
+ for (const mentionedFile of mentionedFiles) {
442
+ const found = [...allAllowedPaths].some(p => p.includes(mentionedFile))
443
+ if (!found) {
444
+ const noChangePattern = new RegExp(exemptionPattern.replaceAll('${file}', mentionedFile), 'i')
445
+ if (!noChangePattern.test(designContent)) {
446
+ errors.push(epRule.failMessage.replaceAll('${file}', mentionedFile))
447
+ }
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ return { ok: errors.length === 0, errors, warnings }
454
+ }
455
+ /**
456
+ * 从 verify-result.md 提取结论关键词(PASS / PASS WITH NOTES / FAIL)。
457
+ * 标题放宽:含「结论/Conclusion/Result/结果」的二级标题均可(B3c),
458
+ * PASS/FAIL 可在标题行本身(如「## 验收结论:✅ PASS」)或紧邻标题的正文里。
459
+ * 历史教训:原正则锚定确切「## 结论」,用户写「## 验收结论:✅ PASS」不被识别。
460
+ */
461
+ function extractVerifyConclusion(verify) {
462
+ const headingRe = /^##\s[^\n]*(?:结论|conclusion|result|结果)/im
463
+ const headingMatch = verify.match(headingRe)
464
+ if (!headingMatch) return ''
465
+ const start = headingMatch.index
466
+ const slice = verify.slice(start, start + 400)
467
+ const kw = slice.match(/\b(PASS(?:\s+WITH\s+NOTES)?|FAIL)\b/i)
468
+ return kw ? kw[1].toUpperCase().replace(/\s+/g, ' ') : ''
469
+ }
470
+
471
+ function validateVerifyOutputs(cwd, changeName, context = {}) {
472
+ const { specRoot } = context
473
+ const changeDir = resolveChangeDir(cwd, changeName, specRoot)
474
+
475
+ if (!existsSync(changeDir)) {
476
+ return { ok: false, errors: [`变更目录缺失: ${changeDir}`], warnings: [] }
477
+ }
478
+
479
+ // 核心文档存在性(verify-result.md / design.md / plan.md)由引擎消费 manifest。
480
+ // verify-result.md 不存在则不能完成——历史教训:AI 可能跳过报告直接 --done 导致"假完成"。
481
+ const engineResult = evaluateRules('verify', { changeDir })
482
+ const errors = [...engineResult.errors]
483
+ const warnings = [...engineResult.warnings]
484
+
485
+ const verifyResult = join(changeDir, 'verify-result.md')
486
+ if (existsSync(verifyResult)) {
487
+ const verify = readFileSync(verifyResult, 'utf8')
488
+ const decisions = readIfExists(join(changeDir, 'decisions.md'))
489
+ const blockers = findBlockingDecisionIssues(decisions)
490
+ for (const issue of blockers) {
491
+ errors.push(getRule('shared.decision-blocker').failMessage.replace('${issue}', issue))
492
+ }
493
+ const decisionIds = extractCurrentDecisionIds(decisions)
494
+ warnMissingIds(warnings, decisionIds, verify, 'verify-result.md', 'decisions.md')
495
+
496
+ // ── FAIL 结论门控(适用于所有变更,不限风险等级)──
497
+ // verify-result.md 结论为 FAIL 时,verify 阶段不能 completed。
498
+ // 历史教训:CLI 曾不校验结论,AI 写 FAIL 后 verify 仍被标记完成并提示"验证通过可以归档"。
499
+ const conclusionStr = extractVerifyConclusion(verify)
500
+ if (conclusionStr === 'FAIL') {
501
+ errors.push(getRule('verify.conclusion.fail-gate').failMessage)
502
+ } else if (!conclusionStr) {
503
+ warnings.push(getRule('verify.conclusion.fail-gate').noConclusionWarning)
504
+ }
505
+
506
+ // ── P0: Change Risk Gate — 核心功能缺少真实集成验证时 FAIL ──
507
+ const changeRiskProfile = detectChangeRisk({
508
+ designContent: readIfExists(join(changeDir, 'design.md')),
509
+ planContent: readIfExists(join(changeDir, 'plan.md')),
510
+ })
511
+ const conclusion = extractVerifyConclusion(verify)
512
+ // ── 否定抑制审计(坑 risk-negation-blindness)──
513
+ // 关键词命中被同句否定语境抑制而不参与判级时必须透出:抑制是把双刃剑(「本次不新增
514
+ // daemon」真豁免 vs 靠堆否定措辞静默降级逃证据门控),这里让每次抑制可见可核对;
515
+ // frontmatter risk_level 仍是双向权威覆盖通道。
516
+ if ((changeRiskProfile.suppressedTriggers || []).length > 0) {
517
+ warnings.push(
518
+ `[${changeRiskProfile.level}] 风险判级:关键词命中 ${changeRiskProfile.suppressedTriggers.join(', ')} 已被同句否定语境(不/未/无/避免…)抑制,未参与判级。` +
519
+ `若本变更实际涉及跨进程/状态机/启动入口改动,请在 design.md frontmatter 加 risk_level: integration-critical(或 deployment-critical)显式声明或提供真实集成证据——抑制可审计,不许用来静默降级。`
520
+ )
521
+ }
522
+ if (['integration-critical', 'deployment-critical'].includes(changeRiskProfile.level)) {
523
+ // 关键词误伤早期引导(坑2,FR-02):命中高危关键词且无 frontmatter risk_level 时,
524
+ // 无条件透出 frontmatter 覆盖指引(不依赖 conclusion / evidence),让 agent 早期即可
525
+ // 判断是否属误判并显式覆盖,而非撞到 evidence gate 末尾才发现出路③。含否定语境的
526
+ // 命中已在 detectChangeRisk 内被同句否定抑制(见 change-risk-profile.js NEGATION_CUES),
527
+ // 此处命中的都是无否定语境的真命中;仍误伤的走 frontmatter 显式覆盖通道。
528
+ if (!changeRiskProfile.explicit) {
529
+ warnings.push(
530
+ `[${changeRiskProfile.level}] 本次变更被关键词判级(命中:${changeRiskProfile.triggers.join(', ')})。` +
531
+ `若属关键词误伤(实际未触碰 daemon/session/启动入口/跨进程),可在 design.md frontmatter 加 risk_level: <真实等级>(如 unit-sufficient)显式覆盖后重跑。`
532
+ )
533
+ }
534
+ // 显式 risk_level 声明(design frontmatter,非关键词误判)下,PASS WITH NOTES 视为对残留项的
535
+ // 诚实声明,不强求全量集成证据——豁免本就来自 design 的明确判断。自动关键词判级维持严格:
536
+ // PASS WITH NOTES 仍要求证据齐全,防 agent 用 PASS WITH NOTES 绕证据门控。
537
+ const requiresEvidence = conclusion === 'PASS' || (conclusion === 'PASS WITH NOTES' && !changeRiskProfile.explicit)
538
+ if (conclusion === 'PASS WITH NOTES' && changeRiskProfile.explicit) {
539
+ warnings.push(`[${changeRiskProfile.level}] 结论 PASS WITH NOTES:design frontmatter 显式声明 risk_level=${changeRiskProfile.level},残留项须为真实集成证据缺口,并在 verify-result.md 如实说明`)
540
+ }
541
+ if (requiresEvidence) {
542
+ // CLI 回执注入(坑 verify-literal-evidence-mismatch,2026-08-22 实证:证据第一轮就齐
543
+ // 但自然措辞不含字面词被误拦三轮):verify 服务回收器落的回执 = 真实启动+PID 登记
544
+ // 的结构化证据,作为附加匹配文本——有回执不依赖 agent 措辞;无回执维持原字面匹配
545
+ let receiptText = ''
546
+ try {
547
+ // 回执按变更分片(坑 verify-pids-cross-session-kill:原单份被并行会话后写覆盖),
548
+ // 优先读本变更分片,兼容旧单份名(升级过渡期 receipt.change 过滤仍生效)
549
+ const runtimeDir = join(cwd, '.sillyspec', '.runtime')
550
+ const receiptPath = existsSync(join(runtimeDir, `verify-services-${changeName}.receipt.json`))
551
+ ? join(runtimeDir, `verify-services-${changeName}.receipt.json`)
552
+ : join(runtimeDir, 'verify-services.receipt.json')
553
+ if (existsSync(receiptPath)) {
554
+ const receipt = JSON.parse(readFileSync(receiptPath, 'utf8'))
555
+ if (receipt && (receipt.reapedPidCount || 0) > 0 && (!receipt.change || receipt.change === changeName)) {
556
+ receiptText = `CLI 回执:verify 服务进程已回收 ${receipt.reapedPidCount} 个(PID 已登记,真实启动,运行时证据 reapedAt=${receipt.reapedAt})`
557
+ }
558
+ }
559
+ } catch { /* 回执损坏按无回执处理(fail 回原字面匹配) */ }
560
+ const evidenceCheck = checkIntegrationEvidence(verify, changeRiskProfile.requiredVerification, { extraEvidenceText: receiptText })
561
+ if (!evidenceCheck.ok) {
562
+ // A: 报错说人话 —— 把「缺哪一项、要写/做什么、判级原因」逐条列出,
563
+ // 让 agent 不必靠改结论文案撞墙。detail 指明真实启动须是本变更实际改动的
564
+ // 部署/启动入口(非无关进程),以及每项的字面期望。
565
+ const needs = changeRiskProfile.requiredVerification
566
+ .filter(k => VERIFICATION_NEEDS[k] && VERIFICATION_NEEDS[k].desc)
567
+ .map(k => {
568
+ const vn = VERIFICATION_NEEDS[k]
569
+ const lit = vn.literals && vn.literals.length ? '字面命中其一:' + vn.literals.join(' / ') : ''
570
+ return `\n 〔${k}〕${vn.desc}${lit}`
571
+ })
572
+ .join('')
573
+ const cause = changeRiskProfile.explicit
574
+ ? RISK_LEVEL_CAUSES.explicit
575
+ : (RISK_LEVEL_CAUSES[changeRiskProfile.level] || '')
576
+ errors.push(
577
+ `[${changeRiskProfile.level}] 验证结论为 ${conclusion},但缺少真实集成证据。\n` +
578
+ ` 缺失项(需在 verify-result.md 如实提供并满足):${evidenceCheck.errors.join('; ')}\n` +
579
+ ` 每项要提供什么:${needs}\n` +
580
+ ` 风险判级原因:${cause}\n` +
581
+ ` 命中触发词:${changeRiskProfile.triggers.join(', ')}\n` +
582
+ ` 出路:① 补全上述缺失的真实集成证据(真实启动 daemon/backend、集成测试、运行日志)后保持 PASS;` +
583
+ `或 ② 如实改结论 FAIL(承认端到端未验,留待部署后补);` +
584
+ `或 ③ 若判级是关键词误伤(实际并未触碰 daemon/session/启动入口),在 design.md frontmatter 加 risk_level: <真实等级>(如 unit-sufficient)显式覆盖后重跑。` +
585
+ `仅改结论文案/措辞蹭字面关键词会被对账。`
586
+ )
587
+ }
588
+ warnings.push(...evidenceCheck.warnings)
589
+ }
590
+ }
591
+ }
592
+
593
+ return { ok: errors.length === 0, errors, warnings }
594
+ }
595
+
596
+ /**
597
+ * 计算归档目标目录名:保持原变更名不变,直接移入 archive/。
598
+ */
599
+ export function archiveDestDirName(date, changeName) {
600
+ return String(changeName)
601
+ }
602
+
603
+ /**
604
+ * archive 完成校验:检查归档目录完整性
605
+ */
606
+ function validateArchiveOutputs(cwd, changeName) {
607
+ const archiveDir = join(cwd, '.sillyspec', 'changes', 'archive')
608
+ const destDir = join(archiveDir, changeName)
609
+
610
+ // 归档目录不存在 early return(引擎在存在时才跑)
611
+ if (!existsSync(destDir)) {
612
+ return { ok: false, errors: [`归档目录缺失: ${destDir}`], warnings: [] }
613
+ }
614
+
615
+ // plan.md(必备)/design.md/module-impact.md(推荐)存在性由引擎消费 manifest
616
+ const engineResult = evaluateRules('archive', { archiveDir: destDir }, undefined, { source: 'validateArchiveOutputs' })
617
+ return { ok: engineResult.errors.length === 0, errors: engineResult.errors, warnings: engineResult.warnings }
618
+ }
619
+
620
+ /**
621
+ * archive 前置校验:所有主流程阶段完成
622
+ */
623
+ function validateChangeClosed(cwd, changeName) {
624
+ const errors = []
625
+ const warnings = []
626
+
627
+ // 检查前置阶段状态
628
+ const progressDir = join(cwd, '.sillyspec', '.runtime')
629
+ // 这里只做文件层面的检查,DB 检查在 run.js 里做
630
+ const changeDir = join(cwd, '.sillyspec', 'changes', changeName)
631
+ if (!existsSync(changeDir)) {
632
+ errors.push(`变更目录不存在: ${changeDir}`)
633
+ return { ok: false, errors, warnings }
634
+ }
635
+
636
+ // plan.md 存在性由引擎消费 manifest(source 区分 validateChangeClosed vs validateArchiveOutputs)
637
+ const engineResult = evaluateRules('archive', { changeDir }, undefined, { source: 'validateChangeClosed' })
638
+ return { ok: engineResult.errors.length === 0, errors: engineResult.errors, warnings: engineResult.warnings }
639
+ }
640
+
641
+ // ============ Execute 代码变更客观核验 ============
642
+
643
+ // QUAL-01 收口:本地 execFileSync 裸调(无 safe.directory)→ git-helper safeGit 统一入口,
644
+ // {ok,out,error} 适配语义与 timeout 15000 不变
645
+ function gitTry(dir, args) {
646
+ const r = safeGit(dir, args, { timeout: 15000 })
647
+ if (r.value !== null) return { ok: true, out: r.value }
648
+ return { ok: false, out: '', error: r.error || 'git 执行失败' }
649
+ }
650
+
651
+ /**
652
+ * 客观核验 execute 阶段是否产生了真实代码变更。
653
+ *
654
+ * 历史漏洞:execute 无 stage-level validator,agent 勾选 plan.md 全部 checkbox
655
+ * 即可让 execute 被标 completed,代码完成度与真实变更完全脱钩。
656
+ *
657
+ * 判定顺序(fail-open on uncertainty,避免环境差异误杀):
658
+ * 1. worktree meta 存在且有 baseHash → 在 worktree 内查 baseHash..HEAD diff + 未提交改动
659
+ * 2. sillyspec/<change> 分支存在 → 查 merge-base..branch diff
660
+ * 3. 主工作区存在未提交改动(apply 后未 commit 的常见形态)→ changed
661
+ * 4. 均无法判定 → unknown(由调用方降级为 warning)
662
+ *
663
+ * @returns {{ status: 'changed'|'unchanged'|'unknown', detail: string }}
664
+ */
665
+ export function checkExecuteCodeEvidence(cwd, changeName) {
666
+ const metaPath = join(cwd, '.sillyspec', '.runtime', 'worktrees', changeName, 'meta.json')
667
+ let meta = null
668
+ if (existsSync(metaPath)) {
669
+ try { meta = JSON.parse(readFileSync(metaPath, 'utf8')) } catch {}
670
+ }
671
+
672
+ // 1. worktree(或 in-place)meta 有 baseHash:最权威的对账基准
673
+ if (meta?.baseHash) {
674
+ const gitDir = (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
675
+ ? meta.worktreePath
676
+ : cwd
677
+ const diff = gitTry(gitDir, ['diff', '--name-only', `${meta.baseHash}..HEAD`])
678
+ const status = gitTry(gitDir, ['status', '--porcelain'])
679
+ if (diff.ok && status.ok) {
680
+ const committedFiles = diff.out ? diff.out.split('\n').filter(Boolean).length : 0
681
+ const hasUncommitted = status.out.trim().length > 0
682
+ if (committedFiles > 0 || hasUncommitted) {
683
+ return { status: 'changed', detail: `${committedFiles} 个已提交变更文件${hasUncommitted ? ' + 未提交改动' : ''}(base ${meta.baseHash.slice(0, 8)})` }
684
+ }
685
+ return { status: 'unchanged', detail: `${gitDir} 相对 base ${meta.baseHash.slice(0, 8)} 无任何提交或未提交改动` }
686
+ }
687
+ return { status: 'unknown', detail: `git 核验失败: ${diff.error || status.error}` }
688
+ }
689
+
690
+ // 2. worktree 已清理但分支残留:diff merge-base..branch
691
+ const branch = `sillyspec/${changeName}`
692
+ const branchHash = gitTry(cwd, ['rev-parse', '--verify', '--quiet', branch])
693
+ if (branchHash.ok && branchHash.out) {
694
+ const mergeBase = gitTry(cwd, ['merge-base', 'HEAD', branch])
695
+ if (mergeBase.ok && mergeBase.out) {
696
+ const diff = gitTry(cwd, ['diff', '--name-only', `${mergeBase.out}..${branch}`])
697
+ if (diff.ok && diff.out.split('\n').filter(Boolean).length > 0) {
698
+ return { status: 'changed', detail: `分支 ${branch} 相对 merge-base 有变更` }
699
+ }
700
+ }
701
+ }
702
+
703
+ // 3. 主工作区未提交改动(worktree apply 后的常见形态)
704
+ const status = gitTry(cwd, ['status', '--porcelain'])
705
+ if (status.ok && status.out.trim()) {
706
+ return { status: 'changed', detail: '主工作区存在未提交改动' }
707
+ }
708
+
709
+ return { status: 'unknown', detail: '无 worktree meta 且无可对账的分支/未提交改动(变更可能已 apply 并提交),无法客观判定' }
710
+ }
711
+
712
+ /**
713
+ * execute 完成校验:plan.md 声明了任务时,必须存在真实代码变更。
714
+ * 防止"勾选 checkbox = 完成 execute"的谎报路径。
715
+ */
716
+ function validateExecuteOutputs(cwd, changeName, context = {}) {
717
+ const { specRoot, evidence: passedEvidence } = context
718
+ const errors = []
719
+ const warnings = []
720
+
721
+ const changeDir = resolveChangeDir(cwd, changeName, specRoot)
722
+ const planPath = join(changeDir, 'plan.md')
723
+ // 无 plan.md 的 execute(旧流程/quick 混用)不在此核验范围
724
+ if (!existsSync(planPath)) return { ok: true, errors, warnings }
725
+
726
+ const planContent = readFileSync(planPath, 'utf8')
727
+ const hasTasks = /^\s*[-*]\s*\[[ xX]\]\s*task-\d+/m.test(planContent)
728
+ if (!hasTasks) return { ok: true, errors, warnings }
729
+
730
+ // W4-G (D-008):evidence 可由调用方(runGate)预先算好传入,避免与 execute-evidence check
731
+ // 重复 spawn git(gate execute 一次省 2 个 git 进程 ≈ 60-200ms on Windows)。未传则内部算(向后兼容)。
732
+ const evidence = passedEvidence || checkExecuteCodeEvidence(cwd, changeName)
733
+ if (evidence.status === 'unchanged') {
734
+ errors.push(`execute 代码变更核验失败:plan.md 声明了任务,但 ${evidence.detail} — 勾选 checkbox 不等于完成实现`)
735
+ } else if (evidence.status === 'unknown') {
736
+ warnings.push(`execute 代码变更无法客观核验:${evidence.detail}`)
737
+ }
738
+
739
+ return { ok: errors.length === 0, errors, warnings }
740
+ }
741
+
742
+ // ============ Contract Registry ============
743
+
744
+ /**
745
+ * 主流程阶段(有严格转换顺序)
746
+ */
747
+ const mainFlowStages = ['brainstorm', 'plan', 'execute', 'verify']
748
+
749
+ // 辅助阶段(可独立运行,无严格转换顺序)定义在 constants.js 的 AUXILIARY_STAGES,
750
+ // 与 stages/index.js 共用单一真相源,避免两处逐字重复分叉。
751
+
752
+ /**
753
+ * @type {Object<string, StageContract>}
754
+ */
755
+ const contracts = {
756
+ // === 主流程 ===
757
+ brainstorm: {
758
+ stage: 'brainstorm',
759
+ description: '需求分析与设计',
760
+ allowedFrom: [], // 任何变更的起始阶段
761
+ allowedTo: ['plan'],
762
+ validators: [validateBrainstormOutputs],
763
+ },
764
+ plan: {
765
+ stage: 'plan',
766
+ description: '任务拆解与规划',
767
+ allowedFrom: ['brainstorm'],
768
+ allowedTo: ['execute'],
769
+ validators: [validatePlanOutputs],
770
+ },
771
+ execute: {
772
+ stage: 'execute',
773
+ description: '代码实现',
774
+ allowedFrom: ['plan'],
775
+ allowedTo: ['verify'],
776
+ validators: [validateExecuteOutputs],
777
+ },
778
+ verify: {
779
+ stage: 'verify',
780
+ description: '验证与测试',
781
+ allowedFrom: ['execute'],
782
+ allowedTo: ['archive'],
783
+ validators: [validateVerifyOutputs],
784
+ },
785
+ archive: {
786
+ stage: 'archive',
787
+ description: '归档与收口',
788
+ allowedFrom: ['verify'],
789
+ allowedTo: [],
790
+ // 阶段级 validator 全部移除,改为 run.js 中 step 4 完成后的硬编码校验。
791
+ // 理由:两个 validator 的生效窗口互斥 ——
792
+ // validateChangeClosed 要求变更目录存在(step 4 --confirm 后已被移到 archive 目录)
793
+ // validateArchiveOutputs 要求 archive 目录存在(step 4 前还不存在)
794
+ // 注册为阶段级 validator 会导致每步都误报错误。
795
+ // run.js:893-909 已在正确的时机(step 4 完成后)执行相同检查。
796
+ validators: [],
797
+ },
798
+
799
+ // === 辅助阶段 ===
800
+ scan: {
801
+ stage: 'scan',
802
+ description: '项目扫描',
803
+ allowedFrom: [], // 无前置要求
804
+ allowedTo: [], // 不进入主流程
805
+ validators: [validateScanOutputs],
806
+ },
807
+ quick: {
808
+ stage: 'quick',
809
+ description: '快速任务',
810
+ allowedFrom: [], // 无前置要求
811
+ allowedTo: [], // 不进入主流程
812
+ validators: [],
813
+ },
814
+ explore: {
815
+ stage: 'explore',
816
+ description: '代码探索',
817
+ allowedFrom: [],
818
+ allowedTo: [],
819
+ validators: [],
820
+ },
821
+ status: {
822
+ stage: 'status',
823
+ description: '项目快照',
824
+ allowedFrom: [],
825
+ allowedTo: [],
826
+ validators: [],
827
+ },
828
+ doctor: {
829
+ stage: 'doctor',
830
+ description: '环境诊断',
831
+ allowedFrom: [],
832
+ allowedTo: [],
833
+ validators: [],
834
+ },
835
+ }
836
+
837
+ // ============ Public API ============
838
+
839
+ /**
840
+ * 获取阶段合约
841
+ */
842
+ export function getContract(stageName) {
843
+ return contracts[stageName] || null
844
+ }
845
+
846
+ /**
847
+ * 校验状态转换是否允许
848
+ * @param {string} fromStage - 当前阶段(空字符串表示变更起始)
849
+ * @param {string} toStage - 目标阶段
850
+ * @param {{ fromStageData?: { status?: string } | undefined }} [options] - 可选,从 progress.stages[prevStage] 提取
851
+ * @returns {{ allowed: boolean, reason?: string }}
852
+ */
853
+ export function checkTransition(fromStage, toStage, options = {}) {
854
+ const { fromStageData } = options // { status?: string } | undefined
855
+ const contract = contracts[toStage]
856
+ if (!contract) {
857
+ return { allowed: false, reason: `未知阶段: ${toStage}` }
858
+ }
859
+
860
+ // 辅助阶段随时可执行(archive 除外:从主流程进入 archive 需要校验)
861
+ if (AUXILIARY_STAGES.includes(toStage) && toStage !== 'archive') {
862
+ return { allowed: true }
863
+ }
864
+
865
+ // 同阶段内重复运行:允许(继续执行当前阶段的下一步、或修订模式继续)
866
+ if (fromStage === toStage) {
867
+ return { allowed: true }
868
+ }
869
+
870
+ // task-07: failed_post_check 门控
871
+ // scan post-check 未通过时,禁止进入主流程的下游阶段(brainstorm/plan/execute/verify/archive)
872
+ // 必须先重跑 scan 修复。toStage === 'scan' 的重跑路径已被上方 fromStage === toStage 放行。
873
+ // fromStageData.status 缺失(旧数据)时门控不触发(向后兼容)。
874
+ if (fromStage === 'scan' && fromStageData?.status === 'failed_post_check' && toStage !== 'scan') {
875
+ return {
876
+ allowed: false,
877
+ reason: 'scan post-check 未通过(failed_post_check),需修复后重跑 scan 再进入 ' + toStage,
878
+ }
879
+ }
880
+
881
+ // archive 特殊处理:从 verify 来的允许,从其他主流程阶段来的需要校验
882
+ if (toStage === 'archive') {
883
+ if (fromStage === 'verify') {
884
+ return { allowed: true }
885
+ }
886
+ // 独立运行 archive(无前置)也允许
887
+ if (!fromStage || AUXILIARY_STAGES.includes(fromStage)) {
888
+ return { allowed: true }
889
+ }
890
+ return { allowed: false, reason: 'archive 的前置阶段是 verify,不能从 ' + fromStage + ' 跳转' }
891
+ }
892
+
893
+ // 从辅助阶段进入主流程:允许
894
+ if (AUXILIARY_STAGES.includes(fromStage)) {
895
+ return { allowed: true }
896
+ }
897
+
898
+ // 无前置阶段(变更起始):只能开始 brainstorm 或辅助阶段
899
+ if (!fromStage) {
900
+ // 主流程必须从 brainstorm 开始
901
+ if (contract.allowedFrom.length === 0) {
902
+ return { allowed: true }
903
+ }
904
+ return { allowed: false, reason: `${toStage} 需要先完成 ${contract.allowedFrom.join(' 或 ')}` }
905
+ }
906
+
907
+ // 主流程内部跳转:检查目标阶段的 allowedFrom 是否包含 fromStage
908
+ if (contract.allowedFrom.includes(fromStage)) {
909
+ return { allowed: true }
910
+ }
911
+
912
+ return { allowed: false, reason: `${toStage} 的前置阶段是 ${contract.allowedFrom.join(' 或 ')},不能从 ${fromStage} 跳转` }
913
+ }
914
+
915
+ /**
916
+ * 执行阶段完成校验
917
+ * @param {string} stageName
918
+ * @param {string} cwd
919
+ * @param {string} changeName
920
+ * @param {object} context - 额外上下文(如 projectName)
921
+ * @returns {ValidationResult}
922
+ */
923
+ export function runValidators(stageName, cwd, changeName, context = {}) {
924
+ const contract = contracts[stageName]
925
+ if (!contract || contract.validators.length === 0) {
926
+ return { ok: true, errors: [], warnings: [] }
927
+ }
928
+
929
+ const allErrors = []
930
+ const allWarnings = []
931
+
932
+ for (const validator of contract.validators) {
933
+ try {
934
+ const result = validator(cwd, changeName, context)
935
+ allErrors.push(...(result.errors || []))
936
+ allWarnings.push(...(result.warnings || []))
937
+ } catch (e) {
938
+ allErrors.push(`校验器 ${validator.name || 'unknown'} 异常: ${e.message}`)
939
+ }
940
+ }
941
+
942
+ return { ok: allErrors.length === 0, errors: allErrors, warnings: allWarnings }
943
+ }
944
+
945
+ /**
946
+ * 获取所有主流程阶段
947
+ */
948
+ export { mainFlowStages, AUXILIARY_STAGES as auxiliaryStages }