sillyspec 3.17.15 → 3.18.1
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/docs/platform-scan-protocol.md +298 -0
- package/package.json +1 -1
- package/src/constants.js +70 -0
- package/src/db.js +4 -0
- package/src/hooks/worktree-guard.js +97 -4
- package/src/index.js +56 -1
- package/src/progress.js +41 -14
- package/src/run.js +315 -83
- package/src/scan-postcheck.js +17 -16
- package/src/stage-contract.js +244 -12
- package/src/stages/brainstorm.js +228 -8
- package/src/stages/index.js +0 -2
- package/src/stages/plan.js +237 -52
- package/src/stages/propose.js +30 -4
- package/src/stages/quick.js +13 -10
- package/src/stages/scan.js +12 -0
- package/src/stages/verify.js +31 -13
- package/src/workflow.js +1 -0
- package/test/platform-artifacts.test.mjs +190 -0
- package/test/platform-failure-samples.test.mjs +195 -0
- package/test/platform-recovery-chain.test.mjs +179 -0
- package/test/platform-recovery.test.mjs +14 -6
- package/test/platform-scan-p0.test.mjs +5 -2
- package/test/run-tests.mjs +31 -3
- package/test/scan-paths.test.mjs +1 -1
- package/test/scan-postcheck.test.mjs +4 -3
- package/test/spec-dir.test.mjs +3 -2
- package/test/stage-contract.test.mjs +120 -7
- package/test/stage-definitions.test.mjs +2 -6
- package/test/wait-gates.test.mjs +501 -0
- package/test/worktree-guard.test.mjs +58 -0
package/test/run-tests.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdirSync } from 'node:fs'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
|
-
import { fileURLToPath
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { execFileSync } from 'node:child_process'
|
|
4
5
|
|
|
5
6
|
const testDir = dirname(fileURLToPath(import.meta.url))
|
|
6
7
|
const files = readdirSync(testDir)
|
|
@@ -12,9 +13,36 @@ if (files.length === 0) {
|
|
|
12
13
|
process.exit(0)
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
let passed = 0
|
|
17
|
+
let failed = 0
|
|
18
|
+
const failures = []
|
|
19
|
+
|
|
15
20
|
for (const file of files) {
|
|
21
|
+
const fullPath = join(testDir, file)
|
|
16
22
|
console.log(`\nRunning ${file}`)
|
|
17
|
-
|
|
23
|
+
try {
|
|
24
|
+
const output = execFileSync(process.execPath, [fullPath], {
|
|
25
|
+
cwd: testDir,
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
timeout: 120_000
|
|
29
|
+
})
|
|
30
|
+
if (output) process.stdout.write(output)
|
|
31
|
+
passed++
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (err.stdout) process.stdout.write(err.stdout)
|
|
34
|
+
if (err.stderr) process.stderr.write(err.stderr)
|
|
35
|
+
failed++
|
|
36
|
+
failures.push(file)
|
|
37
|
+
console.log(` ❌ ${file} exited with code ${err.status || 1}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
42
|
+
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
43
|
+
if (failures.length > 0) {
|
|
44
|
+
console.log(`失败文件: ${failures.join(', ')}`)
|
|
18
45
|
}
|
|
46
|
+
console.log(`${'='.repeat(50)}`)
|
|
19
47
|
|
|
20
|
-
|
|
48
|
+
process.exit(failed > 0 ? 1 : 0)
|
package/test/scan-paths.test.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { join, resolve, dirname, basename } from 'path'
|
|
6
6
|
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'url'
|
|
8
|
+
import { tmpdir } from 'os'
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url)
|
|
10
11
|
const __dirname = dirname(__filename)
|
|
@@ -20,12 +21,12 @@ function assert(cond, msg) {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function setup(name) {
|
|
23
|
-
const cwd = join(
|
|
24
|
+
const cwd = join(tmpdir(), `pc-${name}`)
|
|
24
25
|
mkdirSync(cwd, { recursive: true })
|
|
25
26
|
return cwd
|
|
26
27
|
}
|
|
27
28
|
function specSetup(name) {
|
|
28
|
-
const d = join(
|
|
29
|
+
const d = join(tmpdir(), `pc-${name}-spec`)
|
|
29
30
|
mkdirSync(d, { recursive: true })
|
|
30
31
|
return d
|
|
31
32
|
}
|
|
@@ -176,4 +177,4 @@ console.log('\n=== Test 12: failed 优先 ===')
|
|
|
176
177
|
console.log(`\n${'='.repeat(50)}`)
|
|
177
178
|
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
178
179
|
console.log(`${'='.repeat(50)}`)
|
|
179
|
-
|
|
180
|
+
if (failed > 0) throw new Error(`${failed} test(s) failed`)
|
package/test/spec-dir.test.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { join, resolve, basename, dirname } from 'path'
|
|
|
15
15
|
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from 'url'
|
|
17
17
|
import { execSync } from 'child_process'
|
|
18
|
+
import { tmpdir } from 'os'
|
|
18
19
|
|
|
19
20
|
const __filename = fileURLToPath(import.meta.url)
|
|
20
21
|
const __dirname = dirname(__filename)
|
|
@@ -39,7 +40,7 @@ function assert(condition, msg) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function tmpDir(name) {
|
|
42
|
-
const dir = join(
|
|
43
|
+
const dir = join(tmpdir(), `spec-dir-test-${name}-${Date.now()}`)
|
|
43
44
|
mkdirSync(dir, { recursive: true })
|
|
44
45
|
return dir
|
|
45
46
|
}
|
|
@@ -197,4 +198,4 @@ console.log(`\n${'='.repeat(50)}`)
|
|
|
197
198
|
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
198
199
|
console.log(`${'='.repeat(50)}`)
|
|
199
200
|
|
|
200
|
-
|
|
201
|
+
if (failed > 0) throw new Error(`${failed} test(s) failed`)
|
|
@@ -79,7 +79,7 @@ if (verifyResult.ok === false && verifyResult.errors.length > 0) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// scan validator:文档目录不存在应报错
|
|
82
|
-
const scanResult = runValidators('scan', '
|
|
82
|
+
const scanResult = runValidators('scan', join(tmpdir(), 'nonexistent-project'), 'test', { projectName: 'test' })
|
|
83
83
|
if (scanResult.ok === false && scanResult.errors.length > 0) {
|
|
84
84
|
console.log('✅ scan validator 检测到缺失 scan 文档')
|
|
85
85
|
} else {
|
|
@@ -87,12 +87,12 @@ if (scanResult.ok === false && scanResult.errors.length > 0) {
|
|
|
87
87
|
failed++
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
//
|
|
90
|
+
// brainstorm 有 validator,但变更目录不存在时应该报错(因为产物不存在)
|
|
91
91
|
const brainstormResult = runValidators('brainstorm', '.', 'test')
|
|
92
|
-
if (brainstormResult.ok ===
|
|
93
|
-
console.log('✅ brainstorm
|
|
92
|
+
if (brainstormResult.ok === false && brainstormResult.errors.length > 0) {
|
|
93
|
+
console.log('✅ brainstorm validator 检测到缺失产物文件')
|
|
94
94
|
} else {
|
|
95
|
-
console.log('❌ brainstorm
|
|
95
|
+
console.log('❌ brainstorm validator 未检测到缺失产物')
|
|
96
96
|
failed++
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -138,7 +138,7 @@ if (localResult.ok === false && localResult.errors.length > 0) {
|
|
|
138
138
|
// 测试3:校验路径指向 specRoot 而非 sourceRoot
|
|
139
139
|
const errors1 = localResult.errors.join(' ')
|
|
140
140
|
const errors2 = specResult.errors.join(' ')
|
|
141
|
-
if (errors1.includes(sourceRoot
|
|
141
|
+
if (errors1.includes(sourceRoot) || errors1.includes(join(sourceRoot, '.sillyspec'))) {
|
|
142
142
|
console.log('✅ 未传 specRoot 时校验路径指向 source_root')
|
|
143
143
|
} else {
|
|
144
144
|
console.log('✅ 未传 specRoot 时校验失败(文档确实不在 source_root 下)')
|
|
@@ -153,6 +153,119 @@ if (!errors2.includes(specRoot)) {
|
|
|
153
153
|
rmSync(specRoot, { recursive: true })
|
|
154
154
|
rmSync(sourceRoot, { recursive: true })
|
|
155
155
|
|
|
156
|
+
// === decisions.md traceability validator 测试 ===
|
|
157
|
+
console.log('\n=== decisions traceability validator 测试 ===')
|
|
158
|
+
|
|
159
|
+
const traceRoot = mkdtempSync(join(tmpdir(), 'sillyspec-trace-'))
|
|
160
|
+
const traceDir = join(traceRoot, '.sillyspec', 'changes', 'trace')
|
|
161
|
+
mkdirSync(traceDir, { recursive: true })
|
|
162
|
+
writeFileSync(join(traceDir, 'proposal.md'), '# Proposal\n\n## 不在范围内\n- none\n')
|
|
163
|
+
writeFileSync(join(traceDir, 'design.md'), '# Design\n\n## 文件变更清单\n\n## 风险登记\n\n## 自审\n\nD-001@v1\n')
|
|
164
|
+
writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n## D-001@v1: Choose canonical account term\n- priority: P1\n- status: accepted\n')
|
|
165
|
+
writeFileSync(join(traceDir, 'requirements.md'), '# Requirements\n\n### FR-01: Account naming\nGiven x\nWhen y\nThen z\n')
|
|
166
|
+
writeFileSync(join(traceDir, 'tasks.md'), '- [ ] task-01: implement naming (D-001@v1)\n')
|
|
167
|
+
|
|
168
|
+
const brainstormTrace = runValidators('brainstorm', traceRoot, 'trace')
|
|
169
|
+
if (brainstormTrace.ok === true && brainstormTrace.warnings.some(w => w.includes('requirements.md 未引用') && w.includes('D-001@V1'))) {
|
|
170
|
+
console.log('✅ brainstorm validator 检测到 requirements.md 缺少 D-001@v1 引用')
|
|
171
|
+
} else {
|
|
172
|
+
console.log('❌ brainstorm validator 未检测到 requirements.md 缺少 D-001@v1 引用', brainstormTrace)
|
|
173
|
+
failed++
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
writeFileSync(join(traceDir, 'requirements.md'), '# Requirements\n\n### FR-01: Account naming\n覆盖决策:D-001@v1\nGiven x\nWhen y\nThen z\n')
|
|
177
|
+
writeFileSync(join(traceDir, 'plan.md'), '# Plan\n\n- [ ] task-01: implement naming\n')
|
|
178
|
+
|
|
179
|
+
const planTrace = runValidators('plan', traceRoot, 'trace')
|
|
180
|
+
if (planTrace.ok === true
|
|
181
|
+
&& planTrace.warnings.some(w => w.includes('plan.md 未引用') && w.includes('FR-01'))
|
|
182
|
+
&& planTrace.warnings.some(w => w.includes('plan.md 未引用') && w.includes('D-001@V1'))) {
|
|
183
|
+
console.log('✅ plan validator 检测到 plan.md 缺少 FR-01/D-001@v1 引用')
|
|
184
|
+
} else {
|
|
185
|
+
console.log('❌ plan validator 未检测到 plan.md 缺少追踪 ID', planTrace)
|
|
186
|
+
failed++
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
writeFileSync(join(traceDir, 'plan.md'), '# Plan\n\n- [ ] task-01: implement naming(覆盖:FR-01, D-001@v1)\n')
|
|
190
|
+
writeFileSync(join(traceDir, 'verify-result.md'), '# Verify\n\nPASS\n')
|
|
191
|
+
|
|
192
|
+
const verifyTrace = runValidators('verify', traceRoot, 'trace')
|
|
193
|
+
if (verifyTrace.ok === true && verifyTrace.warnings.some(w => w.includes('verify-result.md 未引用') && w.includes('D-001@V1'))) {
|
|
194
|
+
console.log('✅ verify validator 检测到 verify-result.md 缺少 D-001@v1 引用')
|
|
195
|
+
} else {
|
|
196
|
+
console.log('❌ verify validator 未检测到 verify-result.md 缺少 D-001@v1 引用', verifyTrace)
|
|
197
|
+
failed++
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
writeFileSync(join(traceDir, 'verify-result.md'), '# Verify\n\n## 决策追踪矩阵\n| D-001@v1 | FR-01 | task-01 | evidence | PASS |\n')
|
|
201
|
+
const verifyTraceOk = runValidators('verify', traceRoot, 'trace')
|
|
202
|
+
if (verifyTraceOk.ok === true && !verifyTraceOk.warnings.some(w => w.includes('D-001@V1'))) {
|
|
203
|
+
console.log('✅ verify validator 在 D-001@v1 已覆盖时不再报警')
|
|
204
|
+
} else {
|
|
205
|
+
console.log('❌ verify validator 覆盖后仍报警', verifyTraceOk)
|
|
206
|
+
failed++
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n## D-002@v1: Unresolved schema conflict\n- priority: P0\n- status: unresolved\n')
|
|
210
|
+
const blockerTrace = runValidators('plan', traceRoot, 'trace')
|
|
211
|
+
if (blockerTrace.ok === false && blockerTrace.errors.some(e => e.includes('P0/P1 未决阻塞') && e.includes('D-002@V1'))) {
|
|
212
|
+
console.log('✅ plan validator 阻止 P0 unresolved decision 进入 plan')
|
|
213
|
+
} else {
|
|
214
|
+
console.log('❌ plan validator 未阻止 P0 unresolved decision', blockerTrace)
|
|
215
|
+
failed++
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n- id: D-003@v1\n priority: P1\n status: blocking\n type: boundary\n')
|
|
219
|
+
const yamlBlockerTrace = runValidators('plan', traceRoot, 'trace')
|
|
220
|
+
if (yamlBlockerTrace.ok === false && yamlBlockerTrace.errors.some(e => e.includes('P0/P1 未决阻塞') && e.includes('D-003@V1'))) {
|
|
221
|
+
console.log('✅ plan validator 支持 list/YAML 风格 decision record')
|
|
222
|
+
} else {
|
|
223
|
+
console.log('❌ plan validator 未识别 list/YAML 风格 decision record', yamlBlockerTrace)
|
|
224
|
+
failed++
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n- id: D-004@v1\n status: blocking\n type: boundary\n')
|
|
228
|
+
const missingPriorityTrace = runValidators('plan', traceRoot, 'trace')
|
|
229
|
+
if (missingPriorityTrace.ok === false
|
|
230
|
+
&& missingPriorityTrace.errors.some(e => e.includes('P0/P1 未决阻塞') && e.includes('D-004@V1') && e.includes('priority=missing->P1'))) {
|
|
231
|
+
console.log('✅ plan validator 将缺 priority 的 blocking decision 按 P1 阻断')
|
|
232
|
+
} else {
|
|
233
|
+
console.log('❌ plan validator 未阻断缺 priority 的 blocking decision', missingPriorityTrace)
|
|
234
|
+
failed++
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n- id: D-005@v1\n status: accepted\n type: term\n')
|
|
238
|
+
writeFileSync(join(traceDir, 'plan.md'), '# Plan\n\n- [ ] task-01: implement naming(覆盖:FR-01)\n')
|
|
239
|
+
const yamlAcceptedTrace = runValidators('plan', traceRoot, 'trace')
|
|
240
|
+
if (yamlAcceptedTrace.ok === true && yamlAcceptedTrace.warnings.some(w => w.includes('plan.md 未引用') && w.includes('D-005@V1'))) {
|
|
241
|
+
console.log('✅ plan validator 将 YAML accepted decision 纳入追踪')
|
|
242
|
+
} else {
|
|
243
|
+
console.log('❌ plan validator 未追踪 YAML accepted decision', yamlAcceptedTrace)
|
|
244
|
+
failed++
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n')
|
|
248
|
+
writeFileSync(join(traceDir, 'requirements.md'), '# Requirements\n\n普通说明提到 https://example.test/spec/FR-404 和注释里的 FR-405,但它们不是结构化需求 ID。\n')
|
|
249
|
+
writeFileSync(join(traceDir, 'plan.md'), '# Plan\n\nNo structured requirement IDs here.\n')
|
|
250
|
+
const looseIdTrace = runValidators('plan', traceRoot, 'trace')
|
|
251
|
+
if (!looseIdTrace.warnings.some(w => w.includes('FR-404') || w.includes('FR-405'))) {
|
|
252
|
+
console.log('✅ plan validator 忽略普通正文/URL 中的 FR ID')
|
|
253
|
+
} else {
|
|
254
|
+
console.log('❌ plan validator 误提取普通正文/URL 中的 FR ID', looseIdTrace)
|
|
255
|
+
failed++
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n普通说明提到 https://example.test/spec/D-404@v1 和注释里的 D-405@v1,但它们不是结构化决策 ID。\n')
|
|
259
|
+
const looseBrainstormTrace = runValidators('brainstorm', traceRoot, 'trace')
|
|
260
|
+
if (!looseBrainstormTrace.warnings.some(w => w.includes('D-404@V1') || w.includes('D-405@V1'))) {
|
|
261
|
+
console.log('✅ brainstorm validator 忽略普通正文/URL 中的 D ID')
|
|
262
|
+
} else {
|
|
263
|
+
console.log('❌ brainstorm validator 误提取普通正文/URL 中的 D ID', looseBrainstormTrace)
|
|
264
|
+
failed++
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
rmSync(traceRoot, { recursive: true })
|
|
268
|
+
|
|
156
269
|
// === StageContract 结构测试 ===
|
|
157
270
|
console.log('\n=== Contract 结构测试 ===')
|
|
158
271
|
|
|
@@ -182,4 +295,4 @@ if (unknown === null) {
|
|
|
182
295
|
|
|
183
296
|
// === 结果 ===
|
|
184
297
|
console.log(`\n${failed === 0 ? '✅ 全部通过' : `❌ ${failed} 项失败`}`)
|
|
185
|
-
|
|
298
|
+
if (failed > 0) throw new Error(`${failed} test(s) failed`)
|
|
@@ -5,7 +5,6 @@ import { buildExecuteSteps } from '../src/stages/execute.js'
|
|
|
5
5
|
|
|
6
6
|
const stageSteps = {
|
|
7
7
|
brainstorm: stageRegistry.brainstorm.steps,
|
|
8
|
-
propose: stageRegistry.propose.steps,
|
|
9
8
|
scan: stageRegistry.scan.steps,
|
|
10
9
|
quick: stageRegistry.quick.steps,
|
|
11
10
|
archive: stageRegistry.archive.steps,
|
|
@@ -25,11 +24,8 @@ function assertContains(stage, expectedNames) {
|
|
|
25
24
|
}
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
assert.equal(stageSteps.brainstorm.length,
|
|
29
|
-
assertContains('brainstorm', ['写设计文档并自审', '用户确认并生成规范文件'])
|
|
30
|
-
|
|
31
|
-
assert.equal(stageSteps.propose.length, 7, 'propose should expose generation and self-check separately')
|
|
32
|
-
assertContains('propose', ['生成规范文件', '自检门控', '展示并更新进度'])
|
|
27
|
+
assert.equal(stageSteps.brainstorm.length, 13, 'brainstorm should include optional demand clarification and default Design Grill gates')
|
|
28
|
+
assertContains('brainstorm', ['需求澄清 Grill', '写设计文档并自审', 'Design Grill 交叉审查', '用户确认并生成规范文件'])
|
|
33
29
|
|
|
34
30
|
assert.equal(stageSteps.scan.length, 10, 'scan base definition should stay at 10 steps before per-project expansion')
|
|
35
31
|
assertContains('scan', ['构建扫描项目列表', '生成本地配置', '生成模块映射'])
|