sillyspec 3.20.0 → 3.20.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/package.json +1 -1
- package/src/brainstorm-postcheck.js +158 -0
- package/src/change-risk-profile.js +202 -17
- package/src/classify-change.js +73 -0
- package/src/index.js +7 -4
- package/src/run.js +270 -95
- package/src/scan-postcheck.js +15 -12
- package/src/stages/brainstorm-auto.js +229 -0
- package/src/stages/scan.js +20 -3
- package/test/run-scan-project-parse.test.mjs +166 -43
- package/test/scan-postcheck.test.mjs +18 -4
- package/.npmrc-tmp +0 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brainstorm-auto.js — auto/full 模式使用的 brainstorm 步骤定义
|
|
3
|
+
*
|
|
4
|
+
* 与 brainstorm.js 的区别:
|
|
5
|
+
* 1. artifact-first:直接写文件,对话只输出摘要
|
|
6
|
+
* 2. 自动决策:基于 AC-001~AC-010 checklist,不频繁请示
|
|
7
|
+
* 3. next-action.json:结构化产物,驱动下游推进
|
|
8
|
+
* 4. 只有 blocking questions 才 wait 用户
|
|
9
|
+
* 5. 步骤从 ~13 步精简为 4 步
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const definition = {
|
|
13
|
+
name: 'brainstorm',
|
|
14
|
+
title: '头脑风暴(自动模式)',
|
|
15
|
+
description: '探索需求、分析技术方案、识别风险 — artifact-first,自动决策优先',
|
|
16
|
+
|
|
17
|
+
steps: [
|
|
18
|
+
{
|
|
19
|
+
name: '状态检查与上下文加载',
|
|
20
|
+
prompt: `检查状态、加载项目上下文、匹配模块。
|
|
21
|
+
|
|
22
|
+
### 操作
|
|
23
|
+
1. 运行 \`sillyspec progress show\`,确认 currentStage 为 "brainstorm"
|
|
24
|
+
2. 如果未初始化,提示先运行 sillyspec init
|
|
25
|
+
3. **检查变更名称**:如果当前变更名是自动生成的(如 \`2026-06-02-new-change\`),直接重命名为有意义名称,然后运行 \`sillyspec change-rename <旧名> <新名>\`
|
|
26
|
+
|
|
27
|
+
### 加载上下文
|
|
28
|
+
4. 读取 CODEBASE-OVERVIEW.md + 共享规范 + 子项目上下文
|
|
29
|
+
5. 加载项目信息:\`cat .sillyspec/projects/*.yaml 2>/dev/null\`
|
|
30
|
+
6. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`
|
|
31
|
+
7. 棕地项目:读取 scan 下的 STRUCTURE.md、CONVENTIONS.md、ARCHITECTURE.md
|
|
32
|
+
8. 加载模块索引:读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(如存在)
|
|
33
|
+
9. 查看进行中的变更:\`ls .sillyspec/changes/ | grep -v archive\`
|
|
34
|
+
10. 检查同名变更或可复用模板
|
|
35
|
+
|
|
36
|
+
### 模块匹配
|
|
37
|
+
根据用户描述的需求关键词,匹配相关模块(用 _module-map.yaml 的 tags/aliases/paths)。
|
|
38
|
+
|
|
39
|
+
### 子项目判定
|
|
40
|
+
- 单项目:直接确认
|
|
41
|
+
- 多项目且用户已指定:直接确认
|
|
42
|
+
- 多项目且用户未指定:列出项目列表,需要用户确认
|
|
43
|
+
|
|
44
|
+
### 如果有用户提供的原型/截图
|
|
45
|
+
分析提取:页面结构、表单字段、交互流程、业务规则。
|
|
46
|
+
|
|
47
|
+
### 输出
|
|
48
|
+
状态摘要 + 项目现状理解(3-5 句)+ 涉及模块列表 + 子项目 + 原型分析(如有)
|
|
49
|
+
|
|
50
|
+
### 注意
|
|
51
|
+
- 以 CLI 返回为准,不要自行推断阶段
|
|
52
|
+
- 不要用 mv 命令重命名变更目录,必须用 \`sillyspec change-rename\``,
|
|
53
|
+
outputHint: '状态摘要 + 上下文 + 模块匹配',
|
|
54
|
+
optional: false
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: '需求分析与方案设计',
|
|
58
|
+
requiresWait: true,
|
|
59
|
+
repeatableWait: true,
|
|
60
|
+
maxWaitRounds: 5,
|
|
61
|
+
waitReason: '等待用户回答需求问题',
|
|
62
|
+
waitOptions: ['回答见--answer', '信息够了,进入设计'],
|
|
63
|
+
prompt: `分析需求,必要时追问,然后设计方案。
|
|
64
|
+
|
|
65
|
+
### 操作
|
|
66
|
+
|
|
67
|
+
#### A. 需求评估
|
|
68
|
+
1. 汇总上一步加载的上下文和用户需求
|
|
69
|
+
2. 判断需求是否清晰:
|
|
70
|
+
- 目标明确、范围清楚、无歧义 → 跳过追问,直接进入方案设计
|
|
71
|
+
- 有歧义或不清楚 → 挑**一个最关键的**问题追问
|
|
72
|
+
|
|
73
|
+
#### B. 追问规则(只在需要时)
|
|
74
|
+
- 一次只问一个问题,不要一次性列出多个问题
|
|
75
|
+
- 能从代码/文档确认的不要问用户
|
|
76
|
+
- 多选题优于开放式问题
|
|
77
|
+
- YAGNI — 砍掉不需要的功能
|
|
78
|
+
- 2-3 轮问答就应进入方案设计
|
|
79
|
+
- 优先追问影响架构/数据/接口的问题,实现细节不要问
|
|
80
|
+
|
|
81
|
+
**如果需要追问:**调用 \`sillyspec run brainstorm --wait --reason "等待用户回答需求问题" --options "回答见--answer,信息够了,进入设计" --output "你的单个问题"\`
|
|
82
|
+
|
|
83
|
+
#### C. 复杂度评估(追问结束后或不需要追问时)
|
|
84
|
+
1. 判断是否需要拆分或走批量模式:
|
|
85
|
+
- 3+ 个可独立交付的功能模块 → 建议拆分
|
|
86
|
+
- 任务 > 10 且有重复模式 → 建议批量模式
|
|
87
|
+
- 简单 CRUD → 不拆
|
|
88
|
+
2. 如果需要拆分/批量模式:暂停等用户确认
|
|
89
|
+
- 调用:\`sillyspec run brainstorm --wait --reason "等待用户确认拆分方案" --options "同意拆分,不需要拆分,走批量模式" --output "拆分方案摘要"\`
|
|
90
|
+
3. 不需要拆分 → 继续
|
|
91
|
+
|
|
92
|
+
#### D. 方案设计(自动决策优先)
|
|
93
|
+
1. 基于需求理解和上下文,提出 1-3 种实现方案
|
|
94
|
+
2. **使用自动决策 checklist(AC-001~AC-010)判断是否需要用户选择:**
|
|
95
|
+
|
|
96
|
+
\`\`\`
|
|
97
|
+
AC-001: 未修改公共 API
|
|
98
|
+
AC-002: 未修改数据库 schema
|
|
99
|
+
AC-003: 未涉及鉴权/权限
|
|
100
|
+
AC-004: 未扩大 allowed_paths
|
|
101
|
+
AC-005: 未引入新外部依赖
|
|
102
|
+
AC-006: 未修改核心模块(workflow/daemon/session/lifecycle)
|
|
103
|
+
AC-007: 已有项目约定可复用
|
|
104
|
+
AC-008: 不影响向后兼容
|
|
105
|
+
AC-009: 不涉及数据迁移
|
|
106
|
+
AC-010: 单模块范围内可完成
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
3. **自动决策判定:**
|
|
110
|
+
- 只有一个明显合理方案 + checklist 全部 ✅ → **自动选择,标记 AUTO_DECIDED**
|
|
111
|
+
- 有多个方案但影响不大(checklist 全部 ✅)→ **自动选推荐方案,标记 AUTO_DECIDED**
|
|
112
|
+
- 有多个方案且 checklist 有 ❌(影响架构/数据/接口/权限/兼容性)→ **只有这一种情况才暂停等用户选择**
|
|
113
|
+
|
|
114
|
+
4. 如果需要用户选择方案:
|
|
115
|
+
- 调用:\`sillyspec run brainstorm --wait --reason "等待用户选择方案" --options "方案A,方案B,方案C" --output "方案对比摘要"\`
|
|
116
|
+
|
|
117
|
+
#### E. Design Grill(轻量交叉审查)
|
|
118
|
+
对方案做快速交叉审查:
|
|
119
|
+
- 检查方案与 ARCHITECTURE.md / CONVENTIONS.md 的一致性
|
|
120
|
+
- 检查是否有术语歧义、边界遗漏
|
|
121
|
+
- 能自动解决的直接修正,只有需要业务判断的才问用户
|
|
122
|
+
- 跳过条件:单模块、无状态流转、< 3 个文件变更
|
|
123
|
+
|
|
124
|
+
### 输出
|
|
125
|
+
需求理解摘要 + 复杂度评估 + 方案决策(AUTO_DECIDED 或用户选择)
|
|
126
|
+
|
|
127
|
+
### 注意
|
|
128
|
+
- **不要自问自答。** 不要在自己输出中模拟用户回答然后说"需求已明确"
|
|
129
|
+
- checklist 判定结果必须在 decisions.md 中记录依据`,
|
|
130
|
+
outputHint: '需求理解 + 方案决策',
|
|
131
|
+
optional: false
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: '生成设计产物',
|
|
135
|
+
prompt: `将设计方案写入文件(artifact-first),不回显正文。
|
|
136
|
+
|
|
137
|
+
### 操作
|
|
138
|
+
1. 确保变更目录存在:\`mkdir -p .sillyspec/changes/<change-name>/brainstorm\`
|
|
139
|
+
2. **直接将设计方案写入文件,不要先在对话中输出完整内容再写文件。**
|
|
140
|
+
|
|
141
|
+
### 产物文件
|
|
142
|
+
|
|
143
|
+
#### design.md(必填,写入 \`brainstorm/design.md\`)
|
|
144
|
+
包含:背景、设计目标、非目标、总体方案、文件变更清单、接口定义、数据模型(如涉及)、兼容策略(brownfield)、风险登记、决策追踪。
|
|
145
|
+
|
|
146
|
+
#### decisions.md(必填,写入 \`brainstorm/decisions.md\`)
|
|
147
|
+
记录所有决策:
|
|
148
|
+
\`\`\`markdown
|
|
149
|
+
## D-001@v1: 决策短标题
|
|
150
|
+
- type: architecture | boundary | compatibility | ...
|
|
151
|
+
- priority: P0 | P1 | P2
|
|
152
|
+
- status: AUTO_DECIDED | NEEDS_REVIEW | USER_DECIDED
|
|
153
|
+
- source: user | code | docs
|
|
154
|
+
- question: 被解决的问题
|
|
155
|
+
- answer: 选择或结论
|
|
156
|
+
- checklist: AC-001 ✅, AC-007 ✅(AUTO_DECIDED 时必须列出)
|
|
157
|
+
- normalized_requirement: 可测试约束
|
|
158
|
+
- impacts: [FR-01, task-01]
|
|
159
|
+
- evidence: 代码/文档路径或用户回答轮次
|
|
160
|
+
\`\`\`
|
|
161
|
+
|
|
162
|
+
#### gaps.md(必填,写入 \`brainstorm/gaps.md\`)
|
|
163
|
+
记录已识别的缺口。status=BLOCKER 的缺口会在 plan-postcheck 中触发回退。
|
|
164
|
+
|
|
165
|
+
#### assumptions.md(必填,写入 \`brainstorm/assumptions.md\`)
|
|
166
|
+
记录隐含假设和验证方法。
|
|
167
|
+
|
|
168
|
+
#### next-action.json(必填,写入 \`brainstorm/next-action.json\`)
|
|
169
|
+
\`\`\`json
|
|
170
|
+
{
|
|
171
|
+
"status": "ready_for_plan" | "waiting_for_user",
|
|
172
|
+
"decision_level": "high" | "medium" | "low",
|
|
173
|
+
"has_blocking_questions": true | false,
|
|
174
|
+
"blocking_reasons": [],
|
|
175
|
+
"questions": [],
|
|
176
|
+
"auto_decisions": [
|
|
177
|
+
{ "id": "D-001", "decision": "方案描述", "reason": "AC-007 ✅" }
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
\`\`\`
|
|
181
|
+
|
|
182
|
+
### 自审
|
|
183
|
+
对 design.md 执行自审(需求覆盖、文件变更清单具体性、验收标准可测试、非目标清晰、兼容策略)。发现问题直接修改文件。
|
|
184
|
+
|
|
185
|
+
### 如果有 blocking questions
|
|
186
|
+
将问题写入 next-action.json.questions,在对话中输出问题列表和推荐选项,然后 --wait。
|
|
187
|
+
|
|
188
|
+
### 输出(artifact-first)
|
|
189
|
+
只输出摘要:
|
|
190
|
+
\`\`\`
|
|
191
|
+
已生成设计产物(5 个文件):
|
|
192
|
+
- brainstorm/design.md — 采用 xxx 方案,涉及 N 个文件变更
|
|
193
|
+
- brainstorm/decisions.md — M 个决策,K 个 AUTO_DECIDED
|
|
194
|
+
- brainstorm/gaps.md — J 个缺口,0 个 BLOCKER
|
|
195
|
+
- brainstorm/assumptions.md — L 个假设
|
|
196
|
+
- brainstorm/next-action.json — ready_for_plan / waiting_for_user
|
|
197
|
+
\`\`\``,
|
|
198
|
+
outputHint: '产物摘要',
|
|
199
|
+
optional: false
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: '生成规范文件',
|
|
203
|
+
requiresWait: true,
|
|
204
|
+
waitReason: '等待用户最终确认',
|
|
205
|
+
waitOptions: ['确认', '需要修改', '推翻重来'],
|
|
206
|
+
prompt: `生成 proposal.md / requirements.md / tasks.md,让用户确认。
|
|
207
|
+
|
|
208
|
+
### 操作
|
|
209
|
+
1. 基于 brainstorm/ 下的设计产物,生成规范文件(直接写文件):
|
|
210
|
+
- **proposal.md**:动机、关键问题、变更范围、不在范围内、成功标准
|
|
211
|
+
- **requirements.md**:角色表 + FR 编号需求 + Given/When/Then + 非功能需求
|
|
212
|
+
- **tasks.md**:任务列表(只列名称,细节在 plan 阶段展开)
|
|
213
|
+
2. 如果 brainstorm/decisions.md 有 AUTO_DECIDED 决策,在变更根目录也写一份 decisions.md
|
|
214
|
+
3. 所有规范文件头部包含 YAML frontmatter
|
|
215
|
+
4. \`git add .sillyspec/\` — 暂存规范文件(不要 commit)
|
|
216
|
+
|
|
217
|
+
### 输出(摘要)
|
|
218
|
+
规范文件路径列表(各一句话说明)
|
|
219
|
+
|
|
220
|
+
### 铁律
|
|
221
|
+
- **直接写文件,不在对话中输出完整内容**
|
|
222
|
+
- 暂停等待用户确认:\`sillyspec run brainstorm --wait --reason "等待用户最终确认" --options "确认,需要修改,推翻重来" --output "规范文件摘要"\`
|
|
223
|
+
- 禁止自动 commit
|
|
224
|
+
- 禁止在确认前推进到后续阶段`,
|
|
225
|
+
outputHint: '规范文件摘要',
|
|
226
|
+
optional: false
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
}
|
package/src/stages/scan.js
CHANGED
|
@@ -62,9 +62,26 @@ export const definition = {
|
|
|
62
62
|
- 最终确定的项目列表将用于后续所有步骤
|
|
63
63
|
- **后续每个需要生成文档的步骤,都必须对列表中的每个项目分别执行**
|
|
64
64
|
|
|
65
|
-
###
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
### 输出格式规范
|
|
66
|
+
--output 必须使用以下结构化格式之一(CLI 只解析这些格式,**不要输出自由文本列表**):
|
|
67
|
+
|
|
68
|
+
**格式 A:scan_projects YAML block**
|
|
69
|
+
在 --output 中输出:
|
|
70
|
+
scan_projects:
|
|
71
|
+
- id: backend
|
|
72
|
+
- id: frontend
|
|
73
|
+
- id: daemon
|
|
74
|
+
|
|
75
|
+
**格式 B:BEGIN_PROJECT_LIST 标记块**
|
|
76
|
+
在 --output 中输出:
|
|
77
|
+
BEGIN_PROJECT_LIST
|
|
78
|
+
- backend
|
|
79
|
+
- frontend
|
|
80
|
+
- daemon
|
|
81
|
+
END_PROJECT_LIST
|
|
82
|
+
|
|
83
|
+
如果不需要解析项目列表(如单项目已明确),--output 写普通摘要即可。`,
|
|
84
|
+
outputHint: '结构化项目列表(YAML block 或 BEGIN_PROJECT_LIST)',
|
|
68
85
|
optional: false
|
|
69
86
|
},
|
|
70
87
|
{
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* scan step 2 项目列表解析 — 强契约测试
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* 解析只接受两种结构化格式:
|
|
5
|
+
* A) YAML block: scan_projects:\n - id: name
|
|
6
|
+
* B) BEGIN_PROJECT_LIST ... END_PROJECT_LIST
|
|
7
|
+
*
|
|
8
|
+
* 自由文本列表不再解析,防止误识别垃圾项目名。
|
|
9
9
|
*/
|
|
10
|
-
import { sanitizeProjectName } from '../src/run.js'
|
|
10
|
+
import { sanitizeProjectName, validateParsedProjects } from '../src/run.js'
|
|
11
11
|
|
|
12
12
|
let passed = 0
|
|
13
13
|
let failed = 0
|
|
@@ -17,60 +17,183 @@ function assertDeepEqual (actual, expected, msg) {
|
|
|
17
17
|
if (a === e) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
18
18
|
else { console.error(`❌ FAIL: ${msg}\n expected: ${e}\n actual: ${a}`); failed++ }
|
|
19
19
|
}
|
|
20
|
+
function assertEqual (actual, expected, msg) {
|
|
21
|
+
if (actual === expected) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
22
|
+
else { console.error(`❌ FAIL: ${msg}\n expected: ${expected}\n actual: ${actual}`); failed++ }
|
|
23
|
+
}
|
|
24
|
+
function assertIncludes (str, sub, msg) {
|
|
25
|
+
if (str.includes(sub)) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
26
|
+
else { console.error(`❌ FAIL: ${msg}\n expected to include: ${sub}\n actual: ${str}`); failed++ }
|
|
27
|
+
}
|
|
20
28
|
|
|
21
|
-
// 复刻 run.js
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
// ---- 复刻 run.js 中新的解析逻辑 ----
|
|
30
|
+
function parseProjectListFromOutput(outputText) {
|
|
31
|
+
let parsedProjects = [] // Array<{id, path?}>
|
|
32
|
+
let parsedFromStructuredOutput = false
|
|
33
|
+
if (outputText) {
|
|
34
|
+
// 格式 A: YAML block
|
|
35
|
+
const yamlBlock = outputText.match(/scan_projects:\s*\n([\s\S]+?)(?=$|\n[^\s])/)
|
|
36
|
+
if (yamlBlock) {
|
|
37
|
+
const entries = [...yamlBlock[1].matchAll(/-\s+id:\s*(\S+)(?:[\s\S]*?)(?=\n\s+-\s+id:|$)/g)]
|
|
38
|
+
for (const m of entries) {
|
|
39
|
+
const id = sanitizeProjectName(m[1])
|
|
40
|
+
if (!id) continue
|
|
41
|
+
const pathMatch = m[0].match(/path:\s*(\S+)/)
|
|
42
|
+
const entry = pathMatch ? { id, path: pathMatch[1].trim() } : { id }
|
|
43
|
+
parsedProjects.push(entry)
|
|
44
|
+
}
|
|
45
|
+
parsedFromStructuredOutput = parsedProjects.length > 0
|
|
46
|
+
}
|
|
47
|
+
// 格式 B: BEGIN_PROJECT_LIST
|
|
48
|
+
if (!parsedFromStructuredOutput) {
|
|
49
|
+
const blockMatch = outputText.match(/BEGIN_PROJECT_LIST\s*\n([\s\S]*?)\n*END_PROJECT_LIST/)
|
|
50
|
+
if (blockMatch) {
|
|
51
|
+
const raw = [...blockMatch[1].matchAll(/^-\s+(\S+)/gm)].map(m => m[1])
|
|
52
|
+
parsedProjects = raw.map(s => sanitizeProjectName(s)).filter(Boolean).map(id => ({ id }))
|
|
53
|
+
parsedFromStructuredOutput = parsedProjects.length > 0
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { parsedProjects, parsedFromStructuredOutput }
|
|
30
58
|
}
|
|
31
59
|
|
|
32
|
-
//
|
|
60
|
+
// ======== 解析测试 ========
|
|
61
|
+
|
|
62
|
+
// 1. YAML block 正常解析
|
|
33
63
|
assertDeepEqual(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
parseProjectListFromOutput(`scan_projects:
|
|
65
|
+
- id: backend
|
|
66
|
+
- id: frontend
|
|
67
|
+
- id: daemon
|
|
68
|
+
`).parsedProjects,
|
|
69
|
+
[{ id: 'backend' }, { id: 'frontend' }, { id: 'daemon' }],
|
|
70
|
+
'YAML block 正常解析 3 个项目'
|
|
37
71
|
)
|
|
38
72
|
|
|
39
|
-
//
|
|
73
|
+
// 2. YAML block 带 path 字段(多行属性)
|
|
74
|
+
const r2 = parseProjectListFromOutput(`scan_projects:
|
|
75
|
+
- id: api
|
|
76
|
+
path: backend/
|
|
77
|
+
- id: web
|
|
78
|
+
path: frontend/
|
|
79
|
+
`).parsedProjects
|
|
80
|
+
assertDeepEqual(r2, [{ id: 'api', path: 'backend/' }, { id: 'web', path: 'frontend/' }],
|
|
81
|
+
'YAML block 带 path 字段正常解析')
|
|
82
|
+
|
|
83
|
+
// 3. BEGIN_PROJECT_LIST 正常解析
|
|
40
84
|
assertDeepEqual(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
85
|
+
parseProjectListFromOutput(`BEGIN_PROJECT_LIST
|
|
86
|
+
- backend
|
|
87
|
+
- frontend
|
|
88
|
+
- daemon
|
|
89
|
+
END_PROJECT_LIST`).parsedProjects,
|
|
90
|
+
[{ id: 'backend' }, { id: 'frontend' }, { id: 'daemon' }],
|
|
91
|
+
'BEGIN_PROJECT_LIST 标记块正常解析'
|
|
44
92
|
)
|
|
45
93
|
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
94
|
+
// 4. 自由文本编号列表 → 不解析
|
|
95
|
+
const { parsedProjects: fn, parsedFromStructuredOutput: fnParsed } =
|
|
96
|
+
parseProjectListFromOutput('扫描项目列表:\n1. scan1backendmulti-agent-platform-api FastAPI Python\n2. frontendmulti-agent-platform-web Next.js TS\n3. sillyhub-daemon Node.js TS')
|
|
97
|
+
assertEqual(fnParsed, false, '自由文本编号列表不触发解析')
|
|
98
|
+
assertDeepEqual(fn, [], '自由文本编号列表不产生任何项目名')
|
|
99
|
+
|
|
100
|
+
// 5. 自由文本括号枚举 → 不解析
|
|
101
|
+
const { parsedProjects: fn2, parsedFromStructuredOutput: fn2Parsed } =
|
|
102
|
+
parseProjectListFromOutput('子项目: backend / frontend / user-service')
|
|
103
|
+
assertEqual(fn2Parsed, false, '自由文本括号枚举不触发解析')
|
|
104
|
+
assertDeepEqual(fn2, [], '自由文本括号枚举不产生任何项目名')
|
|
105
|
+
|
|
106
|
+
// 6. 空 outputText → 不解析
|
|
107
|
+
const { parsedFromStructuredOutput: emptyParsed } = parseProjectListFromOutput('')
|
|
108
|
+
assertEqual(emptyParsed, false, '空 outputText 不触发解析')
|
|
109
|
+
|
|
110
|
+
// 7. 普通摘要文本 → 不解析
|
|
111
|
+
const { parsedProjects: summary, parsedFromStructuredOutput: summaryParsed } =
|
|
112
|
+
parseProjectListFromOutput('确认扫描 3 个子项目:backend、frontend、daemon,全部重新扫描')
|
|
113
|
+
assertEqual(summaryParsed, false, '普通摘要文本不触发解析')
|
|
114
|
+
assertDeepEqual(summary, [], '普通摘要文本不产生任何项目名')
|
|
115
|
+
|
|
116
|
+
// ======== validateParsedProjects — 基础校验 ========
|
|
117
|
+
|
|
118
|
+
// 8. 正常列表(无 path)
|
|
51
119
|
assertDeepEqual(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'
|
|
120
|
+
validateParsedProjects([{ id: 'backend' }, { id: 'frontend' }, { id: 'daemon' }], '/tmp/proj'),
|
|
121
|
+
{ ok: true, errors: [] },
|
|
122
|
+
'正常列表校验通过(无 path)'
|
|
55
123
|
)
|
|
56
124
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
125
|
+
// 9. 正常列表(有合法 path)——需要用真实路径
|
|
126
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
|
|
127
|
+
import { join } from 'path'
|
|
128
|
+
import os from 'os'
|
|
129
|
+
const tmpDir = mkdtempSync(join(os.tmpdir(), 'sillyspec-test-'))
|
|
130
|
+
mkdirSync(join(tmpDir, 'backend'))
|
|
131
|
+
mkdirSync(join(tmpDir, 'frontend'))
|
|
61
132
|
assertDeepEqual(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
133
|
+
validateParsedProjects([
|
|
134
|
+
{ id: 'backend', path: 'backend' },
|
|
135
|
+
{ id: 'frontend', path: 'frontend' }
|
|
136
|
+
], tmpDir),
|
|
137
|
+
{ ok: true, errors: [] },
|
|
138
|
+
'合法 path 校验通过'
|
|
65
139
|
)
|
|
66
140
|
|
|
67
|
-
//
|
|
141
|
+
// 10. path 不存在 → 失败
|
|
142
|
+
const v10 = validateParsedProjects([{ id: 'backend', path: 'nonexistent' }], tmpDir)
|
|
143
|
+
assertEqual(v10.ok, false, 'path 不存在校验失败')
|
|
144
|
+
assertIncludes(v10.errors[0], '不存在', '错误提示包含"不存在"')
|
|
145
|
+
|
|
146
|
+
// 11. path 含 .. 越界 → 失败
|
|
147
|
+
const v11 = validateParsedProjects([{ id: 'backend', path: '../etc/passwd' }], tmpDir)
|
|
148
|
+
assertEqual(v11.ok, false, 'path 包含 .. 校验失败')
|
|
149
|
+
assertIncludes(v11.errors[0], '..', '错误提示包含 ..')
|
|
150
|
+
|
|
151
|
+
// 12. path 解析后超出 source_root → 失败
|
|
152
|
+
const v12 = validateParsedProjects([{ id: 'backend', path: '/etc/passwd' }], tmpDir)
|
|
153
|
+
assertEqual(v12.ok, false, '绝对路径超出 source_root 校验失败')
|
|
154
|
+
assertIncludes(v12.errors[0], '超出', '错误提示包含"超出"')
|
|
155
|
+
|
|
156
|
+
// 13. 超过 10 个项目 → 失败
|
|
157
|
+
const many = Array.from({ length: 11 }, (_, i) => ({ id: `project${i}` }))
|
|
158
|
+
const v13 = validateParsedProjects(many, tmpDir)
|
|
159
|
+
assertEqual(v13.ok, false, '超过 10 个项目校验失败')
|
|
160
|
+
assertIncludes(v13.errors[0], '超过上限', '错误提示包含"超过上限"')
|
|
161
|
+
|
|
162
|
+
// 14. 重复项目名 → 失败
|
|
163
|
+
const v14 = validateParsedProjects([{ id: 'backend' }, { id: 'frontend' }, { id: 'backend' }], tmpDir)
|
|
164
|
+
assertEqual(v14.ok, false, '重复项目名校验失败')
|
|
165
|
+
assertIncludes(v14.errors[0], '重复', '错误提示包含"重复"')
|
|
166
|
+
|
|
167
|
+
// 15. 非法 slug(中文)→ 失败
|
|
168
|
+
const v15 = validateParsedProjects([{ id: 'backend' }, { id: '前端服务' }], tmpDir)
|
|
169
|
+
assertEqual(v15.ok, false, '非法 slug 校验失败')
|
|
170
|
+
|
|
171
|
+
// 16. 空列表 → 失败
|
|
172
|
+
const v16 = validateParsedProjects([], tmpDir)
|
|
173
|
+
assertEqual(v16.ok, false, '空列表校验失败')
|
|
174
|
+
|
|
175
|
+
// 17. null 列表 → 失败
|
|
176
|
+
const v17 = validateParsedProjects(null, tmpDir)
|
|
177
|
+
assertEqual(v17.ok, false, 'null 列表校验失败')
|
|
178
|
+
|
|
179
|
+
// 18. 单字符 → 失败
|
|
180
|
+
const v18 = validateParsedProjects([{ id: 'a' }], tmpDir)
|
|
181
|
+
assertEqual(v18.ok, false, '单字符项目名校验失败')
|
|
182
|
+
|
|
183
|
+
// 19. 纯数字 → 失败
|
|
184
|
+
const v19 = validateParsedProjects([{ id: '123' }], tmpDir)
|
|
185
|
+
assertEqual(v19.ok, false, '纯数字项目名校验失败')
|
|
186
|
+
|
|
187
|
+
// 20. 兼容旧 API(纯字符串数组)
|
|
68
188
|
assertDeepEqual(
|
|
69
|
-
|
|
70
|
-
[
|
|
71
|
-
'
|
|
189
|
+
validateParsedProjects(['backend', 'frontend'], tmpDir),
|
|
190
|
+
{ ok: true, errors: [] },
|
|
191
|
+
'旧 API 兼容:纯字符串数组仍通过'
|
|
72
192
|
)
|
|
73
193
|
|
|
194
|
+
// 清理
|
|
195
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
196
|
+
|
|
74
197
|
console.log(`\n${'='.repeat(50)}`)
|
|
75
198
|
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
76
199
|
console.log(`${'='.repeat(50)}`)
|
|
@@ -106,11 +106,10 @@ console.log('\n=== Test 4: local.yaml 命令不存在 → completed_with_warning
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
// ── 5-8: AI 输出错误标记 → warnings ──
|
|
109
|
+
// 注意:tool_use_error 和 fallback 已移除(agent 描述性文本正常提及不应触发)
|
|
109
110
|
const errorCases = [
|
|
110
|
-
{ id: '
|
|
111
|
-
{ id: '
|
|
112
|
-
{ id: 'e7', name: 'rate_limit', output: 'rate limit exhausted' },
|
|
113
|
-
{ id: 'e8', name: 'fallback', output: 'fallback to default, skipped validation' },
|
|
111
|
+
{ id: 'e6', name: 'API Error 529', output: 'API Error 529 server overloaded. API Error 529 retry failed' },
|
|
112
|
+
{ id: 'e7', name: 'rate_limit', output: 'rate limit exhausted, rate limit exhausted again' },
|
|
114
113
|
]
|
|
115
114
|
for (const ec of errorCases) {
|
|
116
115
|
console.log(`\n=== Test: ${ec.name} → completed_with_warnings ===`)
|
|
@@ -121,6 +120,21 @@ for (const ec of errorCases) {
|
|
|
121
120
|
clean(cwd, spec)
|
|
122
121
|
}
|
|
123
122
|
|
|
123
|
+
// ── tool_use_error / fallback 不再触发 warning(描述性文本正常提及) ──
|
|
124
|
+
const noWarnCases = [
|
|
125
|
+
{ id: 'e5', name: 'tool_use_error', output: 'tool_use_error: file not found' },
|
|
126
|
+
{ id: 'e8', name: 'fallback', output: '作为 fallback 方案,跳过了这个步骤' },
|
|
127
|
+
]
|
|
128
|
+
for (const ec of noWarnCases) {
|
|
129
|
+
console.log(`\n=== Test: ${ec.name} → 不触发 warning ===`)
|
|
130
|
+
const cwd = setup(ec.id), spec = specSetup(ec.id)
|
|
131
|
+
writeFull(cwd, spec)
|
|
132
|
+
const r = runScanPostCheck({ cwd, specDir: spec, outputText: ec.output })
|
|
133
|
+
assert(!r.checks.some(c => c.name === 'tool_use_error' || c.name === 'fallback_or_skip'),
|
|
134
|
+
`${ec.name}: 不应有 tool_use_error/fallback_or_skip warning`)
|
|
135
|
+
clean(cwd, spec)
|
|
136
|
+
}
|
|
137
|
+
|
|
124
138
|
// ── 9: 文档缺 header → warnings ──
|
|
125
139
|
console.log('\n=== Test 9: 文档缺 header → completed_with_warnings ===')
|
|
126
140
|
{
|
package/.npmrc-tmp
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
//registry.npmjs.org/:_authToken=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
|