sillyspec 3.17.15 → 3.18.0
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/.npmrc.bak +0 -0
- package/docs/platform-scan-protocol.md +298 -0
- package/package.json +1 -1
- package/src/constants.js +70 -0
- package/src/index.js +55 -0
- package/src/run.js +60 -17
- package/src/scan-postcheck.js +17 -16
- package/src/stages/plan.js +189 -41
- package/src/workflow.js +1 -0
- package/test/platform-artifacts.test.mjs +181 -0
- package/test/platform-failure-samples.test.mjs +194 -0
- package/test/platform-recovery-chain.test.mjs +178 -0
- package/test/platform-recovery.test.mjs +1 -1
- package/test/platform-scan-p0.test.mjs +2 -2
- package/test/run-tests.mjs +31 -3
- package/test/scan-paths.test.mjs +1 -1
- package/test/scan-postcheck.test.mjs +1 -1
- package/test/spec-dir.test.mjs +1 -1
- package/test/stage-contract.test.mjs +1 -1
package/src/scan-postcheck.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'fs'
|
|
9
9
|
import { join, basename } from 'path'
|
|
10
|
+
import { SCAN_STATUS, CHECK_SEVERITY } from './constants.js'
|
|
10
11
|
|
|
11
12
|
const REQUIRED_SCAN_DOCS = [
|
|
12
13
|
'ARCHITECTURE.md',
|
|
@@ -41,7 +42,7 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
41
42
|
// 检查 7 份文档是否存在
|
|
42
43
|
const missing = REQUIRED_SCAN_DOCS.filter(f => !existsSync(join(scanDir, f)))
|
|
43
44
|
if (missing.length > 0) {
|
|
44
|
-
checks.push({ name: 'missing_docs', severity:
|
|
45
|
+
checks.push({ name: 'missing_docs', severity: CHECK_SEVERITY.WARNING, detail: `缺少 ${missing.length} 份 scan 文档: ${missing.join(', ')}` })
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
const hasWarning = checks.some(c => c.severity === 'warning')
|
|
@@ -62,8 +63,8 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
62
63
|
const leaked = readdirSync(localSub, { recursive: true }).filter(e => String(e).endsWith('.md') || String(e).endsWith('.yaml') || String(e).endsWith('.json'))
|
|
63
64
|
if (leaked.length > 0) {
|
|
64
65
|
checks.push({
|
|
65
|
-
name: 'source_root_leak',
|
|
66
|
-
severity:
|
|
66
|
+
name: sub === 'docs' ? 'source_root_docs_leak' : 'source_root_leak',
|
|
67
|
+
severity: CHECK_SEVERITY.FAILED,
|
|
67
68
|
detail: `source_root/.sillyspec/${sub}/ 下存在 ${leaked.length} 个文件(${localSub}/),agent 写入到了错误路径`
|
|
68
69
|
})
|
|
69
70
|
}
|
|
@@ -75,7 +76,7 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
75
76
|
if (existsSync(filePath)) {
|
|
76
77
|
checks.push({
|
|
77
78
|
name: 'source_root_leak',
|
|
78
|
-
severity:
|
|
79
|
+
severity: CHECK_SEVERITY.FAILED,
|
|
79
80
|
detail: `source_root/.sillyspec/${file} 存在,agent 写入到了错误路径(${filePath})`
|
|
80
81
|
})
|
|
81
82
|
}
|
|
@@ -87,7 +88,7 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
87
88
|
if (missingDocs.length > 0) {
|
|
88
89
|
checks.push({
|
|
89
90
|
name: missingDocs.length === REQUIRED_SCAN_DOCS.length ? 'all_docs_missing' : 'partial_docs_missing',
|
|
90
|
-
severity:
|
|
91
|
+
severity: CHECK_SEVERITY.FAILED,
|
|
91
92
|
detail: missingDocs.length === REQUIRED_SCAN_DOCS.length
|
|
92
93
|
? `spec_root 下无任何 scan 文档(${specScanDir}/),扫描可能未执行`
|
|
93
94
|
: `spec_root 缺少必需文档: ${missingDocs.join(', ')}(7 份 scan 文档均为 required)`
|
|
@@ -106,7 +107,7 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
106
107
|
if (docsMissingHeader.length > 0) {
|
|
107
108
|
checks.push({
|
|
108
109
|
name: 'docs_missing_header',
|
|
109
|
-
severity:
|
|
110
|
+
severity: CHECK_SEVERITY.WARNING,
|
|
110
111
|
detail: `${docsMissingHeader.length} 份文档缺少 author/created_at: ${docsMissingHeader.join(', ')}`
|
|
111
112
|
})
|
|
112
113
|
}
|
|
@@ -143,7 +144,7 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
143
144
|
if (invalidCommands.length > 0) {
|
|
144
145
|
checks.push({
|
|
145
146
|
name: 'local_config_invalid',
|
|
146
|
-
severity:
|
|
147
|
+
severity: CHECK_SEVERITY.WARNING,
|
|
147
148
|
detail: `local.yaml 引用不存在的命令: ${invalidCommands.join('; ')}`
|
|
148
149
|
})
|
|
149
150
|
}
|
|
@@ -159,7 +160,7 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
159
160
|
]
|
|
160
161
|
for (const ep of errorPatterns) {
|
|
161
162
|
if (ep.pattern.test(outputText)) {
|
|
162
|
-
checks.push({ name: ep.name, severity:
|
|
163
|
+
checks.push({ name: ep.name, severity: CHECK_SEVERITY.WARNING, detail: ep.detail })
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
166
|
}
|
|
@@ -168,7 +169,7 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
168
169
|
if (scanMeta.manifestWritten === false) {
|
|
169
170
|
checks.push({
|
|
170
171
|
name: 'manifest_write_failed',
|
|
171
|
-
severity:
|
|
172
|
+
severity: CHECK_SEVERITY.FAILED,
|
|
172
173
|
detail: 'manifest.json 写入失败,平台无法消费 scan 结果'
|
|
173
174
|
})
|
|
174
175
|
}
|
|
@@ -177,22 +178,22 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
177
178
|
if (scanMeta.projectListParsed === false) {
|
|
178
179
|
checks.push({
|
|
179
180
|
name: 'project_list_parse_failed',
|
|
180
|
-
severity:
|
|
181
|
+
severity: CHECK_SEVERITY.WARNING,
|
|
181
182
|
detail: 'Step 2 项目列表解析失败,回退到注册项目列表,可能遗漏子项目'
|
|
182
183
|
})
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
// 8. 计算 finalStatus
|
|
186
|
-
const hasFailed = checks.some(c => c.severity ===
|
|
187
|
-
const hasWarning = checks.some(c => c.severity ===
|
|
187
|
+
const hasFailed = checks.some(c => c.severity === CHECK_SEVERITY.FAILED)
|
|
188
|
+
const hasWarning = checks.some(c => c.severity === CHECK_SEVERITY.WARNING)
|
|
188
189
|
|
|
189
190
|
let status
|
|
190
191
|
if (hasFailed) {
|
|
191
|
-
status =
|
|
192
|
+
status = SCAN_STATUS.FAILED_POST_CHECK
|
|
192
193
|
} else if (hasWarning) {
|
|
193
|
-
status =
|
|
194
|
+
status = SCAN_STATUS.COMPLETED_WITH_WARNINGS
|
|
194
195
|
} else {
|
|
195
|
-
status =
|
|
196
|
+
status = SCAN_STATUS.SUCCESS
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
return { status, checks }
|
|
@@ -254,7 +255,7 @@ export function formatStructuredResult(result, meta = {}) {
|
|
|
254
255
|
const entry = { name: check.name, detail: check.detail, severity }
|
|
255
256
|
|
|
256
257
|
// 路径污染类
|
|
257
|
-
if (check.name === 'source_root_leak') {
|
|
258
|
+
if (check.name === 'source_root_leak' || check.name === 'source_root_docs_leak') {
|
|
258
259
|
structured.failure_categories.path_pollution.push(entry)
|
|
259
260
|
structured.failure_categories.violations.push(entry)
|
|
260
261
|
}
|
package/src/stages/plan.js
CHANGED
|
@@ -10,6 +10,60 @@ export const definition = {
|
|
|
10
10
|
|
|
11
11
|
// 固定前缀步骤
|
|
12
12
|
export const fixedPrefix = [
|
|
13
|
+
{
|
|
14
|
+
name: '复杂度分类',
|
|
15
|
+
prompt: `在生成计划之前,先判定本次需求的复杂度等级(plan_level)。
|
|
16
|
+
|
|
17
|
+
### 操作
|
|
18
|
+
1. 读取 tasks.md 和 design.md,了解需求范围
|
|
19
|
+
2. 按「分级规则」判定 plan_level
|
|
20
|
+
|
|
21
|
+
### 分级规则
|
|
22
|
+
判定 plan_level 为 none 时,需**同时满足**以下所有条件:
|
|
23
|
+
- 涉及文件 ≤ 2 个
|
|
24
|
+
- 不跨模块(改动集中在单个模块内)
|
|
25
|
+
- 无 schema / DB / manifest / local.yaml 变更
|
|
26
|
+
- 无状态机 / workflow 状态流转变更
|
|
27
|
+
- 无 source_root / spec_root / runtime_root 路径隔离规则变更
|
|
28
|
+
- 无 validator / postcheck / agent 调度行为变更
|
|
29
|
+
- 需求明确,无设计歧义
|
|
30
|
+
|
|
31
|
+
判定为 light(满足任一即升为 light):
|
|
32
|
+
- 涉及 3-5 个文件
|
|
33
|
+
- 涉及 prompt 行为变更
|
|
34
|
+
- 涉及 validator / postcheck 逻辑
|
|
35
|
+
- 涉及路径规则变更(但范围可控)
|
|
36
|
+
- 涉及 schema/DB/状态机变更,但影响面可控
|
|
37
|
+
- 需要明确验收标准来防止范围漂移
|
|
38
|
+
|
|
39
|
+
判定为 full(满足任一即升为 full):
|
|
40
|
+
- 预计 8 个以上 task
|
|
41
|
+
- 跨 3 个以上模块
|
|
42
|
+
- 涉及 CLI + 平台 + DB 联动
|
|
43
|
+
- 涉及 agent 调度 / worktree / isolation 逻辑
|
|
44
|
+
- 涉及复杂状态恢复(checkpoint / resume)
|
|
45
|
+
- 需要并行 sub-agent 执行
|
|
46
|
+
- 需要人工审查设计方向
|
|
47
|
+
- 涉及 worktree / baseline / sandbox 等基础设施
|
|
48
|
+
|
|
49
|
+
### 输出格式
|
|
50
|
+
在输出开头,以如下格式输出分类结果:
|
|
51
|
+
|
|
52
|
+
\`\`\`
|
|
53
|
+
plan_level: none | light | full
|
|
54
|
+
reason: <一句话说明判定理由>
|
|
55
|
+
estimated_files: <N>
|
|
56
|
+
cross_module: true | false
|
|
57
|
+
has_schema_change: true | false
|
|
58
|
+
has_state_machine_change: true | false
|
|
59
|
+
needs_parallel_execution: true | false
|
|
60
|
+
needs_human_review: true | false
|
|
61
|
+
\`\`\`
|
|
62
|
+
|
|
63
|
+
分类完成后,继续进入下一步。`,
|
|
64
|
+
outputHint: '复杂度分类结果',
|
|
65
|
+
optional: false
|
|
66
|
+
},
|
|
13
67
|
{
|
|
14
68
|
name: '状态检查',
|
|
15
69
|
prompt: `检查当前状态,确认可以执行 plan。
|
|
@@ -62,11 +116,87 @@ export const fixedPrefix = [
|
|
|
62
116
|
optional: false
|
|
63
117
|
},
|
|
64
118
|
{
|
|
65
|
-
name: '
|
|
66
|
-
prompt:
|
|
119
|
+
name: '按复杂度生成分级计划',
|
|
120
|
+
prompt: `根据「复杂度分类」步骤的 plan_level 结果,按对应级别生成计划。
|
|
121
|
+
|
|
122
|
+
### 操作
|
|
123
|
+
1. 读取上一步输出的 plan_level 分类结果
|
|
124
|
+
2. 读取 tasks.md 和 design.md 了解需求范围
|
|
125
|
+
3. 按 plan_level 选择对应模板输出
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
#### plan_level = none
|
|
130
|
+
生成最小 plan.md(占位文件,保持流程兼容),不生成完整蓝图。格式:
|
|
131
|
+
\`\`\`markdown
|
|
132
|
+
---
|
|
133
|
+
plan_level: none
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
# 计划跳过
|
|
137
|
+
|
|
138
|
+
## 原因
|
|
139
|
+
<一句话说明判定理由>
|
|
140
|
+
|
|
141
|
+
## 建议直接 execute
|
|
142
|
+
直接进入 execute 阶段完成下列最小任务。
|
|
143
|
+
|
|
144
|
+
## Tasks
|
|
145
|
+
- [ ] task-01: 按用户需求完成小范围明确修改
|
|
146
|
+
|
|
147
|
+
## 验收
|
|
148
|
+
- 修改范围符合用户需求
|
|
149
|
+
- 不引入额外无关变更
|
|
150
|
+
- 必要测试或检查通过
|
|
151
|
+
\`\`\`
|
|
152
|
+
**注意:** 所有 plan_level 都必须包含 \`- [ ] task-XX:\` 格式的 checkbox 任务,execute 阶段依赖此格式解析任务。
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
#### plan_level = light
|
|
157
|
+
生成轻量 plan.md,保存到变更目录。只包含以下四部分:
|
|
67
158
|
|
|
68
|
-
### plan.md 格式(PM 视角 + 机器可解析)
|
|
69
159
|
\`\`\`markdown
|
|
160
|
+
---
|
|
161
|
+
plan_level: light
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
# 轻量计划:<需求简述>
|
|
165
|
+
|
|
166
|
+
## 来源
|
|
167
|
+
直接引用 brainstorm 结论或用户原始需求,不重新扩写。
|
|
168
|
+
|
|
169
|
+
## 范围
|
|
170
|
+
- 涉及的文件/模块清单
|
|
171
|
+
|
|
172
|
+
## Tasks
|
|
173
|
+
- [ ] task-01: ...
|
|
174
|
+
- [ ] task-02: ...
|
|
175
|
+
- [ ] task-03: ...
|
|
176
|
+
|
|
177
|
+
## 验收
|
|
178
|
+
- 具体可验证的验收条目
|
|
179
|
+
\`\`\`
|
|
180
|
+
|
|
181
|
+
light 计划的约束:
|
|
182
|
+
- **禁止**生成 Mermaid 图
|
|
183
|
+
- **禁止**估时
|
|
184
|
+
- **禁止**泛泛风险 分析(如"需要充分测试")
|
|
185
|
+
- **禁止**放实现细节(函数签名、代码示例)
|
|
186
|
+
- 来源/目标直接引用已有文档,不重新生成
|
|
187
|
+
- 任务列表控制在 10 条以内
|
|
188
|
+
- **任务必须使用 checkbox 格式**(\`- [ ] task-XX:\`),不要用纯编号列表(\`1. 2.\`),execute 阶段依赖此格式解析任务
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
#### plan_level = full
|
|
193
|
+
生成完整 plan.md,保存到变更目录。格式如下:
|
|
194
|
+
|
|
195
|
+
\`\`\`markdown
|
|
196
|
+
---
|
|
197
|
+
plan_level: full
|
|
198
|
+
---
|
|
199
|
+
|
|
70
200
|
# 实现计划
|
|
71
201
|
|
|
72
202
|
## Spike 前置验证(如需要)
|
|
@@ -84,18 +214,11 @@ export const fixedPrefix = [
|
|
|
84
214
|
- [ ] task-03: 用户创建接口联调
|
|
85
215
|
|
|
86
216
|
## 任务总表
|
|
87
|
-
| 编号 | 任务 | Wave | 优先级 |
|
|
88
|
-
|
|
89
|
-
| task-01 | 添加用户创建接口 | W1 | P0 |
|
|
90
|
-
| task-02 | 添加角色创建接口 | W1 | P0 |
|
|
91
|
-
| task-03 | 用户创建接口联调 | W2 | P0 |
|
|
92
|
-
|
|
93
|
-
## 依赖关系图
|
|
94
|
-
\`\`\`mermaid
|
|
95
|
-
graph LR
|
|
96
|
-
task-01 --> task-03
|
|
97
|
-
task-02 --> task-03
|
|
98
|
-
\`\`\`
|
|
217
|
+
| 编号 | 任务 | Wave | 优先级 | 依赖 | 说明 |
|
|
218
|
+
|---|---|---|---|---|---|
|
|
219
|
+
| task-01 | 添加用户创建接口 | W1 | P0 | — | ... |
|
|
220
|
+
| task-02 | 添加角色创建接口 | W1 | P0 | — | ... |
|
|
221
|
+
| task-03 | 用户创建接口联调 | W2 | P0 | task-01,02 | ... |
|
|
99
222
|
|
|
100
223
|
## 关键路径
|
|
101
224
|
task-01 → task-03(最长路径,决定最短交付周期)
|
|
@@ -105,64 +228,89 @@ task-01 → task-03(最长路径,决定最短交付周期)
|
|
|
105
228
|
- [ ] (brownfield)未配置新功能时行为不变
|
|
106
229
|
\`\`\`
|
|
107
230
|
|
|
108
|
-
|
|
109
|
-
-
|
|
231
|
+
full 计划的约束:
|
|
232
|
+
- **禁止**估时(任务总表不含估时列)
|
|
233
|
+
- **禁止**泛泛风险分析("需要充分测试"类废话转为具体验收条目)
|
|
234
|
+
- Mermaid 依赖关系图**仅当依赖关系非平凡时生成**(线性依赖或全并行时不生成)
|
|
235
|
+
- **Wave 下的 checkbox 行必须保留**(execute 阶段解析依赖 \`- [ ] task-XX:\` 格式)
|
|
236
|
+
- plan.md 包含 Wave 分组 + 任务总表 + 关键路径 + 全局验收标准,**不放实现细节**
|
|
110
237
|
- 实现细节写到后续的 tasks/task-NN.md 中
|
|
111
238
|
- 每个任务编号格式:task-01、task-02 ...
|
|
112
|
-
- **Wave 下的 checkbox 行必须保留**(execute 阶段解析依赖 \`- [ ] task-XX:\` 格式)
|
|
113
239
|
- 任务总表的优先级:P0(必须)/ P1(重要)/ P2(可选)
|
|
114
|
-
-
|
|
240
|
+
- 总任务数控制在 15 个以内
|
|
115
241
|
|
|
116
|
-
### Spike
|
|
242
|
+
### Spike 前置验证(仅 full)
|
|
117
243
|
当存在技术不确定性时,在 Wave 之前设计 Spike:
|
|
118
244
|
- 涉及新技术栈/未经验证的集成 → 需要 Spike
|
|
119
245
|
- 涉及安全隔离/性能瓶颈 → 需要 Spike
|
|
120
246
|
- 纯业务逻辑/确定的技术方案 → 不需要 Spike
|
|
121
247
|
- 每个 Spike 定义:验证内容 + 通过标准 + 不通过后果
|
|
122
248
|
|
|
123
|
-
###
|
|
249
|
+
### 批量模式指引(仅 full)
|
|
124
250
|
如果 design.md 或需求中包含批量特征(关键词:批量/模板/引擎/N个相似),按以下原则规划:
|
|
125
|
-
❌ 不要列出每个实例作为独立任务
|
|
126
|
-
❌ 不要在文档中嵌入数据
|
|
127
|
-
✅ 设计通用架构,Wave 1 聚焦架构
|
|
128
|
-
✅ 数据转换用脚本完成,单独一个 Wave
|
|
129
|
-
✅ 总任务数控制在 10 个以内
|
|
251
|
+
- ❌ 不要列出每个实例作为独立任务
|
|
252
|
+
- ❌ 不要在文档中嵌入数据
|
|
253
|
+
- ✅ 设计通用架构,Wave 1 聚焦架构
|
|
254
|
+
- ✅ 数据转换用脚本完成,单独一个 Wave
|
|
255
|
+
- ✅ 总任务数控制在 10 个以内
|
|
130
256
|
|
|
131
|
-
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### 通用操作(所有级别)
|
|
132
260
|
1. 读取 tasks.md 获取任务列表
|
|
133
261
|
2. 读取 design.md 获取文件变更清单
|
|
134
|
-
3.
|
|
135
|
-
4.
|
|
136
|
-
5.
|
|
137
|
-
|
|
138
|
-
7. 标注关键路径
|
|
139
|
-
8. 评估是否需要 Spike 前置验证
|
|
140
|
-
9. 保存到变更目录下的 plan.md(路径格式:\`.sillyspec/changes/<change-name>/plan.md\`,其中 <change-name> 是变更目录名,直接使用,不加子目录。正确路径示例:\`.sillyspec/changes/2026-05-28-agent-log-streaming/plan.md\`)
|
|
262
|
+
3. 读取上一步的 plan_level 分类结果
|
|
263
|
+
4. 按对应级别模板生成内容
|
|
264
|
+
5. 保存到变更目录下的 plan.md(路径格式:\`.sillyspec/changes/<change-name>/plan.md\`,其中 <change-name> 是变更目录名,直接使用,不加子目录。正确路径示例:\`.sillyspec/changes/2026-05-28-agent-log-streaming/plan.md\`)
|
|
265
|
+
**plan_level 为 none 时生成最小 plan.md(占位),不生成完整蓝图。**
|
|
141
266
|
|
|
142
267
|
### 输出
|
|
143
|
-
|
|
144
|
-
outputHint: '
|
|
268
|
+
plan_level + 计划内容(none 级别输出建议操作)`,
|
|
269
|
+
outputHint: '计划内容',
|
|
145
270
|
optional: false
|
|
146
271
|
},
|
|
147
272
|
{
|
|
148
273
|
name: '自检总览',
|
|
149
|
-
prompt:
|
|
274
|
+
prompt: `根据 plan_level 检查对应的计划质量。
|
|
150
275
|
|
|
151
276
|
### 操作
|
|
152
|
-
|
|
277
|
+
读取上一步的 plan_level 分类结果,按级别执行对应的自检:
|
|
278
|
+
|
|
279
|
+
#### plan_level = none
|
|
280
|
+
- [ ] plan.md 文件存在且包含 plan_level: none
|
|
281
|
+
- [ ] 给出了可操作的修改建议(2-5 条)
|
|
282
|
+
- [ ] 不含 Wave、Mermaid、估时、任务总表、依赖关系等完整蓝图内容
|
|
283
|
+
- [ ] 建议了直接 execute
|
|
284
|
+
- [ ] 包含至少一个 \`- [ ] task-XX:\` 格式的 checkbox 任务(execute 解析依赖此格式)
|
|
285
|
+
|
|
286
|
+
#### plan_level = light
|
|
287
|
+
- [ ] 输出明确标注 plan_level: light
|
|
288
|
+
- [ ] 有来源、范围、任务列表、验收标准四个部分
|
|
289
|
+
- [ ] 来源直接引用已有文档,未重新扩写
|
|
290
|
+
- [ ] 任务列表清晰且无实现细节
|
|
291
|
+
- [ ] 任务使用 checkbox 格式(\`- [ ] task-XX:\`),不是纯编号列表
|
|
292
|
+
- [ ] 验收标准具体可验证(非笼统表述)
|
|
293
|
+
- [ ] 没有 Mermaid 图、估时、风险分析
|
|
294
|
+
- [ ] 没有函数签名、代码示例等实现细节
|
|
295
|
+
- [ ] plan.md 与 design.md 的文件变更清单一致
|
|
296
|
+
- [ ] 包含至少一个 \`- [ ] task-XX:\` 格式的 checkbox 任务(execute 解析依赖此格式)
|
|
297
|
+
|
|
298
|
+
#### plan_level = full
|
|
153
299
|
- [ ] 每个 task 有编号(task-01、task-02 ...)
|
|
154
300
|
- [ ] 每个 task 在 Wave 下有 checkbox(\`- [ ] task-XX:\` 格式,execute 解析依赖此格式)
|
|
155
301
|
- [ ] 已标注 Wave 分组和依赖关系
|
|
156
|
-
- [ ]
|
|
157
|
-
- [ ] 有 Mermaid 依赖关系图
|
|
302
|
+
- [ ] 有任务总表(含优先级、依赖列,**无估时列**)
|
|
158
303
|
- [ ] 有关键路径标注
|
|
159
304
|
- [ ] 有全局验收标准
|
|
160
305
|
- [ ] (brownfield)全局验收包含兼容性条款
|
|
161
306
|
- [ ] 没有实现细节(接口定义、代码示例等不应该在 plan.md 里)
|
|
162
307
|
- [ ] plan.md 与 design.md 的文件变更清单一致
|
|
308
|
+
- [ ] 如果有 Mermaid 图,依赖关系确实非平凡(非线性/非全并行)
|
|
309
|
+
- [ ] 没有泛泛风险分析(如"需要充分测试")
|
|
163
310
|
|
|
164
311
|
### 输出
|
|
165
|
-
|
|
312
|
+
自检通过/不通过(附 plan_level)`,
|
|
313
|
+
outputHint: '自检结果',
|
|
166
314
|
outputHint: '自检结果',
|
|
167
315
|
optional: false
|
|
168
316
|
}
|
package/src/workflow.js
CHANGED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 平台 scan 产物协议测试
|
|
3
|
+
*
|
|
4
|
+
* 验证:
|
|
5
|
+
* 1. saveWorkflowRun 传入 runtimeRoot + scanRunId 时写到正确路径
|
|
6
|
+
* 2. manifest.json 结构包含产物指针(postcheck_result_path, workflow_runs_dir)
|
|
7
|
+
* 3. 非平台模式下 workflow-runs 写入 cwd/.sillyspec/.runtime/
|
|
8
|
+
*
|
|
9
|
+
* 跑法: node test/platform-artifacts.test.mjs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join, dirname } from 'path'
|
|
13
|
+
import { existsSync, mkdirSync, rmSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
14
|
+
import { fileURLToPath } from 'url'
|
|
15
|
+
import { randomUUID } from 'crypto'
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
18
|
+
const passed = []
|
|
19
|
+
const failed = []
|
|
20
|
+
|
|
21
|
+
function assert(label, condition, detail) {
|
|
22
|
+
if (condition) {
|
|
23
|
+
passed.push(label)
|
|
24
|
+
console.log(` ✅ PASS: ${label}`)
|
|
25
|
+
} else {
|
|
26
|
+
failed.push({ label, detail })
|
|
27
|
+
console.log(` ❌ FAIL: ${label}`)
|
|
28
|
+
if (detail) console.log(` ${detail}`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cleanup(dir) {
|
|
33
|
+
try { rmSync(dir, { recursive: true, force: true }) } catch {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── 测试 1:saveWorkflowRun 平台模式路径正确 ──
|
|
37
|
+
console.log('\n=== Test 1: saveWorkflowRun 平台模式写入路径 ===')
|
|
38
|
+
{
|
|
39
|
+
const { saveWorkflowRun } = await import('../src/workflow.js')
|
|
40
|
+
const tmpRoot = `/tmp/test-artifacts-${randomUUID().slice(0, 8)}`
|
|
41
|
+
const runtimeRoot = join(tmpRoot, 'runtime')
|
|
42
|
+
const scanRunId = 'scan-20260614-test-001'
|
|
43
|
+
|
|
44
|
+
const result = {
|
|
45
|
+
workflow: 'scan-docs',
|
|
46
|
+
project: 'test-project',
|
|
47
|
+
status: 'pass',
|
|
48
|
+
spec_version: 1,
|
|
49
|
+
roles: [],
|
|
50
|
+
workflow_checks: [],
|
|
51
|
+
failures: [],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const saved = saveWorkflowRun(result, {
|
|
55
|
+
cwd: '/fake/cwd',
|
|
56
|
+
source: 'test',
|
|
57
|
+
stage: 'scan',
|
|
58
|
+
runtimeRoot,
|
|
59
|
+
scanRunId,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const expectedDir = join(runtimeRoot, 'scan-runs', scanRunId, 'workflow-runs')
|
|
63
|
+
assert('workflow-runs 目录存在', existsSync(expectedDir))
|
|
64
|
+
assert('workflow-runs 文件存在', existsSync(saved), `路径: ${saved}`)
|
|
65
|
+
assert('路径在 runtime-root 下', saved.startsWith(runtimeRoot), `路径: ${saved}`)
|
|
66
|
+
assert('路径包含 scan-runs', saved.includes('scan-runs'), `路径: ${saved}`)
|
|
67
|
+
assert('路径包含 scanRunId', saved.includes(scanRunId), `路径: ${saved}`)
|
|
68
|
+
|
|
69
|
+
// 验证 JSON 内容
|
|
70
|
+
const content = JSON.parse(readFileSync(saved, 'utf8'))
|
|
71
|
+
assert('JSON 有 run_id', !!content.run_id)
|
|
72
|
+
assert('JSON 有 created_at', !!content.created_at)
|
|
73
|
+
assert('JSON source = test', content.source === 'test')
|
|
74
|
+
assert('JSON stage = scan', content.stage === 'scan')
|
|
75
|
+
assert('JSON workflow = scan-docs', content.workflow === 'scan-docs')
|
|
76
|
+
|
|
77
|
+
cleanup(tmpRoot)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── 测试 2:saveWorkflowRun 本地模式路径正确 ──
|
|
81
|
+
console.log('\n=== Test 2: saveWorkflowRun 本地模式写入路径 ===')
|
|
82
|
+
{
|
|
83
|
+
const { saveWorkflowRun } = await import('../src/workflow.js')
|
|
84
|
+
const tmpCwd = `/tmp/test-artifacts-local-${randomUUID().slice(0, 8)}`
|
|
85
|
+
const sillyspecDir = join(tmpCwd, '.sillyspec', '.runtime', 'workflow-runs')
|
|
86
|
+
|
|
87
|
+
const result = {
|
|
88
|
+
workflow: 'test-wf',
|
|
89
|
+
project: 'default',
|
|
90
|
+
status: 'fail',
|
|
91
|
+
spec_version: 1,
|
|
92
|
+
roles: [],
|
|
93
|
+
workflow_checks: [],
|
|
94
|
+
failures: ['check-1 failed'],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const saved = saveWorkflowRun(result, {
|
|
98
|
+
cwd: tmpCwd,
|
|
99
|
+
source: 'test',
|
|
100
|
+
stage: 'scan',
|
|
101
|
+
// 不传 runtimeRoot 和 scanRunId
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
assert('本地模式文件存在', existsSync(saved))
|
|
105
|
+
assert('本地路径在 .sillyspec/.runtime 下', saved.includes('.sillyspec/.runtime/workflow-runs'), `路径: ${saved}`)
|
|
106
|
+
|
|
107
|
+
const content = JSON.parse(readFileSync(saved, 'utf8'))
|
|
108
|
+
assert('本地 JSON status = fail', content.status === 'fail')
|
|
109
|
+
assert('本地 JSON 有 failures', Array.isArray(content.failures))
|
|
110
|
+
|
|
111
|
+
cleanup(tmpCwd)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── 测试 3:manifest 结构验证(从 run.js 源码静态检查) ──
|
|
115
|
+
console.log('\n=== Test 3: manifest.json 结构字段 ===')
|
|
116
|
+
{
|
|
117
|
+
const { readFile } = await import('fs/promises')
|
|
118
|
+
const runSrc = await readFile(join(__dirname, '..', 'src', 'run.js'), 'utf8')
|
|
119
|
+
|
|
120
|
+
// 平台模式 manifest 初始化
|
|
121
|
+
assert('manifest 包含 workspace_id', runSrc.includes('workspace_id:'))
|
|
122
|
+
assert('manifest 包含 scan_run_id', runSrc.includes('scan_run_id:'))
|
|
123
|
+
assert('manifest 包含 source_commit', runSrc.includes('source_commit:'))
|
|
124
|
+
assert('manifest 包含 source_commit_error', runSrc.includes('source_commit_error:'))
|
|
125
|
+
assert('manifest 包含 generated_at', runSrc.includes('generated_at:'))
|
|
126
|
+
assert('manifest 包含 schema_version', runSrc.includes('schema_version:'))
|
|
127
|
+
assert('manifest 包含 postcheck_result_path', runSrc.includes('postcheck_result_path:'))
|
|
128
|
+
assert('manifest 包含 workflow_runs_dir', runSrc.includes('workflow_runs_dir:'))
|
|
129
|
+
|
|
130
|
+
// postcheck_result_path 在 postcheck 写入后填充
|
|
131
|
+
assert('manifest.postcheck_result_path 下游填充', runSrc.includes('manifest.postcheck_result_path = postcheckJsonPath'))
|
|
132
|
+
|
|
133
|
+
// workflow_runs_dir 使用 runtimeRoot + scanRunId
|
|
134
|
+
assert('workflow_runs_dir 基于 runtimeRoot', runSrc.includes("join(platformOpts.runtimeRoot, 'scan-runs'"))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── 测试 4:平台指针状态更新(源码检查) ──
|
|
138
|
+
console.log('\n=== Test 4: 平台指针 scan 完成后状态更新 ===')
|
|
139
|
+
{
|
|
140
|
+
const { readFile } = await import('fs/promises')
|
|
141
|
+
const runSrc = await readFile(join(__dirname, '..', 'src', 'run.js'), 'utf8')
|
|
142
|
+
|
|
143
|
+
assert('scan 完成后读取 pointer 文件', runSrc.includes('pointerPath'))
|
|
144
|
+
assert('pointer status 使用 POINTER_STATUS 枚举', runSrc.includes('POINTER_STATUS'))
|
|
145
|
+
assert('pointer 记录 completedAt', runSrc.includes('pointer.completedAt'))
|
|
146
|
+
assert('pointer 记录 scanStatus', runSrc.includes('pointer.scanStatus'))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── 测试 5:saveWorkflowRun 调用点传入 runtimeRoot(源码检查) ──
|
|
150
|
+
console.log('\n=== Test 5: run.js 调用 saveWorkflowRun 传入平台参数 ===')
|
|
151
|
+
{
|
|
152
|
+
const { readFile } = await import('fs/promises')
|
|
153
|
+
const runSrc = await readFile(join(__dirname, '..', 'src', 'run.js'), 'utf8')
|
|
154
|
+
|
|
155
|
+
// 找到 saveWorkflowRun 调用
|
|
156
|
+
const calls = runSrc.match(/saveWorkflowRun\([^)]+\{[^}]+\}/gs)
|
|
157
|
+
assert('至少有 2 处 saveWorkflowRun 调用', calls && calls.length >= 2, `实际: ${calls?.length}`)
|
|
158
|
+
|
|
159
|
+
// 检查调用是否包含 runtimeRoot 传递
|
|
160
|
+
const hasRuntimeRoot = runSrc.includes('platformOpts.runtimeRoot ? { runtimeRoot: platformOpts.runtimeRoot }')
|
|
161
|
+
assert('saveWorkflowRun 调用传入 runtimeRoot', hasRuntimeRoot, '未发现 runtimeRoot 传递')
|
|
162
|
+
|
|
163
|
+
const hasScanRunId = runSrc.includes('platformOpts.scanRunId ? { scanRunId: platformOpts.scanRunId }')
|
|
164
|
+
assert('saveWorkflowRun 调用传入 scanRunId', hasScanRunId, '未发现 scanRunId 传递')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── 结果 ──
|
|
168
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
169
|
+
console.log(`✅ 通过: ${passed.length} ❌ 失败: ${failed.length}`)
|
|
170
|
+
console.log(`${'='.repeat(50)}`)
|
|
171
|
+
|
|
172
|
+
if (failed.length > 0) {
|
|
173
|
+
console.log('\n失败详情:')
|
|
174
|
+
for (const f of failed) {
|
|
175
|
+
console.log(` ❌ ${f.label}`)
|
|
176
|
+
if (f.detail) console.log(` ${f.detail}`)
|
|
177
|
+
}
|
|
178
|
+
throw new Error('platform-artifacts test failed')
|
|
179
|
+
} else {
|
|
180
|
+
console.log('\n🎉 平台产物协议测试全部通过!')
|
|
181
|
+
}
|