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.
- package/.claude/skills/sillyspec-archive/SKILL.md +21 -21
- package/.claude/skills/sillyspec-auto/SKILL.md +83 -83
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -44
- package/.claude/skills/sillyspec-commit/SKILL.md +106 -106
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
- package/.claude/skills/sillyspec-doctor/SKILL.md +31 -31
- package/.claude/skills/sillyspec-execute/SKILL.md +30 -30
- package/.claude/skills/sillyspec-explore/SKILL.md +109 -109
- package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
- package/.claude/skills/sillyspec-plan/SKILL.md +21 -21
- package/.claude/skills/sillyspec-propose/SKILL.md +21 -21
- package/.claude/skills/sillyspec-quick/SKILL.md +21 -21
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
- package/.claude/skills/sillyspec-scan/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +54 -54
- package/.claude/skills/sillyspec-status/SKILL.md +21 -21
- package/.claude/skills/sillyspec-verify/SKILL.md +21 -21
- package/.claude/skills/sillyspec-workspace/SKILL.md +157 -157
- package/.husky/pre-push +13 -13
- package/CLAUDE.md +18 -18
- package/README.md +198 -188
- package/SKILL.md +90 -91
- package/bin/sillyspec.js +2 -2
- package/docs/brainstorm-plan-contract.md +64 -64
- package/docs/plan-execute-contract.md +123 -123
- package/docs/platform-scan-protocol.md +298 -298
- package/docs/revision-mode.md +115 -115
- package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -99
- package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -218
- package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -167
- package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -148
- package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -193
- package/docs/sillyspec/file-lifecycle.md +125 -125
- package/docs/workflow-contract-regression.md +106 -106
- package/docs/worktree-isolation.md +252 -252
- package/package.json +40 -40
- package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/index.html +15 -15
- package/packages/dashboard/package-lock.json +2384 -2384
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/server/executor.js +86 -86
- package/packages/dashboard/server/index.js +588 -588
- package/packages/dashboard/server/parser.js +526 -526
- package/packages/dashboard/server/watcher.js +344 -344
- package/packages/dashboard/src/App.vue +558 -558
- package/packages/dashboard/src/components/ActionBar.vue +93 -93
- package/packages/dashboard/src/components/CommandPalette.vue +96 -96
- package/packages/dashboard/src/components/DetailPanel.vue +137 -137
- package/packages/dashboard/src/components/LogStream.vue +65 -65
- package/packages/dashboard/src/components/PipelineStage.vue +95 -95
- package/packages/dashboard/src/components/PipelineView.vue +156 -156
- package/packages/dashboard/src/components/ProjectList.vue +210 -210
- package/packages/dashboard/src/components/StageBadge.vue +67 -67
- package/packages/dashboard/src/components/StepCard.vue +94 -94
- package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
- package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
- package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
- package/packages/dashboard/src/composables/useDashboard.js +170 -170
- package/packages/dashboard/src/composables/useKeyboard.js +119 -119
- package/packages/dashboard/src/composables/useWebSocket.js +129 -129
- package/packages/dashboard/src/main.js +8 -8
- package/packages/dashboard/src/style.css +132 -132
- package/packages/dashboard/vite.config.js +18 -18
- package/src/brainstorm-postcheck.js +158 -158
- package/src/change-list.js +52 -52
- package/src/change-risk-profile.js +352 -352
- package/src/classify-change.js +73 -73
- package/src/constants.js +70 -70
- package/src/contract-matrix.js +278 -278
- package/src/db.js +201 -201
- package/src/endpoint-extractor.js +315 -315
- package/src/hooks/claude-pre-tool-use.cjs +125 -125
- package/src/hooks/worktree-guard.js +653 -653
- package/src/index.js +922 -900
- package/src/init.js +431 -431
- package/src/knowledge-match.js +130 -130
- package/src/migrate.js +117 -117
- package/src/modules.js +482 -482
- package/src/progress.js +1734 -1734
- package/src/run.js +3465 -3358
- package/src/scan-postcheck.js +387 -383
- package/src/setup.js +398 -398
- package/src/stage-contract.js +700 -700
- package/src/stages/archive.js +160 -160
- package/src/stages/brainstorm-auto.js +229 -229
- package/src/stages/brainstorm.js +645 -645
- package/src/stages/doctor.js +365 -365
- package/src/stages/execute.js +625 -625
- package/src/stages/explore.js +34 -34
- package/src/stages/index.js +29 -29
- package/src/stages/knowledge.js +498 -498
- package/src/stages/plan-postcheck.js +511 -513
- package/src/stages/plan.js +582 -582
- package/src/stages/propose.js +174 -174
- package/src/stages/quick.js +82 -82
- package/src/stages/scan.js +558 -558
- package/src/stages/status.js +65 -65
- package/src/stages/verify.js +322 -322
- package/src/sync.js +497 -497
- package/src/task-review.js +346 -346
- package/src/workflow.js +785 -785
- package/src/worktree-apply.js +549 -549
- package/src/worktree-deps.js +185 -0
- package/src/worktree.js +982 -932
- package/templates/workflows/archive-impact.yaml +79 -79
- package/templates/workflows/scan-docs.yaml +132 -132
- package/test/brainstorm-plan-contract.test.mjs +273 -273
- package/test/check-syntax.mjs +26 -26
- package/test/contract-artifacts.test.mjs +323 -323
- package/test/decision-supersede.test.mjs +277 -277
- package/test/knowledge-match.test.mjs +231 -231
- package/test/plan-execute-contract.test.mjs +330 -330
- package/test/plan-optimization.test.mjs +572 -572
- package/test/platform-artifacts.test.mjs +166 -166
- package/test/platform-failure-samples.test.mjs +199 -199
- package/test/platform-recovery-chain.test.mjs +167 -167
- package/test/platform-recovery.test.mjs +136 -136
- package/test/platform-scan-p0.test.mjs +168 -168
- package/test/revision-v1.test.mjs +1145 -1145
- package/test/run-scan-project-parse.test.mjs +200 -200
- package/test/run-tests.mjs +48 -48
- package/test/scan-knowledge.test.mjs +175 -175
- package/test/scan-paths.test.mjs +68 -68
- package/test/scan-postcheck.test.mjs +197 -197
- package/test/spec-dir.test.mjs +206 -206
- package/test/stage-contract.test.mjs +299 -299
- package/test/stage-definitions.test.mjs +39 -39
- package/test/wait-gates.test.mjs +496 -496
- package/test/worktree-deps-provision.test.mjs +148 -0
- package/test/worktree-guard.test.mjs +71 -71
- 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)
|