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.
- package/README.md +1 -1
- package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -0
- package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -0
- package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -0
- package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -0
- package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +193 -0
- package/docs/sillyspec/file-lifecycle.md +106 -1297
- package/package.json +3 -3
- package/src/hooks/worktree-guard.js +166 -47
- package/src/progress.js +37 -0
- package/src/run.js +309 -56
- package/src/stage-contract.js +349 -0
- package/src/stages/archive.js +6 -10
- package/src/stages/brainstorm.js +4 -1
- package/src/stages/doctor.js +1 -2
- package/src/stages/propose.js +4 -1
- package/src/stages/scan.js +3 -3
- package/test/check-syntax.mjs +26 -0
- package/test/run-tests.mjs +20 -0
- package/test/stage-contract.test.mjs +185 -0
- package/test/stage-definitions.test.mjs +43 -0
- package/test/worktree-guard.test.mjs +78 -0
|
@@ -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 }
|
package/src/stages/archive.js
CHANGED
|
@@ -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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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.
|
|
151
|
-
- 清除当前变更信息(归档后不再活跃)
|
|
152
|
-
- 如果是主变更(有 MASTER.md),标记所有阶段为 ✅,然后清除
|
|
153
|
-
- 历史记录追加时间 + 归档完成
|
|
149
|
+
4. 确认 sillyspec.db 中该变更已不再 active(确认归档步骤由 CLI 调用 unregisterChange)
|
|
154
150
|
|
|
155
151
|
### 输出
|
|
156
152
|
归档完成确认 + 累积规范统计`,
|
package/src/stages/brainstorm.js
CHANGED
package/src/stages/doctor.js
CHANGED
|
@@ -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 缺少
|
|
348
|
+
- local.yaml 缺少 test 命令 → 补充对应命令
|
|
350
349
|
- 缺少 STACK.md → \`sillyspec run scan\` 重新扫描
|
|
351
350
|
- sillyspec.db 状态不一致 → \`sillyspec run <阶段> --reset\` 重置对应阶段
|
|
352
351
|
- 孤儿目录 → 确认后 \`rm -rf .sillyspec/changes/<目录名>\`
|
package/src/stages/propose.js
CHANGED
package/src/stages/scan.js
CHANGED
|
@@ -139,16 +139,16 @@ export const definition = {
|
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
141
|
name: '生成本地配置',
|
|
142
|
-
prompt: `自动生成 .sillyspec
|
|
142
|
+
prompt: `自动生成 .sillyspec/local.yaml 本地配置文件。
|
|
143
143
|
|
|
144
144
|
### 操作
|
|
145
|
-
1. 检查 .sillyspec
|
|
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
|
|
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)
|