sillyspec 3.18.0 → 3.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/db.js +4 -0
- package/src/hooks/worktree-guard.js +97 -4
- package/src/index.js +1 -1
- package/src/progress.js +41 -14
- package/src/run.js +255 -66
- package/src/stage-contract.js +244 -12
- package/src/stages/brainstorm.js +228 -8
- package/src/stages/index.js +0 -2
- package/src/stages/plan.js +55 -18
- package/src/stages/propose.js +30 -4
- package/src/stages/quick.js +13 -10
- package/src/stages/scan.js +12 -0
- package/src/stages/verify.js +31 -13
- package/test/platform-artifacts.test.mjs +14 -5
- package/test/platform-failure-samples.test.mjs +3 -2
- package/test/platform-recovery-chain.test.mjs +10 -9
- package/test/platform-recovery.test.mjs +13 -5
- package/test/platform-scan-p0.test.mjs +3 -0
- package/test/scan-postcheck.test.mjs +3 -2
- package/test/spec-dir.test.mjs +2 -1
- package/test/stage-contract.test.mjs +119 -6
- package/test/stage-definitions.test.mjs +2 -6
- package/test/wait-gates.test.mjs +501 -0
- package/test/worktree-guard.test.mjs +58 -0
- package/.npmrc.bak +0 -0
package/src/stages/plan.js
CHANGED
|
@@ -84,22 +84,25 @@ needs_human_review: true | false
|
|
|
84
84
|
### 操作
|
|
85
85
|
1. 读取 CODEBASE-OVERVIEW.md + 各子项目上下文
|
|
86
86
|
2. 读取 proposal.md、design.md、requirements.md、tasks.md
|
|
87
|
-
3.
|
|
88
|
-
|
|
87
|
+
3. 如果存在 decisions.md,必须读取并提取所有当前版本 D-xxx@vN 决策 ID
|
|
88
|
+
- 如果发现 priority=P0/P1 且 status=unresolved/blocking 的决策,停止生成计划,要求先回到 brainstorm 的 Design Grill 修正
|
|
89
|
+
- 如果发现 superseded 决策,只引用最新版本,不引用旧版本
|
|
90
|
+
4. 读取 CONVENTIONS.md、ARCHITECTURE.md、STACK.md
|
|
91
|
+
5. 读取 local.yaml 获取构建/测试命令
|
|
89
92
|
|
|
90
93
|
### 模块文档加载
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
6. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
|
|
95
|
+
7. 根据 design.md 的文件变更清单匹配 _module-map.yaml 中的模块
|
|
96
|
+
8. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
|
|
97
|
+
9. 将模块文档作为制定计划的上下文,确保计划符合模块当前设计
|
|
98
|
+
10. **利用模块依赖关系辅助分析**:
|
|
96
99
|
- 用 depends_on 判断哪些模块会被间接影响
|
|
97
100
|
- 用 used_by 判断变更会不会影响下游模块
|
|
98
101
|
- 将依赖关系纳入 Wave 分组决策(依赖同一模块的任务尽量同 Wave)
|
|
99
102
|
- 如果变更涉及多个有依赖关系的模块,在 plan.md 的任务总表中标注模块依赖
|
|
100
103
|
|
|
101
104
|
### 输出
|
|
102
|
-
|
|
105
|
+
已加载的文件清单(含 decisions.md 当前版本/未决项状态、模块文档 + 模块依赖关系摘要)`,
|
|
103
106
|
outputHint: '文件清单',
|
|
104
107
|
optional: false
|
|
105
108
|
},
|
|
@@ -170,12 +173,17 @@ plan_level: light
|
|
|
170
173
|
- 涉及的文件/模块清单
|
|
171
174
|
|
|
172
175
|
## Tasks
|
|
173
|
-
- [ ] task-01:
|
|
176
|
+
- [ ] task-01: ...(覆盖:FR-01, D-001@v1)
|
|
174
177
|
- [ ] task-02: ...
|
|
175
178
|
- [ ] task-03: ...
|
|
176
179
|
|
|
177
180
|
## 验收
|
|
178
181
|
- 具体可验证的验收条目
|
|
182
|
+
|
|
183
|
+
## 覆盖矩阵(如存在 decisions.md)
|
|
184
|
+
| ID | 覆盖任务 | 验收证据 |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| D-001@v1 | task-01 | AC-01 |
|
|
179
187
|
\`\`\`
|
|
180
188
|
|
|
181
189
|
light 计划的约束:
|
|
@@ -184,6 +192,8 @@ light 计划的约束:
|
|
|
184
192
|
- **禁止**泛泛风险 分析(如"需要充分测试")
|
|
185
193
|
- **禁止**放实现细节(函数签名、代码示例)
|
|
186
194
|
- 来源/目标直接引用已有文档,不重新生成
|
|
195
|
+
- 如果存在 decisions.md,所有当前版本 D-xxx@vN 必须在 Tasks 或覆盖矩阵中出现
|
|
196
|
+
- 如果存在 P0/P1 unresolved blocker,不生成 plan.md
|
|
187
197
|
- 任务列表控制在 10 条以内
|
|
188
198
|
- **任务必须使用 checkbox 格式**(\`- [ ] task-XX:\`),不要用纯编号列表(\`1. 2.\`),execute 阶段依赖此格式解析任务
|
|
189
199
|
|
|
@@ -207,18 +217,18 @@ plan_level: full
|
|
|
207
217
|
> 技术不确定性高时才需要 Spike。无不确定性则跳过此节。
|
|
208
218
|
|
|
209
219
|
## Wave 1(并行,无依赖)
|
|
210
|
-
- [ ] task-01:
|
|
211
|
-
- [ ] task-02:
|
|
220
|
+
- [ ] task-01: 添加用户创建接口(覆盖:FR-01, D-001@v1)
|
|
221
|
+
- [ ] task-02: 添加角色创建接口(覆盖:FR-02)
|
|
212
222
|
|
|
213
223
|
## Wave 2(依赖 Wave 1)
|
|
214
224
|
- [ ] task-03: 用户创建接口联调
|
|
215
225
|
|
|
216
226
|
## 任务总表
|
|
217
|
-
| 编号 | 任务 | Wave | 优先级 | 依赖 | 说明 |
|
|
218
|
-
|
|
219
|
-
| task-01 | 添加用户创建接口 | W1 | P0 | — | ... |
|
|
220
|
-
| task-02 | 添加角色创建接口 | W1 | P0 | — | ... |
|
|
221
|
-
| task-03 | 用户创建接口联调 | W2 | P0 | task-01,02 | ... |
|
|
227
|
+
| 编号 | 任务 | Wave | 优先级 | 依赖 | 覆盖 FR/D | 说明 |
|
|
228
|
+
|---|---|---|---|---|---|---|
|
|
229
|
+
| task-01 | 添加用户创建接口 | W1 | P0 | — | FR-01, D-001@v1 | ... |
|
|
230
|
+
| task-02 | 添加角色创建接口 | W1 | P0 | — | FR-02 | ... |
|
|
231
|
+
| task-03 | 用户创建接口联调 | W2 | P0 | task-01,02 | FR-03 | ... |
|
|
222
232
|
|
|
223
233
|
## 关键路径
|
|
224
234
|
task-01 → task-03(最长路径,决定最短交付周期)
|
|
@@ -226,6 +236,11 @@ task-01 → task-03(最长路径,决定最短交付周期)
|
|
|
226
236
|
## 全局验收标准
|
|
227
237
|
- [ ] 所有单元测试通过
|
|
228
238
|
- [ ] (brownfield)未配置新功能时行为不变
|
|
239
|
+
|
|
240
|
+
## 覆盖矩阵(如存在 decisions.md)
|
|
241
|
+
| ID | 覆盖任务 | 验收证据 |
|
|
242
|
+
|---|---|---|
|
|
243
|
+
| D-001@v1 | task-01 | AC-01 |
|
|
229
244
|
\`\`\`
|
|
230
245
|
|
|
231
246
|
full 计划的约束:
|
|
@@ -234,6 +249,8 @@ full 计划的约束:
|
|
|
234
249
|
- Mermaid 依赖关系图**仅当依赖关系非平凡时生成**(线性依赖或全并行时不生成)
|
|
235
250
|
- **Wave 下的 checkbox 行必须保留**(execute 阶段解析依赖 \`- [ ] task-XX:\` 格式)
|
|
236
251
|
- plan.md 包含 Wave 分组 + 任务总表 + 关键路径 + 全局验收标准,**不放实现细节**
|
|
252
|
+
- 如果存在 decisions.md,plan.md 必须包含当前版本 D-xxx@vN/FR-xxx 覆盖矩阵
|
|
253
|
+
- 如果存在 P0/P1 unresolved blocker,不生成 plan.md,输出阻塞清单
|
|
237
254
|
- 实现细节写到后续的 tasks/task-NN.md 中
|
|
238
255
|
- 每个任务编号格式:task-01、task-02 ...
|
|
239
256
|
- 任务总表的优先级:P0(必须)/ P1(重要)/ P2(可选)
|
|
@@ -290,6 +307,8 @@ plan_level + 计划内容(none 级别输出建议操作)`,
|
|
|
290
307
|
- [ ] 任务列表清晰且无实现细节
|
|
291
308
|
- [ ] 任务使用 checkbox 格式(\`- [ ] task-XX:\`),不是纯编号列表
|
|
292
309
|
- [ ] 验收标准具体可验证(非笼统表述)
|
|
310
|
+
- [ ] 如果存在 decisions.md,所有当前版本 D-xxx@vN 在 plan.md 中可追踪
|
|
311
|
+
- [ ] 不存在 P0/P1 unresolved blocker
|
|
293
312
|
- [ ] 没有 Mermaid 图、估时、风险分析
|
|
294
313
|
- [ ] 没有函数签名、代码示例等实现细节
|
|
295
314
|
- [ ] plan.md 与 design.md 的文件变更清单一致
|
|
@@ -302,6 +321,8 @@ plan_level + 计划内容(none 级别输出建议操作)`,
|
|
|
302
321
|
- [ ] 有任务总表(含优先级、依赖列,**无估时列**)
|
|
303
322
|
- [ ] 有关键路径标注
|
|
304
323
|
- [ ] 有全局验收标准
|
|
324
|
+
- [ ] 如果存在 decisions.md,任务总表或覆盖矩阵覆盖全部当前版本 D-xxx@vN
|
|
325
|
+
- [ ] 不存在 P0/P1 unresolved blocker
|
|
305
326
|
- [ ] (brownfield)全局验收包含兼容性条款
|
|
306
327
|
- [ ] 没有实现细节(接口定义、代码示例等不应该在 plan.md 里)
|
|
307
328
|
- [ ] plan.md 与 design.md 的文件变更清单一致
|
|
@@ -413,6 +434,8 @@ priority: P0
|
|
|
413
434
|
estimated_hours: N
|
|
414
435
|
depends_on: []
|
|
415
436
|
blocks: []
|
|
437
|
+
requirement_ids: [FR-01]
|
|
438
|
+
decision_ids: [D-001@v1]
|
|
416
439
|
allowed_paths:
|
|
417
440
|
- 允许修改的路径范围
|
|
418
441
|
---
|
|
@@ -422,6 +445,10 @@ allowed_paths:
|
|
|
422
445
|
## 修改文件(必填)
|
|
423
446
|
- 精确到文件路径,列出所有需要新增或修改的文件
|
|
424
447
|
|
|
448
|
+
## 覆盖来源
|
|
449
|
+
- Requirements: FR-xx(来自 requirements.md)
|
|
450
|
+
- Decisions: D-xxx@vN(如存在 decisions.md)
|
|
451
|
+
|
|
425
452
|
## 实现要求
|
|
426
453
|
1. 具体做什么,写清楚
|
|
427
454
|
2. ...
|
|
@@ -463,6 +490,8 @@ allowed_paths:
|
|
|
463
490
|
- \`estimated_hours\`: 预估工时,单个 task ≤ 8h
|
|
464
491
|
- \`depends_on\`: 依赖的前序 task 编号列表
|
|
465
492
|
- \`blocks\`: 被本 task 阻塞的后续 task 编号列表
|
|
493
|
+
- \`requirement_ids\`: 本任务覆盖的 FR-xxx 列表
|
|
494
|
+
- \`decision_ids\`: 本任务覆盖的当前版本 D-xxx@vN 列表;无 decisions.md 时可为空数组
|
|
466
495
|
- \`allowed_paths\`: AI executor 可以修改的文件路径范围(安全边界)
|
|
467
496
|
|
|
468
497
|
### 关键规则
|
|
@@ -471,6 +500,7 @@ allowed_paths:
|
|
|
471
500
|
- 接口定义写到"搬砖工照着做"的程度
|
|
472
501
|
- 边界处理至少覆盖 5 条规则
|
|
473
502
|
- 验收标准用表格格式,每条可点击验证,禁止"功能可演示"类笼统表述
|
|
503
|
+
- 如果存在 decisions.md,不允许丢失当前版本 D-xxx@vN;无法覆盖的 D-xxx@vN 必须写入非目标或剩余风险
|
|
474
504
|
- 写完后保存到文件
|
|
475
505
|
|
|
476
506
|
### 操作
|
|
@@ -518,6 +548,8 @@ priority: P0/P1/P2
|
|
|
518
548
|
estimated_hours: N
|
|
519
549
|
depends_on: [task-XX]
|
|
520
550
|
blocks: [task-XX]
|
|
551
|
+
requirement_ids: [FR-XX]
|
|
552
|
+
decision_ids: [D-XXX@vN]
|
|
521
553
|
allowed_paths:
|
|
522
554
|
- ...
|
|
523
555
|
---
|
|
@@ -527,6 +559,10 @@ allowed_paths:
|
|
|
527
559
|
## 修改文件(必填)
|
|
528
560
|
- 精确到文件路径
|
|
529
561
|
|
|
562
|
+
## 覆盖来源
|
|
563
|
+
- Requirements: FR-xx
|
|
564
|
+
- Decisions: D-xxx@vN(如存在)
|
|
565
|
+
|
|
530
566
|
## 实现要求
|
|
531
567
|
1. 具体做什么
|
|
532
568
|
|
|
@@ -560,6 +596,7 @@ allowed_paths:
|
|
|
560
596
|
- 接口定义写到"搬砖工照着做"的程度
|
|
561
597
|
- 边界处理至少 5 条
|
|
562
598
|
- 验收标准用表格,禁止笼统表述
|
|
599
|
+
- 如果存在 decisions.md,不允许丢失当前版本 D-xxx@vN;无法覆盖的 D-xxx@vN 必须写入非目标或剩余风险
|
|
563
600
|
- 写完后用 Write tool 保存到文件
|
|
564
601
|
\`\`\``
|
|
565
602
|
}).join('\n\n')
|
|
@@ -591,8 +628,8 @@ ${subagentPrompts}
|
|
|
591
628
|
|
|
592
629
|
## 验收
|
|
593
630
|
- 每个 task-N.md 文件存在且非空
|
|
594
|
-
- 包含 YAML frontmatter(id、title、priority、depends_on、blocks、allowed_paths)
|
|
595
|
-
-
|
|
631
|
+
- 包含 YAML frontmatter(id、title、priority、depends_on、blocks、requirement_ids、decision_ids、allowed_paths)
|
|
632
|
+
- 包含所有必要章节:修改文件、覆盖来源、实现要求、接口定义、边界处理(≥5条)、非目标、TDD 步骤、验收标准(表格格式)
|
|
596
633
|
- 边界处理覆盖:null/空值、兼容性、异常处理、参数不可变、歧义场景`
|
|
597
634
|
|
|
598
635
|
return {
|
package/src/stages/propose.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated propose 阶段已移除入口(2026-06-14)。
|
|
3
|
+
* brainstorm 现在拥有四件套(design/proposal/requirements/tasks)的生成职责。
|
|
4
|
+
* 本文件保留以备未来需要恢复,但未注册到 stageRegistry。
|
|
5
|
+
*/
|
|
1
6
|
export const definition = {
|
|
2
7
|
name: 'propose',
|
|
3
8
|
title: '方案设计',
|
|
@@ -87,10 +92,29 @@ export const definition = {
|
|
|
87
92
|
- **非功能需求**:兼容性、可回退、可测试、可扩展
|
|
88
93
|
|
|
89
94
|
### design.md 格式要求
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
|
|
96
|
+
**必须包含的章节:**
|
|
97
|
+
1. **背景**:为什么做、解决什么问题
|
|
98
|
+
2. **设计目标**:要达成什么
|
|
99
|
+
3. **非目标**:明确不做的事(防止 scope creep)
|
|
100
|
+
4. **总体方案**:技术方案(分 Phase/Wave)
|
|
101
|
+
5. **文件变更清单**(必填):
|
|
102
|
+
|
|
103
|
+
| 操作 | 文件路径 | 说明 |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| 新增 | src/xxx/NewFile.java | ... |
|
|
106
|
+
| 修改 | src/xxx/ExistingFile.java | 新增 xx 方法 |
|
|
107
|
+
|
|
108
|
+
6. **接口定义**:方法签名、数据结构(代码类任务必填)
|
|
109
|
+
7. **数据模型**(如涉及):表结构/字段变更
|
|
110
|
+
8. **兼容策略**(brownfield 必填):未配置新功能时行为不变、新旧逻辑的回退路径
|
|
111
|
+
9. **风险登记**:
|
|
112
|
+
|
|
113
|
+
| 编号 | 风险 | 等级 | 应对策略 |
|
|
114
|
+
|---|---|---|---|
|
|
115
|
+
| R-01 | ... | P0/P1/P2 | ... |
|
|
116
|
+
|
|
117
|
+
10. **自审**:需求覆盖、约束一致性、真实性、YAGNI、验收标准、非目标清晰、兼容策略、风险识别
|
|
94
118
|
|
|
95
119
|
### tasks.md 格式要求
|
|
96
120
|
- 任务列表(只列名称,不展开步骤)
|
|
@@ -119,9 +143,11 @@ export const definition = {
|
|
|
119
143
|
### 操作
|
|
120
144
|
检查以下各项:
|
|
121
145
|
- [ ] proposal.md 有动机、关键问题、变更范围、不在范围内、成功标准
|
|
146
|
+
- [ ] design.md 有背景、设计目标、非目标
|
|
122
147
|
- [ ] design.md 有文件变更清单表格
|
|
123
148
|
- [ ] design.md 有兼容策略(brownfield 时)
|
|
124
149
|
- [ ] design.md 有风险登记表格
|
|
150
|
+
- [ ] design.md 有自审
|
|
125
151
|
- [ ] requirements.md 有角色表
|
|
126
152
|
- [ ] requirements.md 有 FR 编号和 Given/When/Then 用户场景
|
|
127
153
|
- [ ] tasks.md 每个 task 有文件路径
|
package/src/stages/quick.js
CHANGED
|
@@ -72,24 +72,27 @@ quicklog 已创建(必须放在输出的第一行确认)+ 任务理解 + 上
|
|
|
72
72
|
prompt: `Git 暂存并更新任务记录。
|
|
73
73
|
|
|
74
74
|
### 操作
|
|
75
|
-
1. \`git
|
|
76
|
-
2.
|
|
75
|
+
1. 查看 \`git status --porcelain\`,确认只包含本次 quick 相关文件
|
|
76
|
+
2. 使用 \`git add -- <file...>\` 暂存本次 quick 实际修改的文件(不要 commit,由用户通过统一提交工具处理)
|
|
77
|
+
- 禁止使用 \`git add -A\`
|
|
78
|
+
- 不要暂存 quick 开始前就已存在的无关改动
|
|
79
|
+
3. 更新 Step 1 创建的记录:
|
|
77
80
|
- 无 \`--change\`:找到对应 ql-ID 的条目,将「状态:进行中」改为「状态:已完成」,补充实际改动文件和结果摘要
|
|
78
81
|
- 有 \`--change\`:勾选 tasks.md 中对应的 task checkbox
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
4. QUICKLOG 轮转:超过 500 行则重命名为 \`QUICKLOG-<USER>-YYYY-MM-DD.md\`(日期取最后一条记录的日期)。新文件从空开始,ql-ID 需扫描同目录所有 QUICKLOG 文件中当天最大序号 +1
|
|
83
|
+
5. 如果发现项目特有的坑,追加到 \`.sillyspec/knowledge/uncategorized.md\`
|
|
84
|
+
6. 任务比预期复杂 → 建议用完整流程
|
|
82
85
|
|
|
83
86
|
### 模块文档同步
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
7. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
|
|
88
|
+
8. 对比本次修改的文件(\`git diff --name-only HEAD\`)与模块映射
|
|
89
|
+
9. 如果命中模块 → 直接同步模块文档:
|
|
87
90
|
- 读取对应的 \`.sillyspec/docs/<project>/modules/<module>.md\`(如不存在则新建)
|
|
88
91
|
- 根据本次改动内容更新模块文档(正文描述当前状态,底部变更索引追加本次 ql-ID)
|
|
89
92
|
- 变更索引格式:\`- ql-YYYYMMDD-NNN-XXXX | <一句话描述>\`
|
|
90
93
|
- 写入模块文档
|
|
91
|
-
-
|
|
92
|
-
|
|
94
|
+
- 使用 \`git add -- <module-doc>\` 暂存更新的模块文件
|
|
95
|
+
10. 未命中任何模块 → 跳过,不做额外操作
|
|
93
96
|
|
|
94
97
|
### 输出
|
|
95
98
|
暂存确认 + 记录路径 + 模块文档同步结果(如有)`,
|
package/src/stages/scan.js
CHANGED
|
@@ -122,6 +122,18 @@ export const definition = {
|
|
|
122
122
|
对每个项目分别执行(将 \`<project>\` 替换为实际项目名)
|
|
123
123
|
4. 如果检查报告有失败项,按报告中的角色和文件重试失败的部分
|
|
124
124
|
|
|
125
|
+
### 覆盖保护
|
|
126
|
+
- 生成每份 scan 文档时,frontmatter 必须包含:
|
|
127
|
+
\`\`\`yaml
|
|
128
|
+
---
|
|
129
|
+
source_commit: <git-head-short>
|
|
130
|
+
updated_at: <now-iso-datetime>
|
|
131
|
+
generator: sillyspec-scan
|
|
132
|
+
---
|
|
133
|
+
\`\`\`
|
|
134
|
+
- 覆盖已有 scan 文档前先读取旧 frontmatter;如果旧文档的 \`source_commit\` 与当前 HEAD 不一致,或旧文档 \`updated_at\` 晚于本次 scan 开始时间,不要覆盖。
|
|
135
|
+
- 如果用户明确传入 \`--force-rescan\`,允许覆盖,但仍需写入新的 \`source_commit\` 和 \`updated_at\`。
|
|
136
|
+
|
|
125
137
|
### 子代理上下文注入
|
|
126
138
|
启动每个子代理前,将以下信息拼入子代理 prompt:
|
|
127
139
|
- 项目名(直接用实际项目名)
|
package/src/stages/verify.js
CHANGED
|
@@ -53,20 +53,23 @@ export const definition = {
|
|
|
53
53
|
prompt: `加载规范文件并确认。
|
|
54
54
|
|
|
55
55
|
### 操作
|
|
56
|
-
1. 读取 proposal.md、design.md、tasks.md、requirements.md
|
|
57
|
-
2.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
1. 读取 proposal.md、design.md、tasks.md、requirements.md、plan.md
|
|
57
|
+
2. 如果存在 decisions.md,必须读取并提取所有当前版本 D-xxx@vN 决策 ID
|
|
58
|
+
- 如果存在 P0/P1 unresolved/blocking 决策,验证结论不能为 PASS
|
|
59
|
+
- 如果发现 superseded 决策被下游引用,标记为 ⚠️ stale decision reference
|
|
60
|
+
3. 加载项目信息:\`cat .sillyspec/projects/*.yaml 2>/dev/null\`
|
|
61
|
+
4. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`(构建命令、测试命令、lint 命令等)
|
|
62
|
+
5. 加载代码规范:\`cat .sillyspec/docs/<project>/scan/CONVENTIONS.md 2>/dev/null\`
|
|
63
|
+
6. 标注每个文件的存在/不存在状态
|
|
61
64
|
|
|
62
65
|
### 模块文档加载
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
7. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
|
|
67
|
+
8. 根据 design.md 的文件变更清单匹配 _module-map.yaml 中的模块
|
|
68
|
+
9. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
|
|
69
|
+
10. **检查模块索引可信度**:如果相关模块的 needs_review 为 true,提示"该模块索引可能不可信,需要回看模块卡片或源码"
|
|
67
70
|
|
|
68
71
|
### 输出
|
|
69
|
-
|
|
72
|
+
文件加载确认清单(含 decisions.md 当前版本/未决项状态、模块文档 + 索引可信度)`,
|
|
70
73
|
outputHint: '文件确认清单',
|
|
71
74
|
optional: false
|
|
72
75
|
},
|
|
@@ -123,9 +126,17 @@ grep -rl "<关键词>" <源码目录>/ --include="*.java" --include="*.js" --inc
|
|
|
123
126
|
2. 对每个 task,检查对应模块目录下是否存在测试文件(*test*、*spec*、*Test*、*Spec*)
|
|
124
127
|
3. 没有测试文件的 task 标记为 ⚠️ 缺少测试
|
|
125
128
|
|
|
129
|
+
**探针 4:决策追踪覆盖探针(如存在 decisions.md)**
|
|
130
|
+
1. 从 decisions.md 提取所有当前版本 D-xxx@vN
|
|
131
|
+
2. 检查 requirements.md 是否引用每个 D-xxx@vN,并映射到 FR-xxx
|
|
132
|
+
3. 检查 plan.md 或 tasks/task-NN.md 是否引用每个 FR-xxx/D-xxx@vN
|
|
133
|
+
4. 检查本步骤收集的实现证据是否能回指到对应 D-xxx@vN/FR-xxx
|
|
134
|
+
5. 任意 D-xxx@vN 无下游覆盖时标记为 ⚠️ 决策未闭环
|
|
135
|
+
6. 任意 P0/P1 unresolved/blocking 决策标记为 FAIL blocker
|
|
136
|
+
|
|
126
137
|
### 探针结果处理
|
|
127
|
-
-
|
|
128
|
-
-
|
|
138
|
+
- 将四个探针的结果汇总为「探针报告」
|
|
139
|
+
- 如果探针发现问题(未实现标记、关键词缺失、测试缺失、决策未闭环),在最终验证报告中明确标注
|
|
129
140
|
- 探针发现的问题不等同于验证失败,但必须在报告中列出
|
|
130
141
|
|
|
131
142
|
### 设计一致性检查
|
|
@@ -136,9 +147,10 @@ grep -rl "<关键词>" <源码目录>/ --include="*.java" --include="*.js" --inc
|
|
|
136
147
|
4. API 设计是否符合
|
|
137
148
|
5. **Reverse Sync 检查**:如果发现实现合理但 design.md 未覆盖,先更新 design.md 补充遗漏
|
|
138
149
|
6. **模块文档一致性检查**:如果在"加载规范并锚定"步骤中加载了模块文档,检查实现是否符合模块文档描述的当前设计(特别关注接口签名、数据流、依赖关系)。不符合时标记 ⚠️(不阻断,模块文档可能未及时更新)
|
|
150
|
+
7. **决策链路检查**:如果存在 decisions.md,输出 D-xxx@vN → FR-xxx → task-xx → evidence 的追踪矩阵;缺失项必须列为风险
|
|
139
151
|
|
|
140
152
|
### 输出
|
|
141
|
-
探针报告 + 设计一致性检查结果 +
|
|
153
|
+
探针报告 + 设计一致性检查结果 + 模块文档一致性检查结果 + 决策追踪矩阵(如有)`,
|
|
142
154
|
outputHint: '设计一致性报告',
|
|
143
155
|
optional: false
|
|
144
156
|
},
|
|
@@ -210,6 +222,12 @@ PASS / PASS WITH NOTES / FAIL
|
|
|
210
222
|
- 未实现标记扫描:...
|
|
211
223
|
- 关键词覆盖:...
|
|
212
224
|
- 测试覆盖:...
|
|
225
|
+
- 决策追踪覆盖:...
|
|
226
|
+
|
|
227
|
+
## 决策追踪矩阵(如存在 decisions.md)
|
|
228
|
+
| 决策 ID | FR | Task | Evidence | 状态 |
|
|
229
|
+
|---|---|---|---|---|
|
|
230
|
+
| D-001@v1 | FR-01 | task-01 | test/file/path | PASS/PARTIAL/MISSING |
|
|
213
231
|
|
|
214
232
|
## 测试结果
|
|
215
233
|
(测试套件执行结果)
|
|
@@ -13,6 +13,7 @@ import { join, dirname } from 'path'
|
|
|
13
13
|
import { existsSync, mkdirSync, rmSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
14
14
|
import { fileURLToPath } from 'url'
|
|
15
15
|
import { randomUUID } from 'crypto'
|
|
16
|
+
import { tmpdir } from 'os'
|
|
16
17
|
|
|
17
18
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
18
19
|
const passed = []
|
|
@@ -33,11 +34,19 @@ function cleanup(dir) {
|
|
|
33
34
|
try { rmSync(dir, { recursive: true, force: true }) } catch {}
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function hasPathSegments(value, segments) {
|
|
38
|
+
const parts = value.split(/[\\/]+/)
|
|
39
|
+
for (let i = 0; i <= parts.length - segments.length; i++) {
|
|
40
|
+
if (segments.every((segment, offset) => parts[i + offset] === segment)) return true
|
|
41
|
+
}
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
// ── 测试 1:saveWorkflowRun 平台模式路径正确 ──
|
|
37
46
|
console.log('\n=== Test 1: saveWorkflowRun 平台模式写入路径 ===')
|
|
38
47
|
{
|
|
39
48
|
const { saveWorkflowRun } = await import('../src/workflow.js')
|
|
40
|
-
const tmpRoot =
|
|
49
|
+
const tmpRoot = join(tmpdir(), `test-artifacts-${randomUUID().slice(0, 8)}`)
|
|
41
50
|
const runtimeRoot = join(tmpRoot, 'runtime')
|
|
42
51
|
const scanRunId = 'scan-20260614-test-001'
|
|
43
52
|
|
|
@@ -63,8 +72,8 @@ console.log('\n=== Test 1: saveWorkflowRun 平台模式写入路径 ===')
|
|
|
63
72
|
assert('workflow-runs 目录存在', existsSync(expectedDir))
|
|
64
73
|
assert('workflow-runs 文件存在', existsSync(saved), `路径: ${saved}`)
|
|
65
74
|
assert('路径在 runtime-root 下', saved.startsWith(runtimeRoot), `路径: ${saved}`)
|
|
66
|
-
assert('路径包含 scan-runs', saved
|
|
67
|
-
assert('路径包含 scanRunId', saved
|
|
75
|
+
assert('路径包含 scan-runs', hasPathSegments(saved, ['scan-runs', scanRunId, 'workflow-runs']), `路径: ${saved}`)
|
|
76
|
+
assert('路径包含 scanRunId', hasPathSegments(saved, [scanRunId]), `路径: ${saved}`)
|
|
68
77
|
|
|
69
78
|
// 验证 JSON 内容
|
|
70
79
|
const content = JSON.parse(readFileSync(saved, 'utf8'))
|
|
@@ -81,7 +90,7 @@ console.log('\n=== Test 1: saveWorkflowRun 平台模式写入路径 ===')
|
|
|
81
90
|
console.log('\n=== Test 2: saveWorkflowRun 本地模式写入路径 ===')
|
|
82
91
|
{
|
|
83
92
|
const { saveWorkflowRun } = await import('../src/workflow.js')
|
|
84
|
-
const tmpCwd =
|
|
93
|
+
const tmpCwd = join(tmpdir(), `test-artifacts-local-${randomUUID().slice(0, 8)}`)
|
|
85
94
|
const sillyspecDir = join(tmpCwd, '.sillyspec', '.runtime', 'workflow-runs')
|
|
86
95
|
|
|
87
96
|
const result = {
|
|
@@ -102,7 +111,7 @@ console.log('\n=== Test 2: saveWorkflowRun 本地模式写入路径 ===')
|
|
|
102
111
|
})
|
|
103
112
|
|
|
104
113
|
assert('本地模式文件存在', existsSync(saved))
|
|
105
|
-
assert('本地路径在 .sillyspec/.runtime 下', saved.
|
|
114
|
+
assert('本地路径在 .sillyspec/.runtime 下', hasPathSegments(saved, ['.sillyspec', '.runtime', 'workflow-runs']), `路径: ${saved}`)
|
|
106
115
|
|
|
107
116
|
const content = JSON.parse(readFileSync(saved, 'utf8'))
|
|
108
117
|
assert('本地 JSON status = fail', content.status === 'fail')
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { join, basename } from 'path'
|
|
14
14
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs'
|
|
15
15
|
import { randomUUID } from 'crypto'
|
|
16
|
+
import { tmpdir } from 'os'
|
|
16
17
|
import { SCAN_STATUS, CHECK_SEVERITY } from '../src/constants.js'
|
|
17
18
|
|
|
18
19
|
const passed = []
|
|
@@ -34,8 +35,8 @@ function cleanup(dir) {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function setup(name) {
|
|
37
|
-
const base =
|
|
38
|
-
const spec =
|
|
38
|
+
const base = join(tmpdir(), `failure-test-${name}-${randomUUID().slice(0, 8)}`)
|
|
39
|
+
const spec = join(tmpdir(), `failure-test-spec-${name}-${randomUUID().slice(0, 8)}`)
|
|
39
40
|
mkdirSync(base, { recursive: true })
|
|
40
41
|
mkdirSync(spec, { recursive: true })
|
|
41
42
|
writeFileSync(join(base, 'package.json'), '{}')
|
|
@@ -16,6 +16,7 @@ import { execSync } from 'child_process'
|
|
|
16
16
|
import { fileURLToPath } from 'url'
|
|
17
17
|
import { dirname } from 'path'
|
|
18
18
|
import { randomUUID } from 'crypto'
|
|
19
|
+
import { tmpdir } from 'os'
|
|
19
20
|
|
|
20
21
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
21
22
|
const binCLI = join(__dirname, '..', 'src', 'index.js')
|
|
@@ -48,9 +49,9 @@ function run(cmd, opts = {}) {
|
|
|
48
49
|
// ── 测试 1:pointer 文件创建和内容 ──
|
|
49
50
|
console.log('\n=== Test 1: pointer 文件创建 ===')
|
|
50
51
|
{
|
|
51
|
-
const tmpCwd =
|
|
52
|
-
const tmpSpec =
|
|
53
|
-
const tmpRuntime =
|
|
52
|
+
const tmpCwd = join(tmpdir(), `recovery-test-${randomUUID().slice(0, 8)}`)
|
|
53
|
+
const tmpSpec = join(tmpdir(), `recovery-test-spec-${randomUUID().slice(0, 8)}`)
|
|
54
|
+
const tmpRuntime = join(tmpdir(), `recovery-test-rt-${randomUUID().slice(0, 8)}`)
|
|
54
55
|
|
|
55
56
|
try {
|
|
56
57
|
mkdirSync(tmpCwd, { recursive: true })
|
|
@@ -82,9 +83,9 @@ console.log('\n=== Test 1: pointer 文件创建 ===')
|
|
|
82
83
|
// ── 测试 2:--done 不带参数能恢复 ──
|
|
83
84
|
console.log('\n=== Test 2: --done 恢复平台参数 ===')
|
|
84
85
|
{
|
|
85
|
-
const tmpCwd =
|
|
86
|
-
const tmpSpec =
|
|
87
|
-
const tmpRuntime =
|
|
86
|
+
const tmpCwd = join(tmpdir(), `recovery-test2-${randomUUID().slice(0, 8)}`)
|
|
87
|
+
const tmpSpec = join(tmpdir(), `recovery-test2-spec-${randomUUID().slice(0, 8)}`)
|
|
88
|
+
const tmpRuntime = join(tmpdir(), `recovery-test2-rt-${randomUUID().slice(0, 8)}`)
|
|
88
89
|
const scanRunId = `scan-${Date.now()}`
|
|
89
90
|
|
|
90
91
|
try {
|
|
@@ -125,8 +126,8 @@ console.log('\n=== Test 3: manifest 路径字段 ===')
|
|
|
125
126
|
// ── 测试 4:异常 pointer 检测 ──
|
|
126
127
|
console.log('\n=== Test 4: 异常 pointer 残留检测 ===')
|
|
127
128
|
{
|
|
128
|
-
const tmpCwd =
|
|
129
|
-
const tmpSpec =
|
|
129
|
+
const tmpCwd = join(tmpdir(), `recovery-test4-${randomUUID().slice(0, 8)}`)
|
|
130
|
+
const tmpSpec = join(tmpdir(), `recovery-test4-spec-${randomUUID().slice(0, 8)}`)
|
|
130
131
|
|
|
131
132
|
try {
|
|
132
133
|
mkdirSync(tmpCwd, { recursive: true })
|
|
@@ -147,7 +148,7 @@ console.log('\n=== Test 4: 异常 pointer 残留检测 ===')
|
|
|
147
148
|
// 模拟有效 pointer(手动修复)
|
|
148
149
|
writeFileSync(pointerPath, JSON.stringify({
|
|
149
150
|
specRoot: tmpSpec,
|
|
150
|
-
runtimeRoot: '
|
|
151
|
+
runtimeRoot: join(tmpdir(), 'fake-rt'),
|
|
151
152
|
workspaceId: 'test',
|
|
152
153
|
scanRunId: 'scan-test',
|
|
153
154
|
savedAt: new Date().toISOString(),
|
|
@@ -6,6 +6,7 @@ import { join, resolve, dirname, basename } from 'path'
|
|
|
6
6
|
import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'fs'
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'url'
|
|
8
8
|
import { execSync } from 'child_process'
|
|
9
|
+
import { tmpdir } from 'os'
|
|
9
10
|
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url)
|
|
11
12
|
const __dirname = dirname(__filename)
|
|
@@ -21,16 +22,23 @@ function assert(cond, msg) {
|
|
|
21
22
|
|
|
22
23
|
const P = 'recover'
|
|
23
24
|
function setup(name) {
|
|
24
|
-
const d = join(
|
|
25
|
+
const d = join(tmpdir(), `${P}-${name}`)
|
|
25
26
|
mkdirSync(d, { recursive: true })
|
|
26
27
|
return d
|
|
27
28
|
}
|
|
28
29
|
function spec(name) {
|
|
29
|
-
const d = join(
|
|
30
|
+
const d = join(tmpdir(), `${P}-${name}-spec`)
|
|
30
31
|
mkdirSync(d, { recursive: true })
|
|
31
32
|
return d
|
|
32
33
|
}
|
|
33
34
|
function clean(...dirs) { for (const d of dirs) try { rmSync(d, { recursive: true, force: true }) } catch {} }
|
|
35
|
+
function hasPathSegments(value, segments) {
|
|
36
|
+
const parts = value.split(/[\\/]+/)
|
|
37
|
+
for (let i = 0; i <= parts.length - segments.length; i++) {
|
|
38
|
+
if (segments.every((segment, offset) => parts[i + offset] === segment)) return true
|
|
39
|
+
}
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
34
42
|
|
|
35
43
|
function run(cmd) {
|
|
36
44
|
return execSync(cmd, { encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
@@ -127,8 +135,8 @@ console.log('\n=== Test 5: specDir 缺文档 → 校验失败,路径不含 .si
|
|
|
127
135
|
assert(!result.ok, `specDir 缺文档: ok=${result.ok}`)
|
|
128
136
|
assert(result.errors.length > 0, `有 errors`)
|
|
129
137
|
const errMsg = result.errors[0]
|
|
130
|
-
assert(!errMsg
|
|
131
|
-
assert(errMsg
|
|
138
|
+
assert(!hasPathSegments(errMsg, ['.sillyspec', 'docs']), `路径不含 .sillyspec: ${errMsg}`)
|
|
139
|
+
assert(hasPathSegments(errMsg, ['docs', proj, 'scan']), `路径含 docs/${proj}/scan: ${errMsg}`)
|
|
132
140
|
clean(cwd, sd)
|
|
133
141
|
}
|
|
134
142
|
|
|
@@ -149,7 +157,7 @@ console.log('\n=== Test 7: 非平台模式缺文档 → 路径含 .sillyspec ===
|
|
|
149
157
|
const result = runValidators('scan', cwd, 'default', { projectName: proj })
|
|
150
158
|
assert(!result.ok, `非平台缺文档: ok=${result.ok}`)
|
|
151
159
|
const errMsg = result.errors[0]
|
|
152
|
-
assert(errMsg
|
|
160
|
+
assert(hasPathSegments(errMsg, ['.sillyspec', 'docs']), `路径含 .sillyspec/docs: ${errMsg}`)
|
|
153
161
|
clean(cwd)
|
|
154
162
|
}
|
|
155
163
|
|
|
@@ -142,11 +142,14 @@ function assert(label, condition, detail) {
|
|
|
142
142
|
{
|
|
143
143
|
const { definition } = await import('../src/stages/quick.js')
|
|
144
144
|
const step1Prompt = definition.steps[0].prompt
|
|
145
|
+
const step3Prompt = definition.steps[2].prompt
|
|
145
146
|
|
|
146
147
|
assert('quick step 1 包含 ⛔ 标记', step1Prompt.includes('⛔'))
|
|
147
148
|
assert('quick step 1 包含「不能跳过」', step1Prompt.includes('不能跳过'))
|
|
148
149
|
assert('quick step 1 包含 quicklog 未创建 warning', step1Prompt.includes('quicklog 未创建'))
|
|
149
150
|
assert('quick step 1 输出要求 quicklog 第一行', step1Prompt.includes('第一行确认'))
|
|
151
|
+
assert('quick step 3 禁止 git add -A', step3Prompt.includes('禁止使用 `git add -A`'))
|
|
152
|
+
assert('quick step 3 使用 scoped git add', step3Prompt.includes('git add -- <file...>'))
|
|
150
153
|
|
|
151
154
|
// run.js 审计包含 quicklog 检查
|
|
152
155
|
const runSrc = await readFile(join(__dirname, '..', 'src', 'run.js'), 'utf8')
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { join, resolve, dirname, basename } from 'path'
|
|
6
6
|
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'url'
|
|
8
|
+
import { tmpdir } from 'os'
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url)
|
|
10
11
|
const __dirname = dirname(__filename)
|
|
@@ -20,12 +21,12 @@ function assert(cond, msg) {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function setup(name) {
|
|
23
|
-
const cwd = join(
|
|
24
|
+
const cwd = join(tmpdir(), `pc-${name}`)
|
|
24
25
|
mkdirSync(cwd, { recursive: true })
|
|
25
26
|
return cwd
|
|
26
27
|
}
|
|
27
28
|
function specSetup(name) {
|
|
28
|
-
const d = join(
|
|
29
|
+
const d = join(tmpdir(), `pc-${name}-spec`)
|
|
29
30
|
mkdirSync(d, { recursive: true })
|
|
30
31
|
return d
|
|
31
32
|
}
|
package/test/spec-dir.test.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { join, resolve, basename, dirname } from 'path'
|
|
|
15
15
|
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from 'url'
|
|
17
17
|
import { execSync } from 'child_process'
|
|
18
|
+
import { tmpdir } from 'os'
|
|
18
19
|
|
|
19
20
|
const __filename = fileURLToPath(import.meta.url)
|
|
20
21
|
const __dirname = dirname(__filename)
|
|
@@ -39,7 +40,7 @@ function assert(condition, msg) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function tmpDir(name) {
|
|
42
|
-
const dir = join(
|
|
43
|
+
const dir = join(tmpdir(), `spec-dir-test-${name}-${Date.now()}`)
|
|
43
44
|
mkdirSync(dir, { recursive: true })
|
|
44
45
|
return dir
|
|
45
46
|
}
|