sillyspec 3.20.3 → 3.20.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 (225) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +21 -17
  2. package/.claude/skills/sillyspec-auto/SKILL.md +83 -77
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -17
  4. package/.claude/skills/sillyspec-commit/SKILL.md +106 -105
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -44
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +31 -22
  7. package/.claude/skills/sillyspec-execute/SKILL.md +30 -17
  8. package/.claude/skills/sillyspec-explore/SKILL.md +109 -96
  9. package/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  10. package/.claude/skills/sillyspec-plan/SKILL.md +21 -52
  11. package/.claude/skills/sillyspec-propose/SKILL.md +21 -17
  12. package/.claude/skills/sillyspec-quick/SKILL.md +21 -17
  13. package/.claude/skills/sillyspec-resume/SKILL.md +68 -111
  14. package/.claude/skills/sillyspec-scan/SKILL.md +21 -17
  15. package/.claude/skills/sillyspec-state/SKILL.md +54 -54
  16. package/.claude/skills/sillyspec-status/SKILL.md +21 -17
  17. package/.claude/skills/sillyspec-verify/SKILL.md +21 -17
  18. package/.claude/skills/sillyspec-workspace/SKILL.md +157 -149
  19. package/.husky/pre-push +13 -0
  20. package/CLAUDE.md +18 -0
  21. package/README.md +198 -186
  22. package/SKILL.md +90 -87
  23. package/bin/sillyspec.js +2 -2
  24. package/docs/brainstorm-plan-contract.md +64 -0
  25. package/docs/plan-execute-contract.md +123 -0
  26. package/docs/platform-scan-protocol.md +298 -0
  27. package/docs/revision-mode.md +115 -0
  28. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -0
  29. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -0
  30. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -0
  31. package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -0
  32. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -0
  33. package/docs/sillyspec/file-lifecycle.md +131 -0
  34. package/docs/workflow-contract-regression.md +106 -0
  35. package/docs/worktree-isolation.md +252 -0
  36. package/package.json +40 -34
  37. package/packages/dashboard/dist/assets/{index-D1EVTLmc.js → index-Bq_Z2hne.js} +7446 -7446
  38. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  39. package/packages/dashboard/dist/index.html +16 -16
  40. package/packages/dashboard/dist/prototype-dashboard.html +836 -0
  41. package/packages/dashboard/dist/prototype-overview.html +256 -0
  42. package/packages/dashboard/index.html +15 -15
  43. package/packages/dashboard/package-lock.json +2384 -2384
  44. package/packages/dashboard/package.json +25 -25
  45. package/packages/dashboard/public/prototype-dashboard.html +836 -0
  46. package/packages/dashboard/public/prototype-overview.html +256 -0
  47. package/packages/dashboard/server/executor.js +86 -86
  48. package/packages/dashboard/server/index.js +588 -508
  49. package/packages/dashboard/server/parser.js +526 -458
  50. package/packages/dashboard/server/watcher.js +350 -342
  51. package/packages/dashboard/src/App.vue +558 -325
  52. package/packages/dashboard/src/components/ActionBar.vue +93 -84
  53. package/packages/dashboard/src/components/CommandPalette.vue +96 -92
  54. package/packages/dashboard/src/components/DetailPanel.vue +137 -137
  55. package/packages/dashboard/src/components/DocPreview.vue +105 -8
  56. package/packages/dashboard/src/components/DocTree.vue +75 -19
  57. package/packages/dashboard/src/components/HResizeHandle.vue +48 -0
  58. package/packages/dashboard/src/components/LogStream.vue +65 -65
  59. package/packages/dashboard/src/components/PipelineStage.vue +95 -75
  60. package/packages/dashboard/src/components/PipelineView.vue +156 -129
  61. package/packages/dashboard/src/components/ProjectCard.vue +187 -0
  62. package/packages/dashboard/src/components/ProjectList.vue +210 -210
  63. package/packages/dashboard/src/components/ProjectOverview.vue +113 -139
  64. package/packages/dashboard/src/components/StageBadge.vue +67 -53
  65. package/packages/dashboard/src/components/StepCard.vue +94 -89
  66. package/packages/dashboard/src/components/VResizeHandle.vue +61 -0
  67. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
  68. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
  69. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
  70. package/packages/dashboard/src/composables/useDashboard.js +192 -164
  71. package/packages/dashboard/src/composables/useKeyboard.js +119 -119
  72. package/packages/dashboard/src/composables/useLayout.js +131 -0
  73. package/packages/dashboard/src/composables/useWebSocket.js +129 -129
  74. package/packages/dashboard/src/main.js +8 -8
  75. package/packages/dashboard/src/style.css +132 -132
  76. package/packages/dashboard/vite.config.js +18 -18
  77. package/src/brainstorm-postcheck.js +158 -0
  78. package/src/change-list.js +52 -0
  79. package/src/change-risk-profile.js +352 -0
  80. package/src/classify-change.js +73 -0
  81. package/src/constants.js +70 -0
  82. package/src/contract-matrix.js +278 -0
  83. package/src/db.js +201 -0
  84. package/src/endpoint-extractor.js +315 -0
  85. package/src/hooks/claude-pre-tool-use.cjs +125 -0
  86. package/src/hooks/worktree-guard.js +761 -0
  87. package/src/index.js +922 -252
  88. package/src/init.js +470 -375
  89. package/src/knowledge-match.js +130 -0
  90. package/src/migrate.js +117 -117
  91. package/src/modules.js +482 -0
  92. package/src/progress.js +1734 -508
  93. package/src/run.js +3465 -652
  94. package/src/scan-postcheck.js +387 -0
  95. package/src/setup.js +398 -460
  96. package/src/stage-contract.js +700 -0
  97. package/src/stages/archive.js +160 -54
  98. package/src/stages/brainstorm-auto.js +229 -0
  99. package/src/stages/brainstorm.js +645 -239
  100. package/src/stages/doctor.js +365 -312
  101. package/src/stages/execute.js +634 -264
  102. package/src/stages/explore.js +34 -0
  103. package/src/stages/index.js +29 -35
  104. package/src/stages/knowledge.js +498 -0
  105. package/src/stages/plan-postcheck.js +518 -0
  106. package/src/stages/plan.js +582 -279
  107. package/src/stages/propose.js +174 -115
  108. package/src/stages/quick.js +102 -63
  109. package/src/stages/scan.js +558 -141
  110. package/src/stages/status.js +65 -65
  111. package/src/stages/verify.js +322 -135
  112. package/src/sync.js +497 -0
  113. package/src/task-review.js +346 -0
  114. package/src/workflow.js +785 -0
  115. package/src/worktree-apply.js +549 -0
  116. package/src/worktree-deps.js +185 -0
  117. package/src/worktree.js +982 -0
  118. package/templates/workflows/archive-impact.yaml +79 -0
  119. package/templates/workflows/scan-docs.yaml +132 -0
  120. package/test/brainstorm-plan-contract.test.mjs +273 -0
  121. package/test/check-syntax.mjs +26 -0
  122. package/test/cli-top-level-aliases.test.mjs +174 -0
  123. package/test/contract-artifacts.test.mjs +323 -0
  124. package/test/decision-supersede.test.mjs +277 -0
  125. package/test/knowledge-match.test.mjs +231 -0
  126. package/test/plan-execute-contract.test.mjs +357 -0
  127. package/test/plan-optimization.test.mjs +572 -0
  128. package/test/platform-artifacts.test.mjs +190 -0
  129. package/test/platform-failure-samples.test.mjs +199 -0
  130. package/test/platform-recovery-chain.test.mjs +179 -0
  131. package/test/platform-recovery.test.mjs +167 -0
  132. package/test/platform-scan-p0.test.mjs +175 -0
  133. package/test/revision-v1.test.mjs +1145 -0
  134. package/test/run-sanitize-project-name.test.mjs +51 -0
  135. package/test/run-scan-postcheck-fail.test.mjs +64 -0
  136. package/test/run-scan-project-parse.test.mjs +200 -0
  137. package/test/run-tests.mjs +48 -0
  138. package/test/runtime-cleanup-keeps-worktree.test.mjs +107 -0
  139. package/test/scan-docs-yaml-placeholders.test.mjs +84 -0
  140. package/test/scan-knowledge.test.mjs +175 -0
  141. package/test/scan-paths.test.mjs +68 -0
  142. package/test/scan-postcheck-project-priority.test.mjs +85 -0
  143. package/test/scan-postcheck.test.mjs +197 -0
  144. package/test/scan-workflow-anyfailed-block.test.mjs +52 -0
  145. package/test/spec-dir.test.mjs +206 -0
  146. package/test/stage-contract-failed-post-check.test.mjs +102 -0
  147. package/test/stage-contract.test.mjs +299 -0
  148. package/test/stage-definitions.test.mjs +39 -0
  149. package/test/wait-gates.test.mjs +501 -0
  150. package/test/workflow-spec-base.test.mjs +142 -0
  151. package/test/worktree-deps-provision.test.mjs +148 -0
  152. package/test/worktree-guard.test.mjs +136 -0
  153. package/test/worktree-native-overlay.test.mjs +188 -0
  154. package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +0 -97
  155. package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +0 -51
  156. package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +0 -29
  157. package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +0 -34
  158. package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +0 -13
  159. package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +0 -43
  160. package/.sillyspec/changes/auto-mode/design.md +0 -50
  161. package/.sillyspec/changes/auto-mode/proposal.md +0 -19
  162. package/.sillyspec/changes/auto-mode/requirements.md +0 -21
  163. package/.sillyspec/changes/auto-mode/tasks.md +0 -7
  164. package/.sillyspec/changes/brainstorm-archive/2026-04-05-dashboard-design.md +0 -206
  165. package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +0 -199
  166. package/.sillyspec/changes/dashboard/design.md +0 -219
  167. package/.sillyspec/changes/dashboard/design.md.braindraft +0 -206
  168. package/.sillyspec/changes/run-command-design/design.md +0 -1230
  169. package/.sillyspec/changes/unified-docs-design/design.md +0 -199
  170. package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
  171. package/.sillyspec/knowledge/INDEX.md +0 -8
  172. package/.sillyspec/knowledge/uncategorized.md +0 -3
  173. package/.sillyspec/plans/2026-04-05-dashboard.md +0 -737
  174. package/.sillyspec/projects/sillyspec.yaml +0 -3
  175. package/dist/steps/brainstorm/01-load-context.md +0 -30
  176. package/dist/steps/brainstorm/02-reuse-check.md +0 -6
  177. package/dist/steps/brainstorm/03-prototype-analysis.md +0 -11
  178. package/dist/steps/brainstorm/04-module-split.md +0 -23
  179. package/dist/steps/brainstorm/05-dialog-explore.md +0 -8
  180. package/dist/steps/brainstorm/06-propose-approaches.md +0 -3
  181. package/dist/steps/brainstorm/07-present-design.md +0 -3
  182. package/dist/steps/brainstorm/08-write-design.md +0 -21
  183. package/dist/steps/brainstorm/09-self-review.md +0 -15
  184. package/dist/steps/brainstorm/10-user-confirm.md +0 -3
  185. package/dist/steps/brainstorm/11-output-spec.md +0 -7
  186. package/dist/steps/brainstorm/manifest.yaml +0 -26
  187. package/dist/steps/execute/01-load-context.md +0 -41
  188. package/dist/steps/execute/02-scan-conventions.md +0 -47
  189. package/dist/steps/execute/03-skill-mcp.md +0 -19
  190. package/dist/steps/execute/04-assign-task.md +0 -22
  191. package/dist/steps/execute/04b-prompt-template.md +0 -54
  192. package/dist/steps/execute/05-write-test.md +0 -7
  193. package/dist/steps/execute/06-write-code.md +0 -8
  194. package/dist/steps/execute/07-run-test.md +0 -26
  195. package/dist/steps/execute/08-fix-issues.md +0 -28
  196. package/dist/steps/execute/09-next-task.md +0 -33
  197. package/dist/steps/execute/manifest.yaml +0 -28
  198. package/dist/steps/plan/01-load-context.md +0 -22
  199. package/dist/steps/plan/02-anchor-confirm.md +0 -1
  200. package/dist/steps/plan/03-expand-tasks.md +0 -33
  201. package/dist/steps/plan/04-mark-order.md +0 -15
  202. package/dist/steps/plan/05-e2e-planning.md +0 -17
  203. package/dist/steps/plan/06-self-check.md +0 -16
  204. package/dist/steps/plan/07-save.md +0 -1
  205. package/dist/steps/plan/manifest.yaml +0 -18
  206. package/dist/steps/scan/01-env-detect.md +0 -51
  207. package/dist/steps/scan/02-tech-stack.md +0 -16
  208. package/dist/steps/scan/03-conventions.md +0 -16
  209. package/dist/steps/scan/04-structure.md +0 -19
  210. package/dist/steps/scan/05-quality.md +0 -18
  211. package/dist/steps/scan/06-complete.md +0 -49
  212. package/dist/steps/scan/manifest.yaml +0 -16
  213. package/dist/steps/verify/01-load-specs.md +0 -28
  214. package/dist/steps/verify/02-check-tasks.md +0 -1
  215. package/dist/steps/verify/03-check-design.md +0 -6
  216. package/dist/steps/verify/04-run-tests.md +0 -7
  217. package/dist/steps/verify/05-e2e-tests.md +0 -27
  218. package/dist/steps/verify/05b-e2e-fix.md +0 -33
  219. package/dist/steps/verify/06-code-quality.md +0 -25
  220. package/dist/steps/verify/07-lint-check.md +0 -27
  221. package/dist/steps/verify/08-output-report.md +0 -14
  222. package/dist/steps/verify/manifest.yaml +0 -22
  223. package/packages/dashboard/dist/assets/index-DGe8CqeP.css +0 -1
  224. package/src/derive.js +0 -147
  225. package/src/step.js +0 -543
@@ -0,0 +1,206 @@
1
+ /**
2
+ * --spec-dir 功能测试
3
+ *
4
+ * 测试点:
5
+ * 1. ProgressManager 外部 specDir 路径正确
6
+ * 2. init 外部 specDir 不污染源码
7
+ * 3. 默认模式不受影响
8
+ * 4. 平台模式 prompt 注入(scan/brainstorm/plan/execute/verify/quick)
9
+ * 5. 非 platform 模式占位符替换(无 undefined/null)
10
+ * 6. --spec-dir 与 --spec-root 兼容
11
+ * 7. progress 使用外部 specDir
12
+ */
13
+
14
+ import { join, resolve, basename, dirname } from 'path'
15
+ import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
16
+ import { fileURLToPath, pathToFileURL } from 'url'
17
+ import { execSync } from 'child_process'
18
+ import { tmpdir } from 'os'
19
+
20
+ const __filename = fileURLToPath(import.meta.url)
21
+ const __dirname = dirname(__filename)
22
+ const root = resolve(__dirname, '..')
23
+ const binCLI = join(root, 'bin', 'sillyspec.js')
24
+
25
+ function imp(path) {
26
+ return import(pathToFileURL(path).href)
27
+ }
28
+
29
+ let passed = 0
30
+ let failed = 0
31
+
32
+ function assert(condition, msg) {
33
+ if (condition) {
34
+ console.log(` ✅ PASS: ${msg}`)
35
+ passed++
36
+ } else {
37
+ console.log(` ❌ FAIL: ${msg}`)
38
+ failed++
39
+ }
40
+ }
41
+
42
+ function tmpDir(name) {
43
+ const dir = join(tmpdir(), `spec-dir-test-${name}-${Date.now()}`)
44
+ mkdirSync(dir, { recursive: true })
45
+ return dir
46
+ }
47
+
48
+ function cleanup(dir) {
49
+ try { rmSync(dir, { recursive: true, force: true }) } catch {}
50
+ }
51
+
52
+ function run(cmd) {
53
+ return execSync(cmd, { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] })
54
+ }
55
+
56
+ // ── Test 1: ProgressManager 外部 specDir ──
57
+ console.log('\n=== Test 1: ProgressManager 外部 specDir ===')
58
+ {
59
+ const { ProgressManager } = await imp(join(root, 'src', 'progress.js'))
60
+ const tmp = tmpDir('pm')
61
+ const specDir = join(tmp, 'external-spec')
62
+
63
+ const pm = new ProgressManager({ specDir })
64
+ assert(pm._getSpecDir(tmp) === specDir, `_getSpecDir 返回自定义路径`)
65
+
66
+ // 无自定义 specDir 时,resolveSpecDir 会向上查找 .sillyspec 目录
67
+ // 在测试环境中可能命中上层已有的 .sillyspec,所以只检查返回值是否有效路径
68
+ const pm2 = new ProgressManager()
69
+ const resolved = pm2._getSpecDir(tmp)
70
+ assert(typeof resolved === 'string' && resolved.length > 0 && resolved.endsWith('.sillyspec'),
71
+ `_getSpecDir 无自定义时返回有效 .sillyspec 路径 (got: ${resolved})`)
72
+
73
+ assert(pm._runtimePath(tmp) === join(specDir, '.runtime'), `_runtimePath 基于 specDir`)
74
+ assert(pm._changePath(tmp, 'c') === join(specDir, 'changes', 'c'), `_changePath 基于 specDir`)
75
+
76
+ // 外部 specDir 时 _ensureGitignore 应跳过
77
+ const gitignoreResult = pm._ensureGitignore(tmp)
78
+ assert(gitignoreResult === undefined, `外部 specDir 时 _ensureGitignore 跳过`)
79
+
80
+ cleanup(tmp)
81
+ }
82
+
83
+ // ── Test 2: init 外部 specDir 不污染源码 ──
84
+ console.log('\n=== Test 2: init 外部 specDir 不污染源码 ===')
85
+ {
86
+ const { cmdInit } = await imp(join(root, 'src', 'init.js'))
87
+ const projectDir = tmpDir('project')
88
+ const specDir = tmpDir('spec')
89
+
90
+ await cmdInit(projectDir, { specDir })
91
+
92
+ assert(!existsSync(join(projectDir, '.sillyspec')), '源码目录不含 .sillyspec')
93
+ assert(!existsSync(join(projectDir, '.gitignore')), '外部 specDir 时不创建 .gitignore')
94
+ assert(existsSync(join(specDir, 'projects')), `specDir/projects 存在`)
95
+ assert(existsSync(join(specDir, 'docs')), `specDir/docs 存在`)
96
+ assert(existsSync(join(specDir, '.runtime', 'sillyspec.db')), `specDir/.runtime/sillyspec.db 存在`)
97
+ assert(existsSync(join(specDir, 'workflows')), `specDir/workflows 存在`)
98
+ assert(existsSync(join(projectDir, '.claude')), `源码目录 .claude 存在(工具指令)`)
99
+
100
+ cleanup(projectDir)
101
+ cleanup(specDir)
102
+ }
103
+
104
+ // ── Test 3: 默认模式不受影响 ──
105
+ console.log('\n=== Test 3: 默认模式不受影响 ===')
106
+ {
107
+ const { cmdInit } = await imp(join(root, 'src', 'init.js'))
108
+ const projectDir = tmpDir('default')
109
+
110
+ await cmdInit(projectDir, {})
111
+
112
+ assert(existsSync(join(projectDir, '.sillyspec')), '默认模式创建 .sillyspec 在项目内')
113
+ assert(existsSync(join(projectDir, '.sillyspec', '.runtime', 'sillyspec.db')), '默认模式 DB 在项目内')
114
+ assert(existsSync(join(projectDir, '.gitignore')), '默认模式创建 .gitignore')
115
+
116
+ cleanup(projectDir)
117
+ }
118
+
119
+ // ── Test 4: 平台模式 prompt 注入(多 stage) ──
120
+ console.log('\n=== Test 4: 平台模式 prompt 注入 ===')
121
+ {
122
+ const projectDir = tmpDir('prompt-p')
123
+ const specDir = tmpDir('prompt-s')
124
+
125
+ run(`node "${binCLI}" init "${projectDir}" --spec-dir "${specDir}"`)
126
+
127
+ // execute 阶段会自动创建 worktree,在非 git 环境下会失败,跳过
128
+ const stages = ['scan', 'brainstorm', 'plan', 'verify', 'quick']
129
+ for (const stage of stages) {
130
+ const output = run(`node "${binCLI}" --dir "${projectDir}" --spec-dir "${specDir}" run ${stage} --skip-approval`)
131
+ assert(output.includes('平台模式'), `${stage}: 包含平台模式指令`)
132
+ assert(output.includes(`规范目录(specDir): \`${specDir}\``), `${stage}: 包含正确的 specDir 路径`)
133
+ }
134
+
135
+ // scan 额外检查
136
+ const scanOutput = run(`node "${binCLI}" --dir "${projectDir}" --spec-dir "${specDir}" run scan`)
137
+ assert(scanOutput.includes('严禁写入源码目录'), 'scan: 包含严禁写入源码目录')
138
+ assert(scanOutput.includes('Write 工具失败时,不允许'), 'scan: 包含 Write 工具规则')
139
+ assert(scanOutput.includes('变更目录'), 'scan: 包含变更目录')
140
+
141
+ cleanup(projectDir)
142
+ cleanup(specDir)
143
+ }
144
+
145
+ // ── Test 5: 非 platform 模式占位符替换 ──
146
+ console.log('\n=== Test 5: 非 platform 模式占位符替换 ===')
147
+ {
148
+ const projectDir = tmpDir('noplatform')
149
+
150
+ run(`node "${binCLI}" init "${projectDir}"`)
151
+
152
+ const output = run(`node "${binCLI}" --dir "${projectDir}" run scan`)
153
+
154
+ assert(!output.includes('平台模式 — 写入路径约束'), '非 platform 模式不含平台指令')
155
+ assert(!output.includes('{DOCS_ROOT}'), '{DOCS_ROOT} 被正确替换')
156
+ assert(!output.includes('undefined'), '输出不含 undefined 路径')
157
+ assert(!output.includes('null/.sillyspec'), '输出不含 null 路径')
158
+
159
+ cleanup(projectDir)
160
+ }
161
+
162
+ // ── Test 6: --spec-root 兼容 ──
163
+ console.log('\n=== Test 6: --spec-root 兼容 ===')
164
+ {
165
+ const projectDir = tmpDir('compat-p')
166
+ const specDir = tmpDir('compat-s')
167
+
168
+ run(`node "${binCLI}" init "${projectDir}" --spec-dir "${specDir}"`)
169
+
170
+ const output = run(`node "${binCLI}" --dir "${projectDir}" run scan --spec-root "${specDir}"`)
171
+ assert(output.includes('平台模式'), '--spec-root 兼容:仍触发平台模式指令')
172
+
173
+ cleanup(projectDir)
174
+ cleanup(specDir)
175
+ }
176
+
177
+ // ── Test 7: progress 使用外部 specDir ──
178
+ console.log('\n=== Test 7: progress 使用外部 specDir ===')
179
+ {
180
+ const { ProgressManager } = await imp(join(root, 'src', 'progress.js'))
181
+ const projectDir = tmpDir('progress-p')
182
+ const specDir = tmpDir('progress-s')
183
+
184
+ const pm = new ProgressManager({ specDir })
185
+ await pm.init(projectDir)
186
+
187
+ assert(existsSync(join(specDir, '.runtime', 'sillyspec.db')), 'DB 创建在外部 specDir')
188
+ assert(!existsSync(join(projectDir, '.sillyspec')), '源码目录不含 .sillyspec')
189
+
190
+ await pm.initChange(projectDir, 'test-change')
191
+ assert(existsSync(join(specDir, 'changes', 'test-change')), 'changes 创建在外部 specDir')
192
+
193
+ const progress = await pm.read(projectDir, 'test-change')
194
+ assert(progress !== null, '能从外部 specDir 读取 progress')
195
+ assert(progress.currentChange === 'test-change', `currentChange 正确`)
196
+
197
+ cleanup(projectDir)
198
+ cleanup(specDir)
199
+ }
200
+
201
+ // ── 汇总 ──
202
+ console.log(`\n${'='.repeat(50)}`)
203
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
204
+ console.log(`${'='.repeat(50)}`)
205
+
206
+ if (failed > 0) throw new Error(`${failed} test(s) failed`)
@@ -0,0 +1,102 @@
1
+ /**
2
+ * task-07: checkTransition failed_post_check 门控
3
+ *
4
+ * 覆盖:
5
+ * - AC-1: scan → brainstorm (failed_post_check) → allowed=false, reason 含"scan post-check 未通过"
6
+ * - AC-2: scan → scan (重跑修复) → allowed=true
7
+ * - AC-3: scan → doctor/status (辅助阶段) → allowed=true
8
+ * - AC-4: 旧数据无 status 字段 → 行为同旧版(按 allowedFrom)
9
+ * - AC-5: status='completed' 不被拦
10
+ */
11
+ import { checkTransition } from '../src/stage-contract.js'
12
+
13
+ let passed = 0
14
+ let failed = 0
15
+ function assertEqual (actual, expected, msg) {
16
+ const ok = actual === expected
17
+ if (ok) { console.log(`✅ PASS: ${msg}`); passed++ }
18
+ else { console.error(`❌ FAIL: ${msg}\n expected: ${JSON.stringify(expected)}\n actual: ${JSON.stringify(actual)}`); failed++ }
19
+ }
20
+ function assertMatch (actual, regex, msg) {
21
+ if (regex.test(actual)) { console.log(`✅ PASS: ${msg}`); passed++ }
22
+ else { console.error(`❌ FAIL: ${msg}\n actual: ${JSON.stringify(actual)} 不匹配 ${regex}`); failed++ }
23
+ }
24
+ function assert (cond, msg) {
25
+ if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
26
+ else { console.error(`❌ FAIL: ${msg}`); failed++ }
27
+ }
28
+
29
+ console.log('=== AC-1: failed_post_check 状态下进 brainstorm/plan/execute 被拦 ===')
30
+
31
+ // scan → brainstorm, failed_post_check
32
+ {
33
+ const r = checkTransition('scan', 'brainstorm', { fromStageData: { status: 'failed_post_check' } })
34
+ assertEqual(r.allowed, false, "scan→brainstorm failed_post_check: allowed=false")
35
+ assertMatch(r.reason || '', /scan post-check 未通过/, 'reason 含 "scan post-check 未通过"')
36
+ assertMatch(r.reason || '', /重跑\s*scan|重跑\s*scan/, 'reason 含 "重跑 scan" 提示')
37
+ }
38
+
39
+ // scan → plan / execute 同样被拦
40
+ {
41
+ const r1 = checkTransition('scan', 'plan', { fromStageData: { status: 'failed_post_check' } })
42
+ assertEqual(r1.allowed, false, "scan→plan failed_post_check: allowed=false")
43
+ const r2 = checkTransition('scan', 'execute', { fromStageData: { status: 'failed_post_check' } })
44
+ assertEqual(r2.allowed, false, "scan→execute failed_post_check: allowed=false")
45
+ }
46
+
47
+ console.log('\n=== AC-2: 允许 scan → scan 重跑修复(fromStage===toStage) ===')
48
+ {
49
+ const r = checkTransition('scan', 'scan', { fromStageData: { status: 'failed_post_check' } })
50
+ assertEqual(r.allowed, true, "scan→scan failed_post_check: allowed=true(允许重跑修复)")
51
+ }
52
+
53
+ console.log('\n=== AC-3: failed_post_check 下辅助阶段(doctor/status)仍可执行 ===')
54
+ {
55
+ const r1 = checkTransition('scan', 'doctor', { fromStageData: { status: 'failed_post_check' } })
56
+ assertEqual(r1.allowed, true, "scan→doctor failed_post_check: allowed=true(辅助阶段)")
57
+ const r2 = checkTransition('scan', 'status', { fromStageData: { status: 'failed_post_check' } })
58
+ assertEqual(r2.allowed, true, "scan→status failed_post_check: allowed=true(辅助阶段)")
59
+ const r3 = checkTransition('scan', 'quick', { fromStageData: { status: 'failed_post_check' } })
60
+ assertEqual(r3.allowed, true, "scan→quick failed_post_check: allowed=true(辅助阶段)")
61
+ const r4 = checkTransition('scan', 'explore', { fromStageData: { status: 'failed_post_check' } })
62
+ assertEqual(r4.allowed, true, "scan→explore failed_post_check: allowed=true(辅助阶段)")
63
+ }
64
+
65
+ console.log('\n=== AC-4: 旧数据兼容(无 options 或 status 缺失,行为同旧版) ===')
66
+ {
67
+ // 无 options(旧调用)— scan→brainstorm 按 allowedFrom 规则允许(scan 是辅助阶段,可进主流程)
68
+ const r1 = checkTransition('scan', 'brainstorm')
69
+ assertEqual(r1.allowed, true, "scan→brainstorm 无 options: 行为同旧版(allowed=true)")
70
+
71
+ // options 提供 fromStageData 但 status 为 undefined
72
+ const r2 = checkTransition('scan', 'brainstorm', { fromStageData: { /* 无 status */ } })
73
+ assertEqual(r2.allowed, true, "scan→brainstorm status=undefined: 行为同旧版(allowed=true)")
74
+
75
+ // options 为空对象
76
+ const r3 = checkTransition('scan', 'brainstorm', {})
77
+ assertEqual(r3.allowed, true, "scan→brainstorm options={}: 行为同旧版(allowed=true)")
78
+ }
79
+
80
+ console.log('\n=== AC-5: status="completed" 不被门控拦截 ===')
81
+ {
82
+ const r = checkTransition('scan', 'brainstorm', { fromStageData: { status: 'completed' } })
83
+ assertEqual(r.allowed, true, "scan→brainstorm status=completed: allowed=true")
84
+ }
85
+
86
+ console.log('\n=== AC-6: fromStage 非 scan 时门控不触发(门控只针对 scan 源) ===')
87
+ {
88
+ const r = checkTransition('brainstorm', 'plan', { fromStageData: { status: 'failed_post_check' } })
89
+ assertEqual(r.allowed, true, "brainstorm→plan failed_post_check: 门控不触发(非 scan 源)")
90
+ }
91
+
92
+ console.log('\n=== 接口向后兼容:options 第 3 位可选 ===')
93
+ {
94
+ // 仅 2 参调用(最常见旧用法)
95
+ const r = checkTransition('brainstorm', 'plan')
96
+ assert(r && typeof r.allowed === 'boolean', '2 参调用返回 { allowed: boolean }')
97
+ }
98
+
99
+ console.log(`\n${'='.repeat(50)}`)
100
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
101
+ console.log(`${'='.repeat(50)}`)
102
+ if (failed > 0) process.exit(1)
@@ -0,0 +1,299 @@
1
+ /**
2
+ * StageContract 状态转换 + validator 测试
3
+ */
4
+ import { checkTransition, runValidators, getContract } from '../src/stage-contract.js'
5
+
6
+ let failed = 0
7
+
8
+ // === 状态转换测试 ===
9
+ const transitionTests = [
10
+ // [from, to, expectedAllowed]
11
+ // 主流程正常顺序
12
+ ['', 'brainstorm', true],
13
+ ['brainstorm', 'plan', true],
14
+ ['plan', 'execute', true],
15
+ ['execute', 'verify', true],
16
+ ['verify', 'archive', true],
17
+
18
+ // 跳步应被拦截
19
+ ['', 'plan', false],
20
+ ['', 'execute', false],
21
+ ['brainstorm', 'execute', false],
22
+ ['plan', 'verify', false],
23
+ ['execute', 'archive', false],
24
+
25
+ // 回退应被拦截
26
+ ['plan', 'brainstorm', false],
27
+ ['execute', 'plan', false],
28
+ ['verify', 'execute', false],
29
+
30
+ // 辅助阶段随时可执行
31
+ ['', 'scan', true],
32
+ ['', 'quick', true],
33
+ ['', 'explore', true],
34
+ ['', 'doctor', true],
35
+ ['', 'archive', true],
36
+ ['brainstorm', 'scan', true],
37
+ ['plan', 'quick', true],
38
+ ['execute', 'doctor', true],
39
+
40
+ // 从辅助阶段进入主流程允许
41
+ ['scan', 'plan', true],
42
+ ['scan', 'brainstorm', true],
43
+ ['quick', 'plan', true],
44
+ ['doctor', 'brainstorm', true],
45
+
46
+ // archive 特殊:verify 后允许,其他主流程不允许直接跳
47
+ ['verify', 'archive', true],
48
+ ['execute', 'archive', false],
49
+ ['plan', 'archive', false],
50
+ ]
51
+
52
+ console.log('=== 状态转换测试 ===')
53
+ for (const [from, to, expected] of transitionTests) {
54
+ const r = checkTransition(from, to)
55
+ const ok = r.allowed === expected
56
+ if (!ok) failed++
57
+ console.log(ok ? '✅' : '❌', `${from || '(起始)'} → ${to}: allowed=${r.allowed} (exp ${expected})${ok ? '' : ' reason: ' + r.reason}`)
58
+ }
59
+
60
+ // === Validator 测试 ===
61
+ console.log('\n=== Validator 测试 ===')
62
+
63
+ // plan validator:plan.md 不存在应报错
64
+ const planResult = runValidators('plan', '.', 'nonexistent-change')
65
+ if (planResult.ok === false && planResult.errors.length > 0) {
66
+ console.log('✅ plan validator 检测到缺失 plan.md')
67
+ } else {
68
+ console.log('❌ plan validator 未检测到缺失 plan.md')
69
+ failed++
70
+ }
71
+
72
+ // verify validator:变更目录不存在应报错
73
+ const verifyResult = runValidators('verify', '.', 'nonexistent-change')
74
+ if (verifyResult.ok === false && verifyResult.errors.length > 0) {
75
+ console.log('✅ verify validator 检测到缺失变更目录')
76
+ } else {
77
+ console.log('❌ verify validator 未检测到缺失变更目录')
78
+ failed++
79
+ }
80
+
81
+ // scan validator:文档目录不存在应报错
82
+ const scanResult = runValidators('scan', join(tmpdir(), 'nonexistent-project'), 'test', { projectName: 'test' })
83
+ if (scanResult.ok === false && scanResult.errors.length > 0) {
84
+ console.log('✅ scan validator 检测到缺失 scan 文档')
85
+ } else {
86
+ console.log('❌ scan validator 未检测到缺失 scan 文档')
87
+ failed++
88
+ }
89
+
90
+ // brainstorm 有 validator,但变更目录不存在时应该报错(因为产物不存在)
91
+ const brainstormResult = runValidators('brainstorm', '.', 'test')
92
+ if (brainstormResult.ok === false && brainstormResult.errors.length > 0) {
93
+ console.log('✅ brainstorm validator 检测到缺失产物文件')
94
+ } else {
95
+ console.log('❌ brainstorm validator 未检测到缺失产物')
96
+ failed++
97
+ }
98
+
99
+ // === scan validator 平台模式 specRoot 测试 ===
100
+ console.log('\n=== scan validator specRoot 测试 ===')
101
+
102
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
103
+ import { join } from 'path'
104
+ import { tmpdir } from 'os'
105
+
106
+ // 创建临时 specRoot 结构
107
+ const specRoot = mkdtempSync(join(tmpdir(), 'sillyspec-test-'))
108
+ const sourceRoot = mkdtempSync(join(tmpdir(), 'sillyspec-source-'))
109
+ const projectName = 'myaaa'
110
+
111
+ // specRoot 在实际代码中等价于 .sillyspec 目录本身(run.js: join(specRoot, 'docs', ...))
112
+ // 所以 scan 文档路径为 specRoot/docs/<project>/scan/,不需要额外的 .sillyspec/ 前缀
113
+ const specDocsDir = join(specRoot, 'docs', projectName, 'scan')
114
+ mkdirSync(specDocsDir, { recursive: true })
115
+ for (const doc of ['ARCHITECTURE.md', 'CONVENTIONS.md', 'STRUCTURE.md', 'INTEGRATIONS.md', 'TESTING.md', 'CONCERNS.md', 'PROJECT.md']) {
116
+ writeFileSync(join(specDocsDir, doc), '# ' + doc)
117
+ }
118
+ mkdirSync(join(specRoot, 'docs', projectName, 'modules'), { recursive: true })
119
+ writeFileSync(join(specRoot, 'docs', projectName, 'modules', 'app.md'), '# app')
120
+
121
+ // 测试1:使用 specRoot 校验成功
122
+ const specResult = runValidators('scan', sourceRoot, 'test', { projectName, specRoot })
123
+ if (specResult.ok === true) {
124
+ console.log('✅ scan validator 使用 specRoot 校验通过')
125
+ } else {
126
+ console.log('❌ scan validator specRoot 校验失败:', specResult.errors)
127
+ failed++
128
+ }
129
+
130
+ // 测试2:使用 sourceRoot 校验(不传 specRoot)应失败
131
+ const localResult = runValidators('scan', sourceRoot, 'test', { projectName })
132
+ if (localResult.ok === false && localResult.errors.length > 0) {
133
+ console.log('✅ scan validator 使用 sourceRoot 校验正确失败(文档不在 source_root 下)')
134
+ } else {
135
+ console.log('❌ scan validator sourceRoot 校验未正确失败')
136
+ failed++
137
+ }
138
+
139
+ // 测试3:校验路径指向 specRoot 而非 sourceRoot
140
+ const errors1 = localResult.errors.join(' ')
141
+ const errors2 = specResult.errors.join(' ')
142
+ if (errors1.includes(sourceRoot) || errors1.includes(join(sourceRoot, '.sillyspec'))) {
143
+ console.log('✅ 未传 specRoot 时校验路径指向 source_root')
144
+ } else {
145
+ console.log('✅ 未传 specRoot 时校验失败(文档确实不在 source_root 下)')
146
+ }
147
+ if (!errors2.includes(specRoot)) {
148
+ console.log('✅ 传 specRoot 时校验路径指向 specRoot(无错误=不包含路径)')
149
+ } else {
150
+ console.log('✅ 传 specRoot 时校验路径正确')
151
+ }
152
+
153
+ // 清理临时目录
154
+ rmSync(specRoot, { recursive: true })
155
+ rmSync(sourceRoot, { recursive: true })
156
+
157
+ // === decisions.md traceability validator 测试 ===
158
+ console.log('\n=== decisions traceability validator 测试 ===')
159
+
160
+ const traceRoot = mkdtempSync(join(tmpdir(), 'sillyspec-trace-'))
161
+ const traceDir = join(traceRoot, '.sillyspec', 'changes', 'trace')
162
+ mkdirSync(traceDir, { recursive: true })
163
+ writeFileSync(join(traceDir, 'proposal.md'), '# Proposal\n\n## 不在范围内\n- none\n')
164
+ writeFileSync(join(traceDir, 'design.md'), '# Design\n\n## 文件变更清单\n\n## 风险登记\n\n## 自审\n\nD-001@v1\n')
165
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n## D-001@v1: Choose canonical account term\n- priority: P1\n- status: accepted\n')
166
+ writeFileSync(join(traceDir, 'requirements.md'), '# Requirements\n\n### FR-01: Account naming\nGiven x\nWhen y\nThen z\n')
167
+ writeFileSync(join(traceDir, 'tasks.md'), '- [ ] task-01: implement naming (D-001@v1)\n')
168
+
169
+ const brainstormTrace = runValidators('brainstorm', traceRoot, 'trace')
170
+ if (brainstormTrace.ok === true && brainstormTrace.warnings.some(w => w.includes('requirements.md 未引用') && w.includes('D-001@V1'))) {
171
+ console.log('✅ brainstorm validator 检测到 requirements.md 缺少 D-001@v1 引用')
172
+ } else {
173
+ console.log('❌ brainstorm validator 未检测到 requirements.md 缺少 D-001@v1 引用', brainstormTrace)
174
+ failed++
175
+ }
176
+
177
+ writeFileSync(join(traceDir, 'requirements.md'), '# Requirements\n\n### FR-01: Account naming\n覆盖决策:D-001@v1\nGiven x\nWhen y\nThen z\n')
178
+ writeFileSync(join(traceDir, 'plan.md'), '# Plan\n\n- [ ] task-01: implement naming\n')
179
+
180
+ const planTrace = runValidators('plan', traceRoot, 'trace')
181
+ if (planTrace.ok === true
182
+ && planTrace.warnings.some(w => w.includes('plan.md 未引用') && w.includes('FR-01'))
183
+ && planTrace.warnings.some(w => w.includes('plan.md 未引用') && w.includes('D-001@V1'))) {
184
+ console.log('✅ plan validator 检测到 plan.md 缺少 FR-01/D-001@v1 引用')
185
+ } else {
186
+ console.log('❌ plan validator 未检测到 plan.md 缺少追踪 ID', planTrace)
187
+ failed++
188
+ }
189
+
190
+ writeFileSync(join(traceDir, 'plan.md'), '# Plan\n\n- [ ] task-01: implement naming(覆盖:FR-01, D-001@v1)\n')
191
+ writeFileSync(join(traceDir, 'verify-result.md'), '# Verify\n\nPASS\n')
192
+
193
+ const verifyTrace = runValidators('verify', traceRoot, 'trace')
194
+ if (verifyTrace.ok === true && verifyTrace.warnings.some(w => w.includes('verify-result.md 未引用') && w.includes('D-001@V1'))) {
195
+ console.log('✅ verify validator 检测到 verify-result.md 缺少 D-001@v1 引用')
196
+ } else {
197
+ console.log('❌ verify validator 未检测到 verify-result.md 缺少 D-001@v1 引用', verifyTrace)
198
+ failed++
199
+ }
200
+
201
+ writeFileSync(join(traceDir, 'verify-result.md'), '# Verify\n\n## 决策追踪矩阵\n| D-001@v1 | FR-01 | task-01 | evidence | PASS |\n')
202
+ const verifyTraceOk = runValidators('verify', traceRoot, 'trace')
203
+ if (verifyTraceOk.ok === true && !verifyTraceOk.warnings.some(w => w.includes('D-001@V1'))) {
204
+ console.log('✅ verify validator 在 D-001@v1 已覆盖时不再报警')
205
+ } else {
206
+ console.log('❌ verify validator 覆盖后仍报警', verifyTraceOk)
207
+ failed++
208
+ }
209
+
210
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n## D-002@v1: Unresolved schema conflict\n- priority: P0\n- status: unresolved\n')
211
+ const blockerTrace = runValidators('plan', traceRoot, 'trace')
212
+ if (blockerTrace.ok === false && blockerTrace.errors.some(e => e.includes('P0/P1 未决阻塞') && e.includes('D-002@V1'))) {
213
+ console.log('✅ plan validator 阻止 P0 unresolved decision 进入 plan')
214
+ } else {
215
+ console.log('❌ plan validator 未阻止 P0 unresolved decision', blockerTrace)
216
+ failed++
217
+ }
218
+
219
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n- id: D-003@v1\n priority: P1\n status: blocking\n type: boundary\n')
220
+ const yamlBlockerTrace = runValidators('plan', traceRoot, 'trace')
221
+ if (yamlBlockerTrace.ok === false && yamlBlockerTrace.errors.some(e => e.includes('P0/P1 未决阻塞') && e.includes('D-003@V1'))) {
222
+ console.log('✅ plan validator 支持 list/YAML 风格 decision record')
223
+ } else {
224
+ console.log('❌ plan validator 未识别 list/YAML 风格 decision record', yamlBlockerTrace)
225
+ failed++
226
+ }
227
+
228
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n- id: D-004@v1\n status: blocking\n type: boundary\n')
229
+ const missingPriorityTrace = runValidators('plan', traceRoot, 'trace')
230
+ if (missingPriorityTrace.ok === false
231
+ && missingPriorityTrace.errors.some(e => e.includes('P0/P1 未决阻塞') && e.includes('D-004@V1') && e.includes('priority=missing->P1'))) {
232
+ console.log('✅ plan validator 将缺 priority 的 blocking decision 按 P1 阻断')
233
+ } else {
234
+ console.log('❌ plan validator 未阻断缺 priority 的 blocking decision', missingPriorityTrace)
235
+ failed++
236
+ }
237
+
238
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n- id: D-005@v1\n status: accepted\n type: term\n')
239
+ writeFileSync(join(traceDir, 'plan.md'), '# Plan\n\n- [ ] task-01: implement naming(覆盖:FR-01)\n')
240
+ const yamlAcceptedTrace = runValidators('plan', traceRoot, 'trace')
241
+ if (yamlAcceptedTrace.ok === true && yamlAcceptedTrace.warnings.some(w => w.includes('plan.md 未引用') && w.includes('D-005@V1'))) {
242
+ console.log('✅ plan validator 将 YAML accepted decision 纳入追踪')
243
+ } else {
244
+ console.log('❌ plan validator 未追踪 YAML accepted decision', yamlAcceptedTrace)
245
+ failed++
246
+ }
247
+
248
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n')
249
+ writeFileSync(join(traceDir, 'requirements.md'), '# Requirements\n\n普通说明提到 https://example.test/spec/FR-404 和注释里的 FR-405,但它们不是结构化需求 ID。\n')
250
+ writeFileSync(join(traceDir, 'plan.md'), '# Plan\n\nNo structured requirement IDs here.\n')
251
+ const looseIdTrace = runValidators('plan', traceRoot, 'trace')
252
+ if (!looseIdTrace.warnings.some(w => w.includes('FR-404') || w.includes('FR-405'))) {
253
+ console.log('✅ plan validator 忽略普通正文/URL 中的 FR ID')
254
+ } else {
255
+ console.log('❌ plan validator 误提取普通正文/URL 中的 FR ID', looseIdTrace)
256
+ failed++
257
+ }
258
+
259
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n普通说明提到 https://example.test/spec/D-404@v1 和注释里的 D-405@v1,但它们不是结构化决策 ID。\n')
260
+ const looseBrainstormTrace = runValidators('brainstorm', traceRoot, 'trace')
261
+ if (!looseBrainstormTrace.warnings.some(w => w.includes('D-404@V1') || w.includes('D-405@V1'))) {
262
+ console.log('✅ brainstorm validator 忽略普通正文/URL 中的 D ID')
263
+ } else {
264
+ console.log('❌ brainstorm validator 误提取普通正文/URL 中的 D ID', looseBrainstormTrace)
265
+ failed++
266
+ }
267
+
268
+ rmSync(traceRoot, { recursive: true })
269
+
270
+ // === StageContract 结构测试 ===
271
+ console.log('\n=== Contract 结构测试 ===')
272
+
273
+ const plan = getContract('plan')
274
+ if (plan.allowedFrom.includes('brainstorm') && plan.allowedTo.includes('execute') && plan.validators.length === 1) {
275
+ console.log('✅ plan contract 结构正确')
276
+ } else {
277
+ console.log('❌ plan contract 结构异常:', JSON.stringify(plan))
278
+ failed++
279
+ }
280
+
281
+ const verify = getContract('verify')
282
+ if (verify.allowedFrom.includes('execute') && verify.allowedTo.includes('archive')) {
283
+ console.log('✅ verify contract 结构正确')
284
+ } else {
285
+ console.log('❌ verify contract 结构异常')
286
+ failed++
287
+ }
288
+
289
+ const unknown = getContract('nonexistent')
290
+ if (unknown === null) {
291
+ console.log('✅ 未知阶段返回 null')
292
+ } else {
293
+ console.log('❌ 未知阶段应返回 null')
294
+ failed++
295
+ }
296
+
297
+ // === 结果 ===
298
+ console.log(`\n${failed === 0 ? '✅ 全部通过' : `❌ ${failed} 项失败`}`)
299
+ if (failed > 0) throw new Error(`${failed} test(s) failed`)
@@ -0,0 +1,39 @@
1
+ import assert from 'node:assert/strict'
2
+ import { stageRegistry } from '../src/stages/index.js'
3
+ import { buildPlanSteps } from '../src/stages/plan.js'
4
+ import { buildExecuteSteps } from '../src/stages/execute.js'
5
+
6
+ const stageSteps = {
7
+ brainstorm: stageRegistry.brainstorm.steps,
8
+ scan: stageRegistry.scan.steps,
9
+ quick: stageRegistry.quick.steps,
10
+ archive: stageRegistry.archive.steps,
11
+ verify: stageRegistry.verify.steps,
12
+ plan: buildPlanSteps(null),
13
+ execute: buildExecuteSteps(null),
14
+ }
15
+
16
+ function names(stage) {
17
+ return stageSteps[stage].map(step => step.name)
18
+ }
19
+
20
+ function assertContains(stage, expectedNames) {
21
+ const actual = names(stage)
22
+ for (const name of expectedNames) {
23
+ assert.ok(actual.includes(name), `${stage} should include step "${name}". Actual: ${actual.join(', ')}`)
24
+ }
25
+ }
26
+
27
+ assert.equal(stageSteps.brainstorm.length, 13, 'brainstorm should include optional demand clarification and default Design Grill gates')
28
+ assertContains('brainstorm', ['需求澄清 Grill', '写设计文档并自审', 'Design Grill 交叉审查', '用户确认并生成规范文件'])
29
+
30
+ assert.equal(stageSteps.scan.length, 11, 'scan base definition should be 11 steps (with Extract Project Knowledge) before per-project expansion')
31
+ assertContains('scan', ['构建扫描项目列表', '生成本地配置', '生成模块映射'])
32
+
33
+ assert.equal(stageSteps.quick.length, 3, 'quick should remain a short auxiliary workflow')
34
+ assertContains('quick', ['理解任务', '实现并验证', '暂存和更新记录'])
35
+
36
+ assert.equal(stageSteps.archive.length, 5, 'archive should keep its five-step lifecycle')
37
+ assertContains('archive', ['extract-module-impact', 'sync-module-docs', '确认归档'])
38
+
39
+ console.log('✅ stage definition regression checks passed')