@tea-agent/loop-agent 0.33.0 → 0.33.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/worker/console/operator-surface-health.js +1 -0
  3. package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
  4. package/dist/worker/console/static/assets/{index-3R-GT3a_.js → index-PzYzcuFG.js} +17 -17
  5. package/dist/worker/console/static/index.html +3 -2
  6. package/dist/worker/console/static-src/app/useRecoveryConsole.js +3 -2
  7. package/dist/worker/observe/routes.js +3 -8
  8. package/dist/worker/observe/static/app.js +11 -0
  9. package/dist/worker/observe/static/index.html +11 -43
  10. package/dist/worker/observe/static/kpi.js +2 -24
  11. package/dist/worker/observe/static/operator-chrome.css +480 -0
  12. package/dist/worker/observe/static/operator-chrome.d.ts +82 -0
  13. package/dist/worker/observe/static/operator-chrome.js +554 -0
  14. package/dist/worker/observe/static/shell-chrome.js +1 -11
  15. package/dist/worker/observe/static/styles.css +62 -269
  16. package/dist/worker/observe/static/views/dashboard.js +52 -4
  17. package/dist/workflows/dag/retry-policy.js +5 -0
  18. package/package.json +1 -1
  19. package/skills/analyze-product-dependencies/SKILL.md +74 -33
  20. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +16 -11
  21. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +20 -10
  22. package/skills/analyze-product-dependencies/references/example.md +9 -9
  23. package/skills/analyze-product-dependencies/references/forward-test-cases.md +93 -18
  24. package/skills/analyze-product-dependencies/references/input-contract.md +27 -4
  25. package/skills/analyze-product-dependencies/references/kb-integration.md +64 -0
  26. package/skills/analyze-product-dependencies/references/scouting-rules.md +25 -10
  27. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +247 -54
  28. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +122 -29
  29. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +94 -36
  30. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +37 -23
  31. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +35 -43
  32. package/skills/analyze-product-requirements/SKILL.md +98 -43
  33. package/skills/analyze-product-requirements/references/acceptance-criteria.md +8 -12
  34. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +28 -14
  35. package/skills/analyze-product-requirements/references/example.md +24 -6
  36. package/skills/analyze-product-requirements/references/forward-test-cases.md +87 -9
  37. package/skills/analyze-product-requirements/references/kb-integration.md +56 -0
  38. package/skills/analyze-product-requirements/references/product-analysis-schema.md +19 -10
  39. package/skills/analyze-product-requirements/references/product-requirement-schema.md +21 -12
  40. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +45 -14
  41. package/skills/analyze-product-requirements/scripts/compute-source-identity.mjs +35 -0
  42. package/skills/analyze-product-requirements/scripts/test-validators.mjs +337 -29
  43. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +38 -7
  44. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +41 -29
  45. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +33 -22
  46. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +43 -24
  47. package/dist/worker/console/static/assets/index-B0EQt_yq.css +0 -1
  48. package/skills/analyze-product-dependencies/agents/openai.yaml +0 -4
  49. 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-v3-validators-"));
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: "3.0"
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: "3.0"
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: "3.0"
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
- expectPass("valid V3 analysis", analysisValidator, [analysisPath, "--target", "frontend"]);
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 rounds cannot exceed three", clarificationValidator, [analysisPath, variant("clarification-four-rounds.md", clarification.replace("clarification_rounds: 0", "clarification_rounds: 4")), requirementPath], "from 0 to 3");
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 V3 requirement", requirementValidator, [requirementPath, "--target", "frontend"]);
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 V3 validator matrix passed: ${checks} checks.`);
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") !== "3.0") errors.push("artifact_version must be 3.0.");
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
- if (!frontend && !/^(?:API|定时任务|事件|消息|内部调用|数据迁移)(?:$|[、,,/])/i.test(field(story.text, "触发方式") ?? "")) errors.push(`${story.id} 触发方式 is invalid.`);
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