@tea-agent/loop-agent 0.13.0-beta.0 → 0.13.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 (282) hide show
  1. package/AGENTS.md +157 -155
  2. package/CHANGELOG.md +301 -322
  3. package/README.md +335 -345
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/commands/cursor-prompt.js +6 -6
  7. package/dist/commands/init.js +597 -528
  8. package/dist/commands/loop-benchmark.js +11 -11
  9. package/dist/commands/pi-reuse-benchmark.js +16 -16
  10. package/dist/executors/shell-executor.js +200 -21
  11. package/dist/infrastructure/evaluation/candidate-store.js +5 -1
  12. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  13. package/dist/task/runtime.js +27 -27
  14. package/dist/worker/observe/static/api.js +46 -46
  15. package/dist/worker/observe/static/app.js +150 -150
  16. package/dist/worker/observe/static/constants.js +148 -148
  17. package/dist/worker/observe/static/copy.js +67 -67
  18. package/dist/worker/observe/static/dag-helpers.js +172 -172
  19. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  20. package/dist/worker/observe/static/dag-layout.js +83 -83
  21. package/dist/worker/observe/static/dag-model.js +72 -72
  22. package/dist/worker/observe/static/dom.js +212 -53
  23. package/dist/worker/observe/static/format-pool.js +67 -67
  24. package/dist/worker/observe/static/format.js +292 -292
  25. package/dist/worker/observe/static/index.html +308 -308
  26. package/dist/worker/observe/static/kpi.js +94 -94
  27. package/dist/worker/observe/static/relations.js +133 -133
  28. package/dist/worker/observe/static/router.js +93 -93
  29. package/dist/worker/observe/static/run-processing.js +148 -148
  30. package/dist/worker/observe/static/shell-chrome.js +68 -68
  31. package/dist/worker/observe/static/state.js +267 -253
  32. package/dist/worker/observe/static/styles.css +1902 -1902
  33. package/dist/worker/observe/static/views/batch.js +227 -227
  34. package/dist/worker/observe/static/views/dag-graph.js +172 -172
  35. package/dist/worker/observe/static/views/dag-inspector.js +627 -607
  36. package/dist/worker/observe/static/views/dag.js +371 -362
  37. package/dist/worker/observe/static/views/dashboard.js +509 -252
  38. package/dist/worker/observe/static/views/failures.js +143 -143
  39. package/dist/worker/observe/static/views/feature.js +492 -492
  40. package/dist/worker/observe/static/views/pool.js +350 -350
  41. package/dist/worker/observe/static/views/run.js +453 -453
  42. package/dist/worker/observe/static/views/session-timeline.js +219 -205
  43. package/dist/worker/observe/static/views/shell.js +7 -7
  44. package/dist/worker/observe/static/views/task.js +314 -314
  45. package/dist/worker/observe/static/views/timeline.js +163 -163
  46. package/dist/workflows/dag/backend-test-case-manifest.js +503 -0
  47. package/dist/workflows/dag/backend-test-execution-contract.js +353 -0
  48. package/dist/workflows/dag/backend-test-result-contract.js +568 -0
  49. package/dist/workflows/dag/canvas-observer.js +275 -275
  50. package/dist/workflows/dag/decision-envelope.js +57 -2
  51. package/dist/workflows/dag/frontend-implementation-contract.js +240 -0
  52. package/dist/workflows/dag/frontend-project-capability.js +309 -0
  53. package/dist/workflows/dag/frontend-repair.js +341 -0
  54. package/dist/workflows/dag/frontend-risk.js +161 -0
  55. package/dist/workflows/dag/frontend-verification-trace.js +190 -0
  56. package/dist/workflows/dag/init-hybrid.js +1020 -125
  57. package/dist/workflows/dag/repair-artifact.js +43 -3
  58. package/dist/workflows/dag/skill-instructions.js +4 -2
  59. package/dist/workflows/dag/types.js +29 -8
  60. package/docs/README.md +105 -104
  61. package/docs/agent-dag-recovery-playbook.md +195 -195
  62. package/docs/agent-dag-runner.md +67 -67
  63. package/docs/architecture/README.md +26 -26
  64. package/docs/architecture/dag-execution.md +140 -140
  65. package/docs/architecture/evolution.md +54 -54
  66. package/docs/architecture/facts-and-state.md +71 -71
  67. package/docs/architecture/runtime-boundaries.md +191 -191
  68. package/docs/architecture/system-overview.md +93 -93
  69. package/docs/architecture/worker-and-feature.md +85 -85
  70. package/docs/cursor-prompt-sidecar.md +36 -36
  71. package/docs/decisions/README.md +18 -18
  72. package/docs/design/README.md +167 -167
  73. package/docs/development-principles.md +73 -73
  74. package/docs/exec-plans/README.md +6 -6
  75. package/docs/exec-plans/active/README.md +1 -4
  76. package/docs/exec-plans/completed/README.md +106 -84
  77. package/docs/feature-workflow.md +414 -389
  78. package/docs/harness-methodology-debugging.md +153 -153
  79. package/docs/harness-methodology-tdd.md +130 -130
  80. package/docs/harness-methodology-verification.md +27 -27
  81. package/docs/init-surface.manifest.json +307 -289
  82. package/docs/loop-agent-harness.md +142 -142
  83. package/docs/production-readiness.md +96 -96
  84. package/docs/progress/README.md +76 -60
  85. package/docs/reports/README.md +150 -108
  86. package/docs/skills/README.md +7 -7
  87. package/docs/skills/vetted-skill-registry.md +29 -29
  88. package/docs/templates/adr.md +60 -60
  89. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  90. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  91. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  92. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  93. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  94. package/docs/templates/agent-dag-report.schema.json +473 -473
  95. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  96. package/docs/templates/agent-dag.base.json +190 -190
  97. package/docs/templates/agent-dag.final-verification.json +185 -185
  98. package/docs/templates/agent-dag.schema.json +411 -411
  99. package/docs/templates/agent-dag.supervised-implementation.json +620 -501
  100. package/docs/templates/backend-test-analysis.schema.json +44 -44
  101. package/docs/templates/backend-test-case-manifest.schema.json +190 -0
  102. package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
  103. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +204 -202
  104. package/docs/templates/backend-test-dag.json +559 -311
  105. package/docs/templates/backend-test-dag.retrospect.prompt.md +139 -125
  106. package/docs/templates/backend-test-dag.review-cases.prompt.md +83 -81
  107. package/docs/templates/backend-test-execution.schema.json +133 -0
  108. package/docs/templates/backend-test-result.schema.json +99 -0
  109. package/docs/templates/branch-merge-report.md +93 -0
  110. package/docs/templates/exec-plan.md +64 -64
  111. package/docs/templates/feature-spec.md +53 -53
  112. package/docs/templates/frontend-design-contract.md +42 -42
  113. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -0
  114. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
  115. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
  116. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
  117. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
  118. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
  119. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
  120. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
  121. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
  122. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
  123. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
  124. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
  125. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
  126. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
  127. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
  128. package/docs/templates/frontend-eval/metrics.md +138 -0
  129. package/docs/templates/frontend-eval/smoke-targets.md +53 -0
  130. package/docs/templates/frontend-implementation-contract.schema.json +27 -0
  131. package/docs/templates/frontend-task-constraints.md +35 -35
  132. package/docs/templates/frontend-task-requirement.md +70 -70
  133. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -5
  134. package/docs/templates/frontend-test-dag.json +23 -23
  135. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -3
  136. package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -3
  137. package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -3
  138. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -3
  139. package/docs/templates/harness.schema.json +221 -221
  140. package/docs/templates/hybrid-dag.json +188 -188
  141. package/docs/templates/init-evolution-review.md +35 -35
  142. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  143. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  144. package/docs/templates/knowledge-sync-dag.json +178 -178
  145. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  146. package/docs/templates/product-line/AGENTS.md +8 -8
  147. package/docs/templates/product-line/README.md +9 -9
  148. package/docs/templates/product-line/acceptance.yaml +14 -14
  149. package/docs/templates/product-line/closeout.yaml +9 -9
  150. package/docs/templates/product-line/design.md +13 -13
  151. package/docs/templates/product-line/links.md +10 -10
  152. package/docs/templates/product-line/requirement.md +17 -17
  153. package/docs/templates/product-line/task-graph.yaml +15 -15
  154. package/docs/templates/product-line/task.yaml +64 -64
  155. package/docs/templates/product-line/test-plan.md +7 -7
  156. package/docs/templates/production-readiness-checklist.md +57 -57
  157. package/docs/templates/progress-log.md +17 -17
  158. package/docs/templates/project-start-checklist.md +9 -9
  159. package/docs/templates/qa-report.md +48 -48
  160. package/docs/templates/sprint-contract.md +29 -29
  161. package/docs/templates/worker-dogfood-evidence.md +80 -80
  162. package/docs/templates/worker-dogfood-setup.md +68 -68
  163. package/docs/verification-matrix.md +70 -70
  164. package/examples/decision-gate-agent-dag.json +177 -177
  165. package/examples/example-dag.json +46 -46
  166. package/examples/hybrid-loop-agent-dag.json +189 -189
  167. package/harness.json +66 -66
  168. package/package.json +52 -88
  169. package/scripts/check-product-line-docs.sh +29 -29
  170. package/scripts/check-task-pool-root.sh +32 -32
  171. package/scripts/kb-bootstrap-init-skeleton.sh +240 -240
  172. package/scripts/kb-graph-incremental-prepare.mjs +386 -386
  173. package/scripts/kb-graph-incremental-prepare.sh +5 -5
  174. package/scripts/kb-graph-materialize.mjs +105 -105
  175. package/scripts/kb-graph-materialize.sh +4 -4
  176. package/scripts/kb-graph-promote.mjs +164 -164
  177. package/scripts/kb-graph-promote.sh +4 -4
  178. package/scripts/kb-query.mjs +554 -554
  179. package/scripts/kb-query.sh +5 -5
  180. package/skills/agent-worker/SKILL.md +39 -39
  181. package/skills/agent-worker/references/agent-worker-operator.md +60 -60
  182. package/skills/ai-engineering-context/SKILL.md +48 -48
  183. package/skills/analyze-product-dependencies/SKILL.md +67 -67
  184. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
  185. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
  186. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
  187. package/skills/analyze-product-dependencies/references/example.md +76 -76
  188. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
  189. package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
  190. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
  191. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
  192. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
  193. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
  194. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
  195. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
  196. package/skills/analyze-product-requirements/SKILL.md +90 -90
  197. package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
  198. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
  199. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
  200. package/skills/analyze-product-requirements/references/example.md +86 -86
  201. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
  202. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
  203. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
  204. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
  205. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
  206. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
  207. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
  208. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
  209. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
  210. package/skills/browser-tools/SKILL.md +196 -0
  211. package/skills/browser-tools/browser-content.js +103 -0
  212. package/skills/browser-tools/browser-cookies.js +35 -0
  213. package/skills/browser-tools/browser-eval.js +53 -0
  214. package/skills/browser-tools/browser-hn-scraper.js +108 -0
  215. package/skills/browser-tools/browser-nav.js +44 -0
  216. package/skills/browser-tools/browser-pick.js +162 -0
  217. package/skills/browser-tools/browser-screenshot.js +34 -0
  218. package/skills/browser-tools/browser-start.js +86 -0
  219. package/skills/browser-tools/package-lock.json +2556 -0
  220. package/skills/browser-tools/package.json +19 -0
  221. package/skills/code-review-core/SKILL.md +20 -20
  222. package/skills/codebase-scout/SKILL.md +19 -19
  223. package/skills/frontend-design-review/SKILL.md +66 -66
  224. package/skills/frontend-design-review/references/review-checklist.md +58 -58
  225. package/skills/frontend-implementation/SKILL.md +49 -47
  226. package/skills/frontend-implementation/references/code-standards.md +32 -32
  227. package/skills/frontend-implementation/references/design-spec.md +46 -46
  228. package/skills/frontend-implementation/references/node-contracts.md +27 -76
  229. package/skills/frontend-review/SKILL.md +59 -59
  230. package/skills/frontend-review/references/review-findings.md +47 -47
  231. package/skills/frontend-verification/SKILL.md +53 -53
  232. package/skills/frontend-verification/references/verification-checklist.md +68 -68
  233. package/skills/grill-me/SKILL.md +10 -10
  234. package/skills/grill-with-docs/SKILL.md +88 -88
  235. package/skills/grill-with-docs/adr-format.md +47 -47
  236. package/skills/grill-with-docs/context-format.md +60 -60
  237. package/skills/init-capability-evolution/SKILL.md +70 -70
  238. package/skills/loop-agent/SKILL.md +151 -151
  239. package/skills/loop-agent/references/README.md +67 -67
  240. package/skills/loop-agent/references/command-reference.md +527 -505
  241. package/skills/loop-agent/references/docs-converge.md +126 -126
  242. package/skills/loop-agent/references/harness-policy.md +263 -263
  243. package/skills/loop-agent/references/hybrid-dag.md +243 -238
  244. package/skills/loop-agent/references/learned/README.md +21 -21
  245. package/skills/loop-agent/references/long-running-loop.md +57 -57
  246. package/skills/loop-agent/references/model-routing.md +36 -36
  247. package/skills/loop-agent/references/multi-worktree.md +54 -54
  248. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  249. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  250. package/skills/loop-agent/references/pi-prompt.md +23 -23
  251. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  252. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  253. package/skills/loop-agent/references/task-workflow.md +89 -89
  254. package/skills/loop-agent/references/verification-and-failure-handling.md +141 -139
  255. package/skills/playwright-cli/SKILL.md +420 -420
  256. package/skills/playwright-cli/references/element-attributes.md +23 -23
  257. package/skills/playwright-cli/references/playwright-tests.md +39 -39
  258. package/skills/playwright-cli/references/request-mocking.md +87 -87
  259. package/skills/playwright-cli/references/running-code.md +241 -241
  260. package/skills/playwright-cli/references/session-management.md +225 -225
  261. package/skills/playwright-cli/references/storage-state.md +275 -275
  262. package/skills/playwright-cli/references/test-generation.md +433 -433
  263. package/skills/playwright-cli/references/tracing.md +139 -139
  264. package/skills/playwright-cli/references/video-recording.md +143 -143
  265. package/skills/playwright-cli-case-generator/SKILL.md +74 -74
  266. package/skills/requesting-code-review/SKILL.md +101 -101
  267. package/skills/requesting-code-review/code-reviewer.md +168 -168
  268. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  269. package/skills/systematic-debugging/SKILL.md +296 -296
  270. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  271. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  272. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  273. package/skills/systematic-debugging/find-polluter.sh +63 -63
  274. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  275. package/skills/systematic-debugging/test-academic.md +14 -14
  276. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  277. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  278. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  279. package/skills/test-driven-development/SKILL.md +20 -20
  280. package/skills/using-git-worktrees/SKILL.md +215 -215
  281. package/skills/verification-before-completion/SKILL.md +154 -154
  282. package/skills/webapp-testing/SKILL.md +19 -19
@@ -1,101 +1,101 @@
1
- #!/usr/bin/env node
2
-
3
- import { existsSync, statSync } from "node:fs";
4
- import { dirname, resolve } from "node:path";
5
- import { apiBlocks, assertHeadings, field, headings, load, metadata, print, scopeIncludes, section, storyBlocks, tableValue, validateArtifactLocation } from "./validation-helpers.mjs";
6
-
7
- const args = process.argv.slice(2);
8
- const targetIndex = args.indexOf("--target");
9
- const expectedTarget = targetIndex >= 0 ? args[targetIndex + 1] : undefined;
10
- const positional = args.filter((_, index) => targetIndex < 0 || (index !== targetIndex && index !== targetIndex + 1));
11
- if (positional.length < 2) {
12
- console.error("Usage: node validate-api-documentation.mjs <product-requirement.md> <api-documentation.md> [--target backend|both]");
13
- process.exit(2);
14
- }
15
- let requirement, api;
16
- try { requirement = load(positional[0], "Product Requirement"); api = load(positional[1], "API Documentation"); }
17
- catch (error) { console.error(error.message); process.exit(2); }
18
-
19
- const errors = [];
20
- validateArtifactLocation(api, errors, "api-documentation.md");
21
- const scope = metadata(api.text, "analysis_scope");
22
- const upstreamScope = metadata(requirement.text, "analysis_scope");
23
- if (metadata(api.text, "artifact_version") !== "3.0") errors.push("artifact_version must be 3.0.");
24
- if (metadata(api.text, "artifact_type") !== "api-documentation") errors.push("artifact_type must be api-documentation.");
25
- if (metadata(api.text, "api_status") !== "complete") errors.push("api_status must be complete.");
26
- if (!["backend", "both"].includes(scope)) errors.push("analysis_scope must be backend or both for API Documentation.");
27
- if (targetIndex >= 0 && !["backend", "both"].includes(expectedTarget)) errors.push("--target must be backend or both for API Documentation.");
28
- else if (expectedTarget && scope !== expectedTarget) errors.push(`analysis_scope ${scope} does not match requested target ${expectedTarget}.`);
29
- if (!scopeIncludes(upstreamScope, scope)) errors.push("analysis_scope must be included in Product Requirement scope.");
30
- if (metadata(api.text, "requirement_id") !== metadata(requirement.text, "requirement_id")) errors.push("requirement_id must match Product Requirement.");
31
- const source = metadata(api.text, "source_product_requirement");
32
- if (!source || resolve(dirname(api.path), source) !== requirement.path) errors.push("source_product_requirement must reference the supplied Product Requirement.");
33
- if (dirname(requirement.path) !== dirname(api.path)) errors.push("API Documentation and Product Requirement must be in the same requirement directory.");
34
- const repo = metadata(api.text, "repository_root");
35
- const repoPath = repo ? resolve(dirname(api.path), repo) : "";
36
- if (!repoPath || !existsSync(repoPath) || !statSync(repoPath).isDirectory()) errors.push("repository_root must be an existing readable directory.");
37
- const projectRoot = resolve(dirname(api.path), metadata(api.text, "project_root") ?? "");
38
- if (repoPath && projectRoot && repoPath !== projectRoot) errors.push("repository_root must resolve to project_root.");
39
-
40
- assertHeadings(api.text, 2, ["通用约定", "API 索引", "API 详情", "数据模型", "错误码"], errors, "API Documentation");
41
- const conventions = section(api.text, 2, "通用约定") ?? "";
42
- assertHeadings(conventions, 3, ["Base URL", "统一响应结构", "错误响应结构", "分页约定", "时间和标识符规范"], errors, "通用约定");
43
- if (/(?:认证方式|认证要求|鉴权方式|鉴权要求|权限要求|权限规则|访问控制要求|Authorization|Bearer|X-API-Key|access[_-]?token|id[_-]?token|业务规则|业务逻辑|实现逻辑|处理逻辑|分支逻辑|数据读写逻辑|实现算法)/i.test(api.text)) errors.push("API Documentation must not contain authentication, permission, business-rule, or implementation-logic content.");
44
- if (/(?:复用检查|通用定义复用)/.test(api.text)) errors.push("API Documentation must not contain reuse-check sections or search evidence.");
45
- const index = section(api.text, 2, "API 索引") ?? "";
46
- const blocks = apiBlocks(api.text);
47
- if (!blocks.length) errors.push("API Documentation must contain at least one API-* detail.");
48
- const ids = new Set();
49
- const operations = new Set();
50
- for (const block of blocks) {
51
- if (ids.has(block.id)) errors.push(`Duplicate API ID: ${block.id}.`);
52
- ids.add(block.id);
53
- const operation = tableValue(block.text, "Operation ID");
54
- if (!operation) errors.push(`${block.id} is missing Operation ID.`);
55
- else if (operations.has(operation)) errors.push(`Duplicate Operation ID: ${operation}.`);
56
- else operations.add(operation);
57
- for (const name of ["Operation ID", "变更类型", "幂等性"]) if (!tableValue(block.text, name)) errors.push(`${block.id} is missing basic information field ${name}.`);
58
- if (tableValue(block.text, "API ID")) errors.push(`${block.id} basic information must not repeat API ID.`);
59
- if (tableValue(block.text, "变更类型") && !["新增", "修改", "复用"].includes(tableValue(block.text, "变更类型"))) errors.push(`${block.id} 变更类型 is invalid.`);
60
- const signature = block.text.match(/^>\s*`(GET|POST|PUT|PATCH|DELETE)\s+(\/[^`]+)`/m);
61
- if (!signature) errors.push(`${block.id} is missing a Swagger-style method and path signature.`);
62
- for (const title of ["基本信息", "成功响应", "错误响应"]) if (!headings(block.text, 4).some((item) => item.title.trim() === title)) errors.push(`${block.id} is missing section ${title}.`);
63
- const query = section(block.text, 4, "Query 参数") ?? "";
64
- if (/\b(?:pageSize|page_size|limit|cursor|page)\b/i.test(query)) {
65
- const pageSizeRow = query.split(/\r?\n/).find((line) => /^\|\s*(?:pageSize|page_size|limit)\s*\|/i.test(line));
66
- if (!pageSizeRow) errors.push(`${block.id} paginated API must define an optional page-size parameter.`);
67
- else {
68
- if (!/^\|\s*(?:pageSize|page_size|limit)\s*\|\s*[^|]+\|\s*(?:否|可选)\s*\|/i.test(pageSizeRow)) errors.push(`${block.id} page-size parameter must be optional.`);
69
- const values = [...new Set(pageSizeRow.match(/\b\d+\b/g) ?? [])].sort((a, b) => Number(a) - Number(b));
70
- if (values.join(",") !== "10,20,50,100") errors.push(`${block.id} page-size values must be exactly 10, 20, 50, and 100.`);
71
- }
72
- }
73
- if (signature) {
74
- const parameters = signature[2].match(/\{([^}]+)\}/g) ?? [];
75
- const pathSection = section(block.text, 4, "Path 参数") ?? "";
76
- if (parameters.length && !pathSection) errors.push(`${block.id} path template requires Path 参数.`);
77
- for (const parameter of parameters) {
78
- const name = parameter.slice(1, -1);
79
- if (!new RegExp(`^\\|\\s*${name}\\s*\\|`, "m").test(pathSection)) errors.push(`${block.id} path parameter ${name} is not documented.`);
80
- }
81
- }
82
- const success = section(block.text, 4, "成功响应") ?? "";
83
- const error = section(block.text, 4, "错误响应") ?? "";
84
- if (!/HTTP(?: 状态码)?[::]\s*2\d\d/.test(success)) errors.push(`${block.id} must define a 2xx success response.`);
85
- if (!/^\|\s*[45]\d\d\s*\|/m.test(error)) errors.push(`${block.id} must define at least one 4xx or 5xx error response.`);
86
- for (const [content, label] of [[success, "success"], [error, "error"]]) {
87
- const example = content.match(/```json\s*([\s\S]*?)```/)?.[1];
88
- if (!example) errors.push(`${block.id} must include a JSON ${label} example.`);
89
- else try { JSON.parse(example); } catch { errors.push(`${block.id} ${label} example must be valid JSON.`); }
90
- }
91
- if (!index.includes(block.id)) errors.push(`${block.id} is missing from API index.`);
92
- if (operation && !index.includes(operation)) errors.push(`${block.id} Operation ID is missing from API index.`);
93
- if (signature && !index.includes(`${signature[1]} ${signature[2]}`)) errors.push(`${block.id} method and path are missing from API index.`);
94
- }
95
-
96
- const requiredApiStories = storyBlocks(section(requirement.text, 2, "后端用户故事"), "BE-US").filter((story) => /\bAPI\b/i.test(field(story.text, "触发方式") ?? ""));
97
- for (const story of requiredApiStories) {
98
- if (!index.includes(story.id)) errors.push(`API index does not cover API-triggered story ${story.id}.`);
99
- for (const ac of field(story.text, "验收标准")?.match(/AC-BE-\d{3,}/g) ?? []) if (!index.includes(ac)) errors.push(`API index does not trace ${ac}.`);
100
- }
101
- print("API Documentation", api.path, errors);
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, statSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import { apiBlocks, assertHeadings, field, headings, load, metadata, print, scopeIncludes, section, storyBlocks, tableValue, validateArtifactLocation } from "./validation-helpers.mjs";
6
+
7
+ const args = process.argv.slice(2);
8
+ const targetIndex = args.indexOf("--target");
9
+ const expectedTarget = targetIndex >= 0 ? args[targetIndex + 1] : undefined;
10
+ const positional = args.filter((_, index) => targetIndex < 0 || (index !== targetIndex && index !== targetIndex + 1));
11
+ if (positional.length < 2) {
12
+ console.error("Usage: node validate-api-documentation.mjs <product-requirement.md> <api-documentation.md> [--target backend|both]");
13
+ process.exit(2);
14
+ }
15
+ let requirement, api;
16
+ try { requirement = load(positional[0], "Product Requirement"); api = load(positional[1], "API Documentation"); }
17
+ catch (error) { console.error(error.message); process.exit(2); }
18
+
19
+ const errors = [];
20
+ validateArtifactLocation(api, errors, "api-documentation.md");
21
+ const scope = metadata(api.text, "analysis_scope");
22
+ const upstreamScope = metadata(requirement.text, "analysis_scope");
23
+ if (metadata(api.text, "artifact_version") !== "3.0") errors.push("artifact_version must be 3.0.");
24
+ if (metadata(api.text, "artifact_type") !== "api-documentation") errors.push("artifact_type must be api-documentation.");
25
+ if (metadata(api.text, "api_status") !== "complete") errors.push("api_status must be complete.");
26
+ if (!["backend", "both"].includes(scope)) errors.push("analysis_scope must be backend or both for API Documentation.");
27
+ if (targetIndex >= 0 && !["backend", "both"].includes(expectedTarget)) errors.push("--target must be backend or both for API Documentation.");
28
+ else if (expectedTarget && scope !== expectedTarget) errors.push(`analysis_scope ${scope} does not match requested target ${expectedTarget}.`);
29
+ if (!scopeIncludes(upstreamScope, scope)) errors.push("analysis_scope must be included in Product Requirement scope.");
30
+ if (metadata(api.text, "requirement_id") !== metadata(requirement.text, "requirement_id")) errors.push("requirement_id must match Product Requirement.");
31
+ const source = metadata(api.text, "source_product_requirement");
32
+ if (!source || resolve(dirname(api.path), source) !== requirement.path) errors.push("source_product_requirement must reference the supplied Product Requirement.");
33
+ if (dirname(requirement.path) !== dirname(api.path)) errors.push("API Documentation and Product Requirement must be in the same requirement directory.");
34
+ const repo = metadata(api.text, "repository_root");
35
+ const repoPath = repo ? resolve(dirname(api.path), repo) : "";
36
+ if (!repoPath || !existsSync(repoPath) || !statSync(repoPath).isDirectory()) errors.push("repository_root must be an existing readable directory.");
37
+ const projectRoot = resolve(dirname(api.path), metadata(api.text, "project_root") ?? "");
38
+ if (repoPath && projectRoot && repoPath !== projectRoot) errors.push("repository_root must resolve to project_root.");
39
+
40
+ assertHeadings(api.text, 2, ["通用约定", "API 索引", "API 详情", "数据模型", "错误码"], errors, "API Documentation");
41
+ const conventions = section(api.text, 2, "通用约定") ?? "";
42
+ assertHeadings(conventions, 3, ["Base URL", "统一响应结构", "错误响应结构", "分页约定", "时间和标识符规范"], errors, "通用约定");
43
+ if (/(?:认证方式|认证要求|鉴权方式|鉴权要求|权限要求|权限规则|访问控制要求|Authorization|Bearer|X-API-Key|access[_-]?token|id[_-]?token|业务规则|业务逻辑|实现逻辑|处理逻辑|分支逻辑|数据读写逻辑|实现算法)/i.test(api.text)) errors.push("API Documentation must not contain authentication, permission, business-rule, or implementation-logic content.");
44
+ if (/(?:复用检查|通用定义复用)/.test(api.text)) errors.push("API Documentation must not contain reuse-check sections or search evidence.");
45
+ const index = section(api.text, 2, "API 索引") ?? "";
46
+ const blocks = apiBlocks(api.text);
47
+ if (!blocks.length) errors.push("API Documentation must contain at least one API-* detail.");
48
+ const ids = new Set();
49
+ const operations = new Set();
50
+ for (const block of blocks) {
51
+ if (ids.has(block.id)) errors.push(`Duplicate API ID: ${block.id}.`);
52
+ ids.add(block.id);
53
+ const operation = tableValue(block.text, "Operation ID");
54
+ if (!operation) errors.push(`${block.id} is missing Operation ID.`);
55
+ else if (operations.has(operation)) errors.push(`Duplicate Operation ID: ${operation}.`);
56
+ else operations.add(operation);
57
+ for (const name of ["Operation ID", "变更类型", "幂等性"]) if (!tableValue(block.text, name)) errors.push(`${block.id} is missing basic information field ${name}.`);
58
+ if (tableValue(block.text, "API ID")) errors.push(`${block.id} basic information must not repeat API ID.`);
59
+ if (tableValue(block.text, "变更类型") && !["新增", "修改", "复用"].includes(tableValue(block.text, "变更类型"))) errors.push(`${block.id} 变更类型 is invalid.`);
60
+ const signature = block.text.match(/^>\s*`(GET|POST|PUT|PATCH|DELETE)\s+(\/[^`]+)`/m);
61
+ if (!signature) errors.push(`${block.id} is missing a Swagger-style method and path signature.`);
62
+ for (const title of ["基本信息", "成功响应", "错误响应"]) if (!headings(block.text, 4).some((item) => item.title.trim() === title)) errors.push(`${block.id} is missing section ${title}.`);
63
+ const query = section(block.text, 4, "Query 参数") ?? "";
64
+ if (/\b(?:pageSize|page_size|limit|cursor|page)\b/i.test(query)) {
65
+ const pageSizeRow = query.split(/\r?\n/).find((line) => /^\|\s*(?:pageSize|page_size|limit)\s*\|/i.test(line));
66
+ if (!pageSizeRow) errors.push(`${block.id} paginated API must define an optional page-size parameter.`);
67
+ else {
68
+ if (!/^\|\s*(?:pageSize|page_size|limit)\s*\|\s*[^|]+\|\s*(?:否|可选)\s*\|/i.test(pageSizeRow)) errors.push(`${block.id} page-size parameter must be optional.`);
69
+ const values = [...new Set(pageSizeRow.match(/\b\d+\b/g) ?? [])].sort((a, b) => Number(a) - Number(b));
70
+ if (values.join(",") !== "10,20,50,100") errors.push(`${block.id} page-size values must be exactly 10, 20, 50, and 100.`);
71
+ }
72
+ }
73
+ if (signature) {
74
+ const parameters = signature[2].match(/\{([^}]+)\}/g) ?? [];
75
+ const pathSection = section(block.text, 4, "Path 参数") ?? "";
76
+ if (parameters.length && !pathSection) errors.push(`${block.id} path template requires Path 参数.`);
77
+ for (const parameter of parameters) {
78
+ const name = parameter.slice(1, -1);
79
+ if (!new RegExp(`^\\|\\s*${name}\\s*\\|`, "m").test(pathSection)) errors.push(`${block.id} path parameter ${name} is not documented.`);
80
+ }
81
+ }
82
+ const success = section(block.text, 4, "成功响应") ?? "";
83
+ const error = section(block.text, 4, "错误响应") ?? "";
84
+ if (!/HTTP(?: 状态码)?[::]\s*2\d\d/.test(success)) errors.push(`${block.id} must define a 2xx success response.`);
85
+ if (!/^\|\s*[45]\d\d\s*\|/m.test(error)) errors.push(`${block.id} must define at least one 4xx or 5xx error response.`);
86
+ for (const [content, label] of [[success, "success"], [error, "error"]]) {
87
+ const example = content.match(/```json\s*([\s\S]*?)```/)?.[1];
88
+ if (!example) errors.push(`${block.id} must include a JSON ${label} example.`);
89
+ else try { JSON.parse(example); } catch { errors.push(`${block.id} ${label} example must be valid JSON.`); }
90
+ }
91
+ if (!index.includes(block.id)) errors.push(`${block.id} is missing from API index.`);
92
+ if (operation && !index.includes(operation)) errors.push(`${block.id} Operation ID is missing from API index.`);
93
+ if (signature && !index.includes(`${signature[1]} ${signature[2]}`)) errors.push(`${block.id} method and path are missing from API index.`);
94
+ }
95
+
96
+ const requiredApiStories = storyBlocks(section(requirement.text, 2, "后端用户故事"), "BE-US").filter((story) => /\bAPI\b/i.test(field(story.text, "触发方式") ?? ""));
97
+ for (const story of requiredApiStories) {
98
+ if (!index.includes(story.id)) errors.push(`API index does not cover API-triggered story ${story.id}.`);
99
+ for (const ac of field(story.text, "验收标准")?.match(/AC-BE-\d{3,}/g) ?? []) if (!index.includes(ac)) errors.push(`API index does not trace ${ac}.`);
100
+ }
101
+ print("API Documentation", api.path, errors);
@@ -1,142 +1,142 @@
1
- #!/usr/bin/env node
2
-
3
- import { existsSync, statSync } from "node:fs";
4
- import { basename, dirname, resolve } from "node:path";
5
- import { apiBlocks, assertHeadings, canonical, field, fieldBlock, headings, load, metadata, print, scopeIncludes, section, storyBlocks, tableValue, validateArtifactLocation } from "./validation-helpers.mjs";
6
-
7
- const args = process.argv.slice(2);
8
- const targetIndex = args.indexOf("--target");
9
- const expectedTarget = targetIndex >= 0 ? args[targetIndex + 1] : undefined;
10
- const positional = args.filter((_, index) => targetIndex < 0 || (index !== targetIndex && index !== targetIndex + 1));
11
- const [requirementArg, dependencyArg, apiArg] = positional;
12
- if (!requirementArg || !dependencyArg) {
13
- console.error("Usage: node validate-dependency-analysis.mjs <product-requirement.md|none> <dependency-analysis.md> [api-documentation.md] [--target frontend|backend|both]");
14
- process.exit(2);
15
- }
16
- let requirement, dependency, api;
17
- try {
18
- if (requirementArg !== "none") requirement = load(requirementArg, "Product Requirement");
19
- dependency = load(dependencyArg, "Dependency Analysis");
20
- if (apiArg) api = load(apiArg, "API Documentation");
21
- } catch (error) { console.error(error.message); process.exit(2); }
22
-
23
- const errors = [];
24
- validateArtifactLocation(dependency, errors, "dependency-analysis.md");
25
- const scope = metadata(dependency.text, "analysis_scope");
26
- const status = metadata(dependency.text, "analysis_status");
27
- const blockedOn = metadata(dependency.text, "blocked_on") ?? "";
28
- const blockedReasons = blockedOn.split(/[\s,,]+/).filter(Boolean);
29
- const source = metadata(dependency.text, "source_product_requirement");
30
- const sourceApi = metadata(dependency.text, "source_api_documentation");
31
- const repo = metadata(dependency.text, "repository_root");
32
- const repoPath = repo && repo !== "none" ? resolve(dirname(dependency.path), repo) : "";
33
- const repoExists = Boolean(repoPath && existsSync(repoPath) && statSync(repoPath).isDirectory());
34
- const projectRoot = resolve(dirname(dependency.path), metadata(dependency.text, "project_root") ?? "");
35
-
36
- if (metadata(dependency.text, "artifact_version") !== "3.0") errors.push("artifact_version must be 3.0.");
37
- if (metadata(dependency.text, "artifact_type") !== "dependency-analysis") errors.push("artifact_type must be dependency-analysis.");
38
- if (!["frontend", "backend", "both"].includes(scope)) errors.push("analysis_scope must be frontend, backend, or both.");
39
- if (targetIndex >= 0 && !["frontend", "backend", "both"].includes(expectedTarget)) errors.push("--target must be frontend, backend, or both.");
40
- else if (expectedTarget && scope !== expectedTarget) errors.push(`analysis_scope ${scope} does not match requested target ${expectedTarget}.`);
41
- if (!["complete", "blocked"].includes(status)) errors.push("analysis_status must be complete or blocked.");
42
-
43
- function validateRequirementLink({ allowScopeMismatch = false } = {}) {
44
- if (!requirement) return;
45
- if (basename(requirement.path) !== "product-requirement.md") errors.push("Product Requirement filename must be product-requirement.md.");
46
- if (!allowScopeMismatch && !scopeIncludes(metadata(requirement.text, "analysis_scope"), scope)) errors.push("analysis_scope must be included in Product Requirement scope.");
47
- if (metadata(dependency.text, "requirement_id") !== metadata(requirement.text, "requirement_id")) errors.push("requirement_id must match Product Requirement.");
48
- if (!source || resolve(dirname(dependency.path), source) !== requirement.path) errors.push("source_product_requirement must reference the supplied Product Requirement.");
49
- if (dirname(requirement.path) !== dirname(dependency.path)) errors.push("Dependency Analysis and Product Requirement must be in the same requirement directory.");
50
- }
51
-
52
- if (status === "blocked") {
53
- const allowed = new Set(["requirement-missing", "requirement-invalid", "requirement-not-complete", "repository-missing", "repository-unreadable", "scope-mismatch", "missing-stories", "missing-acceptance", "api-business-contract-incomplete"]);
54
- if (!blockedReasons.length || blockedOn === "none") errors.push("blocked analysis must declare blocked_on reasons.");
55
- for (const reason of blockedReasons) if (!allowed.has(reason)) errors.push(`Unknown blocked_on reason: ${reason}.`);
56
- if (requirement) validateRequirementLink({ allowScopeMismatch: blockedReasons.includes("scope-mismatch") });
57
- else {
58
- if (!blockedReasons.includes("requirement-missing")) errors.push("A missing Product Requirement requires blocked_on: requirement-missing.");
59
- if (source !== "none") errors.push("Missing Product Requirement must use source_product_requirement: none.");
60
- }
61
- if (!blockedReasons.some((reason) => ["repository-missing", "repository-unreadable"].includes(reason)) && !repoExists) errors.push("repository_root must be readable unless blocked on repository availability.");
62
- if (sourceApi !== "none") errors.push("Blocked Dependency Analysis must use source_api_documentation: none.");
63
- const required = ["分析范围", "输入与代码基线", "阻断原因", "恢复条件"];
64
- assertHeadings(dependency.text, 2, required, errors, "Blocked Dependency Analysis");
65
- for (const heading of headings(dependency.text, 2)) if (!required.includes(canonical(heading.title))) errors.push(`Blocked Dependency Analysis must not contain section ${canonical(heading.title)}.`);
66
- print("Dependency Analysis", dependency.path, errors);
67
- process.exit(0);
68
- }
69
-
70
- if (!requirement) errors.push("Complete Dependency Analysis requires Product Requirement input.");
71
- else validateRequirementLink();
72
- if (blockedOn !== "none") errors.push("complete analysis must use blocked_on: none.");
73
- if (!repoExists) errors.push("repository_root must be an existing readable directory.");
74
- if (repoPath && repoPath !== projectRoot) errors.push("repository_root must resolve to project_root.");
75
- assertHeadings(dependency.text, 2, ["输入与代码基线", "用户故事覆盖矩阵", "跨故事共享依赖", "风险与未定位项"], errors, "Dependency Analysis");
76
- if (/^##\s+(?:\d+[.、]?\s*)?(?:分析范围|追溯汇总)\s*$/m.test(dependency.text)) errors.push("Complete V3 Dependency Analysis must not contain 分析范围 or 追溯汇总 sections.");
77
- if (scope === "frontend" || scope === "both") assertHeadings(dependency.text, 2, ["前端依赖详情"], errors, "Dependency Analysis");
78
- if (scope === "backend" || scope === "both") assertHeadings(dependency.text, 2, ["后端依赖详情", "API 实现映射"], errors, "Dependency Analysis");
79
- if (scope === "frontend" && /(?:^##\s+.*(?:后端依赖详情|API 实现映射)|\bBE-US-\d{3,}\b|\bAC-BE-\d{3,}\b|\bAPI-\d{3,}\b)/m.test(dependency.text)) errors.push("frontend scope must not contain backend dependency details or API mappings.");
80
- if (scope === "backend" && /(?:^##\s+.*前端依赖详情|\bFE-US-\d{3,}\b|\bAC-FE-\d{3,}\b)/m.test(dependency.text)) errors.push("backend scope must not contain frontend dependency details.");
81
-
82
- const frontendStories = requirement && (scope === "frontend" || scope === "both") ? storyBlocks(section(requirement.text, 2, "前端用户故事"), "FE-US") : [];
83
- const backendStories = requirement && (scope === "backend" || scope === "both") ? storyBlocks(section(requirement.text, 2, "后端用户故事"), "BE-US") : [];
84
- const apiRequired = backendStories.some((story) => /\bAPI\b/i.test(field(story.text, "触发方式") ?? ""));
85
- if (apiRequired) {
86
- if (!api) errors.push("API-triggered backend stories require api-documentation.md.");
87
- if (!sourceApi || sourceApi === "none") errors.push("API-triggered backend stories require source_api_documentation.");
88
- if (api && resolve(dirname(dependency.path), sourceApi) !== api.path) errors.push("source_api_documentation must reference the supplied API Documentation.");
89
- } else {
90
- if (api) errors.push("API Documentation must not be supplied when no selected backend story uses API.");
91
- if (sourceApi !== "none") errors.push("Non-API analysis must use source_api_documentation: none.");
92
- if ((scope === "backend" || scope === "both") && !/不适用.*不涉及 HTTP API/s.test(section(dependency.text, 2, "API 实现映射") ?? "")) errors.push("Non-API analysis must mark API 实现映射 not applicable.");
93
- }
94
-
95
- function validateDetails(frontend, expectedStories) {
96
- const title = frontend ? "前端依赖详情" : "后端依赖详情";
97
- const prefix = frontend ? "FE-US" : "BE-US";
98
- const acPrefix = frontend ? "AC-FE" : "AC-BE";
99
- const requiredFields = frontend
100
- ? ["验收标准", "页面/路由", "组件", "状态", "API client/类型", "状态与边界落点", "定位证据", "风险", "置信度"]
101
- : ["验收标准", "API 文档引用", "路由/入口", "Controller/Handler", "Service/领域逻辑", "DTO/Schema", "数据依赖", "权限依赖", "错误/日志/审计", "测试落点", "定位证据", "风险", "置信度"];
102
- const details = storyBlocks(section(dependency.text, 2, title), prefix);
103
- for (const story of expectedStories) if (!details.some((item) => item.id === story.id)) errors.push(`Dependency Analysis is missing ${title} for ${story.id}.`);
104
- for (const detail of details) {
105
- const story = expectedStories.find((item) => item.id === detail.id);
106
- if (!story) errors.push(`Dependency Analysis contains unselected story ${detail.id}.`);
107
- for (const name of requiredFields) if (!field(detail.text, name)) errors.push(`${detail.id} dependency detail is missing field ${name}.`);
108
- const impact = fieldBlock(detail.text, "影响文件");
109
- if (!impact) errors.push(`${detail.id} dependency detail is missing field 影响文件.`);
110
- const declarations = [...impact.matchAll(/^\s*-\s*(F\d+)\s+(add|modify|reuse|新增|修改|复用)\s+`?([^`\r\n]+)`?/gmi)];
111
- if (!declarations.length) errors.push(`${detail.id} 影响文件 must declare F<number>, add/modify/reuse, and a path.`);
112
- const declared = declarations.map((item) => item[1]);
113
- if (new Set(declared).size !== declared.length) errors.push(`${detail.id} 影响文件 contains duplicate file IDs.`);
114
- for (const ref of detail.text.match(/\bF\d+\b/g) ?? []) if (!declared.includes(ref)) errors.push(`${detail.id} references undeclared impact file ${ref}.`);
115
- if (!/^(?:high|medium|low)$/i.test(field(detail.text, "置信度") ?? "")) errors.push(`${detail.id} 置信度 must be high, medium, or low.`);
116
- if (story) {
117
- const expected = field(story.text, "验收标准")?.match(new RegExp(`${acPrefix}-\\d{3,}`, "g")) ?? [];
118
- const actual = field(detail.text, "验收标准")?.match(new RegExp(`${acPrefix}-\\d{3,}`, "g")) ?? [];
119
- if ([...expected].sort().join() !== [...actual].sort().join()) errors.push(`${detail.id} acceptance references must match Product Requirement.`);
120
- }
121
- }
122
- }
123
-
124
- if (scope === "frontend" || scope === "both") validateDetails(true, frontendStories);
125
- if (scope === "backend" || scope === "both") validateDetails(false, backendStories);
126
- const coverage = section(dependency.text, 2, "用户故事覆盖矩阵") ?? "";
127
- for (const story of [...frontendStories, ...backendStories]) {
128
- if (!coverage.includes(story.id)) errors.push(`Coverage matrix is missing ${story.id}.`);
129
- for (const ac of field(story.text, "验收标准")?.match(/AC-(?:FE|BE)-\d{3,}/g) ?? []) if (!coverage.includes(ac)) errors.push(`Coverage matrix is missing ${ac}.`);
130
- }
131
-
132
- if (api) {
133
- const mapping = section(dependency.text, 2, "API 实现映射") ?? "";
134
- for (const item of apiBlocks(api.text)) {
135
- const operation = tableValue(item.text, "Operation ID");
136
- const signature = item.text.match(/^>\s*`((?:GET|POST|PUT|PATCH|DELETE)\s+\/[^`]+)`/m)?.[1];
137
- if (!mapping.includes(item.id)) errors.push(`API 实现映射 is missing ${item.id}.`);
138
- if (operation && !mapping.includes(operation)) errors.push(`API 实现映射 is missing Operation ID ${operation}.`);
139
- if (signature && !mapping.includes(signature)) errors.push(`API 实现映射 is missing ${signature}.`);
140
- }
141
- }
142
- print("Dependency Analysis", dependency.path, errors);
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, statSync } from "node:fs";
4
+ import { basename, dirname, resolve } from "node:path";
5
+ import { apiBlocks, assertHeadings, canonical, field, fieldBlock, headings, load, metadata, print, scopeIncludes, section, storyBlocks, tableValue, validateArtifactLocation } from "./validation-helpers.mjs";
6
+
7
+ const args = process.argv.slice(2);
8
+ const targetIndex = args.indexOf("--target");
9
+ const expectedTarget = targetIndex >= 0 ? args[targetIndex + 1] : undefined;
10
+ const positional = args.filter((_, index) => targetIndex < 0 || (index !== targetIndex && index !== targetIndex + 1));
11
+ const [requirementArg, dependencyArg, apiArg] = positional;
12
+ if (!requirementArg || !dependencyArg) {
13
+ console.error("Usage: node validate-dependency-analysis.mjs <product-requirement.md|none> <dependency-analysis.md> [api-documentation.md] [--target frontend|backend|both]");
14
+ process.exit(2);
15
+ }
16
+ let requirement, dependency, api;
17
+ try {
18
+ if (requirementArg !== "none") requirement = load(requirementArg, "Product Requirement");
19
+ dependency = load(dependencyArg, "Dependency Analysis");
20
+ if (apiArg) api = load(apiArg, "API Documentation");
21
+ } catch (error) { console.error(error.message); process.exit(2); }
22
+
23
+ const errors = [];
24
+ validateArtifactLocation(dependency, errors, "dependency-analysis.md");
25
+ const scope = metadata(dependency.text, "analysis_scope");
26
+ const status = metadata(dependency.text, "analysis_status");
27
+ const blockedOn = metadata(dependency.text, "blocked_on") ?? "";
28
+ const blockedReasons = blockedOn.split(/[\s,,]+/).filter(Boolean);
29
+ const source = metadata(dependency.text, "source_product_requirement");
30
+ const sourceApi = metadata(dependency.text, "source_api_documentation");
31
+ const repo = metadata(dependency.text, "repository_root");
32
+ const repoPath = repo && repo !== "none" ? resolve(dirname(dependency.path), repo) : "";
33
+ const repoExists = Boolean(repoPath && existsSync(repoPath) && statSync(repoPath).isDirectory());
34
+ const projectRoot = resolve(dirname(dependency.path), metadata(dependency.text, "project_root") ?? "");
35
+
36
+ if (metadata(dependency.text, "artifact_version") !== "3.0") errors.push("artifact_version must be 3.0.");
37
+ if (metadata(dependency.text, "artifact_type") !== "dependency-analysis") errors.push("artifact_type must be dependency-analysis.");
38
+ if (!["frontend", "backend", "both"].includes(scope)) errors.push("analysis_scope must be frontend, backend, or both.");
39
+ if (targetIndex >= 0 && !["frontend", "backend", "both"].includes(expectedTarget)) errors.push("--target must be frontend, backend, or both.");
40
+ else if (expectedTarget && scope !== expectedTarget) errors.push(`analysis_scope ${scope} does not match requested target ${expectedTarget}.`);
41
+ if (!["complete", "blocked"].includes(status)) errors.push("analysis_status must be complete or blocked.");
42
+
43
+ function validateRequirementLink({ allowScopeMismatch = false } = {}) {
44
+ if (!requirement) return;
45
+ if (basename(requirement.path) !== "product-requirement.md") errors.push("Product Requirement filename must be product-requirement.md.");
46
+ if (!allowScopeMismatch && !scopeIncludes(metadata(requirement.text, "analysis_scope"), scope)) errors.push("analysis_scope must be included in Product Requirement scope.");
47
+ if (metadata(dependency.text, "requirement_id") !== metadata(requirement.text, "requirement_id")) errors.push("requirement_id must match Product Requirement.");
48
+ if (!source || resolve(dirname(dependency.path), source) !== requirement.path) errors.push("source_product_requirement must reference the supplied Product Requirement.");
49
+ if (dirname(requirement.path) !== dirname(dependency.path)) errors.push("Dependency Analysis and Product Requirement must be in the same requirement directory.");
50
+ }
51
+
52
+ if (status === "blocked") {
53
+ const allowed = new Set(["requirement-missing", "requirement-invalid", "requirement-not-complete", "repository-missing", "repository-unreadable", "scope-mismatch", "missing-stories", "missing-acceptance", "api-business-contract-incomplete"]);
54
+ if (!blockedReasons.length || blockedOn === "none") errors.push("blocked analysis must declare blocked_on reasons.");
55
+ for (const reason of blockedReasons) if (!allowed.has(reason)) errors.push(`Unknown blocked_on reason: ${reason}.`);
56
+ if (requirement) validateRequirementLink({ allowScopeMismatch: blockedReasons.includes("scope-mismatch") });
57
+ else {
58
+ if (!blockedReasons.includes("requirement-missing")) errors.push("A missing Product Requirement requires blocked_on: requirement-missing.");
59
+ if (source !== "none") errors.push("Missing Product Requirement must use source_product_requirement: none.");
60
+ }
61
+ if (!blockedReasons.some((reason) => ["repository-missing", "repository-unreadable"].includes(reason)) && !repoExists) errors.push("repository_root must be readable unless blocked on repository availability.");
62
+ if (sourceApi !== "none") errors.push("Blocked Dependency Analysis must use source_api_documentation: none.");
63
+ const required = ["分析范围", "输入与代码基线", "阻断原因", "恢复条件"];
64
+ assertHeadings(dependency.text, 2, required, errors, "Blocked Dependency Analysis");
65
+ for (const heading of headings(dependency.text, 2)) if (!required.includes(canonical(heading.title))) errors.push(`Blocked Dependency Analysis must not contain section ${canonical(heading.title)}.`);
66
+ print("Dependency Analysis", dependency.path, errors);
67
+ process.exit(0);
68
+ }
69
+
70
+ if (!requirement) errors.push("Complete Dependency Analysis requires Product Requirement input.");
71
+ else validateRequirementLink();
72
+ if (blockedOn !== "none") errors.push("complete analysis must use blocked_on: none.");
73
+ if (!repoExists) errors.push("repository_root must be an existing readable directory.");
74
+ if (repoPath && repoPath !== projectRoot) errors.push("repository_root must resolve to project_root.");
75
+ assertHeadings(dependency.text, 2, ["输入与代码基线", "用户故事覆盖矩阵", "跨故事共享依赖", "风险与未定位项"], errors, "Dependency Analysis");
76
+ if (/^##\s+(?:\d+[.、]?\s*)?(?:分析范围|追溯汇总)\s*$/m.test(dependency.text)) errors.push("Complete V3 Dependency Analysis must not contain 分析范围 or 追溯汇总 sections.");
77
+ if (scope === "frontend" || scope === "both") assertHeadings(dependency.text, 2, ["前端依赖详情"], errors, "Dependency Analysis");
78
+ if (scope === "backend" || scope === "both") assertHeadings(dependency.text, 2, ["后端依赖详情", "API 实现映射"], errors, "Dependency Analysis");
79
+ if (scope === "frontend" && /(?:^##\s+.*(?:后端依赖详情|API 实现映射)|\bBE-US-\d{3,}\b|\bAC-BE-\d{3,}\b|\bAPI-\d{3,}\b)/m.test(dependency.text)) errors.push("frontend scope must not contain backend dependency details or API mappings.");
80
+ if (scope === "backend" && /(?:^##\s+.*前端依赖详情|\bFE-US-\d{3,}\b|\bAC-FE-\d{3,}\b)/m.test(dependency.text)) errors.push("backend scope must not contain frontend dependency details.");
81
+
82
+ const frontendStories = requirement && (scope === "frontend" || scope === "both") ? storyBlocks(section(requirement.text, 2, "前端用户故事"), "FE-US") : [];
83
+ const backendStories = requirement && (scope === "backend" || scope === "both") ? storyBlocks(section(requirement.text, 2, "后端用户故事"), "BE-US") : [];
84
+ const apiRequired = backendStories.some((story) => /\bAPI\b/i.test(field(story.text, "触发方式") ?? ""));
85
+ if (apiRequired) {
86
+ if (!api) errors.push("API-triggered backend stories require api-documentation.md.");
87
+ if (!sourceApi || sourceApi === "none") errors.push("API-triggered backend stories require source_api_documentation.");
88
+ if (api && resolve(dirname(dependency.path), sourceApi) !== api.path) errors.push("source_api_documentation must reference the supplied API Documentation.");
89
+ } else {
90
+ if (api) errors.push("API Documentation must not be supplied when no selected backend story uses API.");
91
+ if (sourceApi !== "none") errors.push("Non-API analysis must use source_api_documentation: none.");
92
+ if ((scope === "backend" || scope === "both") && !/不适用.*不涉及 HTTP API/s.test(section(dependency.text, 2, "API 实现映射") ?? "")) errors.push("Non-API analysis must mark API 实现映射 not applicable.");
93
+ }
94
+
95
+ function validateDetails(frontend, expectedStories) {
96
+ const title = frontend ? "前端依赖详情" : "后端依赖详情";
97
+ const prefix = frontend ? "FE-US" : "BE-US";
98
+ const acPrefix = frontend ? "AC-FE" : "AC-BE";
99
+ const requiredFields = frontend
100
+ ? ["验收标准", "页面/路由", "组件", "状态", "API client/类型", "状态与边界落点", "定位证据", "风险", "置信度"]
101
+ : ["验收标准", "API 文档引用", "路由/入口", "Controller/Handler", "Service/领域逻辑", "DTO/Schema", "数据依赖", "权限依赖", "错误/日志/审计", "测试落点", "定位证据", "风险", "置信度"];
102
+ const details = storyBlocks(section(dependency.text, 2, title), prefix);
103
+ for (const story of expectedStories) if (!details.some((item) => item.id === story.id)) errors.push(`Dependency Analysis is missing ${title} for ${story.id}.`);
104
+ for (const detail of details) {
105
+ const story = expectedStories.find((item) => item.id === detail.id);
106
+ if (!story) errors.push(`Dependency Analysis contains unselected story ${detail.id}.`);
107
+ for (const name of requiredFields) if (!field(detail.text, name)) errors.push(`${detail.id} dependency detail is missing field ${name}.`);
108
+ const impact = fieldBlock(detail.text, "影响文件");
109
+ if (!impact) errors.push(`${detail.id} dependency detail is missing field 影响文件.`);
110
+ const declarations = [...impact.matchAll(/^\s*-\s*(F\d+)\s+(add|modify|reuse|新增|修改|复用)\s+`?([^`\r\n]+)`?/gmi)];
111
+ if (!declarations.length) errors.push(`${detail.id} 影响文件 must declare F<number>, add/modify/reuse, and a path.`);
112
+ const declared = declarations.map((item) => item[1]);
113
+ if (new Set(declared).size !== declared.length) errors.push(`${detail.id} 影响文件 contains duplicate file IDs.`);
114
+ for (const ref of detail.text.match(/\bF\d+\b/g) ?? []) if (!declared.includes(ref)) errors.push(`${detail.id} references undeclared impact file ${ref}.`);
115
+ if (!/^(?:high|medium|low)$/i.test(field(detail.text, "置信度") ?? "")) errors.push(`${detail.id} 置信度 must be high, medium, or low.`);
116
+ if (story) {
117
+ const expected = field(story.text, "验收标准")?.match(new RegExp(`${acPrefix}-\\d{3,}`, "g")) ?? [];
118
+ const actual = field(detail.text, "验收标准")?.match(new RegExp(`${acPrefix}-\\d{3,}`, "g")) ?? [];
119
+ if ([...expected].sort().join() !== [...actual].sort().join()) errors.push(`${detail.id} acceptance references must match Product Requirement.`);
120
+ }
121
+ }
122
+ }
123
+
124
+ if (scope === "frontend" || scope === "both") validateDetails(true, frontendStories);
125
+ if (scope === "backend" || scope === "both") validateDetails(false, backendStories);
126
+ const coverage = section(dependency.text, 2, "用户故事覆盖矩阵") ?? "";
127
+ for (const story of [...frontendStories, ...backendStories]) {
128
+ if (!coverage.includes(story.id)) errors.push(`Coverage matrix is missing ${story.id}.`);
129
+ for (const ac of field(story.text, "验收标准")?.match(/AC-(?:FE|BE)-\d{3,}/g) ?? []) if (!coverage.includes(ac)) errors.push(`Coverage matrix is missing ${ac}.`);
130
+ }
131
+
132
+ if (api) {
133
+ const mapping = section(dependency.text, 2, "API 实现映射") ?? "";
134
+ for (const item of apiBlocks(api.text)) {
135
+ const operation = tableValue(item.text, "Operation ID");
136
+ const signature = item.text.match(/^>\s*`((?:GET|POST|PUT|PATCH|DELETE)\s+\/[^`]+)`/m)?.[1];
137
+ if (!mapping.includes(item.id)) errors.push(`API 实现映射 is missing ${item.id}.`);
138
+ if (operation && !mapping.includes(operation)) errors.push(`API 实现映射 is missing Operation ID ${operation}.`);
139
+ if (signature && !mapping.includes(signature)) errors.push(`API 实现映射 is missing ${signature}.`);
140
+ }
141
+ }
142
+ print("Dependency Analysis", dependency.path, errors);