sillyspec 3.17.14 → 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.
@@ -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: 'warning', detail: `缺少 ${missing.length} 份 scan 文档: ${missing.join(', ')}` })
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: 'failed',
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: 'failed',
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: 'failed',
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: 'warning',
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: 'warning',
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: 'warning', detail: ep.detail })
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: 'failed',
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: 'warning',
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 === 'failed')
187
- const hasWarning = checks.some(c => c.severity === 'warning')
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 = 'failed_post_check'
192
+ status = SCAN_STATUS.FAILED_POST_CHECK
192
193
  } else if (hasWarning) {
193
- status = 'completed_with_warnings'
194
+ status = SCAN_STATUS.COMPLETED_WITH_WARNINGS
194
195
  } else {
195
- status = 'success'
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
  }
@@ -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: `把 tasks.md 每个 checkbox 展开为任务描述,按 Wave 分组,产出 plan.md 总览。
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 | 4h | — | ... |
90
- | task-02 | 添加角色创建接口 | W1 | P0 | 3h | — | ... |
91
- | task-03 | 用户创建接口联调 | W2 | P0 | 4h | task-01,02 | ... |
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
- - plan.md 包含:Wave 分组 + 任务总表 + 依赖图 + 关键路径 + 全局验收标准,**不放实现细节**
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
- - 估时参考:单个 task ≤ 8h,超过则拆分
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. 分析依赖关系,按 Wave 分组
136
- 5. 生成任务总表(含优先级、估时、依赖)
137
- 6. 生成 Mermaid 依赖关系图
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
- plan.md 总览内容`,
144
- outputHint: 'plan.md 总览',
268
+ plan_level + 计划内容(none 级别输出建议操作)`,
269
+ outputHint: '计划内容',
145
270
  optional: false
146
271
  },
147
272
  {
148
273
  name: '自检总览',
149
- prompt: `自检 plan.md 总览质量。
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
@@ -12,6 +12,7 @@
12
12
  import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'fs'
13
13
  import { join, resolve, basename } from 'path'
14
14
  import jsYaml from 'js-yaml'
15
+ import { WORKFLOW_STATUS } from './constants.js'
15
16
 
16
17
  // ─── Workflow 加载 ───
17
18
 
@@ -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
+ }