sillyspec 3.20.2 → 3.20.4

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 (133) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +21 -21
  2. package/.claude/skills/sillyspec-auto/SKILL.md +83 -83
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -44
  4. package/.claude/skills/sillyspec-commit/SKILL.md +106 -106
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +31 -31
  7. package/.claude/skills/sillyspec-execute/SKILL.md +30 -30
  8. package/.claude/skills/sillyspec-explore/SKILL.md +109 -109
  9. package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
  10. package/.claude/skills/sillyspec-plan/SKILL.md +21 -21
  11. package/.claude/skills/sillyspec-propose/SKILL.md +21 -21
  12. package/.claude/skills/sillyspec-quick/SKILL.md +21 -21
  13. package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
  14. package/.claude/skills/sillyspec-scan/SKILL.md +21 -21
  15. package/.claude/skills/sillyspec-state/SKILL.md +54 -54
  16. package/.claude/skills/sillyspec-status/SKILL.md +21 -21
  17. package/.claude/skills/sillyspec-verify/SKILL.md +21 -21
  18. package/.claude/skills/sillyspec-workspace/SKILL.md +157 -157
  19. package/.husky/pre-push +13 -13
  20. package/CLAUDE.md +18 -18
  21. package/README.md +198 -188
  22. package/SKILL.md +90 -91
  23. package/bin/sillyspec.js +2 -2
  24. package/docs/brainstorm-plan-contract.md +64 -64
  25. package/docs/plan-execute-contract.md +123 -123
  26. package/docs/platform-scan-protocol.md +298 -298
  27. package/docs/revision-mode.md +115 -115
  28. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -99
  29. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -218
  30. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -167
  31. package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -148
  32. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -193
  33. package/docs/sillyspec/file-lifecycle.md +125 -125
  34. package/docs/workflow-contract-regression.md +106 -106
  35. package/docs/worktree-isolation.md +252 -252
  36. package/package.json +40 -40
  37. package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
  38. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
  39. package/packages/dashboard/dist/index.html +16 -16
  40. package/packages/dashboard/index.html +15 -15
  41. package/packages/dashboard/package-lock.json +2384 -2384
  42. package/packages/dashboard/package.json +25 -25
  43. package/packages/dashboard/server/executor.js +86 -86
  44. package/packages/dashboard/server/index.js +588 -588
  45. package/packages/dashboard/server/parser.js +526 -526
  46. package/packages/dashboard/server/watcher.js +344 -344
  47. package/packages/dashboard/src/App.vue +558 -558
  48. package/packages/dashboard/src/components/ActionBar.vue +93 -93
  49. package/packages/dashboard/src/components/CommandPalette.vue +96 -96
  50. package/packages/dashboard/src/components/DetailPanel.vue +137 -137
  51. package/packages/dashboard/src/components/LogStream.vue +65 -65
  52. package/packages/dashboard/src/components/PipelineStage.vue +95 -95
  53. package/packages/dashboard/src/components/PipelineView.vue +156 -156
  54. package/packages/dashboard/src/components/ProjectList.vue +210 -210
  55. package/packages/dashboard/src/components/StageBadge.vue +67 -67
  56. package/packages/dashboard/src/components/StepCard.vue +94 -94
  57. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
  58. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
  59. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
  60. package/packages/dashboard/src/composables/useDashboard.js +170 -170
  61. package/packages/dashboard/src/composables/useKeyboard.js +119 -119
  62. package/packages/dashboard/src/composables/useWebSocket.js +129 -129
  63. package/packages/dashboard/src/main.js +8 -8
  64. package/packages/dashboard/src/style.css +132 -132
  65. package/packages/dashboard/vite.config.js +18 -18
  66. package/src/brainstorm-postcheck.js +158 -158
  67. package/src/change-list.js +52 -52
  68. package/src/change-risk-profile.js +352 -352
  69. package/src/classify-change.js +73 -73
  70. package/src/constants.js +70 -70
  71. package/src/contract-matrix.js +278 -278
  72. package/src/db.js +201 -201
  73. package/src/endpoint-extractor.js +315 -315
  74. package/src/hooks/claude-pre-tool-use.cjs +125 -125
  75. package/src/hooks/worktree-guard.js +653 -653
  76. package/src/index.js +922 -900
  77. package/src/init.js +431 -431
  78. package/src/knowledge-match.js +130 -130
  79. package/src/migrate.js +117 -117
  80. package/src/modules.js +482 -482
  81. package/src/progress.js +1734 -1734
  82. package/src/run.js +3465 -3358
  83. package/src/scan-postcheck.js +387 -383
  84. package/src/setup.js +398 -398
  85. package/src/stage-contract.js +700 -700
  86. package/src/stages/archive.js +160 -160
  87. package/src/stages/brainstorm-auto.js +229 -229
  88. package/src/stages/brainstorm.js +645 -645
  89. package/src/stages/doctor.js +365 -365
  90. package/src/stages/execute.js +625 -625
  91. package/src/stages/explore.js +34 -34
  92. package/src/stages/index.js +29 -29
  93. package/src/stages/knowledge.js +498 -498
  94. package/src/stages/plan-postcheck.js +511 -513
  95. package/src/stages/plan.js +582 -582
  96. package/src/stages/propose.js +174 -174
  97. package/src/stages/quick.js +82 -82
  98. package/src/stages/scan.js +558 -558
  99. package/src/stages/status.js +65 -65
  100. package/src/stages/verify.js +322 -322
  101. package/src/sync.js +497 -497
  102. package/src/task-review.js +346 -346
  103. package/src/workflow.js +785 -785
  104. package/src/worktree-apply.js +549 -549
  105. package/src/worktree-deps.js +185 -0
  106. package/src/worktree.js +982 -932
  107. package/templates/workflows/archive-impact.yaml +79 -79
  108. package/templates/workflows/scan-docs.yaml +132 -132
  109. package/test/brainstorm-plan-contract.test.mjs +273 -273
  110. package/test/check-syntax.mjs +26 -26
  111. package/test/contract-artifacts.test.mjs +323 -323
  112. package/test/decision-supersede.test.mjs +277 -277
  113. package/test/knowledge-match.test.mjs +231 -231
  114. package/test/plan-execute-contract.test.mjs +330 -330
  115. package/test/plan-optimization.test.mjs +572 -572
  116. package/test/platform-artifacts.test.mjs +166 -166
  117. package/test/platform-failure-samples.test.mjs +199 -199
  118. package/test/platform-recovery-chain.test.mjs +167 -167
  119. package/test/platform-recovery.test.mjs +136 -136
  120. package/test/platform-scan-p0.test.mjs +168 -168
  121. package/test/revision-v1.test.mjs +1145 -1145
  122. package/test/run-scan-project-parse.test.mjs +200 -200
  123. package/test/run-tests.mjs +48 -48
  124. package/test/scan-knowledge.test.mjs +175 -175
  125. package/test/scan-paths.test.mjs +68 -68
  126. package/test/scan-postcheck.test.mjs +197 -197
  127. package/test/spec-dir.test.mjs +206 -206
  128. package/test/stage-contract.test.mjs +299 -299
  129. package/test/stage-definitions.test.mjs +39 -39
  130. package/test/wait-gates.test.mjs +496 -496
  131. package/test/worktree-deps-provision.test.mjs +148 -0
  132. package/test/worktree-guard.test.mjs +71 -71
  133. package/test/worktree-native-overlay.test.mjs +188 -188
@@ -1,572 +1,572 @@
1
- /**
2
- * Plan 优化测试:验证新架构的 4 个关键功能
3
- *
4
- * 1. plan fast path 能生成合法 plan.md(buildPlanSteps 返回正确步骤数)
5
- * 2. light plan 最多 1~3 个 task
6
- * 3. depends_on 能正确排序 Wave(topoSortWaves)
7
- * 4. 循环依赖要 fail-fast(topoSortWaves)
8
- * 5. 缺 allowed_paths / acceptance / verify 时 validator 失败
9
- * 6. execute 能正常解析新 plan.md(validatePlanForExecute 仍通过)
10
- */
11
-
12
- import {
13
- buildPlanSteps,
14
- fixedPrefix,
15
- fixedSuffix,
16
- topoSortWaves,
17
- validateBlueprintConsistency,
18
- validateDesignForPlan,
19
- validatePlanFeasibility
20
- } from '../src/stages/plan.js'
21
- import { validatePlanForExecute } from '../src/stages/execute.js'
22
- import { existsSync, mkdirSync, writeFileSync, mkdtempSync, rmSync } from 'fs'
23
- import { join } from 'path'
24
- import { tmpdir } from 'os'
25
-
26
- let failed = 0
27
- const failures = []
28
-
29
- function assert(condition, msg) {
30
- if (!condition) {
31
- failed++
32
- failures.push(msg)
33
- console.log(` ❌ FAIL: ${msg}`)
34
- } else {
35
- console.log(` ✅ PASS: ${msg}`)
36
- }
37
- }
38
-
39
- console.log('=== Plan 优化测试 ===\n')
40
-
41
- // ─────────────────────────────────────────
42
- // Test 1: buildPlanSteps 返回正确步骤数(fast path = 无 task)
43
- // ─────────────────────────────────────────
44
- console.log('--- Test 1: buildPlanSteps fast path(无 plan.md) ---')
45
- {
46
- // 没有 changeDir 也没有 planContent → fast path
47
- const steps = buildPlanSteps(null, null)
48
- // 应该只有 fixedPrefix(2 步),无蓝图无 postcheck
49
- assert(steps.length === 2, `无 task 时应有 2 步(fixedPrefix),实际 ${steps.length}`)
50
- assert(steps[0].name === '复杂度分类与上下文加载', `第 1 步应为「复杂度分类与上下文加载」,实际 "${steps[0].name}"`)
51
- assert(steps[1].name === '生成分级计划与自检', `第 2 步应为「生成分级计划与自检」,实际 "${steps[1].name}"`)
52
- }
53
-
54
- // ─────────────────────────────────────────
55
- // Test 1b: buildPlanSteps 有 task 时返回 4 步
56
- // ─────────────────────────────────────────
57
- console.log('\n--- Test 1b: buildPlanSteps 有 task 时返回 4 步 ---')
58
- {
59
- const planContent = `# Plan\n\n## Wave 1\n- [ ] task-01: 做 A\n- [ ] task-02: 做 B\n`
60
- const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-test-'))
61
- const changeDir = join(tmpDir, 'test-change')
62
- mkdirSync(join(changeDir, 'tasks'), { recursive: true })
63
- writeFileSync(join(changeDir, 'plan.md'), planContent)
64
-
65
- const steps = buildPlanSteps(changeDir, planContent)
66
- assert(steps.length === 4, `有 task 时应有 4 步,实际 ${steps.length}`)
67
- assert(steps[2].name === '生成 TaskCard(子代理并行)', `第 3 步应为 TaskCard 生成,实际 "${steps[2].name}"`)
68
- assert(steps[3].name === 'Wave 重排与可行性校验', `第 4 步应为 postcheck,实际 "${steps[3].name}"`)
69
- assert(steps[3].noAI === true, `第 4 步应为 noAI`)
70
- assert(steps[3]._cliAction === 'planPostcheck', `第 4 步 _cliAction 应为 planPostcheck`)
71
-
72
- rmSync(tmpDir, { recursive: true, force: true })
73
- }
74
-
75
- // ─────────────────────────────────────────
76
- // Test 2: light plan 最多 1~3 个 task(通过 parseTaskCount 验证)
77
- // ─────────────────────────────────────────
78
- console.log('\n--- Test 2: light plan checkbox 解析 ---')
79
- {
80
- const lightPlan = `---
81
- plan_level: light
82
- ---
83
-
84
- # 轻量计划
85
-
86
- ## Wave 1
87
- - [ ] task-01: 修改字段
88
- - [ ] task-02: 更新测试
89
- - [ ] task-03: 文档补全
90
- `
91
- const result = validatePlanForExecute(lightPlan)
92
- assert(result.ok, 'light plan 应通过 execute contract')
93
- assert(result.tasks.length === 3, `应有 3 个 task,实际 ${result.tasks.length}`)
94
- }
95
-
96
- // ─────────────────────────────────────────
97
- // Test 3: depends_on 拓扑排序
98
- // ─────────────────────────────────────────
99
- console.log('\n--- Test 3: topoSortWaves 正确排序 ---')
100
- {
101
- // task-01 无依赖 → Wave 1
102
- // task-02 无依赖 → Wave 1
103
- // task-03 依赖 task-01 → Wave 2
104
- // task-04 依赖 task-03 → Wave 3
105
- const depMap = new Map([
106
- ['task-01', []],
107
- ['task-02', []],
108
- ['task-03', ['task-01']],
109
- ['task-04', ['task-03']],
110
- ])
111
- const { waves, error } = topoSortWaves(depMap)
112
- assert(error === null, `不应有错误`)
113
- assert(waves.length === 3, `应有 3 个 Wave,实际 ${waves.length}`)
114
- assert(waves[0].includes('task-01') && waves[0].includes('task-02'), `Wave 1 应含 task-01/02`)
115
- assert(waves[1].includes('task-03'), `Wave 2 应含 task-03`)
116
- assert(waves[2].includes('task-04'), `Wave 3 应含 task-04`)
117
- }
118
-
119
- // ─────────────────────────────────────────
120
- // Test 3b: 全并行(无依赖)→ 单 Wave
121
- // ─────────────────────────────────────────
122
- console.log('\n--- Test 3b: 全并行 → 单 Wave ---')
123
- {
124
- const depMap = new Map([
125
- ['task-01', []],
126
- ['task-02', []],
127
- ['task-03', []],
128
- ])
129
- const { waves, error } = topoSortWaves(depMap)
130
- assert(error === null, `不应有错误`)
131
- assert(waves.length === 1, `应有 1 个 Wave,实际 ${waves.length}`)
132
- assert(waves[0].length === 3, `Wave 1 应有 3 个 task`)
133
- }
134
-
135
- // ─────────────────────────────────────────
136
- // Test 4: 循环依赖 fail-fast
137
- // ─────────────────────────────────────────
138
- console.log('\n--- Test 4: 循环依赖 fail-fast ---')
139
- {
140
- // task-01 → task-02 → task-01(循环)
141
- const depMap = new Map([
142
- ['task-01', ['task-02']],
143
- ['task-02', ['task-01']],
144
- ])
145
- const { waves, error } = topoSortWaves(depMap)
146
- assert(error !== null, `循环依赖应报错`)
147
- assert(error.includes('循环依赖'), `错误应包含「循环依赖」,实际: ${error}`)
148
- assert(waves.length === 0, `循环依赖时 waves 应为空`)
149
- }
150
-
151
- // ─────────────────────────────────────────
152
- // Test 4b: 自循环依赖
153
- // ─────────────────────────────────────────
154
- console.log('\n--- Test 4b: 自循环依赖 ---')
155
- {
156
- const depMap = new Map([
157
- ['task-01', ['task-01']],
158
- ])
159
- const { error } = topoSortWaves(depMap)
160
- assert(error !== null, `自循环应报错`)
161
- }
162
-
163
- // ─────────────────────────────────────────
164
- // Test 5: validateBlueprintConsistency 缺 allowed_paths 失败
165
- // ─────────────────────────────────────────
166
- console.log('\n--- Test 5a: 缺 allowed_paths 失败 ---')
167
- {
168
- const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-test-'))
169
- const tasksDir = join(tmpDir, 'tasks')
170
- mkdirSync(tasksDir, { recursive: true })
171
-
172
- writeFileSync(join(tasksDir, 'task-01.md'), `---
173
- id: task-01
174
- title: 测试
175
- priority: P0
176
- depends_on: []
177
- blocks: []
178
- requirement_ids: []
179
- decision_ids: []
180
- allowed_paths: []
181
- ---
182
-
183
- # task-01: 测试
184
-
185
- ## 验收标准
186
- | # | 验证步骤 | 通过标准 |
187
- |---|---|---|
188
- | AC-01 | 操作 | 结果 |
189
- `)
190
-
191
- const result = validateBlueprintConsistency(tmpDir)
192
- assert(!result.ok, `缺 allowed_paths 应失败`)
193
- assert(result.errors.some(e => e.includes('allowed_paths')), `错误应提到 allowed_paths`)
194
- rmSync(tmpDir, { recursive: true, force: true })
195
- }
196
-
197
- // ─────────────────────────────────────────
198
- // Test 5b: 缺验收标准失败
199
- // ─────────────────────────────────────────
200
- console.log('\n--- Test 5b: 缺验收标准失败 ---')
201
- {
202
- const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-test-'))
203
- const tasksDir = join(tmpDir, 'tasks')
204
- mkdirSync(tasksDir, { recursive: true })
205
-
206
- writeFileSync(join(tasksDir, 'task-01.md'), `---
207
- id: task-01
208
- title: 测试
209
- priority: P0
210
- depends_on: []
211
- blocks: []
212
- requirement_ids: []
213
- decision_ids: []
214
- allowed_paths:
215
- - src/foo.js
216
- ---
217
-
218
- # task-01: 测试
219
-
220
- ## 修改文件
221
- - src/foo.js
222
-
223
- ## TDD 步骤
224
- 1. 写测试
225
- `)
226
-
227
- const result = validateBlueprintConsistency(tmpDir)
228
- assert(!result.ok, `缺验收标准应失败`)
229
- assert(result.errors.some(e => e.includes('验收标准')), `错误应提到验收标准`)
230
- rmSync(tmpDir, { recursive: true, force: true })
231
- }
232
-
233
- // ─────────────────────────────────────────
234
- // Test 5c: 完整蓝图通过校验
235
- // ─────────────────────────────────────────
236
- console.log('\n--- Test 5c: 完整蓝图通过校验 ---')
237
- {
238
- const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-test-'))
239
- const tasksDir = join(tmpDir, 'tasks')
240
- mkdirSync(tasksDir, { recursive: true })
241
-
242
- writeFileSync(join(tasksDir, 'task-01.md'), `---
243
- id: task-01
244
- title: 做 A
245
- author: test
246
- created_at: 2026-06-25 12:00:00
247
- priority: P0
248
- depends_on: []
249
- blocks: []
250
- requirement_ids: [FR-01]
251
- decision_ids: []
252
- allowed_paths:
253
- - src/a.js
254
- ---
255
-
256
- # task-01: 做 A
257
-
258
- ## 修改文件
259
- - src/a.js
260
-
261
- ## TDD 步骤
262
- 1. 写测试
263
-
264
- ## 验收标准
265
- | # | 验证步骤 | 通过标准 |
266
- |---|---|---|
267
- | AC-01 | 操作 | 结果 |
268
- `)
269
-
270
- writeFileSync(join(tasksDir, 'task-02.md'), `---
271
- id: task-02
272
- title: 做 B
273
- author: test
274
- created_at: 2026-06-25 12:00:00
275
- priority: P0
276
- depends_on: [task-01]
277
- blocks: []
278
- requirement_ids: [FR-02]
279
- decision_ids: []
280
- allowed_paths:
281
- - src/b.js
282
- ---
283
-
284
- # task-02: 做 B
285
-
286
- ## 修改文件
287
- - src/b.js
288
-
289
- ## TDD 步骤
290
- 1. 写测试
291
-
292
- ## 验收标准
293
- | # | 验证步骤 | 通过标准 |
294
- |---|---|---|
295
- | AC-01 | 操作 | 结果 |
296
- `)
297
-
298
- const result = validateBlueprintConsistency(tmpDir)
299
- assert(result.ok, `完整蓝图应通过校验,errors: ${JSON.stringify(result.errors)}`)
300
- rmSync(tmpDir, { recursive: true, force: true })
301
- }
302
-
303
- // ─────────────────────────────────────────
304
- // Test 6: execute 能正常解析新 plan.md
305
- // ─────────────────────────────────────────
306
- console.log('\n--- Test 6: execute contract 兼容 ---')
307
- {
308
- // none plan
309
- const nonePlan = `---
310
- plan_level: none
311
- ---
312
-
313
- # 计划跳过
314
-
315
- ## Wave 1
316
- - [ ] task-01: 按用户需求完成修改
317
- `
318
- const noneResult = validatePlanForExecute(nonePlan)
319
- assert(noneResult.ok, `none plan 应通过 execute contract`)
320
-
321
- // light plan
322
- const lightPlan = `---
323
- plan_level: light
324
- ---
325
-
326
- # 轻量计划
327
-
328
- ## Wave 1
329
- - [ ] task-01: 修改 A
330
- - [ ] task-02: 修改 B
331
-
332
- ## 验收
333
- - 测试通过
334
- `
335
- const lightResult = validatePlanForExecute(lightPlan)
336
- assert(lightResult.ok, `light plan 应通过 execute contract`)
337
- assert(lightResult.tasks.length === 2, `light plan 应有 2 个 task`)
338
-
339
- // full plan with waves
340
- const fullPlan = `---
341
- plan_level: full
342
- ---
343
-
344
- # 实现计划
345
-
346
- ## Wave 1
347
- - [ ] task-01: 基础
348
- - [ ] task-02: 配置
349
-
350
- ## Wave 2
351
- - [ ] task-03: 业务逻辑
352
-
353
- ## 任务总表
354
- | 编号 | 任务 | Wave | 优先级 | 依赖 | 覆盖 FR/D | 说明 |
355
- |---|---|---|---|---|---|---|
356
- | task-01 | 基础 | W1 | P0 | — | FR-01 | ... |
357
- | task-02 | 配置 | W1 | P0 | — | FR-02 | ... |
358
- | task-03 | 业务逻辑 | W2 | P0 | task-01,02 | FR-03 | ... |
359
- `
360
- const fullResult = validatePlanForExecute(fullPlan)
361
- assert(fullResult.ok, `full plan 应通过 execute contract`)
362
- assert(fullResult.tasks.length === 3, `full plan 应有 3 个 task`)
363
- assert(fullResult.waves.length === 2, `full plan 应有 2 个 wave`)
364
- }
365
-
366
- // ─────────────────────────────────────────
367
- // Test 7: fixedSuffix 为空(新架构变更)
368
- // ─────────────────────────────────────────
369
- console.log('\n--- Test 7: fixedSuffix 为空(新架构) ---')
370
- {
371
- assert(Array.isArray(fixedSuffix), `fixedSuffix 应为数组`)
372
- assert(fixedSuffix.length === 0, `fixedSuffix 应为空数组,实际长度 ${fixedSuffix.length}`)
373
- assert(Array.isArray(fixedPrefix), `fixedPrefix 应为数组`)
374
- assert(fixedPrefix.length === 2, `fixedPrefix 应有 2 个步骤,实际 ${fixedPrefix.length}`)
375
- }
376
-
377
- // ─────────────────────────────────────────
378
- // Test 8: 链式依赖(A→B→C→D)正确分 4 Wave
379
- // ─────────────────────────────────────────
380
- console.log('\n--- Test 8: 链式依赖分 4 Wave ---')
381
- {
382
- const depMap = new Map([
383
- ['task-01', []],
384
- ['task-02', ['task-01']],
385
- ['task-03', ['task-02']],
386
- ['task-04', ['task-03']],
387
- ])
388
- const { waves, error } = topoSortWaves(depMap)
389
- assert(error === null, `不应有错误`)
390
- assert(waves.length === 4, `链式依赖应有 4 个 Wave,实际 ${waves.length}`)
391
- assert(waves[0][0] === 'task-01', `Wave 1 应为 task-01`)
392
- assert(waves[3][0] === 'task-04', `Wave 4 应为 task-04`)
393
- }
394
-
395
- // ───────────────────────────────────────
396
- // Test 9: validatePlanFeasibility 缺必要字段失败
397
- // ───────────────────────────────────────
398
- console.log('\n--- Test 9: validatePlanFeasibility 缺字段失败 ---')
399
- {
400
- const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-feas-'))
401
- const tasksDir = join(tmpDir, 'tasks')
402
- mkdirSync(tasksDir, { recursive: true })
403
-
404
- writeFileSync(join(tasksDir, 'task-01.md'), `---
405
- id: task-01
406
- title: 测试
407
- priority: P0
408
- depends_on: []
409
- blocks: []
410
- allowed_paths:
411
- - src/foo.js
412
- ---
413
-
414
- some body
415
- `)
416
-
417
- const result = validatePlanFeasibility(tmpDir)
418
- assert(!result.ok, '缺必要字段应失败')
419
- assert(result.errors.some(e => e.includes('goal')), '应有 goal 错误')
420
- assert(result.errors.some(e => e.includes('implementation')), '应有 implementation 错误')
421
- assert(result.errors.some(e => e.includes('acceptance')), '应有 acceptance 错误')
422
- assert(result.errors.some(e => e.includes('verify')), '应有 verify 错误')
423
- assert(result.errors.some(e => e.includes('constraints')), '应有 constraints 错误')
424
-
425
- rmSync(tmpDir, { recursive: true, force: true })
426
- }
427
-
428
- // ───────────────────────────────────────
429
- // Test 10: validatePlanFeasibility 完整 TaskCard 通过
430
- // ───────────────────────────────────────
431
- console.log('\n--- Test 10: validatePlanFeasibility 完整 TaskCard 通过 ---')
432
- {
433
- const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-feas-ok-'))
434
- const tasksDir = join(tmpDir, 'tasks')
435
- mkdirSync(tasksDir, { recursive: true })
436
-
437
- writeFileSync(join(tasksDir, 'task-01.md'), `---
438
- id: task-01
439
- title: 做 A
440
- priority: P0
441
- depends_on: []
442
- blocks: []
443
- allowed_paths:
444
- - src/a.js
445
- goal: >
446
- 做事 A。
447
- implementation:
448
- - 步骤 1
449
- - 步骤 2
450
- acceptance:
451
- - 验收 1
452
- verify:
453
- - npm test
454
- constraints:
455
- - 不加测试
456
- ---
457
- `)
458
-
459
- const result = validatePlanFeasibility(tmpDir)
460
- assert(result.ok, `完整 TaskCard 应通过,errors: ${JSON.stringify(result.errors)}`)
461
-
462
- rmSync(tmpDir, { recursive: true, force: true })
463
- }
464
-
465
- // ───────────────────────────────────────
466
- // Test 11: validatePlanFeasibility depends_on 引用不存在失败
467
- // ───────────────────────────────────────
468
- console.log('\n--- Test 11: depends_on 引用不存在失败 ---')
469
- {
470
- const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-dep-'))
471
- const tasksDir = join(tmpDir, 'tasks')
472
- mkdirSync(tasksDir, { recursive: true })
473
-
474
- writeFileSync(join(tasksDir, 'task-01.md'), `---
475
- id: task-01
476
- title: A
477
- priority: P0
478
- depends_on: []
479
- blocks: []
480
- allowed_paths:
481
- - src/a.js
482
- goal: >
483
- A.
484
- implementation:
485
- - do
486
- acceptance:
487
- - ok
488
- verify:
489
- - npm test
490
- constraints:
491
- - none
492
- ---
493
- `)
494
-
495
- writeFileSync(join(tasksDir, 'task-02.md'), `---
496
- id: task-02
497
- title: B
498
- priority: P0
499
- depends_on: [task-99]
500
- blocks: []
501
- allowed_paths:
502
- - src/b.js
503
- goal: >
504
- B.
505
- implementation:
506
- - do
507
- acceptance:
508
- - ok
509
- verify:
510
- - npm test
511
- constraints:
512
- - none
513
- ---
514
- `)
515
-
516
- const result = validatePlanFeasibility(tmpDir)
517
- assert(!result.ok, '引用不存在的 task-99 应失败')
518
- assert(result.errors.some(e => e.includes('task-99')), '错误应提到 task-99')
519
-
520
- rmSync(tmpDir, { recursive: true, force: true })
521
- }
522
-
523
- // ───────────────────────────────────────
524
- // Test 12: validatePlanFeasibility task id 不连续失败
525
- // ───────────────────────────────────────
526
- console.log('\n--- Test 12: task id 不连续失败 ---')
527
- {
528
- const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-gap-'))
529
- const tasksDir = join(tmpDir, 'tasks')
530
- mkdirSync(tasksDir, { recursive: true })
531
-
532
- for (const { id, num } of [{ id: 'task-01', num: '01' }, { id: 'task-03', num: '03' }]) {
533
- writeFileSync(join(tasksDir, `task-${num}.md`), `---
534
- id: ${id}
535
- title: ${id}
536
- priority: P0
537
- depends_on: []
538
- blocks: []
539
- allowed_paths:
540
- - src/a.js
541
- goal: >
542
- test.
543
- implementation:
544
- - do
545
- acceptance:
546
- - ok
547
- verify:
548
- - npm test
549
- constraints:
550
- - none
551
- ---
552
- `)
553
- }
554
-
555
- const result = validatePlanFeasibility(tmpDir)
556
- assert(!result.ok, 'task id 不连续应失败')
557
- assert(result.errors.some(e => e.includes('不连续')), '错误应提到不连续')
558
-
559
- rmSync(tmpDir, { recursive: true, force: true })
560
- }
561
-
562
- // ── 结果 ──
563
- console.log(`\n${'='.repeat(50)}`)
564
- const total = 12
565
- console.log(`✅ 通过: ${total - failed} ❌ 失败: ${failed}`)
566
- if (failures.length > 0) {
567
- console.log(`失败项:`)
568
- failures.forEach(f => console.log(` - ${f}`))
569
- }
570
- console.log(`${'='.repeat(50)}`)
571
-
572
- if (failed > 0) process.exit(1)
1
+ /**
2
+ * Plan 优化测试:验证新架构的 4 个关键功能
3
+ *
4
+ * 1. plan fast path 能生成合法 plan.md(buildPlanSteps 返回正确步骤数)
5
+ * 2. light plan 最多 1~3 个 task
6
+ * 3. depends_on 能正确排序 Wave(topoSortWaves)
7
+ * 4. 循环依赖要 fail-fast(topoSortWaves)
8
+ * 5. 缺 allowed_paths / acceptance / verify 时 validator 失败
9
+ * 6. execute 能正常解析新 plan.md(validatePlanForExecute 仍通过)
10
+ */
11
+
12
+ import {
13
+ buildPlanSteps,
14
+ fixedPrefix,
15
+ fixedSuffix,
16
+ topoSortWaves,
17
+ validateBlueprintConsistency,
18
+ validateDesignForPlan,
19
+ validatePlanFeasibility
20
+ } from '../src/stages/plan.js'
21
+ import { validatePlanForExecute } from '../src/stages/execute.js'
22
+ import { existsSync, mkdirSync, writeFileSync, mkdtempSync, rmSync } from 'fs'
23
+ import { join } from 'path'
24
+ import { tmpdir } from 'os'
25
+
26
+ let failed = 0
27
+ const failures = []
28
+
29
+ function assert(condition, msg) {
30
+ if (!condition) {
31
+ failed++
32
+ failures.push(msg)
33
+ console.log(` ❌ FAIL: ${msg}`)
34
+ } else {
35
+ console.log(` ✅ PASS: ${msg}`)
36
+ }
37
+ }
38
+
39
+ console.log('=== Plan 优化测试 ===\n')
40
+
41
+ // ─────────────────────────────────────────
42
+ // Test 1: buildPlanSteps 返回正确步骤数(fast path = 无 task)
43
+ // ─────────────────────────────────────────
44
+ console.log('--- Test 1: buildPlanSteps fast path(无 plan.md) ---')
45
+ {
46
+ // 没有 changeDir 也没有 planContent → fast path
47
+ const steps = buildPlanSteps(null, null)
48
+ // 应该只有 fixedPrefix(2 步),无蓝图无 postcheck
49
+ assert(steps.length === 2, `无 task 时应有 2 步(fixedPrefix),实际 ${steps.length}`)
50
+ assert(steps[0].name === '复杂度分类与上下文加载', `第 1 步应为「复杂度分类与上下文加载」,实际 "${steps[0].name}"`)
51
+ assert(steps[1].name === '生成分级计划与自检', `第 2 步应为「生成分级计划与自检」,实际 "${steps[1].name}"`)
52
+ }
53
+
54
+ // ─────────────────────────────────────────
55
+ // Test 1b: buildPlanSteps 有 task 时返回 4 步
56
+ // ─────────────────────────────────────────
57
+ console.log('\n--- Test 1b: buildPlanSteps 有 task 时返回 4 步 ---')
58
+ {
59
+ const planContent = `# Plan\n\n## Wave 1\n- [ ] task-01: 做 A\n- [ ] task-02: 做 B\n`
60
+ const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-test-'))
61
+ const changeDir = join(tmpDir, 'test-change')
62
+ mkdirSync(join(changeDir, 'tasks'), { recursive: true })
63
+ writeFileSync(join(changeDir, 'plan.md'), planContent)
64
+
65
+ const steps = buildPlanSteps(changeDir, planContent)
66
+ assert(steps.length === 4, `有 task 时应有 4 步,实际 ${steps.length}`)
67
+ assert(steps[2].name === '生成 TaskCard(子代理并行)', `第 3 步应为 TaskCard 生成,实际 "${steps[2].name}"`)
68
+ assert(steps[3].name === 'Wave 重排与可行性校验', `第 4 步应为 postcheck,实际 "${steps[3].name}"`)
69
+ assert(steps[3].noAI === true, `第 4 步应为 noAI`)
70
+ assert(steps[3]._cliAction === 'planPostcheck', `第 4 步 _cliAction 应为 planPostcheck`)
71
+
72
+ rmSync(tmpDir, { recursive: true, force: true })
73
+ }
74
+
75
+ // ─────────────────────────────────────────
76
+ // Test 2: light plan 最多 1~3 个 task(通过 parseTaskCount 验证)
77
+ // ─────────────────────────────────────────
78
+ console.log('\n--- Test 2: light plan checkbox 解析 ---')
79
+ {
80
+ const lightPlan = `---
81
+ plan_level: light
82
+ ---
83
+
84
+ # 轻量计划
85
+
86
+ ## Wave 1
87
+ - [ ] task-01: 修改字段
88
+ - [ ] task-02: 更新测试
89
+ - [ ] task-03: 文档补全
90
+ `
91
+ const result = validatePlanForExecute(lightPlan)
92
+ assert(result.ok, 'light plan 应通过 execute contract')
93
+ assert(result.tasks.length === 3, `应有 3 个 task,实际 ${result.tasks.length}`)
94
+ }
95
+
96
+ // ─────────────────────────────────────────
97
+ // Test 3: depends_on 拓扑排序
98
+ // ─────────────────────────────────────────
99
+ console.log('\n--- Test 3: topoSortWaves 正确排序 ---')
100
+ {
101
+ // task-01 无依赖 → Wave 1
102
+ // task-02 无依赖 → Wave 1
103
+ // task-03 依赖 task-01 → Wave 2
104
+ // task-04 依赖 task-03 → Wave 3
105
+ const depMap = new Map([
106
+ ['task-01', []],
107
+ ['task-02', []],
108
+ ['task-03', ['task-01']],
109
+ ['task-04', ['task-03']],
110
+ ])
111
+ const { waves, error } = topoSortWaves(depMap)
112
+ assert(error === null, `不应有错误`)
113
+ assert(waves.length === 3, `应有 3 个 Wave,实际 ${waves.length}`)
114
+ assert(waves[0].includes('task-01') && waves[0].includes('task-02'), `Wave 1 应含 task-01/02`)
115
+ assert(waves[1].includes('task-03'), `Wave 2 应含 task-03`)
116
+ assert(waves[2].includes('task-04'), `Wave 3 应含 task-04`)
117
+ }
118
+
119
+ // ─────────────────────────────────────────
120
+ // Test 3b: 全并行(无依赖)→ 单 Wave
121
+ // ─────────────────────────────────────────
122
+ console.log('\n--- Test 3b: 全并行 → 单 Wave ---')
123
+ {
124
+ const depMap = new Map([
125
+ ['task-01', []],
126
+ ['task-02', []],
127
+ ['task-03', []],
128
+ ])
129
+ const { waves, error } = topoSortWaves(depMap)
130
+ assert(error === null, `不应有错误`)
131
+ assert(waves.length === 1, `应有 1 个 Wave,实际 ${waves.length}`)
132
+ assert(waves[0].length === 3, `Wave 1 应有 3 个 task`)
133
+ }
134
+
135
+ // ─────────────────────────────────────────
136
+ // Test 4: 循环依赖 fail-fast
137
+ // ─────────────────────────────────────────
138
+ console.log('\n--- Test 4: 循环依赖 fail-fast ---')
139
+ {
140
+ // task-01 → task-02 → task-01(循环)
141
+ const depMap = new Map([
142
+ ['task-01', ['task-02']],
143
+ ['task-02', ['task-01']],
144
+ ])
145
+ const { waves, error } = topoSortWaves(depMap)
146
+ assert(error !== null, `循环依赖应报错`)
147
+ assert(error.includes('循环依赖'), `错误应包含「循环依赖」,实际: ${error}`)
148
+ assert(waves.length === 0, `循环依赖时 waves 应为空`)
149
+ }
150
+
151
+ // ─────────────────────────────────────────
152
+ // Test 4b: 自循环依赖
153
+ // ─────────────────────────────────────────
154
+ console.log('\n--- Test 4b: 自循环依赖 ---')
155
+ {
156
+ const depMap = new Map([
157
+ ['task-01', ['task-01']],
158
+ ])
159
+ const { error } = topoSortWaves(depMap)
160
+ assert(error !== null, `自循环应报错`)
161
+ }
162
+
163
+ // ─────────────────────────────────────────
164
+ // Test 5: validateBlueprintConsistency 缺 allowed_paths 失败
165
+ // ─────────────────────────────────────────
166
+ console.log('\n--- Test 5a: 缺 allowed_paths 失败 ---')
167
+ {
168
+ const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-test-'))
169
+ const tasksDir = join(tmpDir, 'tasks')
170
+ mkdirSync(tasksDir, { recursive: true })
171
+
172
+ writeFileSync(join(tasksDir, 'task-01.md'), `---
173
+ id: task-01
174
+ title: 测试
175
+ priority: P0
176
+ depends_on: []
177
+ blocks: []
178
+ requirement_ids: []
179
+ decision_ids: []
180
+ allowed_paths: []
181
+ ---
182
+
183
+ # task-01: 测试
184
+
185
+ ## 验收标准
186
+ | # | 验证步骤 | 通过标准 |
187
+ |---|---|---|
188
+ | AC-01 | 操作 | 结果 |
189
+ `)
190
+
191
+ const result = validateBlueprintConsistency(tmpDir)
192
+ assert(!result.ok, `缺 allowed_paths 应失败`)
193
+ assert(result.errors.some(e => e.includes('allowed_paths')), `错误应提到 allowed_paths`)
194
+ rmSync(tmpDir, { recursive: true, force: true })
195
+ }
196
+
197
+ // ─────────────────────────────────────────
198
+ // Test 5b: 缺验收标准失败
199
+ // ─────────────────────────────────────────
200
+ console.log('\n--- Test 5b: 缺验收标准失败 ---')
201
+ {
202
+ const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-test-'))
203
+ const tasksDir = join(tmpDir, 'tasks')
204
+ mkdirSync(tasksDir, { recursive: true })
205
+
206
+ writeFileSync(join(tasksDir, 'task-01.md'), `---
207
+ id: task-01
208
+ title: 测试
209
+ priority: P0
210
+ depends_on: []
211
+ blocks: []
212
+ requirement_ids: []
213
+ decision_ids: []
214
+ allowed_paths:
215
+ - src/foo.js
216
+ ---
217
+
218
+ # task-01: 测试
219
+
220
+ ## 修改文件
221
+ - src/foo.js
222
+
223
+ ## TDD 步骤
224
+ 1. 写测试
225
+ `)
226
+
227
+ const result = validateBlueprintConsistency(tmpDir)
228
+ assert(!result.ok, `缺验收标准应失败`)
229
+ assert(result.errors.some(e => e.includes('验收标准')), `错误应提到验收标准`)
230
+ rmSync(tmpDir, { recursive: true, force: true })
231
+ }
232
+
233
+ // ─────────────────────────────────────────
234
+ // Test 5c: 完整蓝图通过校验
235
+ // ─────────────────────────────────────────
236
+ console.log('\n--- Test 5c: 完整蓝图通过校验 ---')
237
+ {
238
+ const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-test-'))
239
+ const tasksDir = join(tmpDir, 'tasks')
240
+ mkdirSync(tasksDir, { recursive: true })
241
+
242
+ writeFileSync(join(tasksDir, 'task-01.md'), `---
243
+ id: task-01
244
+ title: 做 A
245
+ author: test
246
+ created_at: 2026-06-25 12:00:00
247
+ priority: P0
248
+ depends_on: []
249
+ blocks: []
250
+ requirement_ids: [FR-01]
251
+ decision_ids: []
252
+ allowed_paths:
253
+ - src/a.js
254
+ ---
255
+
256
+ # task-01: 做 A
257
+
258
+ ## 修改文件
259
+ - src/a.js
260
+
261
+ ## TDD 步骤
262
+ 1. 写测试
263
+
264
+ ## 验收标准
265
+ | # | 验证步骤 | 通过标准 |
266
+ |---|---|---|
267
+ | AC-01 | 操作 | 结果 |
268
+ `)
269
+
270
+ writeFileSync(join(tasksDir, 'task-02.md'), `---
271
+ id: task-02
272
+ title: 做 B
273
+ author: test
274
+ created_at: 2026-06-25 12:00:00
275
+ priority: P0
276
+ depends_on: [task-01]
277
+ blocks: []
278
+ requirement_ids: [FR-02]
279
+ decision_ids: []
280
+ allowed_paths:
281
+ - src/b.js
282
+ ---
283
+
284
+ # task-02: 做 B
285
+
286
+ ## 修改文件
287
+ - src/b.js
288
+
289
+ ## TDD 步骤
290
+ 1. 写测试
291
+
292
+ ## 验收标准
293
+ | # | 验证步骤 | 通过标准 |
294
+ |---|---|---|
295
+ | AC-01 | 操作 | 结果 |
296
+ `)
297
+
298
+ const result = validateBlueprintConsistency(tmpDir)
299
+ assert(result.ok, `完整蓝图应通过校验,errors: ${JSON.stringify(result.errors)}`)
300
+ rmSync(tmpDir, { recursive: true, force: true })
301
+ }
302
+
303
+ // ─────────────────────────────────────────
304
+ // Test 6: execute 能正常解析新 plan.md
305
+ // ─────────────────────────────────────────
306
+ console.log('\n--- Test 6: execute contract 兼容 ---')
307
+ {
308
+ // none plan
309
+ const nonePlan = `---
310
+ plan_level: none
311
+ ---
312
+
313
+ # 计划跳过
314
+
315
+ ## Wave 1
316
+ - [ ] task-01: 按用户需求完成修改
317
+ `
318
+ const noneResult = validatePlanForExecute(nonePlan)
319
+ assert(noneResult.ok, `none plan 应通过 execute contract`)
320
+
321
+ // light plan
322
+ const lightPlan = `---
323
+ plan_level: light
324
+ ---
325
+
326
+ # 轻量计划
327
+
328
+ ## Wave 1
329
+ - [ ] task-01: 修改 A
330
+ - [ ] task-02: 修改 B
331
+
332
+ ## 验收
333
+ - 测试通过
334
+ `
335
+ const lightResult = validatePlanForExecute(lightPlan)
336
+ assert(lightResult.ok, `light plan 应通过 execute contract`)
337
+ assert(lightResult.tasks.length === 2, `light plan 应有 2 个 task`)
338
+
339
+ // full plan with waves
340
+ const fullPlan = `---
341
+ plan_level: full
342
+ ---
343
+
344
+ # 实现计划
345
+
346
+ ## Wave 1
347
+ - [ ] task-01: 基础
348
+ - [ ] task-02: 配置
349
+
350
+ ## Wave 2
351
+ - [ ] task-03: 业务逻辑
352
+
353
+ ## 任务总表
354
+ | 编号 | 任务 | Wave | 优先级 | 依赖 | 覆盖 FR/D | 说明 |
355
+ |---|---|---|---|---|---|---|
356
+ | task-01 | 基础 | W1 | P0 | — | FR-01 | ... |
357
+ | task-02 | 配置 | W1 | P0 | — | FR-02 | ... |
358
+ | task-03 | 业务逻辑 | W2 | P0 | task-01,02 | FR-03 | ... |
359
+ `
360
+ const fullResult = validatePlanForExecute(fullPlan)
361
+ assert(fullResult.ok, `full plan 应通过 execute contract`)
362
+ assert(fullResult.tasks.length === 3, `full plan 应有 3 个 task`)
363
+ assert(fullResult.waves.length === 2, `full plan 应有 2 个 wave`)
364
+ }
365
+
366
+ // ─────────────────────────────────────────
367
+ // Test 7: fixedSuffix 为空(新架构变更)
368
+ // ─────────────────────────────────────────
369
+ console.log('\n--- Test 7: fixedSuffix 为空(新架构) ---')
370
+ {
371
+ assert(Array.isArray(fixedSuffix), `fixedSuffix 应为数组`)
372
+ assert(fixedSuffix.length === 0, `fixedSuffix 应为空数组,实际长度 ${fixedSuffix.length}`)
373
+ assert(Array.isArray(fixedPrefix), `fixedPrefix 应为数组`)
374
+ assert(fixedPrefix.length === 2, `fixedPrefix 应有 2 个步骤,实际 ${fixedPrefix.length}`)
375
+ }
376
+
377
+ // ─────────────────────────────────────────
378
+ // Test 8: 链式依赖(A→B→C→D)正确分 4 Wave
379
+ // ─────────────────────────────────────────
380
+ console.log('\n--- Test 8: 链式依赖分 4 Wave ---')
381
+ {
382
+ const depMap = new Map([
383
+ ['task-01', []],
384
+ ['task-02', ['task-01']],
385
+ ['task-03', ['task-02']],
386
+ ['task-04', ['task-03']],
387
+ ])
388
+ const { waves, error } = topoSortWaves(depMap)
389
+ assert(error === null, `不应有错误`)
390
+ assert(waves.length === 4, `链式依赖应有 4 个 Wave,实际 ${waves.length}`)
391
+ assert(waves[0][0] === 'task-01', `Wave 1 应为 task-01`)
392
+ assert(waves[3][0] === 'task-04', `Wave 4 应为 task-04`)
393
+ }
394
+
395
+ // ───────────────────────────────────────
396
+ // Test 9: validatePlanFeasibility 缺必要字段失败
397
+ // ───────────────────────────────────────
398
+ console.log('\n--- Test 9: validatePlanFeasibility 缺字段失败 ---')
399
+ {
400
+ const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-feas-'))
401
+ const tasksDir = join(tmpDir, 'tasks')
402
+ mkdirSync(tasksDir, { recursive: true })
403
+
404
+ writeFileSync(join(tasksDir, 'task-01.md'), `---
405
+ id: task-01
406
+ title: 测试
407
+ priority: P0
408
+ depends_on: []
409
+ blocks: []
410
+ allowed_paths:
411
+ - src/foo.js
412
+ ---
413
+
414
+ some body
415
+ `)
416
+
417
+ const result = validatePlanFeasibility(tmpDir)
418
+ assert(!result.ok, '缺必要字段应失败')
419
+ assert(result.errors.some(e => e.includes('goal')), '应有 goal 错误')
420
+ assert(result.errors.some(e => e.includes('implementation')), '应有 implementation 错误')
421
+ assert(result.errors.some(e => e.includes('acceptance')), '应有 acceptance 错误')
422
+ assert(result.errors.some(e => e.includes('verify')), '应有 verify 错误')
423
+ assert(result.errors.some(e => e.includes('constraints')), '应有 constraints 错误')
424
+
425
+ rmSync(tmpDir, { recursive: true, force: true })
426
+ }
427
+
428
+ // ───────────────────────────────────────
429
+ // Test 10: validatePlanFeasibility 完整 TaskCard 通过
430
+ // ───────────────────────────────────────
431
+ console.log('\n--- Test 10: validatePlanFeasibility 完整 TaskCard 通过 ---')
432
+ {
433
+ const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-feas-ok-'))
434
+ const tasksDir = join(tmpDir, 'tasks')
435
+ mkdirSync(tasksDir, { recursive: true })
436
+
437
+ writeFileSync(join(tasksDir, 'task-01.md'), `---
438
+ id: task-01
439
+ title: 做 A
440
+ priority: P0
441
+ depends_on: []
442
+ blocks: []
443
+ allowed_paths:
444
+ - src/a.js
445
+ goal: >
446
+ 做事 A。
447
+ implementation:
448
+ - 步骤 1
449
+ - 步骤 2
450
+ acceptance:
451
+ - 验收 1
452
+ verify:
453
+ - npm test
454
+ constraints:
455
+ - 不加测试
456
+ ---
457
+ `)
458
+
459
+ const result = validatePlanFeasibility(tmpDir)
460
+ assert(result.ok, `完整 TaskCard 应通过,errors: ${JSON.stringify(result.errors)}`)
461
+
462
+ rmSync(tmpDir, { recursive: true, force: true })
463
+ }
464
+
465
+ // ───────────────────────────────────────
466
+ // Test 11: validatePlanFeasibility depends_on 引用不存在失败
467
+ // ───────────────────────────────────────
468
+ console.log('\n--- Test 11: depends_on 引用不存在失败 ---')
469
+ {
470
+ const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-dep-'))
471
+ const tasksDir = join(tmpDir, 'tasks')
472
+ mkdirSync(tasksDir, { recursive: true })
473
+
474
+ writeFileSync(join(tasksDir, 'task-01.md'), `---
475
+ id: task-01
476
+ title: A
477
+ priority: P0
478
+ depends_on: []
479
+ blocks: []
480
+ allowed_paths:
481
+ - src/a.js
482
+ goal: >
483
+ A.
484
+ implementation:
485
+ - do
486
+ acceptance:
487
+ - ok
488
+ verify:
489
+ - npm test
490
+ constraints:
491
+ - none
492
+ ---
493
+ `)
494
+
495
+ writeFileSync(join(tasksDir, 'task-02.md'), `---
496
+ id: task-02
497
+ title: B
498
+ priority: P0
499
+ depends_on: [task-99]
500
+ blocks: []
501
+ allowed_paths:
502
+ - src/b.js
503
+ goal: >
504
+ B.
505
+ implementation:
506
+ - do
507
+ acceptance:
508
+ - ok
509
+ verify:
510
+ - npm test
511
+ constraints:
512
+ - none
513
+ ---
514
+ `)
515
+
516
+ const result = validatePlanFeasibility(tmpDir)
517
+ assert(!result.ok, '引用不存在的 task-99 应失败')
518
+ assert(result.errors.some(e => e.includes('task-99')), '错误应提到 task-99')
519
+
520
+ rmSync(tmpDir, { recursive: true, force: true })
521
+ }
522
+
523
+ // ───────────────────────────────────────
524
+ // Test 12: validatePlanFeasibility task id 不连续失败
525
+ // ───────────────────────────────────────
526
+ console.log('\n--- Test 12: task id 不连续失败 ---')
527
+ {
528
+ const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-gap-'))
529
+ const tasksDir = join(tmpDir, 'tasks')
530
+ mkdirSync(tasksDir, { recursive: true })
531
+
532
+ for (const { id, num } of [{ id: 'task-01', num: '01' }, { id: 'task-03', num: '03' }]) {
533
+ writeFileSync(join(tasksDir, `task-${num}.md`), `---
534
+ id: ${id}
535
+ title: ${id}
536
+ priority: P0
537
+ depends_on: []
538
+ blocks: []
539
+ allowed_paths:
540
+ - src/a.js
541
+ goal: >
542
+ test.
543
+ implementation:
544
+ - do
545
+ acceptance:
546
+ - ok
547
+ verify:
548
+ - npm test
549
+ constraints:
550
+ - none
551
+ ---
552
+ `)
553
+ }
554
+
555
+ const result = validatePlanFeasibility(tmpDir)
556
+ assert(!result.ok, 'task id 不连续应失败')
557
+ assert(result.errors.some(e => e.includes('不连续')), '错误应提到不连续')
558
+
559
+ rmSync(tmpDir, { recursive: true, force: true })
560
+ }
561
+
562
+ // ── 结果 ──
563
+ console.log(`\n${'='.repeat(50)}`)
564
+ const total = 12
565
+ console.log(`✅ 通过: ${total - failed} ❌ 失败: ${failed}`)
566
+ if (failures.length > 0) {
567
+ console.log(`失败项:`)
568
+ failures.forEach(f => console.log(` - ${f}`))
569
+ }
570
+ console.log(`${'='.repeat(50)}`)
571
+
572
+ if (failed > 0) process.exit(1)