@tea-agent/loop-agent 0.25.5 → 0.26.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.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +1020 -986
- package/bin/loop-agent.js +21 -21
- package/dist/commands/cursor-prompt.js +6 -6
- package/dist/commands/init.js +68 -129
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/executors/dag-pi-executor.js +26 -20
- package/dist/executors/model-routing.js +34 -18
- package/dist/executors/shell-executor.js +1 -1
- package/dist/governance/manifest-types.js +33 -5
- package/dist/sidecars/cursor-prompt/executor.js +1 -1
- package/dist/task/task-demand-routing.js +3 -1
- package/dist/worker/console/chat/model-resolver.js +15 -3
- package/dist/worker/observe/static/constants.js +3 -2
- package/dist/worker/observe/static/copy.js +67 -67
- package/dist/worker/observe/static/dag-layout.d.ts +31 -31
- package/dist/worker/observe/static/dag-layout.js +83 -83
- package/dist/worker/observe/static/dag-model.js +1 -0
- package/dist/worker/observe/static/dom.js +220 -220
- package/dist/worker/observe/static/relations.js +133 -133
- package/dist/worker/observe/static/router.js +93 -93
- package/dist/worker/observe/static/run-processing.js +148 -148
- package/dist/worker/observe/static/styles.css +182 -42
- package/dist/worker/observe/static/views/batch.js +227 -227
- package/dist/worker/observe/static/views/dag-graph.js +172 -172
- package/dist/worker/observe/static/views/failures.js +143 -143
- package/dist/worker/observe/static/views/feature.js +492 -492
- package/dist/worker/observe/static/views/run.js +453 -453
- package/dist/worker/observe/static/views/shell.js +7 -7
- package/dist/worker/observe/static/views/timeline.js +163 -163
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +362 -45
- package/dist/workflows/dag/backend-test-case-manifest.js +20 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +17 -0
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/dist/workflows/dag/init-hybrid.js +18 -15
- package/dist/workflows/dag/lifecycle.js +40 -30
- package/dist/workflows/dag/node-execution.js +13 -0
- package/dist/workflows/dag/types.js +59 -19
- package/docs/init-surface.manifest.json +3 -1
- package/docs/skills/README.md +7 -7
- package/docs/templates/README.md +1 -0
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +473 -473
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/backend-test-dag.json +9 -9
- package/docs/templates/backend-test-result.schema.json +99 -99
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/frontend-design-contract.md +42 -42
- package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
- package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
- package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
- package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
- package/docs/templates/frontend-eval/metrics.md +138 -138
- package/docs/templates/frontend-eval/smoke-targets.md +53 -53
- package/docs/templates/frontend-task-constraints.md +35 -35
- package/docs/templates/frontend-task-requirement.md +70 -70
- package/docs/templates/harness.schema.json +29 -7
- package/docs/templates/init-evolution-review.md +35 -35
- package/docs/templates/init-managed-agents.md +137 -0
- package/docs/templates/interactive-ui-round2-experiment.md +66 -66
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
- package/docs/templates/knowledge-sync-dag.json +178 -178
- package/docs/templates/knowledge-sync-draft.schema.json +71 -71
- package/docs/templates/product-line/closeout.yaml +9 -9
- package/docs/templates/product-line/design.md +13 -13
- package/docs/templates/product-line/links.md +10 -10
- package/docs/templates/product-line/requirement.md +17 -17
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +80 -80
- package/docs/templates/worker-dogfood-setup.md +68 -68
- package/harness.json +1 -2
- package/package.json +1 -1
- package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
- package/scripts/kb-graph-incremental-prepare.mjs +386 -386
- package/scripts/kb-graph-materialize.mjs +105 -105
- package/scripts/kb-graph-promote.mjs +164 -164
- package/scripts/kb-query.mjs +554 -554
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/analyze-product-dependencies/SKILL.md +67 -67
- package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
- package/skills/analyze-product-dependencies/references/example.md +76 -76
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
- package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
- package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
- package/skills/analyze-product-requirements/SKILL.md +90 -90
- package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
- package/skills/analyze-product-requirements/references/example.md +86 -86
- package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
- package/skills/browser-tools/browser-content.js +103 -103
- package/skills/browser-tools/browser-cookies.js +35 -35
- package/skills/browser-tools/browser-eval.js +53 -53
- package/skills/browser-tools/browser-hn-scraper.js +108 -108
- package/skills/browser-tools/browser-nav.js +44 -44
- package/skills/browser-tools/browser-pick.js +162 -162
- package/skills/browser-tools/browser-screenshot.js +34 -34
- package/skills/browser-tools/browser-start.js +86 -86
- package/skills/browser-tools/package-lock.json +2556 -2556
- package/skills/browser-tools/package.json +19 -19
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +5 -4
- package/skills/loop-agent/references/docs-converge.md +126 -126
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +57 -57
- package/skills/loop-agent/references/model-routing.md +2 -0
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
- package/skills/playwright-cli/SKILL.md +420 -420
- package/skills/playwright-cli/references/element-attributes.md +23 -23
- package/skills/playwright-cli/references/playwright-tests.md +39 -39
- package/skills/playwright-cli/references/request-mocking.md +87 -87
- package/skills/playwright-cli/references/running-code.md +241 -241
- package/skills/playwright-cli/references/session-management.md +225 -225
- package/skills/playwright-cli/references/storage-state.md +275 -275
- package/skills/playwright-cli/references/test-generation.md +433 -433
- package/skills/playwright-cli/references/tracing.md +139 -139
- package/skills/playwright-cli/references/video-recording.md +143 -143
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/using-git-worktrees/SKILL.md +215 -215
- package/skills/verification-before-completion/SKILL.md +154 -154
- package/skills/webapp-testing/SKILL.md +19 -19
|
@@ -1,97 +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);
|
|
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);
|
|
@@ -1,98 +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);
|
|
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);
|