@tea-agent/loop-agent 0.33.1 → 0.33.3
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/CHANGELOG.md +26 -0
- package/dist/workflows/dag/frontend-test-html-report.js +38 -23
- package/dist/workflows/dag/frontend-test-result-contract.js +77 -6
- package/dist/workflows/dag/init-hybrid.js +150 -50
- package/dist/workflows/dag/retry-policy.js +5 -0
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.json +11 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/package.json +1 -1
- package/skills/analyze-product-dependencies/SKILL.md +74 -33
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +16 -11
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +20 -10
- package/skills/analyze-product-dependencies/references/example.md +9 -9
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +93 -18
- package/skills/analyze-product-dependencies/references/input-contract.md +27 -4
- package/skills/analyze-product-dependencies/references/kb-integration.md +64 -0
- package/skills/analyze-product-dependencies/references/scouting-rules.md +25 -10
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +247 -54
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +122 -29
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +94 -36
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +37 -23
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +35 -43
- package/skills/analyze-product-requirements/SKILL.md +98 -43
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +8 -12
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +28 -14
- package/skills/analyze-product-requirements/references/example.md +24 -6
- package/skills/analyze-product-requirements/references/forward-test-cases.md +87 -9
- package/skills/analyze-product-requirements/references/kb-integration.md +56 -0
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +19 -10
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +21 -12
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +45 -14
- package/skills/analyze-product-requirements/scripts/compute-source-identity.mjs +35 -0
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +337 -29
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +38 -7
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +41 -29
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +33 -22
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +43 -24
- package/skills/playwright-cli/SKILL.md +7 -0
- package/skills/analyze-product-dependencies/agents/openai.yaml +0 -4
- package/skills/analyze-product-requirements/agents/openai.yaml +0 -4
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
|
|
9
9
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const temp = mkdtempSync(join(tmpdir(), "product-requirement-
|
|
10
|
+
const temp = mkdtempSync(join(tmpdir(), "product-requirement-v4-validators-"));
|
|
11
|
+
process.once("exit", () => rmSync(temp, { recursive: true, force: true }));
|
|
11
12
|
const projectRoot = join(temp, "project");
|
|
12
13
|
const artifactDir = join(projectRoot, "docs", "product-analysis", "nickname");
|
|
13
14
|
mkdirSync(artifactDir, { recursive: true });
|
|
14
15
|
const analysisValidator = join(scriptDir, "validate-product-analysis.mjs");
|
|
15
16
|
const clarificationValidator = join(scriptDir, "validate-requirement-clarification.mjs");
|
|
16
17
|
const requirementValidator = join(scriptDir, "validate-product-requirement.mjs");
|
|
18
|
+
const sourceIdentityTool = join(scriptDir, "compute-source-identity.mjs");
|
|
17
19
|
|
|
18
20
|
const run = (script, args) => spawnSync(process.execPath, [script, ...args], { encoding: "utf8" });
|
|
19
21
|
function pass(name, result) {
|
|
@@ -32,19 +34,18 @@ function variant(name, text) {
|
|
|
32
34
|
const analysisPath = join(artifactDir, "product-analysis.md");
|
|
33
35
|
const clarificationPath = join(artifactDir, "requirement-clarification.md");
|
|
34
36
|
const requirementPath = join(artifactDir, "product-requirement.md");
|
|
37
|
+
const sourceIdentity = `inline:sha256:${"1".repeat(64)}`;
|
|
35
38
|
|
|
36
39
|
const analysis = `---
|
|
37
|
-
artifact_version: "
|
|
40
|
+
artifact_version: "4.0"
|
|
38
41
|
artifact_type: product-analysis
|
|
39
42
|
requirement_id: nickname
|
|
40
|
-
project_root: ../../..
|
|
41
43
|
analysis_scope: frontend
|
|
42
44
|
analysis_status: no-clarification-required
|
|
43
|
-
source_requirement: inline
|
|
44
|
-
repository_root: none
|
|
45
45
|
---
|
|
46
46
|
# Product Analysis
|
|
47
47
|
## 1. 原始需求
|
|
48
|
+
- 原始需求身份:${sourceIdentity}
|
|
48
49
|
登录用户可以修改昵称,长度为 2–20 个字符。
|
|
49
50
|
## 2. 需求概述
|
|
50
51
|
在个人资料页维护展示昵称。
|
|
@@ -54,14 +55,15 @@ repository_root: none
|
|
|
54
55
|
### 4.1 明确需求
|
|
55
56
|
- 用户只能修改自己的昵称。
|
|
56
57
|
### 4.2 推断需求
|
|
57
|
-
-
|
|
58
|
+
- 无未确认的推断需求。
|
|
58
59
|
### 4.3 待确认问题
|
|
59
|
-
-
|
|
60
|
+
- 无待确认问题。
|
|
61
|
+
- 理由:范围、权限和结果已经明确。
|
|
60
62
|
### 4.4 初步非目标
|
|
61
63
|
- 不修改头像。
|
|
62
64
|
## 5. 外部事实
|
|
63
65
|
### 5.1 知识库事实
|
|
64
|
-
-
|
|
66
|
+
- 检索状态:executed-no-match。
|
|
65
67
|
### 5.2 代码库事实
|
|
66
68
|
- 未提供代码仓库。
|
|
67
69
|
## 6. 初步前端用户故事
|
|
@@ -74,6 +76,7 @@ repository_root: none
|
|
|
74
76
|
## 7. 初步前端输出规范
|
|
75
77
|
### FE-US-001 修改昵称
|
|
76
78
|
- 页面/组件:个人资料页、昵称编辑表单
|
|
79
|
+
- 页面路由:/account/profile
|
|
77
80
|
- 展示内容:当前昵称、字符计数、校验提示
|
|
78
81
|
- 交互动作:编辑、保存、取消、重试
|
|
79
82
|
- UI 状态:normal、dirty、loading、success、error、disabled
|
|
@@ -81,38 +84,32 @@ repository_root: none
|
|
|
81
84
|
- 权限可见性:仅当前登录用户
|
|
82
85
|
- 边界处理:失败时保留输入,不得静默失败
|
|
83
86
|
`;
|
|
84
|
-
|
|
85
87
|
const clarification = `---
|
|
86
|
-
artifact_version: "
|
|
88
|
+
artifact_version: "4.0"
|
|
87
89
|
artifact_type: requirement-clarification
|
|
88
90
|
requirement_id: nickname
|
|
89
|
-
project_root: ../../..
|
|
90
91
|
analysis_scope: frontend
|
|
91
92
|
clarification_status: complete
|
|
92
93
|
clarification_rounds: 0
|
|
93
|
-
source_product_analysis: ./product-analysis.md
|
|
94
|
-
target_product_requirement: ./product-requirement.md
|
|
95
94
|
---
|
|
96
95
|
# Requirement Clarification
|
|
97
96
|
## 1. 澄清结论
|
|
98
97
|
- 状态:no-clarification-required
|
|
99
98
|
- 原因:范围、权限、数据语义和验收结果已经明确。
|
|
99
|
+
- 内容补充:用户已确认无需补充
|
|
100
100
|
## 2. 来源
|
|
101
101
|
- Product Analysis:./product-analysis.md
|
|
102
|
+
- 原始需求身份:${sourceIdentity}
|
|
102
103
|
## 3. 合并结果
|
|
103
104
|
- Product Requirement 根据明确需求生成,无额外决策标记。
|
|
104
105
|
`;
|
|
105
106
|
|
|
106
107
|
const requirement = `---
|
|
107
|
-
artifact_version: "
|
|
108
|
+
artifact_version: "4.0"
|
|
108
109
|
artifact_type: product-requirement
|
|
109
110
|
requirement_id: nickname
|
|
110
|
-
project_root: ../../..
|
|
111
111
|
requirement_status: complete
|
|
112
112
|
analysis_scope: frontend
|
|
113
|
-
source_requirement: inline
|
|
114
|
-
source_product_analysis: ./product-analysis.md
|
|
115
|
-
source_clarification: ./requirement-clarification.md
|
|
116
113
|
---
|
|
117
114
|
# Product Requirement
|
|
118
115
|
## 1. 需求概述
|
|
@@ -120,14 +117,10 @@ source_clarification: ./requirement-clarification.md
|
|
|
120
117
|
## 2. 业务目标
|
|
121
118
|
- 允许用户维护准确的展示名称。
|
|
122
119
|
## 3. 需求范围
|
|
123
|
-
### 3.1
|
|
120
|
+
### 3.1 已确认范围
|
|
124
121
|
- 当前用户可以修改自己的昵称。
|
|
125
122
|
### 3.2 非目标
|
|
126
123
|
- 不修改头像。
|
|
127
|
-
### 3.3 默认假设
|
|
128
|
-
- 无未经确认的默认假设。
|
|
129
|
-
### 3.4 未决事项
|
|
130
|
-
- 无。
|
|
131
124
|
## 4. 业务规则
|
|
132
125
|
- 昵称长度必须为 2–20 个字符。
|
|
133
126
|
## 5. 前端用户故事
|
|
@@ -140,6 +133,7 @@ source_clarification: ./requirement-clarification.md
|
|
|
140
133
|
## 6. 前端输出规范
|
|
141
134
|
### FE-US-001 修改昵称
|
|
142
135
|
- 页面/组件:个人资料页、昵称编辑表单
|
|
136
|
+
- 页面路由:/account/profile
|
|
143
137
|
- 展示内容:当前昵称、字符计数、校验提示
|
|
144
138
|
- 交互动作:编辑、保存、取消、重试
|
|
145
139
|
- UI 状态:normal、dirty、loading、success、error、disabled
|
|
@@ -161,6 +155,95 @@ Then:
|
|
|
161
155
|
- 本轮无需澄清决策标记;需求直接来源于明确需求。
|
|
162
156
|
`;
|
|
163
157
|
|
|
158
|
+
const paginatedRequirement = `---
|
|
159
|
+
artifact_version: "4.0"
|
|
160
|
+
artifact_type: product-requirement
|
|
161
|
+
requirement_id: nickname
|
|
162
|
+
requirement_status: complete
|
|
163
|
+
analysis_scope: backend
|
|
164
|
+
---
|
|
165
|
+
# Product Requirement
|
|
166
|
+
## 1. 需求概述
|
|
167
|
+
提供分页列表查询。
|
|
168
|
+
## 2. 业务目标
|
|
169
|
+
- 稳定查询列表数据。
|
|
170
|
+
## 3. 需求范围
|
|
171
|
+
### 3.1 已确认范围
|
|
172
|
+
- 支持分页查询。
|
|
173
|
+
### 3.2 非目标
|
|
174
|
+
- 不支持批量修改。
|
|
175
|
+
## 4. 业务规则
|
|
176
|
+
- 每页条数参数必填,允许值为 10、20、50、100。
|
|
177
|
+
## 5. 后端用户故事
|
|
178
|
+
### BE-US-001 查询列表
|
|
179
|
+
- 系统能力:分页返回列表
|
|
180
|
+
- 使用方:列表页面
|
|
181
|
+
- 业务价值:按需加载数据
|
|
182
|
+
- 触发方式:API(Web)
|
|
183
|
+
- 验收标准:AC-BE-001
|
|
184
|
+
## 6. 后端输出规范
|
|
185
|
+
### BE-US-001 查询列表
|
|
186
|
+
- 输入语义:pageSize 为必填,每页条数允许值为 10、20、50、100
|
|
187
|
+
- 输出语义:返回列表和分页信息
|
|
188
|
+
- 数据读写:只读列表数据
|
|
189
|
+
- 权限规则:仅允许已授权调用方
|
|
190
|
+
- 业务规则:pageSize 必须属于允许值集合
|
|
191
|
+
- 安全要求:不得泄露未授权数据
|
|
192
|
+
- 幂等与并发:重复查询不修改数据
|
|
193
|
+
- 错误与边界:空数据返回空列表和有效分页信息
|
|
194
|
+
#### AC-BE-001 返回分页列表
|
|
195
|
+
Given:
|
|
196
|
+
- 调用方已获得查询权限。
|
|
197
|
+
When:
|
|
198
|
+
- 调用方使用允许的 pageSize 查询列表。
|
|
199
|
+
Then:
|
|
200
|
+
- 返回成功状态和列表数据。
|
|
201
|
+
- 返回有效分页信息。
|
|
202
|
+
- 查询不得修改业务数据。
|
|
203
|
+
异常场景:
|
|
204
|
+
- pageSize 不在允许集合时返回标准参数错误。
|
|
205
|
+
## 7. 决策追溯
|
|
206
|
+
- 需求直接来源于明确需求。
|
|
207
|
+
`;
|
|
208
|
+
|
|
209
|
+
const readyAnalysis = analysis
|
|
210
|
+
.replace("analysis_status: no-clarification-required", "analysis_status: ready-for-clarification")
|
|
211
|
+
.replace("- 无未确认的推断需求。", "- 推断保存成功后展示项目统一提示。")
|
|
212
|
+
.replace("- 无待确认问题。", "- P1:保存成功后使用哪种反馈?");
|
|
213
|
+
const pendingFourthRoundClarification = `---
|
|
214
|
+
artifact_version: "4.0"
|
|
215
|
+
artifact_type: requirement-clarification
|
|
216
|
+
requirement_id: nickname
|
|
217
|
+
analysis_scope: frontend
|
|
218
|
+
clarification_status: pending
|
|
219
|
+
clarification_rounds: 4
|
|
220
|
+
---
|
|
221
|
+
# Requirement Clarification
|
|
222
|
+
## 1. 澄清来源
|
|
223
|
+
- Product Analysis:./product-analysis.md
|
|
224
|
+
- 原始需求身份:${sourceIdentity}
|
|
225
|
+
## 2. 决策分支
|
|
226
|
+
| 分支 ID | 名称 | 优先级 | 依赖 | 状态 |
|
|
227
|
+
|---|---|---|---|---|
|
|
228
|
+
| BR-001 | 保存反馈 | P1 | 无 | in-progress |
|
|
229
|
+
## 3. 问题记录
|
|
230
|
+
### Q-001 保存成功反馈
|
|
231
|
+
- 澄清轮次:4
|
|
232
|
+
- 分支:BR-001
|
|
233
|
+
- 优先级:P1
|
|
234
|
+
- 影响范围:frontend
|
|
235
|
+
- 推荐答案:使用项目统一成功提示
|
|
236
|
+
- 推荐理由:保持一致体验
|
|
237
|
+
- 用户回答:待确认
|
|
238
|
+
- 最终决策:未形成
|
|
239
|
+
- 决策来源:user
|
|
240
|
+
- 状态:pending-blocking
|
|
241
|
+
- 决策标记:DEC-Q-001
|
|
242
|
+
- 目标位置:前端输出规范
|
|
243
|
+
## 4. 决策索引
|
|
244
|
+
- DEC-Q-001:待确认
|
|
245
|
+
`;
|
|
246
|
+
|
|
164
247
|
writeFileSync(analysisPath, analysis);
|
|
165
248
|
writeFileSync(clarificationPath, clarification);
|
|
166
249
|
writeFileSync(requirementPath, requirement);
|
|
@@ -169,25 +252,250 @@ let checks = 0;
|
|
|
169
252
|
function expectPass(name, script, args) { pass(name, run(script, args)); checks += 1; }
|
|
170
253
|
function expectFail(name, script, args, message) { fail(name, run(script, args), message); checks += 1; }
|
|
171
254
|
|
|
172
|
-
|
|
255
|
+
const sourceDirA = join(temp, "source-a");
|
|
256
|
+
const sourceDirB = join(temp, "source-b");
|
|
257
|
+
mkdirSync(sourceDirA, { recursive: true });
|
|
258
|
+
mkdirSync(sourceDirB, { recursive: true });
|
|
259
|
+
const sourcePathA = join(sourceDirA, "requirement.md");
|
|
260
|
+
const sourcePathB = join(sourceDirB, "requirement.md");
|
|
261
|
+
writeFileSync(sourcePathA, "same requirement body\n");
|
|
262
|
+
writeFileSync(sourcePathB, "same requirement body\n");
|
|
263
|
+
const fileIdentityA = run(sourceIdentityTool, ["file-source", sourcePathA]);
|
|
264
|
+
const fileIdentityB = run(sourceIdentityTool, ["file-source", sourcePathB]);
|
|
265
|
+
pass("file source identity A", fileIdentityA);
|
|
266
|
+
pass("file source identity B", fileIdentityB);
|
|
267
|
+
if (fileIdentityA.stdout.trim() === fileIdentityB.stdout.trim()) throw new Error("file-source identity must bind normalized path as well as body.");
|
|
268
|
+
checks += 3;
|
|
269
|
+
const inlineIdentityA = run(sourceIdentityTool, ["inline-source", sourcePathA]);
|
|
270
|
+
const inlineIdentityB = run(sourceIdentityTool, ["inline-source", sourcePathB]);
|
|
271
|
+
pass("inline source identity A", inlineIdentityA);
|
|
272
|
+
pass("inline source identity B", inlineIdentityB);
|
|
273
|
+
if (inlineIdentityA.stdout.trim() !== inlineIdentityB.stdout.trim()) throw new Error("inline-source identity must depend only on exact body.");
|
|
274
|
+
checks += 3;
|
|
275
|
+
|
|
276
|
+
expectPass("valid V4 analysis", analysisValidator, [analysisPath, "--target", "frontend"]);
|
|
173
277
|
expectFail("analysis target mismatch", analysisValidator, [analysisPath, "--target", "backend"], "does not match");
|
|
278
|
+
expectFail("analysis rejects extra frontmatter fields", analysisValidator, [variant("analysis-extra-frontmatter.md", analysis.replace("analysis_status: no-clarification-required", "analysis_status: no-clarification-required\nsource_path: /private/customer/prd.md"))], "unsupported field source_path");
|
|
279
|
+
expectFail("analysis requires source identity", analysisValidator, [variant("analysis-no-source-identity.md", analysis.replace(`- 原始需求身份:${sourceIdentity}\n`, ""))], "原始需求身份");
|
|
280
|
+
const analysisWithKnowledgeHit = analysis.replace("- 检索状态:executed-no-match。", "- 检索状态:executed-hit。\n- KB-FACT-001:采用项目统一空状态组件;来源为当前有效设计系统。");
|
|
281
|
+
writeFileSync(analysisPath, analysisWithKnowledgeHit);
|
|
282
|
+
expectPass("analysis accepts executed-hit with KB fact", analysisValidator, [analysisPath, "--target", "frontend"]);
|
|
283
|
+
writeFileSync(analysisPath, analysis);
|
|
284
|
+
expectFail("analysis requires explicit knowledge status", analysisValidator, [variant("analysis-kb-no-status.md", analysis.replace("- 检索状态:executed-no-match。", "- 未执行知识库。"))], "must contain exactly one knowledge status");
|
|
285
|
+
expectFail("analysis rejects not-executed", analysisValidator, [variant("analysis-kb-not-executed.md", analysis.replace("executed-no-match", "not-executed"))], "must not use not-executed");
|
|
286
|
+
expectFail("analysis executed-hit requires KB fact", analysisValidator, [variant("analysis-kb-hit-no-fact.md", analysis.replace("executed-no-match", "executed-hit"))], "must contain at least one KB-FACT");
|
|
287
|
+
expectFail("analysis KB fact requires executed-hit", analysisValidator, [variant("analysis-kb-fact-wrong-status.md", analysis.replace("- 检索状态:executed-no-match。", "- 检索状态:executed-no-match。\n- KB-FACT-001:不应存在。"))], "require executed-hit");
|
|
288
|
+
expectFail("analysis unavailable requires user skip confirmation", analysisValidator, [variant("analysis-kb-unavailable-no-confirmation.md", analysis.replace("executed-no-match", "unavailable"))], "用户处置:已确认跳过知识库");
|
|
289
|
+
const analysisWithKnowledgeUnavailable = analysis.replace("- 检索状态:executed-no-match。", "- 检索状态:unavailable。\n- 用户处置:已确认跳过知识库");
|
|
290
|
+
writeFileSync(analysisPath, analysisWithKnowledgeUnavailable);
|
|
291
|
+
expectPass("analysis unavailable accepts user skip confirmation", analysisValidator, [analysisPath, "--target", "frontend"]);
|
|
292
|
+
writeFileSync(analysisPath, analysis);
|
|
174
293
|
expectFail("analysis rejects formal AC", analysisValidator, [variant("analysis-ac.md", analysis.replace("验收关注点:", "验收关注点:AC-FE-001 "))], "must use acceptance concerns");
|
|
175
294
|
expectFail("analysis requires matching spec", analysisValidator, [variant("analysis-no-spec.md", analysis.replace("### FE-US-001 修改昵称\n- 页面/组件", "### FE-US-002 修改昵称\n- 页面/组件"))], "missing output specification");
|
|
176
295
|
expectFail("analysis rejects standalone roles", analysisValidator, [variant("analysis-roles.md", analysis.replace("## 4. 需求分析", "## 用户角色\n- 已登录用户。\n## 4. 需求分析"))], "must not contain standalone");
|
|
296
|
+
expectFail("no-clarification analysis rejects unconfirmed inferred requirements", analysisValidator, [variant("analysis-unconfirmed-inference.md", analysis.replace("- 无未确认的推断需求。", "- 推断保存成功后展示通用提示。"))], "must declare no unconfirmed inferred requirements");
|
|
297
|
+
expectFail("no-clarification analysis requires an explicit no-question sentinel", analysisValidator, [variant("analysis-implicit-question.md", analysis.replace("- 无待确认问题。", "- 保存反馈尚未确定。"))], "must declare no pending questions");
|
|
298
|
+
expectFail("no-clarification analysis rejects questions hidden after the sentinel", analysisValidator, [variant("analysis-hidden-question.md", analysis.replace("- 无待确认问题。", "- 无待确认问题。\n- 保存反馈尚未确定。"))], "must declare no pending questions");
|
|
177
299
|
|
|
178
300
|
expectPass("valid compact clarification", clarificationValidator, [analysisPath, clarificationPath, requirementPath]);
|
|
179
|
-
expectFail("clarification
|
|
301
|
+
expectFail("clarification rejects extra frontmatter fields", clarificationValidator, [analysisPath, variant("clarification-extra-frontmatter.md", clarification.replace("clarification_rounds: 0", "clarification_rounds: 0\nsource_path: /private/customer/prd.md")), requirementPath], "unsupported field source_path");
|
|
302
|
+
expectFail("clarification rejects source identity mismatch", clarificationValidator, [analysisPath, variant("clarification-source-identity-mismatch.md", clarification.replace(sourceIdentity, `inline:sha256:${"2".repeat(64)}`)), requirementPath], "原始需求身份 must match");
|
|
303
|
+
expectFail("complete compact clarification requires supplement confirmation", clarificationValidator, [analysisPath, variant("clarification-no-supplement-confirmation.md", clarification.replace("- 内容补充:用户已确认无需补充\n", "")), requirementPath], "内容补充:用户已确认无需补充");
|
|
304
|
+
expectFail("compact clarification must use zero rounds", clarificationValidator, [analysisPath, variant("clarification-nonzero-rounds.md", clarification.replace("clarification_rounds: 0", "clarification_rounds: 4")), requirementPath], "must set clarification_rounds to 0");
|
|
180
305
|
expectFail("compact clarification rejects extra section", clarificationValidator, [analysisPath, variant("clarification-extra.md", clarification + "\n## 4. 完成门禁\n- 已完成。\n"), requirementPath], "must not contain section");
|
|
306
|
+
writeFileSync(analysisPath, readyAnalysis);
|
|
307
|
+
writeFileSync(clarificationPath, pendingFourthRoundClarification);
|
|
308
|
+
expectFail("pending clarification rejects complete requirement", clarificationValidator, [analysisPath, clarificationPath, requirementPath, "--allow-pending"], "same pending/complete status");
|
|
309
|
+
writeFileSync(requirementPath, requirement.replace("requirement_status: complete", "requirement_status: pending").replace("### 3.2 非目标\n- 不修改头像。", "### 3.2 非目标\n- 不修改头像。\n### 3.3 未决事项\n- P1:保存成功提示待用户确认。"));
|
|
310
|
+
expectPass("question clarification supports more than three rounds", clarificationValidator, [analysisPath, clarificationPath, requirementPath, "--allow-pending"]);
|
|
311
|
+
expectFail("question clarification rejects source requirement as a Q decision", clarificationValidator, [analysisPath, variant("clarification-source-question.md", pendingFourthRoundClarification.replace("- 决策来源:user", "- 决策来源:source-requirement")), requirementPath, "--allow-pending"], "must be user");
|
|
312
|
+
expectFail("question clarification rejects removed default status", clarificationValidator, [analysisPath, variant("clarification-default-status.md", pendingFourthRoundClarification.replace("- 状态:pending-blocking", "- 状态:default-confirmed")), requirementPath, "--allow-pending"], "must not use");
|
|
313
|
+
writeFileSync(requirementPath, requirement);
|
|
314
|
+
writeFileSync(analysisPath, analysis);
|
|
315
|
+
writeFileSync(clarificationPath, clarification);
|
|
181
316
|
|
|
182
|
-
expectPass("valid
|
|
317
|
+
expectPass("valid V4 requirement", requirementValidator, [requirementPath, "--target", "frontend"]);
|
|
318
|
+
expectFail("requirement rejects extra frontmatter fields", requirementValidator, [variant("requirement-extra-frontmatter.md", requirement.replace("analysis_scope: frontend", "analysis_scope: frontend\nsource_path: /private/customer/prd.md"))], "unsupported field source_path");
|
|
183
319
|
expectFail("requirement target mismatch", requirementValidator, [requirementPath, "--target", "backend"], "does not match");
|
|
184
320
|
expectFail("requirement requires role", requirementValidator, [variant("requirement-no-role.md", requirement.replace("- 角色:已登录用户\n", ""))], "missing story field 角色");
|
|
185
321
|
expectFail("requirement requires matching spec", requirementValidator, [variant("requirement-no-spec.md", requirement.replace("### FE-US-001 修改昵称\n- 页面/组件", "### FE-US-002 修改昵称\n- 页面/组件"))], "missing output specification");
|
|
186
322
|
expectFail("requirement AC refs match spec", requirementValidator, [variant("requirement-bad-ac.md", requirement.replace("- 验收标准:AC-FE-001", "- 验收标准:AC-FE-002"))], "must match its output specification");
|
|
323
|
+
const duplicateAcStory = `### FE-US-002 再次修改昵称
|
|
324
|
+
- 角色:已登录用户
|
|
325
|
+
- 目标:再次修改昵称
|
|
326
|
+
- 价值:保持个人资料准确
|
|
327
|
+
- 入口:个人资料页
|
|
328
|
+
- 验收标准:AC-FE-001
|
|
329
|
+
`;
|
|
330
|
+
const duplicateAcSpec = `### FE-US-002 再次修改昵称
|
|
331
|
+
- 页面/组件:个人资料页、昵称编辑表单
|
|
332
|
+
- 页面路由:/account/profile
|
|
333
|
+
- 展示内容:当前昵称
|
|
334
|
+
- 交互动作:保存
|
|
335
|
+
- UI 状态:normal、loading、error、disabled
|
|
336
|
+
- 表单校验:昵称长度为 2–20 个字符
|
|
337
|
+
- 权限可见性:仅当前用户
|
|
338
|
+
- 边界处理:失败时保留输入并允许重试
|
|
339
|
+
#### AC-FE-001 再次保存昵称
|
|
340
|
+
Given:
|
|
341
|
+
- 用户已登录。
|
|
342
|
+
When:
|
|
343
|
+
- 用户点击保存。
|
|
344
|
+
Then:
|
|
345
|
+
- 页面提交请求。
|
|
346
|
+
- 页面展示新昵称。
|
|
347
|
+
- 页面不得泄露他人数据。
|
|
348
|
+
异常场景:
|
|
349
|
+
- 失败时保留输入并允许重试。
|
|
350
|
+
`;
|
|
351
|
+
const duplicateAcRequirement = requirement
|
|
352
|
+
.replace("## 6. 前端输出规范", `${duplicateAcStory}## 6. 前端输出规范`)
|
|
353
|
+
.replace("## 7. 决策追溯", `${duplicateAcSpec}## 7. 决策追溯`);
|
|
354
|
+
expectFail("requirement rejects duplicate AC IDs across stories", requirementValidator, [variant("requirement-duplicate-ac.md", duplicateAcRequirement)], "Duplicate acceptance criterion ID");
|
|
355
|
+
expectFail("frontend output requires page route", requirementValidator, [variant("requirement-no-route.md", requirement.replace("- 页面路由:/account/profile\n", ""))], "missing field 页面路由");
|
|
356
|
+
expectFail("frontend route must be a product path", requirementValidator, [variant("requirement-bad-route.md", requirement.replace("- 页面路由:/account/profile", "- 页面路由:ProfileRoute 组件"))], "must start with /");
|
|
357
|
+
writeFileSync(requirementPath, requirement.replace("- 页面路由:/account/profile", "- 页面路由:不适用;嵌入 /account/profile 页面"));
|
|
358
|
+
expectPass("embedded component route is accepted", requirementValidator, [requirementPath]);
|
|
359
|
+
writeFileSync(requirementPath, requirement);
|
|
187
360
|
expectFail("requirement rejects standalone roles", requirementValidator, [variant("requirement-roles.md", requirement.replace("## 3. 需求范围", "## 用户角色\n- 已登录用户。\n## 3. 需求范围"))], "must not contain standalone");
|
|
361
|
+
expectFail("complete requirement rejects default assumptions", requirementValidator, [variant("requirement-defaults.md", requirement.replace("### 3.2 非目标\n- 不修改头像。", "### 3.2 非目标\n- 不修改头像。\n### 3.3 默认假设\n- 使用通用成功提示。"))], "must not contain 默认假设");
|
|
362
|
+
expectFail("complete requirement rejects unresolved items", requirementValidator, [variant("requirement-unresolved.md", requirement.replace("### 3.2 非目标\n- 不修改头像。", "### 3.2 非目标\n- 不修改头像。\n### 3.3 未决事项\n- P2:待确认成功提示。"))], "must not contain 未决事项");
|
|
363
|
+
writeFileSync(requirementPath, requirement.replace("- 昵称长度必须为 2–20 个字符。", "- 昵称长度必须为 2–20 个字符。\n- 审核状态为“待确认”时,页面展示“待确认”标签。"));
|
|
364
|
+
expectPass("complete requirement accepts literal pending business state", requirementValidator, [requirementPath, "--target", "frontend"]);
|
|
365
|
+
writeFileSync(requirementPath, requirement);
|
|
188
366
|
expectFail("pending requirement rejected by default", requirementValidator, [variant("requirement-pending.md", requirement.replace("requirement_status: complete", "requirement_status: pending"))], "must be complete");
|
|
189
|
-
writeFileSync(requirementPath, requirement.replace("requirement_status: complete", "requirement_status: pending"));
|
|
367
|
+
writeFileSync(requirementPath, requirement.replace("requirement_status: complete", "requirement_status: pending").replace("### 3.2 非目标\n- 不修改头像。", "### 3.2 非目标\n- 不修改头像。\n### 3.3 未决事项\n- P1:保存成功提示待用户确认。"));
|
|
368
|
+
expectFail("pending requirement rejects complete source clarification", requirementValidator, [requirementPath, "--allow-pending"], "same pending/complete status");
|
|
369
|
+
writeFileSync(clarificationPath, pendingFourthRoundClarification);
|
|
190
370
|
expectPass("pending requirement allowed explicitly", requirementValidator, [requirementPath, "--allow-pending"]);
|
|
371
|
+
writeFileSync(clarificationPath, clarification);
|
|
372
|
+
const unspecifiedPageSizes = paginatedRequirement
|
|
373
|
+
.replaceAll("10、20、50、100", "")
|
|
374
|
+
.replace("- 输入语义:pageSize 为必填,每页条数允许值为 ", "- 输入语义:pageSize 为必填,支持分页")
|
|
375
|
+
.replace("- 业务规则:pageSize 必须属于允许值集合", "- 业务规则:pageSize 必须合法")
|
|
376
|
+
.replace("- 每页条数参数必填,允许值为 。", "- 支持分页查询。");
|
|
377
|
+
writeFileSync(requirementPath, unspecifiedPageSizes);
|
|
378
|
+
expectPass("requirement validator does not infer pagination value sets", requirementValidator, [requirementPath, "--target", "backend"]);
|
|
379
|
+
writeFileSync(requirementPath, requirement);
|
|
380
|
+
expectFail("requirement rejects CODE-FACT leakage", requirementValidator, [variant("requirement-code-fact.md", requirement.replace("- 昵称长度必须为 2–20 个字符。", "- 昵称长度必须为 2–20 个字符。\n- 依据 CODE-FACT-001。"))], "must not contain CODE-FACT");
|
|
381
|
+
expectFail("requirement rejects KB-FACT leakage", requirementValidator, [variant("requirement-kb-fact.md", requirement.replace("- 昵称长度必须为 2–20 个字符。", "- 昵称长度必须为 2–20 个字符。\n- 依据 KB-FACT-001。"))], "must not contain KB-FACT");
|
|
382
|
+
expectFail("requirement rejects repository path leakage", requirementValidator, [variant("requirement-path.md", requirement.replace("- 昵称长度必须为 2–20 个字符。", "- 实现位于 src/account/profile.ts。"))], "must not contain repository file paths");
|
|
383
|
+
expectFail("requirement rejects vague AC language", requirementValidator, [variant("requirement-vague-ac.md", requirement.replace("- 保存成功后展示新昵称。", "- 保存后功能正常。"))], "must not use vague acceptance language");
|
|
384
|
+
expectFail("clarification scope must match requirement", clarificationValidator, [analysisPath, clarificationPath, variant("requirement-scope-mismatch.md", requirement.replace("analysis_scope: frontend", "analysis_scope: both"))], "must match Product Requirement");
|
|
385
|
+
|
|
386
|
+
const bothAnalysis = analysis
|
|
387
|
+
.replace("analysis_scope: frontend", "analysis_scope: both")
|
|
388
|
+
.replace("## 6. 初步前端用户故事", `## 6. 初步前端用户故事`)
|
|
389
|
+
+ `
|
|
390
|
+
## 8. 初步后端用户故事
|
|
391
|
+
### BE-US-001 保存昵称
|
|
392
|
+
- 系统能力:更新当前用户昵称
|
|
393
|
+
- 使用方:个人资料页
|
|
394
|
+
- 业务价值:持久化展示名称
|
|
395
|
+
- 触发方式:API(Web)
|
|
396
|
+
- 验收关注点:合法值可保存;非法值拒绝;失败可重试
|
|
397
|
+
## 9. 初步后端输出规范
|
|
398
|
+
### BE-US-001 保存昵称
|
|
399
|
+
- 输入语义:当前用户与新昵称
|
|
400
|
+
- 输出语义:更新后的昵称
|
|
401
|
+
- 数据读写:更新用户资料
|
|
402
|
+
- 权限规则:仅本人
|
|
403
|
+
- 业务规则:长度 2–20
|
|
404
|
+
- 安全要求:不得泄露他人资料
|
|
405
|
+
- 幂等与并发:重复提交不产生脏数据
|
|
406
|
+
- 错误与边界:非法值返回标准错误
|
|
407
|
+
`;
|
|
408
|
+
const bothRequirement = requirement
|
|
409
|
+
.replace("analysis_scope: frontend", "analysis_scope: both")
|
|
410
|
+
.replace("## 7. 决策追溯", `## 7. 后端用户故事
|
|
411
|
+
### BE-US-001 保存昵称
|
|
412
|
+
- 系统能力:更新当前用户昵称
|
|
413
|
+
- 使用方:个人资料页
|
|
414
|
+
- 业务价值:持久化展示名称
|
|
415
|
+
- 触发方式:API(Web)
|
|
416
|
+
- 验收标准:AC-BE-001
|
|
417
|
+
## 8. 后端输出规范
|
|
418
|
+
### BE-US-001 保存昵称
|
|
419
|
+
- 输入语义:当前用户与新昵称
|
|
420
|
+
- 输出语义:更新后的昵称
|
|
421
|
+
- 数据读写:更新用户资料
|
|
422
|
+
- 权限规则:仅本人
|
|
423
|
+
- 业务规则:长度 2–20
|
|
424
|
+
- 安全要求:不得泄露他人资料
|
|
425
|
+
- 幂等与并发:重复提交不产生脏数据
|
|
426
|
+
- 错误与边界:非法值返回标准错误
|
|
427
|
+
#### AC-BE-001 保存合法昵称
|
|
428
|
+
Given:
|
|
429
|
+
- 调用方已认证且为目标用户本人。
|
|
430
|
+
When:
|
|
431
|
+
- 调用方提交合法昵称。
|
|
432
|
+
Then:
|
|
433
|
+
- 返回成功状态和新昵称。
|
|
434
|
+
- 昵称被持久化。
|
|
435
|
+
- 保存期间不得产生部分写入。
|
|
436
|
+
异常场景:
|
|
437
|
+
- 非法长度时返回标准参数错误且不得更新昵称。
|
|
438
|
+
## 9. 决策追溯`);
|
|
439
|
+
const bothClarification = clarification
|
|
440
|
+
.replace("analysis_scope: frontend", "analysis_scope: both");
|
|
441
|
+
writeFileSync(analysisPath, bothAnalysis);
|
|
442
|
+
writeFileSync(clarificationPath, bothClarification);
|
|
443
|
+
writeFileSync(requirementPath, bothRequirement);
|
|
444
|
+
expectPass("both scope analysis/requirement/clarification", analysisValidator, [analysisPath, "--target", "both"]);
|
|
445
|
+
expectFail("plain API trigger requires Web or Remote clarification", analysisValidator, [variant("analysis-plain-api.md", bothAnalysis.replace("API(Web)", "API")), "--target", "both"], "API must declare Web, Remote, or Web + Remote");
|
|
446
|
+
expectPass("both scope requirement", requirementValidator, [requirementPath, "--target", "both"]);
|
|
447
|
+
expectPass("both scope clarification", clarificationValidator, [analysisPath, clarificationPath, requirementPath]);
|
|
448
|
+
|
|
449
|
+
writeFileSync(analysisPath, readyAnalysis);
|
|
450
|
+
const completeQuestionClarification = `---
|
|
451
|
+
artifact_version: "4.0"
|
|
452
|
+
artifact_type: requirement-clarification
|
|
453
|
+
requirement_id: nickname
|
|
454
|
+
analysis_scope: frontend
|
|
455
|
+
clarification_status: complete
|
|
456
|
+
clarification_rounds: 1
|
|
457
|
+
---
|
|
458
|
+
# Requirement Clarification
|
|
459
|
+
## 1. 澄清来源
|
|
460
|
+
- Product Analysis:./product-analysis.md
|
|
461
|
+
- 原始需求身份:${sourceIdentity}
|
|
462
|
+
## 2. 决策分支
|
|
463
|
+
| 分支 ID | 名称 | 优先级 | 依赖 | 状态 |
|
|
464
|
+
|---|---|---|---|---|
|
|
465
|
+
| BR-001 | 保存反馈 | P1 | 无 | resolved |
|
|
466
|
+
## 3. 问题记录
|
|
467
|
+
### Q-001 保存成功反馈
|
|
468
|
+
- 澄清轮次:1
|
|
469
|
+
- 分支:BR-001
|
|
470
|
+
- 优先级:P1
|
|
471
|
+
- 影响范围:frontend
|
|
472
|
+
- 推荐答案:使用项目统一成功提示
|
|
473
|
+
- 推荐理由:保持一致体验
|
|
474
|
+
- 用户回答:同意使用项目统一成功提示
|
|
475
|
+
- 最终决策:保存成功后展示项目统一成功提示
|
|
476
|
+
- 决策来源:user
|
|
477
|
+
- 状态:confirmed
|
|
478
|
+
- 决策标记:DEC-Q-001
|
|
479
|
+
- 目标位置:前端输出规范
|
|
480
|
+
## 4. 决策索引
|
|
481
|
+
- DEC-Q-001:保存成功后展示项目统一成功提示
|
|
482
|
+
- 共同理解:已确认
|
|
483
|
+
`;
|
|
484
|
+
const requirementWithDecision = requirement.replace("## 7. 决策追溯\n- 本轮无需澄清决策标记;需求直接来源于明确需求。", "## 7. 决策追溯\n- DEC-Q-001:保存成功后展示项目统一成功提示,已合并到前端输出规范。");
|
|
485
|
+
writeFileSync(clarificationPath, completeQuestionClarification);
|
|
486
|
+
writeFileSync(requirementPath, requirementWithDecision);
|
|
487
|
+
expectPass("complete question clarification with DEC markers", clarificationValidator, [analysisPath, clarificationPath, requirementPath]);
|
|
488
|
+
expectFail("complete clarification rejects unformed final decision", clarificationValidator, [analysisPath, variant("clarification-unformed-decision.md", completeQuestionClarification.replace("- 最终决策:保存成功后展示项目统一成功提示", "- 最终决策:未形成")), requirementPath], "must contain a formed final decision");
|
|
489
|
+
expectPass("requirement with DEC markers", requirementValidator, [requirementPath, "--target", "frontend"]);
|
|
490
|
+
expectFail("complete clarification requires shared-understanding marker", clarificationValidator, [analysisPath, variant("clarification-no-shared.md", completeQuestionClarification.replace("- 共同理解:已确认\n", "")), requirementPath], "共同理解:已确认");
|
|
491
|
+
expectFail("analysis frontend output requires page route", analysisValidator, [variant("analysis-no-route.md", analysis.replace("- 页面路由:/account/profile\n", "")), "--target", "frontend"], "missing field 页面路由");
|
|
492
|
+
expectFail("analysis route must be a product path", analysisValidator, [variant("analysis-bad-route.md", analysis.replace("- 页面路由:/account/profile", "- 页面路由:ProfileRoute 组件")), "--target", "frontend"], "must start with /");
|
|
493
|
+
const embeddedRouteAnalysis = analysis.replace("- 页面路由:/account/profile", "- 页面路由:不适用;嵌入 /account/profile 页面");
|
|
494
|
+
writeFileSync(analysisPath, embeddedRouteAnalysis);
|
|
495
|
+
expectPass("analysis embedded component route is accepted", analysisValidator, [analysisPath, "--target", "frontend"]);
|
|
496
|
+
|
|
497
|
+
writeFileSync(analysisPath, analysis);
|
|
498
|
+
writeFileSync(clarificationPath, clarification);
|
|
191
499
|
writeFileSync(requirementPath, requirement);
|
|
192
500
|
|
|
193
|
-
console.log(`Requirement
|
|
501
|
+
console.log(`Requirement V4 validator matrix passed: ${checks} checks.`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { assertHeadings, field, loadMarkdown, metadata, outputSpecBlocks, printResult, section, storyBlocks, validateArtifactLocation, validateStorySpecCoverage } from "./validation-helpers.mjs";
|
|
3
|
+
import { assertHeadings, field, loadMarkdown, metadata, outputSpecBlocks, printResult, section, storyBlocks, validateArtifactLocation, validateFrontmatter, validateStorySpecCoverage } from "./validation-helpers.mjs";
|
|
4
4
|
|
|
5
5
|
const args = process.argv.slice(2);
|
|
6
6
|
const fileArg = args[0];
|
|
@@ -17,26 +17,46 @@ catch (error) { console.error(error.message); process.exit(2); }
|
|
|
17
17
|
|
|
18
18
|
const { path, text } = artifact;
|
|
19
19
|
const errors = [];
|
|
20
|
+
validateFrontmatter(text, ["artifact_version", "artifact_type", "requirement_id", "analysis_scope", "analysis_status"], errors, "Product Analysis");
|
|
20
21
|
validateArtifactLocation(artifact, errors, "product-analysis.md");
|
|
21
22
|
const scope = metadata(text, "analysis_scope");
|
|
22
23
|
const status = metadata(text, "analysis_status");
|
|
23
24
|
|
|
24
|
-
if (metadata(text, "artifact_version") !== "
|
|
25
|
+
if (metadata(text, "artifact_version") !== "4.0") errors.push("artifact_version must be 4.0.");
|
|
25
26
|
if (metadata(text, "artifact_type") !== "product-analysis") errors.push("artifact_type must be product-analysis.");
|
|
26
27
|
if (!["frontend", "backend", "both"].includes(scope)) errors.push("analysis_scope must be frontend, backend, or both.");
|
|
27
28
|
if (targetIndex >= 0 && !["frontend", "backend", "both"].includes(expectedTarget)) errors.push("--target must be frontend, backend, or both.");
|
|
28
29
|
else if (expectedTarget && scope !== expectedTarget) errors.push(`analysis_scope ${scope} does not match requested target ${expectedTarget}.`);
|
|
29
30
|
if (!["ready-for-clarification", "no-clarification-required"].includes(status)) errors.push("analysis_status must be ready-for-clarification or no-clarification-required.");
|
|
30
|
-
if (!metadata(text, "source_requirement")) errors.push("source_requirement is required.");
|
|
31
|
-
if (!metadata(text, "repository_root")) errors.push("repository_root is required; use none when absent.");
|
|
32
31
|
|
|
33
32
|
assertHeadings(text, 2, ["原始需求", "需求概述", "业务目标", "需求分析", "外部事实"], errors, "Product Analysis");
|
|
33
|
+
const originalRequirement = section(text, 2, "原始需求") ?? "";
|
|
34
|
+
const sourceIdentity = field(originalRequirement, "原始需求身份") ?? "";
|
|
35
|
+
if (!/^(?:inline|file):sha256:[a-f0-9]{64}$/.test(sourceIdentity)) {
|
|
36
|
+
errors.push("原始需求 must contain 原始需求身份 as inline:sha256:<64 hex> or file:sha256:<64 hex>.");
|
|
37
|
+
}
|
|
34
38
|
const analysis = section(text, 2, "需求分析") ?? "";
|
|
35
39
|
assertHeadings(analysis, 3, ["明确需求", "推断需求", "待确认问题", "初步非目标"], errors, "需求分析");
|
|
36
40
|
const facts = section(text, 2, "外部事实") ?? "";
|
|
37
41
|
assertHeadings(facts, 3, ["知识库事实", "代码库事实"], errors, "外部事实");
|
|
42
|
+
const knowledgeFacts = section(facts, 3, "知识库事实") ?? "";
|
|
43
|
+
const knowledgeStatuses = knowledgeFacts.match(/\b(?:executed-hit|executed-no-match|unavailable)\b/g) ?? [];
|
|
44
|
+
if (/\bnot-executed\b/.test(knowledgeFacts)) errors.push("知识库事实 must not use not-executed; every new requirement requires a real knowledge search.");
|
|
45
|
+
if (knowledgeStatuses.length !== 1) errors.push("知识库事实 must contain exactly one knowledge status: executed-hit, executed-no-match, or unavailable.");
|
|
46
|
+
if (knowledgeStatuses[0] === "executed-hit" && !/\bKB-FACT-\d+\b/.test(knowledgeFacts)) errors.push("executed-hit 知识库事实 must contain at least one KB-FACT-* marker.");
|
|
47
|
+
if (knowledgeStatuses[0] !== "executed-hit" && /\bKB-FACT-\d+\b/.test(knowledgeFacts)) errors.push("KB-FACT-* markers require executed-hit knowledge status.");
|
|
48
|
+
if (knowledgeStatuses[0] === "unavailable" && !/^[ \t]*-[ \t]*用户处置[::][ \t]*已确认跳过知识库\s*$/m.test(knowledgeFacts)) errors.push("unavailable 知识库事实 must record 用户处置:已确认跳过知识库.");
|
|
38
49
|
|
|
39
50
|
const questions = section(analysis, 3, "待确认问题") ?? "";
|
|
51
|
+
const inferences = section(analysis, 3, "推断需求") ?? "";
|
|
52
|
+
const contentLines = (block) => block.split(/\r?\n/).slice(1).map((line) => line.trim()).filter(Boolean);
|
|
53
|
+
const inferenceLines = contentLines(inferences);
|
|
54
|
+
const questionLines = contentLines(questions);
|
|
55
|
+
const noInferenceSentinel = /^-\s*无未确认的推断需求[。.]?$/.test(inferenceLines[0] ?? "");
|
|
56
|
+
const noQuestionSentinel = /^-\s*无待确认问题[。.]?$/.test(questionLines[0] ?? "");
|
|
57
|
+
const unexpectedQuestionLines = questionLines.slice(1).filter((line) => !/^-\s*理由[::]\s*\S/.test(line));
|
|
58
|
+
if (status === "no-clarification-required" && (!noInferenceSentinel || inferenceLines.length !== 1)) errors.push("no-clarification-required analysis must declare no unconfirmed inferred requirements.");
|
|
59
|
+
if (status === "no-clarification-required" && (!noQuestionSentinel || unexpectedQuestionLines.length)) errors.push("no-clarification-required analysis must declare no pending questions.");
|
|
40
60
|
if (status === "no-clarification-required" && /(?:\bQ-\d{3,}\b|\bBR-\d{3,}\b|\bP[012]\b|[??])/.test(questions)) errors.push("no-clarification-required analysis cannot contain question markers, priorities, branches, or question sentences.");
|
|
41
61
|
if (status === "ready-for-clarification" && !/(?:\bP[012]\b|[??])/.test(questions)) errors.push("ready-for-clarification analysis must contain at least one prioritized or explicit question.");
|
|
42
62
|
if (/\bAC-(?:FE|BE)-\d{3,}\b|^Given[::]|^When[::]|^Then[::]/m.test(text)) errors.push("Product Analysis must use acceptance concerns, not formal AC IDs or Given/When/Then blocks.");
|
|
@@ -53,12 +73,23 @@ function validateDomain(domain) {
|
|
|
53
73
|
const specs = outputSpecBlocks(section(text, 2, specHeading), prefix);
|
|
54
74
|
if (!stories.length) errors.push(`${storyHeading} must contain at least one ${prefix}-* story.`);
|
|
55
75
|
const storyFields = frontend ? ["角色", "目标", "价值", "入口", "验收关注点"] : ["系统能力", "使用方", "业务价值", "触发方式", "验收关注点"];
|
|
56
|
-
const specFields = frontend ? ["页面/组件", "展示内容", "交互动作", "UI 状态", "表单校验", "权限可见性", "边界处理"] : ["输入语义", "输出语义", "数据读写", "权限规则", "业务规则", "安全要求", "幂等与并发", "错误与边界"];
|
|
76
|
+
const specFields = frontend ? ["页面/组件", "页面路由", "展示内容", "交互动作", "UI 状态", "表单校验", "权限可见性", "边界处理"] : ["输入语义", "输出语义", "数据读写", "权限规则", "业务规则", "安全要求", "幂等与并发", "错误与边界"];
|
|
57
77
|
for (const story of stories) {
|
|
58
78
|
for (const name of storyFields) if (!field(story.text, name)) errors.push(`${story.id} is missing story field ${name}.`);
|
|
59
|
-
|
|
79
|
+
const trigger = field(story.text, "触发方式") ?? "";
|
|
80
|
+
const apiTrigger = /^API[((](?:Web|Remote|Web\s*\+\s*Remote)[))]$/i.test(trigger);
|
|
81
|
+
if (!frontend && !apiTrigger && !/^(?:定时任务|事件|消息|内部调用|数据迁移)$/i.test(trigger)) errors.push(`${story.id} 触发方式 is invalid; API must declare Web, Remote, or Web + Remote.`);
|
|
82
|
+
}
|
|
83
|
+
for (const spec of specs) {
|
|
84
|
+
for (const name of specFields) if (!field(spec.text, name)) errors.push(`${spec.id} output specification is missing field ${name}.`);
|
|
85
|
+
if (frontend) {
|
|
86
|
+
const route = field(spec.text, "页面路由") ?? "";
|
|
87
|
+
const directRoute = /^`?\/[^`\s]+`?(?:\s|$|[;;,,])/.test(route);
|
|
88
|
+
const embeddedRoute = /^不适用;.+`?\/[^`\s]+`?.*页面/.test(route);
|
|
89
|
+
if (!directRoute && !embeddedRoute) errors.push(`${spec.id} 页面路由 must start with / or use 不适用; and name its containing page.`);
|
|
90
|
+
if (/(?:route config|middleware|路由注册文件|路由组件)/i.test(route)) errors.push(`${spec.id} 页面路由 must describe product routing, not implementation files or framework components.`);
|
|
91
|
+
}
|
|
60
92
|
}
|
|
61
|
-
for (const spec of specs) for (const name of specFields) if (!field(spec.text, name)) errors.push(`${spec.id} output specification is missing field ${name}.`);
|
|
62
93
|
validateStorySpecCoverage(stories, specs, errors, `${title} scope`);
|
|
63
94
|
}
|
|
64
95
|
|