sillyspec 3.16.0 → 3.16.2

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.
@@ -0,0 +1,349 @@
1
+ /**
2
+ * StageContract — 阶段协议
3
+ *
4
+ * 每个阶段声明:允许的前置阶段、必须的产出、校验器、后续阶段。
5
+ * CLI 不再相信 prompt 完成,completeStep 后必须过 validator。
6
+ */
7
+
8
+ import { existsSync, readdirSync } from 'fs'
9
+ import { join, basename } from 'path'
10
+
11
+ /**
12
+ * 校验结果
13
+ * @typedef {{ ok: boolean, errors: string[], warnings: string[] }} ValidationResult
14
+ */
15
+
16
+ /**
17
+ * 阶段合约
18
+ * @typedef {{
19
+ * stage: string,
20
+ * description: string,
21
+ * allowedFrom: string[],
22
+ * allowedTo: string[],
23
+ * validators: Function[],
24
+ * }} StageContract
25
+ */
26
+
27
+ // ============ Validators ============
28
+
29
+ /**
30
+ * scan 完成校验:检查 7 份 scan 文档 + manifest
31
+ */
32
+ function validateScanOutputs(cwd, changeName, context = {}) {
33
+ const { projectName, specRoot } = context
34
+ // 平台模式使用 specRoot,本地模式使用 cwd
35
+ const base = specRoot || cwd
36
+ const docsRoot = projectName
37
+ ? join(base, '.sillyspec', 'docs', projectName, 'scan')
38
+ : join(base, '.sillyspec', 'docs', 'scan')
39
+
40
+ const requiredDocs = [
41
+ 'ARCHITECTURE.md',
42
+ 'CONVENTIONS.md',
43
+ 'STRUCTURE.md',
44
+ 'INTEGRATIONS.md',
45
+ 'TESTING.md',
46
+ 'CONCERNS.md',
47
+ 'PROJECT.md',
48
+ ]
49
+
50
+ const errors = []
51
+ const warnings = []
52
+
53
+ for (const doc of requiredDocs) {
54
+ if (!existsSync(join(docsRoot, doc))) {
55
+ errors.push(`scan 文档缺失: ${docsRoot}/${doc}`)
56
+ }
57
+ }
58
+
59
+ // 检查 modules 目录
60
+ const modulesRoot = projectName
61
+ ? join(base, '.sillyspec', 'docs', projectName, 'modules')
62
+ : join(base, '.sillyspec', 'docs', 'modules')
63
+ if (!existsSync(modulesRoot)) {
64
+ warnings.push('modules 目录不存在')
65
+ } else {
66
+ const modules = readdirSync(modulesRoot).filter(f => f.endsWith('.md'))
67
+ if (modules.length === 0) {
68
+ warnings.push('modules 目录为空')
69
+ }
70
+ }
71
+
72
+ return { ok: errors.length === 0, errors, warnings }
73
+ }
74
+
75
+ /**
76
+ * plan 完成校验:检查 plan.md 生成
77
+ */
78
+ function validatePlanOutputs(cwd, changeName) {
79
+ const planDir = join(cwd, '.sillyspec', 'changes', changeName)
80
+ const planFile = join(planDir, 'plan.md')
81
+ const errors = []
82
+
83
+ if (!existsSync(planFile)) {
84
+ errors.push(`plan.md 缺失: ${planFile}`)
85
+ }
86
+
87
+ const warnings = []
88
+ return { ok: errors.length === 0, errors, warnings }
89
+ }
90
+
91
+ /**
92
+ * verify 完成校验:检查 verify 报告存在
93
+ */
94
+ function validateVerifyOutputs(cwd, changeName) {
95
+ const planDir = join(cwd, '.sillyspec', 'changes', changeName)
96
+ const errors = []
97
+ const warnings = []
98
+
99
+ // verify 至少应该有 run 记录
100
+ if (!existsSync(join(planDir, 'plan.md'))) {
101
+ errors.push(`变更目录缺失: ${planDir}`)
102
+ }
103
+
104
+ return { ok: errors.length === 0, errors, warnings }
105
+ }
106
+
107
+ /**
108
+ * archive 完成校验:检查归档目录完整性
109
+ */
110
+ function validateArchiveOutputs(cwd, changeName) {
111
+ const errors = []
112
+ const warnings = []
113
+ const archiveDir = join(cwd, '.sillyspec', 'changes', 'archive')
114
+ const date = new Date().toISOString().slice(0, 10)
115
+ const destDir = join(archiveDir, `${date}-${changeName}`)
116
+
117
+ // 检查归档目录是否存在
118
+ if (!existsSync(destDir)) {
119
+ errors.push(`归档目录缺失: ${destDir}`)
120
+ return { ok: false, errors, warnings }
121
+ }
122
+
123
+ // 检查核心文档
124
+ const requiredDocs = ['plan.md']
125
+ const recommendedDocs = ['design.md', 'module-impact.md']
126
+
127
+ for (const doc of requiredDocs) {
128
+ if (!existsSync(join(destDir, doc))) {
129
+ errors.push(`归档目录缺失核心文档: ${doc}`)
130
+ }
131
+ }
132
+
133
+ for (const doc of recommendedDocs) {
134
+ if (!existsSync(join(destDir, doc))) {
135
+ warnings.push(`归档目录缺少推荐文档: ${doc}`)
136
+ }
137
+ }
138
+
139
+ return { ok: errors.length === 0, errors, warnings }
140
+ }
141
+
142
+ /**
143
+ * archive 前置校验:所有主流程阶段完成
144
+ */
145
+ function validateChangeClosed(cwd, changeName) {
146
+ const errors = []
147
+ const warnings = []
148
+
149
+ // 检查前置阶段状态
150
+ const progressDir = join(cwd, '.sillyspec', '.runtime')
151
+ // 这里只做文件层面的检查,DB 检查在 run.js 里做
152
+ const changeDir = join(cwd, '.sillyspec', 'changes', changeName)
153
+ if (!existsSync(changeDir)) {
154
+ errors.push(`变更目录不存在: ${changeDir}`)
155
+ return { ok: false, errors, warnings }
156
+ }
157
+
158
+ if (!existsSync(join(changeDir, 'plan.md'))) {
159
+ errors.push(`plan.md 缺失 — 请确保 plan 阶段已完成`)
160
+ }
161
+
162
+ return { ok: errors.length === 0, errors, warnings }
163
+ }
164
+
165
+ // ============ Contract Registry ============
166
+
167
+ /**
168
+ * 主流程阶段(有严格转换顺序)
169
+ */
170
+ const mainFlowStages = ['brainstorm', 'plan', 'execute', 'verify']
171
+
172
+ /**
173
+ * 辅助阶段(可独立运行,无严格转换顺序)
174
+ */
175
+ const auxiliaryStages = ['scan', 'quick', 'explore', 'archive', 'status', 'doctor']
176
+
177
+ /**
178
+ * @type {Object<string, StageContract>}
179
+ */
180
+ const contracts = {
181
+ // === 主流程 ===
182
+ brainstorm: {
183
+ stage: 'brainstorm',
184
+ description: '需求分析与设计',
185
+ allowedFrom: [], // 任何变更的起始阶段
186
+ allowedTo: ['plan'],
187
+ validators: [],
188
+ },
189
+ plan: {
190
+ stage: 'plan',
191
+ description: '任务拆解与规划',
192
+ allowedFrom: ['brainstorm'],
193
+ allowedTo: ['execute'],
194
+ validators: [validatePlanOutputs],
195
+ },
196
+ execute: {
197
+ stage: 'execute',
198
+ description: '代码实现',
199
+ allowedFrom: ['plan'],
200
+ allowedTo: ['verify'],
201
+ validators: [],
202
+ },
203
+ verify: {
204
+ stage: 'verify',
205
+ description: '验证与测试',
206
+ allowedFrom: ['execute'],
207
+ allowedTo: ['archive'],
208
+ validators: [validateVerifyOutputs],
209
+ },
210
+ archive: {
211
+ stage: 'archive',
212
+ description: '归档与收口',
213
+ allowedFrom: ['verify'],
214
+ allowedTo: [],
215
+ validators: [validateChangeClosed, validateArchiveOutputs],
216
+ },
217
+
218
+ // === 辅助阶段 ===
219
+ scan: {
220
+ stage: 'scan',
221
+ description: '项目扫描',
222
+ allowedFrom: [], // 无前置要求
223
+ allowedTo: [], // 不进入主流程
224
+ validators: [validateScanOutputs],
225
+ },
226
+ quick: {
227
+ stage: 'quick',
228
+ description: '快速任务',
229
+ allowedFrom: [], // 无前置要求
230
+ allowedTo: [], // 不进入主流程
231
+ validators: [],
232
+ },
233
+ explore: {
234
+ stage: 'explore',
235
+ description: '代码探索',
236
+ allowedFrom: [],
237
+ allowedTo: [],
238
+ validators: [],
239
+ },
240
+ status: {
241
+ stage: 'status',
242
+ description: '状态查看',
243
+ allowedFrom: [],
244
+ allowedTo: [],
245
+ validators: [],
246
+ },
247
+ doctor: {
248
+ stage: 'doctor',
249
+ description: '环境诊断',
250
+ allowedFrom: [],
251
+ allowedTo: [],
252
+ validators: [],
253
+ },
254
+ }
255
+
256
+ // ============ Public API ============
257
+
258
+ /**
259
+ * 获取阶段合约
260
+ */
261
+ export function getContract(stageName) {
262
+ return contracts[stageName] || null
263
+ }
264
+
265
+ /**
266
+ * 校验状态转换是否允许
267
+ * @param {string} fromStage - 当前阶段(空字符串表示变更起始)
268
+ * @param {string} toStage - 目标阶段
269
+ * @returns {{ allowed: boolean, reason?: string }}
270
+ */
271
+ export function checkTransition(fromStage, toStage) {
272
+ const contract = contracts[toStage]
273
+ if (!contract) {
274
+ return { allowed: false, reason: `未知阶段: ${toStage}` }
275
+ }
276
+
277
+ // 辅助阶段随时可执行(archive 除外:从主流程进入 archive 需要校验)
278
+ if (auxiliaryStages.includes(toStage) && toStage !== 'archive') {
279
+ return { allowed: true }
280
+ }
281
+
282
+ // archive 特殊处理:从 verify 来的允许,从其他主流程阶段来的需要校验
283
+ if (toStage === 'archive') {
284
+ if (fromStage === 'verify') {
285
+ return { allowed: true }
286
+ }
287
+ // 独立运行 archive(无前置)也允许
288
+ if (!fromStage || auxiliaryStages.includes(fromStage)) {
289
+ return { allowed: true }
290
+ }
291
+ return { allowed: false, reason: 'archive 的前置阶段是 verify,不能从 ' + fromStage + ' 跳转' }
292
+ }
293
+
294
+ // 从辅助阶段进入主流程:允许(用户可能 scan 完直接 brainstorm 或 plan)
295
+ if (auxiliaryStages.includes(fromStage)) {
296
+ return { allowed: true }
297
+ }
298
+
299
+ // 无前置阶段(变更起始):只能开始 brainstorm 或辅助阶段
300
+ if (!fromStage) {
301
+ // 主流程必须从 brainstorm 开始
302
+ if (contract.allowedFrom.length === 0) {
303
+ return { allowed: true }
304
+ }
305
+ return { allowed: false, reason: `${toStage} 需要先完成 ${contract.allowedFrom.join(' 或 ')}` }
306
+ }
307
+
308
+ // 主流程内部跳转:检查目标阶段的 allowedFrom 是否包含 fromStage
309
+ if (contract.allowedFrom.includes(fromStage)) {
310
+ return { allowed: true }
311
+ }
312
+
313
+ return { allowed: false, reason: `${toStage} 的前置阶段是 ${contract.allowedFrom.join(' 或 ')},不能从 ${fromStage} 跳转` }
314
+ }
315
+
316
+ /**
317
+ * 执行阶段完成校验
318
+ * @param {string} stageName
319
+ * @param {string} cwd
320
+ * @param {string} changeName
321
+ * @param {object} context - 额外上下文(如 projectName)
322
+ * @returns {ValidationResult}
323
+ */
324
+ export function runValidators(stageName, cwd, changeName, context = {}) {
325
+ const contract = contracts[stageName]
326
+ if (!contract || contract.validators.length === 0) {
327
+ return { ok: true, errors: [], warnings: [] }
328
+ }
329
+
330
+ const allErrors = []
331
+ const allWarnings = []
332
+
333
+ for (const validator of contract.validators) {
334
+ try {
335
+ const result = validator(cwd, changeName, context)
336
+ allErrors.push(...(result.errors || []))
337
+ allWarnings.push(...(result.warnings || []))
338
+ } catch (e) {
339
+ allErrors.push(`校验器 ${validator.name || 'unknown'} 异常: ${e.message}`)
340
+ }
341
+ }
342
+
343
+ return { ok: allErrors.length === 0, errors: allErrors, warnings: allWarnings }
344
+ }
345
+
346
+ /**
347
+ * 获取所有主流程阶段
348
+ */
349
+ export { mainFlowStages, auxiliaryStages }
@@ -124,15 +124,14 @@ module_id: <module-id>
124
124
  },
125
125
  {
126
126
  name: '确认归档',
127
- prompt: `确认归档内容并执行目录移动。
127
+ prompt: `确认归档内容,由 CLI 执行目录移动。
128
128
 
129
129
  ### 操作
130
130
  1. 展示:变更目录名、包含的文件列表(含 module-impact.md)、生成总结
131
- 2. **直接执行归档**(本步骤完成后自动移动):
132
- - 创建 archive 目录:\`mkdir -p .sillyspec/changes/archive\`
133
- - 移动变更目录:\`mv .sillyspec/changes/<change-name> .sillyspec/changes/archive/<change-name>\`
134
- - 确认移动成功:\`ls .sillyspec/changes/archive/<change-name>/\`
135
- 3. 确保所有 checkbox 都已勾选
131
+ 2. 确保所有 checkbox 都已勾选
132
+ 3. 让用户确认后,用 \`--confirm\` 完成本步骤:
133
+ \`sillyspec run archive --done --confirm --output "确认归档"\`
134
+ 4. CLI 会创建 \`.sillyspec/changes/archive/\`,并将变更目录移动到 \`.sillyspec/changes/archive/YYYY-MM-DD-<change-name>/\`
136
135
 
137
136
  ### 输出
138
137
  归档完成 + archive 目录路径`,
@@ -147,10 +146,7 @@ module_id: <module-id>
147
146
  1. 如果 \`.sillyspec/ROADMAP.md\` 存在,标记对应 Phase 为已完成
148
147
  2. \`git add .sillyspec/changes/\` — 暂存归档结果(不要 commit,由用户通过统一提交工具处理)
149
148
  3. \`git add .sillyspec/docs/\` — 暂存模块文档更新(如有)
150
- 4. 更新 sillyspec.db 中的阶段状态:
151
- - 清除当前变更信息(归档后不再活跃)
152
- - 如果是主变更(有 MASTER.md),标记所有阶段为 ✅,然后清除
153
- - 历史记录追加时间 + 归档完成
149
+ 4. 确认 sillyspec.db 中该变更已不再 active(确认归档步骤由 CLI 调用 unregisterChange)
154
150
 
155
151
  ### 输出
156
152
  归档完成确认 + 累积规范统计`,
@@ -274,7 +274,10 @@ design.md 文件路径 + 自审结果
274
274
  ### 注意
275
275
  - 自审不通过不要进入下一步
276
276
  - 不确定的问题标注「⚠️ 自审存疑」`,
277
-
277
+ outputHint: 'design.md 文件路径 + 自审结果',
278
+ optional: false
279
+ },
280
+ {
278
281
  name: '用户确认并生成规范文件',
279
282
  prompt: `用户确认设计方案,生成规范文件。
280
283
 
@@ -84,7 +84,6 @@ for f in .sillyspec/projects/*.yaml; do
84
84
  stack_md="$p/.sillyspec/STACK.md"
85
85
  [ -f "$local_yaml" ] && echo "✅ local.yaml ($name)" || echo "⚠️ local.yaml ($name) — 不存在"
86
86
  if [ -f "$local_yaml" ]; then
87
- grep -q 'build:' "$local_yaml" && echo " ✅ build 命令已配置" || echo " ⚠️ 缺少 build 命令"
88
87
  grep -q 'test:' "$local_yaml" && echo " ✅ test 命令已配置" || echo " ⚠️ 缺少 test 命令"
89
88
  fi
90
89
  [ -f "$stack_md" ] && echo "✅ STACK.md ($name)" || echo "⚠️ STACK.md ($name) — 不存在"
@@ -346,7 +345,7 @@ timeout 5 which docker 2>/dev/null && echo "✅ Docker 可用" || echo "ℹ️ D
346
345
  **常见问题及修复:**
347
346
  - CLI 未安装 → \`npm install -g sillyspec\`
348
347
  - 缺少 local.yaml → \`sillyspec init\` 重新生成,或手动创建
349
- - local.yaml 缺少 build/test → 补充对应命令
348
+ - local.yaml 缺少 test 命令 → 补充对应命令
350
349
  - 缺少 STACK.md → \`sillyspec run scan\` 重新扫描
351
350
  - sillyspec.db 状态不一致 → \`sillyspec run <阶段> --reset\` 重置对应阶段
352
351
  - 孤儿目录 → 确认后 \`rm -rf .sillyspec/changes/<目录名>\`
@@ -109,7 +109,10 @@ export const definition = {
109
109
  - 表名/字段名/类名必须来自真实代码或标注"新增"
110
110
  - 用户场景必须用 Given/When/Then 格式
111
111
  - tasks.md 只列任务名,细节在 plan 阶段展开`,
112
-
112
+ outputHint: '四个文件路径',
113
+ optional: false
114
+ },
115
+ {
113
116
  name: '自检门控',
114
117
  prompt: `自检生成的规范文件。
115
118
 
@@ -139,16 +139,16 @@ export const definition = {
139
139
  },
140
140
  {
141
141
  name: '生成本地配置',
142
- prompt: `自动生成 .sillyspec/.runtime/local.yaml 本地配置文件。
142
+ prompt: `自动生成 .sillyspec/local.yaml 本地配置文件。
143
143
 
144
144
  ### 操作
145
- 1. 检查 .sillyspec/.runtime/local.yaml 是否已存在,已存在则跳过(提示"local.yaml 已存在,跳过生成")
145
+ 1. 检查 .sillyspec/local.yaml 是否已存在,已存在则跳过(提示"local.yaml 已存在,跳过生成")
146
146
  2. 根据项目类型生成默认配置:
147
147
  - **Node.js**(有 package.json):build: "npm run build", test: "npm test", lint: "npm run lint", type: nodejs
148
148
  - **Maven**(有 pom.xml):build: "mvn compile", test: "mvn test", lint: "mvn checkstyle:check", type: maven
149
149
  - **Gradle**(有 build.gradle):build: "./gradlew build", test: "./gradlew test", type: gradle
150
150
  - **通用项目**:只写注释模板, type: generic
151
- 3. 确保目录存在:mkdir -p .sillyspec/.runtime
151
+ 3. 确保目录存在:mkdir -p .sillyspec
152
152
  4. 原子写入(先写 tmp 文件再 rename)
153
153
 
154
154
  ### 文件格式
@@ -0,0 +1,26 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { readdirSync, statSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ const roots = ['src']
6
+ const files = []
7
+
8
+ function walk(dir) {
9
+ for (const entry of readdirSync(dir)) {
10
+ const full = join(dir, entry)
11
+ const st = statSync(full)
12
+ if (st.isDirectory()) {
13
+ walk(full)
14
+ continue
15
+ }
16
+ if (/\.(js|cjs|mjs)$/.test(entry)) files.push(full)
17
+ }
18
+ }
19
+
20
+ for (const root of roots) walk(root)
21
+
22
+ for (const file of files.sort()) {
23
+ execFileSync(process.execPath, ['--check', file], { stdio: 'inherit' })
24
+ }
25
+
26
+ console.log(`Checked ${files.length} JavaScript files`)
@@ -0,0 +1,20 @@
1
+ import { readdirSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath, pathToFileURL } from 'node:url'
4
+
5
+ const testDir = dirname(fileURLToPath(import.meta.url))
6
+ const files = readdirSync(testDir)
7
+ .filter(file => file.endsWith('.test.mjs'))
8
+ .sort()
9
+
10
+ if (files.length === 0) {
11
+ console.log('No test files found')
12
+ process.exit(0)
13
+ }
14
+
15
+ for (const file of files) {
16
+ console.log(`\nRunning ${file}`)
17
+ await import(pathToFileURL(join(testDir, file)).href)
18
+ }
19
+
20
+ console.log(`\nAll ${files.length} test file(s) passed`)
@@ -0,0 +1,185 @@
1
+ /**
2
+ * StageContract 状态转换 + validator 测试
3
+ */
4
+ import { checkTransition, runValidators, getContract } from '../src/stage-contract.js'
5
+
6
+ let failed = 0
7
+
8
+ // === 状态转换测试 ===
9
+ const transitionTests = [
10
+ // [from, to, expectedAllowed]
11
+ // 主流程正常顺序
12
+ ['', 'brainstorm', true],
13
+ ['brainstorm', 'plan', true],
14
+ ['plan', 'execute', true],
15
+ ['execute', 'verify', true],
16
+ ['verify', 'archive', true],
17
+
18
+ // 跳步应被拦截
19
+ ['', 'plan', false],
20
+ ['', 'execute', false],
21
+ ['brainstorm', 'execute', false],
22
+ ['plan', 'verify', false],
23
+ ['execute', 'archive', false],
24
+
25
+ // 回退应被拦截
26
+ ['plan', 'brainstorm', false],
27
+ ['execute', 'plan', false],
28
+ ['verify', 'execute', false],
29
+
30
+ // 辅助阶段随时可执行
31
+ ['', 'scan', true],
32
+ ['', 'quick', true],
33
+ ['', 'explore', true],
34
+ ['', 'doctor', true],
35
+ ['', 'archive', true],
36
+ ['brainstorm', 'scan', true],
37
+ ['plan', 'quick', true],
38
+ ['execute', 'doctor', true],
39
+
40
+ // 从辅助阶段进入主流程允许
41
+ ['scan', 'plan', true],
42
+ ['scan', 'brainstorm', true],
43
+ ['quick', 'plan', true],
44
+ ['doctor', 'brainstorm', true],
45
+
46
+ // archive 特殊:verify 后允许,其他主流程不允许直接跳
47
+ ['verify', 'archive', true],
48
+ ['execute', 'archive', false],
49
+ ['plan', 'archive', false],
50
+ ]
51
+
52
+ console.log('=== 状态转换测试 ===')
53
+ for (const [from, to, expected] of transitionTests) {
54
+ const r = checkTransition(from, to)
55
+ const ok = r.allowed === expected
56
+ if (!ok) failed++
57
+ console.log(ok ? '✅' : '❌', `${from || '(起始)'} → ${to}: allowed=${r.allowed} (exp ${expected})${ok ? '' : ' reason: ' + r.reason}`)
58
+ }
59
+
60
+ // === Validator 测试 ===
61
+ console.log('\n=== Validator 测试 ===')
62
+
63
+ // plan validator:plan.md 不存在应报错
64
+ const planResult = runValidators('plan', '.', 'nonexistent-change')
65
+ if (planResult.ok === false && planResult.errors.length > 0) {
66
+ console.log('✅ plan validator 检测到缺失 plan.md')
67
+ } else {
68
+ console.log('❌ plan validator 未检测到缺失 plan.md')
69
+ failed++
70
+ }
71
+
72
+ // verify validator:变更目录不存在应报错
73
+ const verifyResult = runValidators('verify', '.', 'nonexistent-change')
74
+ if (verifyResult.ok === false && verifyResult.errors.length > 0) {
75
+ console.log('✅ verify validator 检测到缺失变更目录')
76
+ } else {
77
+ console.log('❌ verify validator 未检测到缺失变更目录')
78
+ failed++
79
+ }
80
+
81
+ // scan validator:文档目录不存在应报错
82
+ const scanResult = runValidators('scan', '/tmp/nonexistent-project', 'test', { projectName: 'test' })
83
+ if (scanResult.ok === false && scanResult.errors.length > 0) {
84
+ console.log('✅ scan validator 检测到缺失 scan 文档')
85
+ } else {
86
+ console.log('❌ scan validator 未检测到缺失 scan 文档')
87
+ failed++
88
+ }
89
+
90
+ // 无 validator 的阶段应该 pass
91
+ const brainstormResult = runValidators('brainstorm', '.', 'test')
92
+ if (brainstormResult.ok === true) {
93
+ console.log('✅ brainstorm 无 validator 直接通过')
94
+ } else {
95
+ console.log('❌ brainstorm 无 validator 但失败了')
96
+ failed++
97
+ }
98
+
99
+ // === scan validator 平台模式 specRoot 测试 ===
100
+ console.log('\n=== scan validator specRoot 测试 ===')
101
+
102
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
103
+ import { join } from 'path'
104
+ import { tmpdir } from 'os'
105
+
106
+ // 创建临时 specRoot 结构
107
+ const specRoot = mkdtempSync(join(tmpdir(), 'sillyspec-test-'))
108
+ const sourceRoot = mkdtempSync(join(tmpdir(), 'sillyspec-source-'))
109
+ const projectName = 'myaaa'
110
+
111
+ // 在 specRoot 下创建正确的 scan 文档
112
+ const specDocsDir = join(specRoot, '.sillyspec', 'docs', projectName, 'scan')
113
+ mkdirSync(specDocsDir, { recursive: true })
114
+ for (const doc of ['ARCHITECTURE.md', 'CONVENTIONS.md', 'STRUCTURE.md', 'INTEGRATIONS.md', 'TESTING.md', 'CONCERNS.md', 'PROJECT.md']) {
115
+ writeFileSync(join(specDocsDir, doc), '# ' + doc)
116
+ }
117
+ mkdirSync(join(specRoot, '.sillyspec', 'docs', projectName, 'modules'), { recursive: true })
118
+ writeFileSync(join(specRoot, '.sillyspec', 'docs', projectName, 'modules', 'app.md'), '# app')
119
+
120
+ // 测试1:使用 specRoot 校验成功
121
+ const specResult = runValidators('scan', sourceRoot, 'test', { projectName, specRoot })
122
+ if (specResult.ok === true) {
123
+ console.log('✅ scan validator 使用 specRoot 校验通过')
124
+ } else {
125
+ console.log('❌ scan validator specRoot 校验失败:', specResult.errors)
126
+ failed++
127
+ }
128
+
129
+ // 测试2:使用 sourceRoot 校验(不传 specRoot)应失败
130
+ const localResult = runValidators('scan', sourceRoot, 'test', { projectName })
131
+ if (localResult.ok === false && localResult.errors.length > 0) {
132
+ console.log('✅ scan validator 使用 sourceRoot 校验正确失败(文档不在 source_root 下)')
133
+ } else {
134
+ console.log('❌ scan validator sourceRoot 校验未正确失败')
135
+ failed++
136
+ }
137
+
138
+ // 测试3:校验路径指向 specRoot 而非 sourceRoot
139
+ const errors1 = localResult.errors.join(' ')
140
+ const errors2 = specResult.errors.join(' ')
141
+ if (errors1.includes(sourceRoot.replace('/tmp/', '')) || errors1.includes(join(sourceRoot, '.sillyspec').slice(-30))) {
142
+ console.log('✅ 未传 specRoot 时校验路径指向 source_root')
143
+ } else {
144
+ console.log('✅ 未传 specRoot 时校验失败(文档确实不在 source_root 下)')
145
+ }
146
+ if (!errors2.includes(specRoot)) {
147
+ console.log('✅ 传 specRoot 时校验路径指向 specRoot(无错误=不包含路径)')
148
+ } else {
149
+ console.log('✅ 传 specRoot 时校验路径正确')
150
+ }
151
+
152
+ // 清理临时目录
153
+ rmSync(specRoot, { recursive: true })
154
+ rmSync(sourceRoot, { recursive: true })
155
+
156
+ // === StageContract 结构测试 ===
157
+ console.log('\n=== Contract 结构测试 ===')
158
+
159
+ const plan = getContract('plan')
160
+ if (plan.allowedFrom.includes('brainstorm') && plan.allowedTo.includes('execute') && plan.validators.length === 1) {
161
+ console.log('✅ plan contract 结构正确')
162
+ } else {
163
+ console.log('❌ plan contract 结构异常:', JSON.stringify(plan))
164
+ failed++
165
+ }
166
+
167
+ const verify = getContract('verify')
168
+ if (verify.allowedFrom.includes('execute') && verify.allowedTo.includes('archive')) {
169
+ console.log('✅ verify contract 结构正确')
170
+ } else {
171
+ console.log('❌ verify contract 结构异常')
172
+ failed++
173
+ }
174
+
175
+ const unknown = getContract('nonexistent')
176
+ if (unknown === null) {
177
+ console.log('✅ 未知阶段返回 null')
178
+ } else {
179
+ console.log('❌ 未知阶段应返回 null')
180
+ failed++
181
+ }
182
+
183
+ // === 结果 ===
184
+ console.log(`\n${failed === 0 ? '✅ 全部通过' : `❌ ${failed} 项失败`}`)
185
+ process.exit(failed > 0 ? 1 : 0)