@tea-agent/loop-agent 0.12.0 → 0.13.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/AGENTS.md +155 -153
  2. package/CHANGELOG.md +338 -265
  3. package/README.md +345 -298
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/application/dag/generate-task-dag.js +28 -28
  7. package/dist/application/evaluation/candidate-hash.js +75 -0
  8. package/dist/application/evaluation/candidate.js +52 -0
  9. package/dist/application/evaluation/replay.js +289 -0
  10. package/dist/application/evaluation/types.js +130 -0
  11. package/dist/cli/command-definitions.js +27 -7
  12. package/dist/cli/program.js +8 -4
  13. package/dist/commands/cursor-prompt.js +6 -6
  14. package/dist/commands/eval.js +235 -0
  15. package/dist/commands/init.js +544 -506
  16. package/dist/commands/knowledge.js +129 -31
  17. package/dist/commands/loop-benchmark.js +11 -11
  18. package/dist/commands/pi-reuse-benchmark.js +16 -16
  19. package/dist/executors/pi-sdk-executor.js +38 -24
  20. package/dist/executors/shell-executor.js +34 -2
  21. package/dist/executors/shell-presets.js +20 -0
  22. package/dist/executors/shell-verification.js +7 -0
  23. package/dist/governance/manifest-types.js +4 -0
  24. package/dist/infrastructure/evaluation/candidate-store.js +435 -0
  25. package/dist/infrastructure/evaluation/store.js +40 -0
  26. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  27. package/dist/task/config-types.js +28 -1
  28. package/dist/task/runtime.js +27 -27
  29. package/dist/worker/cli.js +96 -1
  30. package/dist/worker/delivery/package.js +3 -3
  31. package/dist/worker/feature/decision-loader.js +37 -6
  32. package/dist/worker/feature/next-action.js +10 -2
  33. package/dist/worker/feature/ready-plan-projection.js +81 -0
  34. package/dist/worker/feature/reducer.js +2 -1
  35. package/dist/worker/feature/review.js +19 -2
  36. package/dist/worker/feature/run.js +27 -2
  37. package/dist/worker/follow-up/approve.js +5 -2
  38. package/dist/worker/follow-up/factory.js +1 -1
  39. package/dist/worker/observability/read-model.js +246 -41
  40. package/dist/worker/observe/routes.js +173 -15
  41. package/dist/worker/observe/spec-evidence.js +281 -0
  42. package/dist/worker/observe/static/api.js +46 -27
  43. package/dist/worker/observe/static/app.js +150 -150
  44. package/dist/worker/observe/static/constants.js +148 -148
  45. package/dist/worker/observe/static/copy.js +67 -67
  46. package/dist/worker/observe/static/dag-helpers.js +172 -172
  47. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  48. package/dist/worker/observe/static/dag-layout.js +83 -83
  49. package/dist/worker/observe/static/dag-model.js +72 -72
  50. package/dist/worker/observe/static/dom.js +61 -61
  51. package/dist/worker/observe/static/format-pool.js +67 -67
  52. package/dist/worker/observe/static/format.js +292 -292
  53. package/dist/worker/observe/static/index.html +308 -308
  54. package/dist/worker/observe/static/kpi.js +94 -94
  55. package/dist/worker/observe/static/relations.js +133 -128
  56. package/dist/worker/observe/static/router.js +93 -85
  57. package/dist/worker/observe/static/run-processing.js +148 -148
  58. package/dist/worker/observe/static/shell-chrome.js +68 -68
  59. package/dist/worker/observe/static/state.js +253 -253
  60. package/dist/worker/observe/static/styles.css +1902 -1890
  61. package/dist/worker/observe/static/views/batch.js +227 -226
  62. package/dist/worker/observe/static/views/dag-graph.js +172 -172
  63. package/dist/worker/observe/static/views/dag-inspector.js +607 -477
  64. package/dist/worker/observe/static/views/dag.js +362 -362
  65. package/dist/worker/observe/static/views/dashboard.js +445 -442
  66. package/dist/worker/observe/static/views/failures.js +143 -143
  67. package/dist/worker/observe/static/views/feature.js +492 -453
  68. package/dist/worker/observe/static/views/pool.js +350 -347
  69. package/dist/worker/observe/static/views/run.js +453 -453
  70. package/dist/worker/observe/static/views/session-timeline.js +205 -205
  71. package/dist/worker/observe/static/views/shell.js +7 -7
  72. package/dist/worker/observe/static/views/task.js +314 -260
  73. package/dist/worker/observe/static/views/timeline.js +163 -163
  74. package/dist/worker/pool/doctor.js +165 -0
  75. package/dist/worker/pool/migrate-state.js +303 -0
  76. package/dist/worker/pool/run-store.js +205 -17
  77. package/dist/worker/pool/types.js +17 -1
  78. package/dist/worker/pool/validation.js +100 -15
  79. package/dist/worker/report/morning-report.js +12 -2
  80. package/dist/worker/runner/run-ready.js +41 -26
  81. package/dist/worker/task-graph/ready-planner.js +136 -0
  82. package/dist/workflows/dag/backend-test-analysis-contract.js +120 -0
  83. package/dist/workflows/dag/canvas-observer.js +275 -275
  84. package/dist/workflows/dag/convergence/controller.js +16 -8
  85. package/dist/workflows/dag/dynamic-runtime/map.js +90 -2
  86. package/dist/workflows/dag/failure-routing.js +12 -1
  87. package/dist/workflows/dag/init-hybrid.js +2404 -360
  88. package/dist/workflows/dag/node-execution.js +9 -0
  89. package/dist/workflows/dag/prompt.js +9 -0
  90. package/dist/workflows/dag/report.js +35 -1
  91. package/dist/workflows/dag/runner.js +28 -2
  92. package/dist/workflows/dag/task-demand-routing.js +383 -0
  93. package/dist/workflows/dag/types.js +51 -13
  94. package/dist/workflows/dag/upstream-artifacts.js +1 -0
  95. package/dist/workflows/dag/validate.js +59 -1
  96. package/docs/README.md +106 -104
  97. package/docs/agent-dag-recovery-playbook.md +195 -184
  98. package/docs/agent-dag-runner.md +67 -67
  99. package/docs/architecture/README.md +26 -26
  100. package/docs/architecture/dag-execution.md +140 -140
  101. package/docs/architecture/evolution.md +54 -53
  102. package/docs/architecture/facts-and-state.md +71 -58
  103. package/docs/architecture/runtime-boundaries.md +191 -191
  104. package/docs/architecture/system-overview.md +93 -93
  105. package/docs/architecture/worker-and-feature.md +85 -81
  106. package/docs/cursor-prompt-sidecar.md +36 -36
  107. package/docs/decisions/README.md +18 -15
  108. package/docs/design/README.md +167 -77
  109. package/docs/development-principles.md +73 -73
  110. package/docs/exec-plans/README.md +6 -6
  111. package/docs/exec-plans/active/README.md +15 -9
  112. package/docs/exec-plans/completed/README.md +85 -73
  113. package/docs/feature-workflow.md +389 -261
  114. package/docs/harness-methodology-debugging.md +153 -153
  115. package/docs/harness-methodology-tdd.md +130 -130
  116. package/docs/harness-methodology-verification.md +27 -27
  117. package/docs/init-surface.manifest.json +289 -280
  118. package/docs/loop-agent-harness.md +142 -130
  119. package/docs/production-readiness.md +96 -96
  120. package/docs/progress/README.md +64 -54
  121. package/docs/reports/README.md +117 -94
  122. package/docs/skills/README.md +7 -7
  123. package/docs/skills/vetted-skill-registry.md +29 -27
  124. package/docs/templates/adr.md +60 -60
  125. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  126. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  127. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  128. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  129. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  130. package/docs/templates/agent-dag-report.schema.json +473 -473
  131. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  132. package/docs/templates/agent-dag.base.json +190 -190
  133. package/docs/templates/agent-dag.final-verification.json +185 -185
  134. package/docs/templates/agent-dag.schema.json +411 -383
  135. package/docs/templates/agent-dag.supervised-implementation.json +501 -501
  136. package/docs/templates/backend-test-analysis.schema.json +44 -0
  137. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +202 -139
  138. package/docs/templates/backend-test-dag.json +311 -276
  139. package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -125
  140. package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -81
  141. package/docs/templates/exec-plan.md +64 -64
  142. package/docs/templates/feature-spec.md +53 -53
  143. package/docs/templates/frontend-design-contract.md +42 -33
  144. package/docs/templates/frontend-task-constraints.md +35 -25
  145. package/docs/templates/frontend-task-requirement.md +70 -61
  146. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -0
  147. package/docs/templates/frontend-test-dag.json +23 -0
  148. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -0
  149. package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -0
  150. package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -0
  151. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -0
  152. package/docs/templates/harness.schema.json +221 -221
  153. package/docs/templates/hybrid-dag.json +188 -188
  154. package/docs/templates/init-evolution-review.md +35 -35
  155. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  156. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
  157. package/docs/templates/knowledge-sync-dag.json +178 -0
  158. package/docs/templates/knowledge-sync-draft.schema.json +71 -0
  159. package/docs/templates/product-line/AGENTS.md +8 -8
  160. package/docs/templates/product-line/README.md +9 -9
  161. package/docs/templates/product-line/acceptance.yaml +14 -14
  162. package/docs/templates/product-line/closeout.yaml +9 -9
  163. package/docs/templates/product-line/design.md +13 -13
  164. package/docs/templates/product-line/links.md +10 -10
  165. package/docs/templates/product-line/requirement.md +17 -17
  166. package/docs/templates/product-line/task-graph.yaml +15 -15
  167. package/docs/templates/product-line/task.yaml +64 -64
  168. package/docs/templates/product-line/test-plan.md +7 -7
  169. package/docs/templates/production-readiness-checklist.md +57 -57
  170. package/docs/templates/progress-log.md +17 -17
  171. package/docs/templates/project-start-checklist.md +9 -9
  172. package/docs/templates/qa-report.md +48 -48
  173. package/docs/templates/sprint-contract.md +29 -29
  174. package/docs/templates/worker-dogfood-evidence.md +80 -80
  175. package/docs/templates/worker-dogfood-setup.md +68 -68
  176. package/docs/verification-matrix.md +70 -66
  177. package/examples/decision-gate-agent-dag.json +177 -177
  178. package/examples/example-dag.json +46 -46
  179. package/examples/hybrid-loop-agent-dag.json +189 -189
  180. package/harness.json +66 -66
  181. package/package.json +88 -46
  182. package/scripts/check-product-line-docs.sh +29 -29
  183. package/scripts/check-task-pool-root.sh +32 -32
  184. package/scripts/kb-bootstrap-init-skeleton.sh +240 -0
  185. package/scripts/kb-graph-incremental-prepare.mjs +386 -0
  186. package/scripts/kb-graph-incremental-prepare.sh +5 -0
  187. package/scripts/kb-graph-materialize.mjs +105 -0
  188. package/scripts/kb-graph-materialize.sh +4 -0
  189. package/scripts/kb-graph-promote.mjs +164 -0
  190. package/scripts/kb-graph-promote.sh +4 -0
  191. package/scripts/kb-query.mjs +554 -0
  192. package/scripts/kb-query.sh +5 -0
  193. package/skills/agent-worker/SKILL.md +39 -37
  194. package/skills/agent-worker/references/agent-worker-operator.md +60 -43
  195. package/skills/ai-engineering-context/SKILL.md +48 -48
  196. package/skills/analyze-product-dependencies/SKILL.md +67 -0
  197. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -0
  198. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -0
  199. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -0
  200. package/skills/analyze-product-dependencies/references/example.md +76 -0
  201. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -0
  202. package/skills/analyze-product-dependencies/references/input-contract.md +11 -0
  203. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -0
  204. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -0
  205. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -0
  206. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -0
  207. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -0
  208. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -0
  209. package/skills/analyze-product-requirements/SKILL.md +90 -0
  210. package/skills/analyze-product-requirements/agents/openai.yaml +4 -0
  211. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -0
  212. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -0
  213. package/skills/analyze-product-requirements/references/example.md +86 -0
  214. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -0
  215. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -0
  216. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -0
  217. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -0
  218. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -0
  219. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -0
  220. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -0
  221. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -0
  222. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -0
  223. package/skills/code-review-core/SKILL.md +20 -20
  224. package/skills/codebase-scout/SKILL.md +19 -19
  225. package/skills/frontend-design-review/SKILL.md +66 -59
  226. package/skills/frontend-design-review/references/review-checklist.md +58 -37
  227. package/skills/frontend-implementation/SKILL.md +47 -51
  228. package/skills/frontend-implementation/references/code-standards.md +32 -34
  229. package/skills/frontend-implementation/references/design-spec.md +46 -46
  230. package/skills/frontend-implementation/references/node-contracts.md +76 -32
  231. package/skills/frontend-review/SKILL.md +59 -53
  232. package/skills/frontend-review/references/review-findings.md +47 -42
  233. package/skills/frontend-verification/SKILL.md +53 -40
  234. package/skills/frontend-verification/references/verification-checklist.md +68 -56
  235. package/skills/grill-me/SKILL.md +10 -10
  236. package/skills/grill-with-docs/SKILL.md +88 -88
  237. package/skills/grill-with-docs/adr-format.md +47 -47
  238. package/skills/grill-with-docs/context-format.md +60 -60
  239. package/skills/init-capability-evolution/SKILL.md +70 -70
  240. package/skills/loop-agent/SKILL.md +151 -151
  241. package/skills/loop-agent/references/README.md +67 -67
  242. package/skills/loop-agent/references/command-reference.md +505 -452
  243. package/skills/loop-agent/references/docs-converge.md +126 -126
  244. package/skills/loop-agent/references/harness-policy.md +263 -263
  245. package/skills/loop-agent/references/hybrid-dag.md +238 -233
  246. package/skills/loop-agent/references/learned/README.md +21 -21
  247. package/skills/loop-agent/references/long-running-loop.md +57 -57
  248. package/skills/loop-agent/references/model-routing.md +36 -36
  249. package/skills/loop-agent/references/multi-worktree.md +54 -54
  250. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  251. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  252. package/skills/loop-agent/references/pi-prompt.md +23 -23
  253. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  254. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  255. package/skills/loop-agent/references/task-workflow.md +89 -89
  256. package/skills/loop-agent/references/verification-and-failure-handling.md +139 -139
  257. package/skills/playwright-cli/SKILL.md +420 -0
  258. package/skills/playwright-cli/references/element-attributes.md +23 -0
  259. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  260. package/skills/playwright-cli/references/request-mocking.md +87 -0
  261. package/skills/playwright-cli/references/running-code.md +241 -0
  262. package/skills/playwright-cli/references/session-management.md +225 -0
  263. package/skills/playwright-cli/references/storage-state.md +275 -0
  264. package/skills/playwright-cli/references/test-generation.md +433 -0
  265. package/skills/playwright-cli/references/tracing.md +139 -0
  266. package/skills/playwright-cli/references/video-recording.md +143 -0
  267. package/skills/playwright-cli-case-generator/SKILL.md +74 -0
  268. package/skills/requesting-code-review/SKILL.md +101 -101
  269. package/skills/requesting-code-review/code-reviewer.md +168 -168
  270. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  271. package/skills/systematic-debugging/SKILL.md +296 -296
  272. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  273. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  274. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  275. package/skills/systematic-debugging/find-polluter.sh +63 -63
  276. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  277. package/skills/systematic-debugging/test-academic.md +14 -14
  278. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  279. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  280. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  281. package/skills/test-driven-development/SKILL.md +20 -20
  282. package/skills/using-git-worktrees/SKILL.md +215 -215
  283. package/skills/verification-before-completion/SKILL.md +154 -154
  284. package/skills/webapp-testing/SKILL.md +19 -19
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { spawnSync } from "node:child_process";
8
+
9
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
10
+ const temp = mkdtempSync(join(tmpdir(), "product-requirement-v3-validators-"));
11
+ const projectRoot = join(temp, "project");
12
+ const artifactDir = join(projectRoot, "docs", "product-analysis", "nickname");
13
+ mkdirSync(artifactDir, { recursive: true });
14
+ const analysisValidator = join(scriptDir, "validate-product-analysis.mjs");
15
+ const clarificationValidator = join(scriptDir, "validate-requirement-clarification.mjs");
16
+ const requirementValidator = join(scriptDir, "validate-product-requirement.mjs");
17
+
18
+ const run = (script, args) => spawnSync(process.execPath, [script, ...args], { encoding: "utf8" });
19
+ function pass(name, result) {
20
+ if (result.status !== 0) throw new Error(`${name} should pass:\n${result.stderr}${result.stdout}`);
21
+ }
22
+ function fail(name, result, message) {
23
+ if (result.status === 0) throw new Error(`${name} should fail.`);
24
+ if (message && !`${result.stderr}${result.stdout}`.includes(message)) throw new Error(`${name} should mention ${message}:\n${result.stderr}${result.stdout}`);
25
+ }
26
+ function variant(name, text) {
27
+ const path = join(artifactDir, name);
28
+ writeFileSync(path, text);
29
+ return path;
30
+ }
31
+
32
+ const analysisPath = join(artifactDir, "product-analysis.md");
33
+ const clarificationPath = join(artifactDir, "requirement-clarification.md");
34
+ const requirementPath = join(artifactDir, "product-requirement.md");
35
+
36
+ const analysis = `---
37
+ artifact_version: "3.0"
38
+ artifact_type: product-analysis
39
+ requirement_id: nickname
40
+ project_root: ../../..
41
+ analysis_scope: frontend
42
+ analysis_status: no-clarification-required
43
+ source_requirement: inline
44
+ repository_root: none
45
+ ---
46
+ # Product Analysis
47
+ ## 1. 原始需求
48
+ 登录用户可以修改昵称,长度为 2–20 个字符。
49
+ ## 2. 需求概述
50
+ 在个人资料页维护展示昵称。
51
+ ## 3. 业务目标
52
+ - 允许用户维护展示名称。
53
+ ## 4. 需求分析
54
+ ### 4.1 明确需求
55
+ - 用户只能修改自己的昵称。
56
+ ### 4.2 推断需求
57
+ - 无未经确认的推断。
58
+ ### 4.3 待确认问题
59
+ - 无。范围、权限和结果已经明确。
60
+ ### 4.4 初步非目标
61
+ - 不修改头像。
62
+ ## 5. 外部事实
63
+ ### 5.1 知识库事实
64
+ - 未集成知识库。
65
+ ### 5.2 代码库事实
66
+ - 未提供代码仓库。
67
+ ## 6. 初步前端用户故事
68
+ ### FE-US-001 修改昵称
69
+ - 角色:已登录用户
70
+ - 目标:修改自己的展示昵称
71
+ - 价值:保持个人资料准确
72
+ - 入口:个人资料页
73
+ - 验收关注点:合法值可保存;非法值不可提交;失败时保留输入
74
+ ## 7. 初步前端输出规范
75
+ ### FE-US-001 修改昵称
76
+ - 页面/组件:个人资料页、昵称编辑表单
77
+ - 展示内容:当前昵称、字符计数、校验提示
78
+ - 交互动作:编辑、保存、取消、重试
79
+ - UI 状态:normal、dirty、loading、success、error、disabled
80
+ - 表单校验:昵称长度为 2–20 个字符
81
+ - 权限可见性:仅当前登录用户
82
+ - 边界处理:失败时保留输入,不得静默失败
83
+ `;
84
+
85
+ const clarification = `---
86
+ artifact_version: "3.0"
87
+ artifact_type: requirement-clarification
88
+ requirement_id: nickname
89
+ project_root: ../../..
90
+ analysis_scope: frontend
91
+ clarification_status: complete
92
+ clarification_rounds: 0
93
+ source_product_analysis: ./product-analysis.md
94
+ target_product_requirement: ./product-requirement.md
95
+ ---
96
+ # Requirement Clarification
97
+ ## 1. 澄清结论
98
+ - 状态:no-clarification-required
99
+ - 原因:范围、权限、数据语义和验收结果已经明确。
100
+ ## 2. 来源
101
+ - Product Analysis:./product-analysis.md
102
+ ## 3. 合并结果
103
+ - Product Requirement 根据明确需求生成,无额外决策标记。
104
+ `;
105
+
106
+ const requirement = `---
107
+ artifact_version: "3.0"
108
+ artifact_type: product-requirement
109
+ requirement_id: nickname
110
+ project_root: ../../..
111
+ requirement_status: complete
112
+ analysis_scope: frontend
113
+ source_requirement: inline
114
+ source_product_analysis: ./product-analysis.md
115
+ source_clarification: ./requirement-clarification.md
116
+ ---
117
+ # Product Requirement
118
+ ## 1. 需求概述
119
+ 在个人资料页维护当前用户的展示昵称。
120
+ ## 2. 业务目标
121
+ - 允许用户维护准确的展示名称。
122
+ ## 3. 需求范围
123
+ ### 3.1 已确认需求
124
+ - 当前用户可以修改自己的昵称。
125
+ ### 3.2 非目标
126
+ - 不修改头像。
127
+ ### 3.3 默认假设
128
+ - 无未经确认的默认假设。
129
+ ### 3.4 未决事项
130
+ - 无。
131
+ ## 4. 业务规则
132
+ - 昵称长度必须为 2–20 个字符。
133
+ ## 5. 前端用户故事
134
+ ### FE-US-001 修改昵称
135
+ - 角色:已登录用户
136
+ - 目标:修改自己的展示昵称
137
+ - 价值:保持个人资料准确
138
+ - 入口:个人资料页
139
+ - 验收标准:AC-FE-001
140
+ ## 6. 前端输出规范
141
+ ### FE-US-001 修改昵称
142
+ - 页面/组件:个人资料页、昵称编辑表单
143
+ - 展示内容:当前昵称、字符计数、校验提示
144
+ - 交互动作:编辑、保存、取消、重试
145
+ - UI 状态:normal、dirty、loading、success、error、disabled
146
+ - 表单校验:昵称长度为 2–20 个字符
147
+ - 权限可见性:仅当前登录用户
148
+ - 边界处理:失败时保留输入,不得静默失败
149
+ #### AC-FE-001 保存合法昵称
150
+ Given:
151
+ - 用户已登录并打开个人资料页。
152
+ When:
153
+ - 用户输入合法昵称并点击保存。
154
+ Then:
155
+ - 页面提交昵称修改请求。
156
+ - 保存成功后展示新昵称。
157
+ - 保存期间按钮保持 disabled。
158
+ 异常场景:
159
+ - 保存失败时保留输入并提供重试入口。
160
+ ## 7. 决策追溯
161
+ - 本轮无需澄清决策标记;需求直接来源于明确需求。
162
+ `;
163
+
164
+ writeFileSync(analysisPath, analysis);
165
+ writeFileSync(clarificationPath, clarification);
166
+ writeFileSync(requirementPath, requirement);
167
+
168
+ let checks = 0;
169
+ function expectPass(name, script, args) { pass(name, run(script, args)); checks += 1; }
170
+ function expectFail(name, script, args, message) { fail(name, run(script, args), message); checks += 1; }
171
+
172
+ expectPass("valid V3 analysis", analysisValidator, [analysisPath, "--target", "frontend"]);
173
+ expectFail("analysis target mismatch", analysisValidator, [analysisPath, "--target", "backend"], "does not match");
174
+ expectFail("analysis rejects formal AC", analysisValidator, [variant("analysis-ac.md", analysis.replace("验收关注点:", "验收关注点:AC-FE-001 "))], "must use acceptance concerns");
175
+ 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
+ expectFail("analysis rejects standalone roles", analysisValidator, [variant("analysis-roles.md", analysis.replace("## 4. 需求分析", "## 用户角色\n- 已登录用户。\n## 4. 需求分析"))], "must not contain standalone");
177
+
178
+ 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");
180
+ expectFail("compact clarification rejects extra section", clarificationValidator, [analysisPath, variant("clarification-extra.md", clarification + "\n## 4. 完成门禁\n- 已完成。\n"), requirementPath], "must not contain section");
181
+
182
+ expectPass("valid V3 requirement", requirementValidator, [requirementPath, "--target", "frontend"]);
183
+ expectFail("requirement target mismatch", requirementValidator, [requirementPath, "--target", "backend"], "does not match");
184
+ expectFail("requirement requires role", requirementValidator, [variant("requirement-no-role.md", requirement.replace("- 角色:已登录用户\n", ""))], "missing story field 角色");
185
+ 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
+ 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");
187
+ expectFail("requirement rejects standalone roles", requirementValidator, [variant("requirement-roles.md", requirement.replace("## 3. 需求范围", "## 用户角色\n- 已登录用户。\n## 3. 需求范围"))], "must not contain standalone");
188
+ 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"));
190
+ expectPass("pending requirement allowed explicitly", requirementValidator, [requirementPath, "--allow-pending"]);
191
+ writeFileSync(requirementPath, requirement);
192
+
193
+ console.log(`Requirement V3 validator matrix passed: ${checks} checks.`);
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { assertHeadings, field, loadMarkdown, metadata, outputSpecBlocks, printResult, section, storyBlocks, validateArtifactLocation, validateStorySpecCoverage } from "./validation-helpers.mjs";
4
+
5
+ const args = process.argv.slice(2);
6
+ const fileArg = args[0];
7
+ const targetIndex = args.indexOf("--target");
8
+ const expectedTarget = targetIndex >= 0 ? args[targetIndex + 1] : undefined;
9
+ if (!fileArg) {
10
+ console.error("Usage: node validate-product-analysis.mjs <product-analysis.md> [--target frontend|backend|both]");
11
+ process.exit(2);
12
+ }
13
+
14
+ let artifact;
15
+ try { artifact = loadMarkdown(fileArg, "Product Analysis"); }
16
+ catch (error) { console.error(error.message); process.exit(2); }
17
+
18
+ const { path, text } = artifact;
19
+ const errors = [];
20
+ validateArtifactLocation(artifact, errors, "product-analysis.md");
21
+ const scope = metadata(text, "analysis_scope");
22
+ const status = metadata(text, "analysis_status");
23
+
24
+ if (metadata(text, "artifact_version") !== "3.0") errors.push("artifact_version must be 3.0.");
25
+ if (metadata(text, "artifact_type") !== "product-analysis") errors.push("artifact_type must be product-analysis.");
26
+ if (!["frontend", "backend", "both"].includes(scope)) errors.push("analysis_scope must be frontend, backend, or both.");
27
+ if (targetIndex >= 0 && !["frontend", "backend", "both"].includes(expectedTarget)) errors.push("--target must be frontend, backend, or both.");
28
+ else if (expectedTarget && scope !== expectedTarget) errors.push(`analysis_scope ${scope} does not match requested target ${expectedTarget}.`);
29
+ 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
+
33
+ assertHeadings(text, 2, ["原始需求", "需求概述", "业务目标", "需求分析", "外部事实"], errors, "Product Analysis");
34
+ const analysis = section(text, 2, "需求分析") ?? "";
35
+ assertHeadings(analysis, 3, ["明确需求", "推断需求", "待确认问题", "初步非目标"], errors, "需求分析");
36
+ const facts = section(text, 2, "外部事实") ?? "";
37
+ assertHeadings(facts, 3, ["知识库事实", "代码库事实"], errors, "外部事实");
38
+
39
+ const questions = section(analysis, 3, "待确认问题") ?? "";
40
+ 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
+ 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
+ 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.");
43
+ if (/^##\s+(?:\d+[.、]?\s*)?(?:用户角色|初步验收标准汇总)\s*$/m.test(text)) errors.push("Product Analysis must not contain standalone 用户角色 or 初步验收标准汇总 sections.");
44
+
45
+ function validateDomain(domain) {
46
+ const frontend = domain === "frontend";
47
+ const title = frontend ? "前端" : "后端";
48
+ const prefix = frontend ? "FE-US" : "BE-US";
49
+ const storyHeading = `初步${title}用户故事`;
50
+ const specHeading = `初步${title}输出规范`;
51
+ assertHeadings(text, 2, [storyHeading, specHeading], errors, "Product Analysis");
52
+ const stories = storyBlocks(section(text, 2, storyHeading), prefix);
53
+ const specs = outputSpecBlocks(section(text, 2, specHeading), prefix);
54
+ if (!stories.length) errors.push(`${storyHeading} must contain at least one ${prefix}-* story.`);
55
+ const storyFields = frontend ? ["角色", "目标", "价值", "入口", "验收关注点"] : ["系统能力", "使用方", "业务价值", "触发方式", "验收关注点"];
56
+ const specFields = frontend ? ["页面/组件", "展示内容", "交互动作", "UI 状态", "表单校验", "权限可见性", "边界处理"] : ["输入语义", "输出语义", "数据读写", "权限规则", "业务规则", "安全要求", "幂等与并发", "错误与边界"];
57
+ for (const story of stories) {
58
+ 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.`);
60
+ }
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
+ validateStorySpecCoverage(stories, specs, errors, `${title} scope`);
63
+ }
64
+
65
+ if (scope === "frontend" || scope === "both") validateDomain("frontend");
66
+ if (scope === "backend" || scope === "both") validateDomain("backend");
67
+ if (scope === "frontend" && /(?:^##\s+.*后端|^###\s+BE-US-)/m.test(text)) errors.push("frontend scope must not contain backend sections or stories.");
68
+ if (scope === "backend" && /(?:^##\s+.*前端|^###\s+FE-US-)/m.test(text)) errors.push("backend scope must not contain frontend sections or stories.");
69
+ printResult("Product Analysis", path, errors);
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import { acceptanceBlocks, assertHeadings, containsUnresolvedBlockingPriority, field, loadMarkdown, metadata, outputSpecBlocks, printResult, section, storyBlocks, validateAcceptance, validateArtifactLocation, validateStorySpecCoverage } from "./validation-helpers.mjs";
6
+
7
+ const args = process.argv.slice(2);
8
+ const allowPending = args.includes("--allow-pending");
9
+ const targetIndex = args.indexOf("--target");
10
+ const expectedTarget = targetIndex >= 0 ? args[targetIndex + 1] : undefined;
11
+ const fileArg = args.find((arg, index) => !arg.startsWith("--") && (targetIndex < 0 || index !== targetIndex + 1));
12
+ if (!fileArg) {
13
+ console.error("Usage: node validate-product-requirement.mjs <product-requirement.md> [--target frontend|backend|both] [--allow-pending]");
14
+ process.exit(2);
15
+ }
16
+
17
+ let artifact;
18
+ try { artifact = loadMarkdown(fileArg, "Product Requirement"); }
19
+ catch (error) { console.error(error.message); process.exit(2); }
20
+
21
+ const { path, text } = artifact;
22
+ const errors = [];
23
+ validateArtifactLocation(artifact, errors, "product-requirement.md");
24
+ const scope = metadata(text, "analysis_scope");
25
+ const status = metadata(text, "requirement_status");
26
+ if (metadata(text, "artifact_version") !== "3.0") errors.push("artifact_version must be 3.0.");
27
+ if (metadata(text, "artifact_type") !== "product-requirement") errors.push("artifact_type must be product-requirement.");
28
+ if (!["frontend", "backend", "both"].includes(scope)) errors.push("analysis_scope must be frontend, backend, or both.");
29
+ if (targetIndex >= 0 && !["frontend", "backend", "both"].includes(expectedTarget)) errors.push("--target must be frontend, backend, or both.");
30
+ else if (expectedTarget && scope !== expectedTarget) errors.push(`analysis_scope ${scope} does not match requested target ${expectedTarget}.`);
31
+ if (!["pending", "complete"].includes(status)) errors.push("requirement_status must be pending or complete.");
32
+ if (!allowPending && status !== "complete") errors.push("Final Product Requirement must be complete.");
33
+
34
+ function sourcePath(name) {
35
+ const value = metadata(text, name);
36
+ if (!value || ["inline", "none"].includes(value)) return value;
37
+ return resolve(dirname(path), value);
38
+ }
39
+ for (const name of ["source_requirement", "source_product_analysis", "source_clarification"]) {
40
+ const value = sourcePath(name);
41
+ if (!value) errors.push(`${name} is required.`);
42
+ else if (!["inline", "none"].includes(value) && !existsSync(value)) errors.push(`${name} does not exist: ${value}`);
43
+ }
44
+ for (const name of ["source_product_analysis", "source_clarification"]) {
45
+ const value = sourcePath(name);
46
+ if (value && !["inline", "none"].includes(value) && dirname(value) !== dirname(path)) errors.push(`${name} must reference an artifact in the same requirement directory.`);
47
+ }
48
+
49
+ assertHeadings(text, 2, ["需求概述", "业务目标", "需求范围", "业务规则", "决策追溯"], errors, "Product Requirement");
50
+ const range = section(text, 2, "需求范围") ?? "";
51
+ assertHeadings(range, 3, ["已确认需求", "非目标", "默认假设", "未决事项"], errors, "需求范围");
52
+ if (status === "complete" && containsUnresolvedBlockingPriority(section(range, 3, "未决事项") ?? "")) errors.push("Complete Product Requirement cannot contain unresolved P0/P1 items.");
53
+ if (/^##\s+(?:\d+[.、]?\s*)?(?:用户角色|验收标准汇总)\s*$/m.test(text)) errors.push("Product Requirement must not contain standalone 用户角色 or 验收标准汇总 sections.");
54
+
55
+ function validateDomain(domain) {
56
+ const frontend = domain === "frontend";
57
+ const title = frontend ? "前端" : "后端";
58
+ const prefix = frontend ? "FE-US" : "BE-US";
59
+ const acPrefix = frontend ? "AC-FE" : "AC-BE";
60
+ const storyHeading = `${title}用户故事`;
61
+ const specHeading = `${title}输出规范`;
62
+ assertHeadings(text, 2, [storyHeading, specHeading], errors, "Product Requirement");
63
+ const stories = storyBlocks(section(text, 2, storyHeading), prefix);
64
+ const specs = outputSpecBlocks(section(text, 2, specHeading), prefix);
65
+ if (!stories.length) errors.push(`${storyHeading} must contain at least one ${prefix}-* story.`);
66
+ validateStorySpecCoverage(stories, specs, errors, `${title} scope`);
67
+ const storyFields = frontend ? ["角色", "目标", "价值", "入口", "验收标准"] : ["系统能力", "使用方", "业务价值", "触发方式", "验收标准"];
68
+ const specFields = frontend ? ["页面/组件", "展示内容", "交互动作", "UI 状态", "表单校验", "权限可见性", "边界处理"] : ["输入语义", "输出语义", "数据读写", "权限规则", "业务规则", "安全要求", "幂等与并发", "错误与边界"];
69
+ for (const story of stories) {
70
+ for (const name of storyFields) if (!field(story.text, name)) errors.push(`${story.id} is missing story field ${name}.`);
71
+ if (!frontend && !/^(?:API|定时任务|事件|消息|内部调用|数据迁移)(?:$|[、,,/])/i.test(field(story.text, "触发方式") ?? "")) errors.push(`${story.id} 触发方式 is invalid.`);
72
+ const spec = specs.find((item) => item.id === story.id);
73
+ if (!spec) continue;
74
+ for (const name of specFields) if (!field(spec.text, name)) errors.push(`${spec.id} output specification is missing field ${name}.`);
75
+ const criteria = acceptanceBlocks(spec, acPrefix);
76
+ if (!criteria.length) errors.push(`${spec.id} output specification must contain at least one ${acPrefix}-* acceptance criterion.`);
77
+ for (const item of criteria) validateAcceptance(item.text, item.id, errors);
78
+ const referenced = [...(field(story.text, "验收标准") ?? "").matchAll(new RegExp(`${acPrefix}-\\d{3,}`, "g"))].map((item) => item[0]).sort();
79
+ const actual = criteria.map((item) => item.id).sort();
80
+ if (referenced.join() !== actual.join()) errors.push(`${story.id} acceptance references must match its output specification criteria.`);
81
+ }
82
+ }
83
+
84
+ if (scope === "frontend" || scope === "both") validateDomain("frontend");
85
+ if (scope === "backend" || scope === "both") validateDomain("backend");
86
+ if (scope === "frontend" && /(?:^##\s+.*后端|^###\s+BE-US-)/m.test(text)) errors.push("frontend scope must not contain backend sections or stories.");
87
+ if (scope === "backend" && /(?:^##\s+.*前端|^###\s+FE-US-)/m.test(text)) errors.push("backend scope must not contain frontend sections or stories.");
88
+
89
+ const clarificationSource = sourcePath("source_clarification");
90
+ if (status === "complete" && clarificationSource && !["inline", "none"].includes(clarificationSource) && existsSync(clarificationSource)) {
91
+ const clarification = readFileSync(clarificationSource, "utf8");
92
+ if (metadata(clarification, "clarification_status") !== "complete") errors.push("Complete Product Requirement requires complete source clarification.");
93
+ const markers = [...clarification.matchAll(/^-\s*决策标记[::]\s*(DEC-Q-\d{3,})/gm)].map((item) => item[1]);
94
+ for (const marker of markers) if (!text.includes(marker)) errors.push(`${marker} from source clarification is missing from Product Requirement.`);
95
+ }
96
+
97
+ printResult("Product Requirement", path, errors);
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import { assertHeadings, canonical, field, headings, loadMarkdown, metadata, printResult, section, validateArtifactLocation } from "./validation-helpers.mjs";
6
+
7
+ const args = process.argv.slice(2);
8
+ const allowPending = args.includes("--allow-pending");
9
+ const positional = args.filter((arg) => !arg.startsWith("--"));
10
+ if (positional.length < 3) {
11
+ console.error("Usage: node validate-requirement-clarification.mjs <product-analysis.md> <requirement-clarification.md> <product-requirement.md> [--allow-pending]");
12
+ process.exit(2);
13
+ }
14
+
15
+ let analysis, clarification, requirement;
16
+ try {
17
+ analysis = loadMarkdown(positional[0], "Product Analysis");
18
+ clarification = loadMarkdown(positional[1], "Requirement Clarification");
19
+ requirement = loadMarkdown(positional[2], "Product Requirement");
20
+ } catch (error) { console.error(error.message); process.exit(2); }
21
+
22
+ const errors = [];
23
+ validateArtifactLocation(clarification, errors, "requirement-clarification.md");
24
+ const scope = metadata(clarification.text, "analysis_scope");
25
+ const status = metadata(clarification.text, "clarification_status");
26
+ const clarificationRounds = Number(metadata(clarification.text, "clarification_rounds"));
27
+ if (metadata(clarification.text, "artifact_version") !== "3.0") errors.push("artifact_version must be 3.0.");
28
+ if (metadata(clarification.text, "artifact_type") !== "requirement-clarification") errors.push("artifact_type must be requirement-clarification.");
29
+ if (!["frontend", "backend", "both"].includes(scope)) errors.push("analysis_scope must be frontend, backend, or both.");
30
+ if (!["pending", "complete"].includes(status)) errors.push("clarification_status must be pending or complete.");
31
+ if (!Number.isInteger(clarificationRounds) || clarificationRounds < 0 || clarificationRounds > 3) errors.push("clarification_rounds must be an integer from 0 to 3.");
32
+ if (!allowPending && status !== "complete") errors.push("Final Requirement Clarification must be complete.");
33
+ if (scope !== metadata(analysis.text, "analysis_scope")) errors.push("analysis_scope must match Product Analysis.");
34
+
35
+ const resolveRef = (value) => value ? resolve(dirname(clarification.path), value) : "";
36
+ const source = metadata(clarification.text, "source_product_analysis");
37
+ const target = metadata(clarification.text, "target_product_requirement");
38
+ if (resolveRef(source) !== analysis.path) errors.push("source_product_analysis must reference the supplied Product Analysis.");
39
+ if (resolveRef(target) !== requirement.path) errors.push("target_product_requirement must reference the supplied Product Requirement.");
40
+ if (source && dirname(resolveRef(source)) !== dirname(clarification.path)) errors.push("source_product_analysis must be in the same requirement directory.");
41
+ if (target && dirname(resolveRef(target)) !== dirname(clarification.path)) errors.push("target_product_requirement must be in the same requirement directory.");
42
+ if (source && !existsSync(resolveRef(source))) errors.push(`source_product_analysis does not exist: ${resolveRef(source)}`);
43
+ if (target && !existsSync(resolveRef(target))) errors.push(`target_product_requirement does not exist: ${resolveRef(target)}`);
44
+
45
+ const analysisStatus = metadata(analysis.text, "analysis_status");
46
+ const questionBlocks = headings(clarification.text, 3).filter((item) => /^Q-\d{3,}\s+/.test(item.title));
47
+
48
+ if (!questionBlocks.length) {
49
+ if (clarificationRounds !== 0) errors.push("Compact clarification must set clarification_rounds to 0.");
50
+ if (analysisStatus !== "no-clarification-required") errors.push("Compact clarification is allowed only when Product Analysis is no-clarification-required.");
51
+ const required = ["澄清结论", "来源", "合并结果"];
52
+ assertHeadings(clarification.text, 2, required, errors, "Compact Requirement Clarification");
53
+ for (const heading of headings(clarification.text, 2)) if (!required.includes(canonical(heading.title))) errors.push(`Compact Requirement Clarification must not contain section ${canonical(heading.title)}.`);
54
+ if (!/状态[::]\s*no-clarification-required/.test(section(clarification.text, 2, "澄清结论") ?? "")) errors.push("Compact clarification must declare no-clarification-required.");
55
+ if (/\b(?:BR|Q|DEC-Q)-\d{3,}\b/.test(clarification.text)) errors.push("Compact clarification must not fabricate BR, Q, or DEC-Q markers.");
56
+ } else {
57
+ if (clarificationRounds < 1 || clarificationRounds > 3) errors.push("Question-based clarification must set clarification_rounds from 1 to 3.");
58
+ if (analysisStatus !== "ready-for-clarification") errors.push("Question records require Product Analysis status ready-for-clarification.");
59
+ assertHeadings(clarification.text, 2, ["澄清来源", "决策分支", "问题记录", "决策索引"], errors, "Requirement Clarification");
60
+ const branchSection = section(clarification.text, 2, "决策分支") ?? "";
61
+ const branchIds = [...new Set(branchSection.match(/\bBR-\d{3,}\b/g) ?? [])];
62
+ if (branchIds.length < 3 || branchIds.length > 6) errors.push("Question-based clarification must define 3–6 top-level BR-* branches.");
63
+ if (status === "complete") {
64
+ const unresolved = branchSection.split(/\r?\n/).filter((line) => /\|\s*BR-\d{3,}\s*\|/.test(line) && !/\|\s*resolved\s*\|\s*$/.test(line));
65
+ if (unresolved.length) errors.push("Complete clarification requires every branch to be resolved.");
66
+ }
67
+ const requiredFields = ["澄清轮次", "分支", "优先级", "影响范围", "推荐答案", "推荐理由", "用户回答", "最终决策", "决策来源", "状态", "决策标记", "目标位置"];
68
+ const markers = new Set();
69
+ for (const block of questionBlocks) {
70
+ const id = block.title.match(/^(Q-\d{3,})/)?.[1];
71
+ for (const name of requiredFields) if (!field(block.text, name)) errors.push(`${id} is missing field ${name}.`);
72
+ const branch = field(block.text, "分支");
73
+ const round = Number(field(block.text, "澄清轮次"));
74
+ const priority = field(block.text, "优先级");
75
+ const impactScope = field(block.text, "影响范围");
76
+ const answer = field(block.text, "用户回答");
77
+ const decisionStatus = field(block.text, "状态");
78
+ const decisionSource = field(block.text, "决策来源");
79
+ if (!branchIds.includes(branch)) errors.push(`${id} 分支 must reference a defined BR-* branch.`);
80
+ if (!Number.isInteger(round) || round < 1 || round > clarificationRounds) errors.push(`${id} 澄清轮次 must be between 1 and clarification_rounds.`);
81
+ if (!["P0", "P1", "P2"].includes(priority)) errors.push(`${id} 优先级 must be P0, P1, or P2.`);
82
+ if (!["common", "frontend", "backend", "both"].includes(impactScope)) errors.push(`${id} 影响范围 is invalid.`);
83
+ if (!["confirmed", "default-confirmed", "pending-blocking", "pending-non-blocking"].includes(decisionStatus)) errors.push(`${id} 状态 is invalid.`);
84
+ if (!["user", "source-requirement", "code-evidence", "confirmed-default"].includes(decisionSource)) errors.push(`${id} 决策来源 is invalid.`);
85
+ const marker = field(block.text, "决策标记");
86
+ if (marker !== `DEC-${id}`) errors.push(`${id} 决策标记 must be DEC-${id}.`);
87
+ if (markers.has(marker)) errors.push(`Duplicate decision marker: ${marker}.`);
88
+ markers.add(marker);
89
+ if (decisionSource === "code-evidence" && !field(block.text, "代码证据")) errors.push(`${id} code-evidence decision requires 代码证据.`);
90
+ if (decisionStatus === "pending-non-blocking" && !field(block.text, "未确认影响")) errors.push(`${id} pending-non-blocking decision requires 未确认影响.`);
91
+ if (status === "complete" && ["P0", "P1"].includes(priority) && (decisionStatus !== "confirmed" || decisionSource !== "user" || /^(?:未回答|待确认|无)$/.test(answer ?? ""))) errors.push(`${id} ${priority} must contain an explicit user confirmation.`);
92
+ if (status === "complete" && marker && !requirement.text.includes(marker)) errors.push(`${marker} is missing from Product Requirement decision traceability.`);
93
+ if (!(section(clarification.text, 2, "决策索引") ?? "").includes(marker ?? "__missing__")) errors.push(`${marker ?? id} is missing from 决策索引.`);
94
+ }
95
+ }
96
+
97
+ if (status === "complete" && metadata(requirement.text, "requirement_status") !== "complete") errors.push("Complete clarification requires a complete Product Requirement.");
98
+ printResult("Requirement Clarification", clarification.path, errors);
@@ -0,0 +1,156 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+
4
+ export function loadMarkdown(fileArg, label = "artifact") {
5
+ const path = resolve(fileArg);
6
+ try {
7
+ return { path, text: readFileSync(path, "utf8") };
8
+ } catch (error) {
9
+ throw new Error(`Cannot read ${label} ${path}: ${error.message}`);
10
+ }
11
+ }
12
+
13
+ export function frontmatter(text) {
14
+ return text.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? "";
15
+ }
16
+
17
+ export function metadata(text, name) {
18
+ const block = frontmatter(text);
19
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20
+ return block.match(new RegExp(`^${escaped}:[ \\t]*["']?([^"'\\r\\n]+)["']?[ \\t]*$`, "m"))?.[1].trim();
21
+ }
22
+
23
+ export function headings(text, level) {
24
+ const prefix = "#".repeat(level);
25
+ const matches = [...text.matchAll(new RegExp(`^${prefix}\\s+(.+)$`, "gm"))];
26
+ return matches.map((match, index) => ({
27
+ title: match[1].trim(),
28
+ start: match.index,
29
+ text: text.slice(match.index, matches[index + 1]?.index ?? text.length),
30
+ }));
31
+ }
32
+
33
+ export function canonical(title) {
34
+ return title.replace(/^\d+(?:\.\d+)*[.、]?[ \t]*/, "").trim();
35
+ }
36
+
37
+ export function section(text, level, title) {
38
+ return headings(text, level).find((item) => canonical(item.title) === title)?.text;
39
+ }
40
+
41
+ export function sections(text, level, title) {
42
+ return headings(text, level).filter((item) => canonical(item.title) === title);
43
+ }
44
+
45
+ export function hasSubstance(block) {
46
+ if (!block) return false;
47
+ return block.split(/\r?\n/).slice(1).some((line) => line.trim() && !/^\|?[-:| ]+\|?$/.test(line.trim()));
48
+ }
49
+
50
+ export function field(block, name) {
51
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
52
+ return block.match(new RegExp(`^-[ \\t]*${escaped}[::][ \\t]*(.+)$`, "m"))?.[1].trim().replace(/[。.]$/, "");
53
+ }
54
+
55
+ export function storyBlocks(block, prefix) {
56
+ if (!block) return [];
57
+ const matches = [...block.matchAll(new RegExp(`^###\\s+(${prefix}-\\d{3,})\\s+(.+)$`, "gm"))];
58
+ return matches.map((match, index) => ({
59
+ id: match[1],
60
+ title: match[2].trim(),
61
+ text: block.slice(match.index, matches[index + 1]?.index ?? block.length),
62
+ }));
63
+ }
64
+
65
+ export function outputSpecBlocks(block, prefix) {
66
+ return storyBlocks(block, prefix);
67
+ }
68
+
69
+ export function validateStorySpecCoverage(stories, specs, errors, label) {
70
+ const storyIds = stories.map((item) => item.id);
71
+ const specIds = specs.map((item) => item.id);
72
+ for (const id of new Set(storyIds)) if (storyIds.filter((value) => value === id).length > 1) errors.push(`Duplicate story ID: ${id}.`);
73
+ for (const id of new Set(specIds)) if (specIds.filter((value) => value === id).length > 1) errors.push(`Duplicate output specification: ${id}.`);
74
+ for (const id of storyIds) if (!specIds.includes(id)) errors.push(`${label} is missing output specification for ${id}.`);
75
+ for (const id of specIds) if (!storyIds.includes(id)) errors.push(`${label} contains output specification for unknown story ${id}.`);
76
+ }
77
+
78
+ export function acceptanceBlocks(story, prefix) {
79
+ const matches = [...story.text.matchAll(new RegExp(`^####\\s+(${prefix}-\\d{3,})\\s+(.+)$`, "gm"))];
80
+ return matches.map((match, index) => ({
81
+ id: match[1],
82
+ title: match[2].trim(),
83
+ text: story.text.slice(match.index, matches[index + 1]?.index ?? story.text.length),
84
+ }));
85
+ }
86
+
87
+ export function validateAcceptance(block, owner, errors) {
88
+ const labels = ["Given", "When", "Then", "异常场景"];
89
+ const positions = labels.map((label) => ({ label, index: block.search(new RegExp(`^${label}[::]`, "m")) }));
90
+ for (const item of positions) if (item.index < 0) errors.push(`${owner} is missing ${item.label}.`);
91
+ if (positions.some((item) => item.index < 0)) return;
92
+ const counts = positions.map((item, index) => {
93
+ const end = positions[index + 1]?.index ?? block.length;
94
+ return (block.slice(item.index, end).match(/^-\s+\S.+$/gm) ?? []).length;
95
+ });
96
+ if (counts[0] < 1 || counts[1] < 1 || counts[2] < 2 || counts[3] < 1) {
97
+ errors.push(`${owner} must contain at least 1 Given, 1 When, 2 Then, and 1 exception bullet.`);
98
+ }
99
+ if (counts.reduce((sum, count) => sum + count, 0) < 6) errors.push(`${owner} must contain at least six concrete bullets.`);
100
+ if (!/(?:不得|不应|禁止|保留|恢复|重试|兜底|回滚|disabled)/.test(block)) {
101
+ errors.push(`${owner} must include an observable protection or recovery result.`);
102
+ }
103
+ }
104
+
105
+ export function containsUnresolvedBlockingPriority(block) {
106
+ if (!block) return false;
107
+ return block.split(/\r?\n/).slice(1).some((line) => {
108
+ const plain = line.trim().replace(/^[-*+]\s*/, "").replace(/^\[[ xX]\]\s*/, "");
109
+ if (!/\bP[01]\b/.test(plain)) return false;
110
+ if (/^(?:不存在|没有|无)(?:任何)?(?:尚未|未)?(?:解决|确认|关闭|完成)?(?:的)?\s*P[01]\b/.test(plain)) return false;
111
+ if (/^P[01]\b/.test(plain)) return true;
112
+ return /(?:pending(?:-blocking)?|unresolved|未解决|未确认|待确认|未决|阻塞)/i.test(plain);
113
+ });
114
+ }
115
+
116
+ export function assertHeadings(text, level, required, errors, owner) {
117
+ for (const title of required) {
118
+ const matches = sections(text, level, title);
119
+ if (matches.length !== 1) errors.push(`${owner} must contain exactly one ${title} heading.`);
120
+ else if (!hasSubstance(matches[0].text)) errors.push(`${owner} section ${title} must not be empty.`);
121
+ }
122
+ }
123
+
124
+ export function printResult(label, path, errors) {
125
+ if (errors.length) {
126
+ console.error(`${label} validation failed (${errors.length}):`);
127
+ for (const error of errors) console.error(`- ${error}`);
128
+ process.exit(1);
129
+ }
130
+ console.log(`${label} validation passed: ${path}`);
131
+ }
132
+
133
+ export function validateArtifactLocation(artifact, errors, expectedName) {
134
+ const requirementId = metadata(artifact.text, "requirement_id");
135
+ const projectRootValue = metadata(artifact.text, "project_root");
136
+ if (!requirementId || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(requirementId)) {
137
+ errors.push("requirement_id must use lowercase letters, digits, and single hyphens.");
138
+ return;
139
+ }
140
+ if (!projectRootValue) {
141
+ errors.push("project_root is required.");
142
+ return;
143
+ }
144
+ const projectRoot = resolve(dirname(artifact.path), projectRootValue);
145
+ if (!existsSync(projectRoot) || !statSync(projectRoot).isDirectory()) {
146
+ errors.push(`project_root does not resolve to an existing directory: ${projectRoot}`);
147
+ return;
148
+ }
149
+ const expected = resolve(join(projectRoot, "docs", "product-analysis", requirementId));
150
+ if (dirname(artifact.path) !== expected) {
151
+ errors.push(`Artifact must be located under ${expected}.`);
152
+ }
153
+ if (expectedName && basename(artifact.path) !== expectedName) {
154
+ errors.push(`Artifact filename must be ${expectedName}.`);
155
+ }
156
+ }