@tea-agent/loop-agent 0.33.1 → 0.33.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/workflows/dag/frontend-test-html-report.js +38 -23
  3. package/dist/workflows/dag/frontend-test-result-contract.js +77 -6
  4. package/dist/workflows/dag/init-hybrid.js +150 -50
  5. package/dist/workflows/dag/retry-policy.js +5 -0
  6. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +1 -1
  7. package/docs/templates/frontend-test-dag.json +11 -10
  8. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  9. package/package.json +1 -1
  10. package/skills/analyze-product-dependencies/SKILL.md +74 -33
  11. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +16 -11
  12. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +20 -10
  13. package/skills/analyze-product-dependencies/references/example.md +9 -9
  14. package/skills/analyze-product-dependencies/references/forward-test-cases.md +93 -18
  15. package/skills/analyze-product-dependencies/references/input-contract.md +27 -4
  16. package/skills/analyze-product-dependencies/references/kb-integration.md +64 -0
  17. package/skills/analyze-product-dependencies/references/scouting-rules.md +25 -10
  18. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +247 -54
  19. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +122 -29
  20. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +94 -36
  21. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +37 -23
  22. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +35 -43
  23. package/skills/analyze-product-requirements/SKILL.md +98 -43
  24. package/skills/analyze-product-requirements/references/acceptance-criteria.md +8 -12
  25. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +28 -14
  26. package/skills/analyze-product-requirements/references/example.md +24 -6
  27. package/skills/analyze-product-requirements/references/forward-test-cases.md +87 -9
  28. package/skills/analyze-product-requirements/references/kb-integration.md +56 -0
  29. package/skills/analyze-product-requirements/references/product-analysis-schema.md +19 -10
  30. package/skills/analyze-product-requirements/references/product-requirement-schema.md +21 -12
  31. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +45 -14
  32. package/skills/analyze-product-requirements/scripts/compute-source-identity.mjs +35 -0
  33. package/skills/analyze-product-requirements/scripts/test-validators.mjs +337 -29
  34. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +38 -7
  35. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +41 -29
  36. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +33 -22
  37. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +43 -24
  38. package/skills/playwright-cli/SKILL.md +7 -0
  39. package/skills/analyze-product-dependencies/agents/openai.yaml +0 -4
  40. package/skills/analyze-product-requirements/agents/openai.yaml +0 -4
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
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";
4
+ import { dirname, join } from "node:path";
5
+ import { acceptanceBlocks, assertHeadings, field, loadMarkdown, metadata, outputSpecBlocks, printResult, section, storyBlocks, validateAcceptance, validateArtifactLocation, validateFrontmatter, validateNoImplementationLeakage, validateStorySpecCoverage } from "./validation-helpers.mjs";
6
6
 
7
7
  const args = process.argv.slice(2);
8
8
  const allowPending = args.includes("--allow-pending");
@@ -20,10 +20,11 @@ catch (error) { console.error(error.message); process.exit(2); }
20
20
 
21
21
  const { path, text } = artifact;
22
22
  const errors = [];
23
+ validateFrontmatter(text, ["artifact_version", "artifact_type", "requirement_id", "requirement_status", "analysis_scope"], errors, "Product Requirement");
23
24
  validateArtifactLocation(artifact, errors, "product-requirement.md");
24
25
  const scope = metadata(text, "analysis_scope");
25
26
  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_version") !== "4.0") errors.push("artifact_version must be 4.0.");
27
28
  if (metadata(text, "artifact_type") !== "product-requirement") errors.push("artifact_type must be product-requirement.");
28
29
  if (!["frontend", "backend", "both"].includes(scope)) errors.push("analysis_scope must be frontend, backend, or both.");
29
30
  if (targetIndex >= 0 && !["frontend", "backend", "both"].includes(expectedTarget)) errors.push("--target must be frontend, backend, or both.");
@@ -31,27 +32,20 @@ else if (expectedTarget && scope !== expectedTarget) errors.push(`analysis_scope
31
32
  if (!["pending", "complete"].includes(status)) errors.push("requirement_status must be pending or complete.");
32
33
  if (!allowPending && status !== "complete") errors.push("Final Product Requirement must be complete.");
33
34
 
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
-
35
+ validateNoImplementationLeakage(text, errors);
49
36
  assertHeadings(text, 2, ["需求概述", "业务目标", "需求范围", "业务规则", "决策追溯"], errors, "Product Requirement");
50
37
  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.");
38
+ assertHeadings(range, 3, ["已确认范围", "非目标"], errors, "需求范围");
39
+ if (/^###\s+(?:\d+(?:\.\d+)*[.、]?\s*)?默认假设\s*$/m.test(range)) errors.push("Product Requirement must not contain 默认假设.");
40
+ const pendingSection = section(range, 3, "未决事项");
41
+ if (status === "complete" && pendingSection) errors.push("Complete Product Requirement must not contain 未决事项.");
42
+ if (status === "pending") assertHeadings(range, 3, ["未决事项"], errors, "Pending Product Requirement");
43
+ if (status === "complete" && /(?:pending-(?:blocking|non-blocking)|未形成|尚未确认|未决)/i.test(text)) errors.push("Complete Product Requirement must not contain unconfirmed product behavior.");
44
+ if (/^##\s+(?:\d+[.、]?\s*)?(?:用户角色|验收标准汇总|前后端契约|Open Questions|测试建议|边界 case|边界Case)\s*$/im.test(text)) {
45
+ errors.push("Product Requirement must not contain standalone 用户角色, 验收标准汇总, 前后端契约, Open Questions, 测试建议, or 边界 case sections.");
46
+ }
54
47
 
48
+ const acceptanceIds = new Set();
55
49
  function validateDomain(domain) {
56
50
  const frontend = domain === "frontend";
57
51
  const title = frontend ? "前端" : "后端";
@@ -65,16 +59,29 @@ function validateDomain(domain) {
65
59
  if (!stories.length) errors.push(`${storyHeading} must contain at least one ${prefix}-* story.`);
66
60
  validateStorySpecCoverage(stories, specs, errors, `${title} scope`);
67
61
  const storyFields = frontend ? ["角色", "目标", "价值", "入口", "验收标准"] : ["系统能力", "使用方", "业务价值", "触发方式", "验收标准"];
68
- const specFields = frontend ? ["页面/组件", "展示内容", "交互动作", "UI 状态", "表单校验", "权限可见性", "边界处理"] : ["输入语义", "输出语义", "数据读写", "权限规则", "业务规则", "安全要求", "幂等与并发", "错误与边界"];
62
+ const specFields = frontend ? ["页面/组件", "页面路由", "展示内容", "交互动作", "UI 状态", "表单校验", "权限可见性", "边界处理"] : ["输入语义", "输出语义", "数据读写", "权限规则", "业务规则", "安全要求", "幂等与并发", "错误与边界"];
69
63
  for (const story of stories) {
70
64
  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.`);
65
+ const trigger = field(story.text, "触发方式") ?? "";
66
+ const apiTrigger = /^API[((](?:Web|Remote|Web\s*\+\s*Remote)[))]$/i.test(trigger);
67
+ if (!frontend && !apiTrigger && !/^(?:定时任务|事件|消息|内部调用|数据迁移)$/i.test(trigger)) errors.push(`${story.id} 触发方式 is invalid; API must declare Web, Remote, or Web + Remote.`);
72
68
  const spec = specs.find((item) => item.id === story.id);
73
69
  if (!spec) continue;
74
70
  for (const name of specFields) if (!field(spec.text, name)) errors.push(`${spec.id} output specification is missing field ${name}.`);
71
+ if (frontend) {
72
+ const route = field(spec.text, "页面路由") ?? "";
73
+ const directRoute = /^`?\/[^`\s]+`?(?:\s|$|[;;,,])/.test(route);
74
+ const embeddedRoute = /^不适用;.+`?\/[^`\s]+`?.*页面/.test(route);
75
+ if (!directRoute && !embeddedRoute) errors.push(`${spec.id} 页面路由 must start with / or use 不适用; and name its containing page.`);
76
+ if (/(?:route config|middleware|路由注册文件|路由组件)/i.test(route)) errors.push(`${spec.id} 页面路由 must describe product routing, not implementation files or framework components.`);
77
+ }
75
78
  const criteria = acceptanceBlocks(spec, acPrefix);
76
79
  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);
80
+ for (const item of criteria) {
81
+ if (acceptanceIds.has(item.id)) errors.push(`Duplicate acceptance criterion ID: ${item.id}.`);
82
+ acceptanceIds.add(item.id);
83
+ validateAcceptance(item.text, item.id, errors);
84
+ }
78
85
  const referenced = [...(field(story.text, "验收标准") ?? "").matchAll(new RegExp(`${acPrefix}-\\d{3,}`, "g"))].map((item) => item[0]).sort();
79
86
  const actual = criteria.map((item) => item.id).sort();
80
87
  if (referenced.join() !== actual.join()) errors.push(`${story.id} acceptance references must match its output specification criteria.`);
@@ -86,12 +93,17 @@ if (scope === "backend" || scope === "both") validateDomain("backend");
86
93
  if (scope === "frontend" && /(?:^##\s+.*后端|^###\s+BE-US-)/m.test(text)) errors.push("frontend scope must not contain backend sections or stories.");
87
94
  if (scope === "backend" && /(?:^##\s+.*前端|^###\s+FE-US-)/m.test(text)) errors.push("backend scope must not contain frontend sections or stories.");
88
95
 
89
- const clarificationSource = sourcePath("source_clarification");
90
- if (status === "complete" && clarificationSource && !["inline", "none"].includes(clarificationSource) && existsSync(clarificationSource)) {
96
+ const clarificationSource = join(dirname(path), "requirement-clarification.md");
97
+ if (!existsSync(clarificationSource)) {
98
+ errors.push("Product Requirement requires requirement-clarification.md in the same requirement directory.");
99
+ } else {
91
100
  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.`);
101
+ const clarificationStatus = metadata(clarification, "clarification_status");
102
+ if (clarificationStatus !== status) errors.push("Product Requirement and source clarification must have the same pending/complete status.");
103
+ if (status === "complete") {
104
+ const markers = [...clarification.matchAll(/^-\s*决策标记[::]\s*(DEC-Q-\d{3,})/gm)].map((item) => item[1]);
105
+ for (const marker of markers) if (!text.includes(marker)) errors.push(`${marker} from source clarification is missing from Product Requirement.`);
106
+ }
95
107
  }
96
108
 
97
109
  printResult("Product Requirement", path, errors);
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
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";
3
+ import { dirname } from "node:path";
4
+ import { assertHeadings, canonical, field, headings, loadMarkdown, metadata, printResult, section, validateArtifactLocation, validateFrontmatter } from "./validation-helpers.mjs";
6
5
 
7
6
  const args = process.argv.slice(2);
8
7
  const allowPending = args.includes("--allow-pending");
@@ -20,46 +19,55 @@ try {
20
19
  } catch (error) { console.error(error.message); process.exit(2); }
21
20
 
22
21
  const errors = [];
22
+ validateFrontmatter(clarification.text, ["artifact_version", "artifact_type", "requirement_id", "analysis_scope", "clarification_status", "clarification_rounds"], errors, "Requirement Clarification");
23
23
  validateArtifactLocation(clarification, errors, "requirement-clarification.md");
24
24
  const scope = metadata(clarification.text, "analysis_scope");
25
25
  const status = metadata(clarification.text, "clarification_status");
26
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.");
27
+ if (metadata(clarification.text, "artifact_version") !== "4.0") errors.push("artifact_version must be 4.0.");
28
28
  if (metadata(clarification.text, "artifact_type") !== "requirement-clarification") errors.push("artifact_type must be requirement-clarification.");
29
29
  if (!["frontend", "backend", "both"].includes(scope)) errors.push("analysis_scope must be frontend, backend, or both.");
30
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.");
31
+ if (!Number.isInteger(clarificationRounds) || clarificationRounds < 0) errors.push("clarification_rounds must be a non-negative integer.");
32
32
  if (!allowPending && status !== "complete") errors.push("Final Requirement Clarification must be complete.");
33
+ const requirementStatus = metadata(requirement.text, "requirement_status");
34
+ if (status !== requirementStatus) errors.push("Requirement Clarification and Product Requirement must have the same pending/complete status.");
33
35
  if (scope !== metadata(analysis.text, "analysis_scope")) errors.push("analysis_scope must match Product Analysis.");
36
+ if (scope !== metadata(requirement.text, "analysis_scope")) errors.push("analysis_scope must match Product Requirement.");
34
37
 
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)}`);
38
+ if (dirname(analysis.path) !== dirname(clarification.path) || dirname(requirement.path) !== dirname(clarification.path)) errors.push("All requirement artifacts must be in the same requirement directory.");
39
+ const requirementId = metadata(clarification.text, "requirement_id");
40
+ if (requirementId !== metadata(analysis.text, "requirement_id") || requirementId !== metadata(requirement.text, "requirement_id")) errors.push("requirement_id must match across all requirement artifacts.");
44
41
 
45
42
  const analysisStatus = metadata(analysis.text, "analysis_status");
46
43
  const questionBlocks = headings(clarification.text, 3).filter((item) => /^Q-\d{3,}\s+/.test(item.title));
44
+ const sourceSection = section(clarification.text, 2, questionBlocks.length ? "澄清来源" : "来源") ?? "";
45
+ const recordedSourceIdentity = field(sourceSection, "原始需求身份") ?? "";
46
+ const analysisSourceIdentity = field(section(analysis.text, 2, "原始需求") ?? "", "原始需求身份") ?? "";
47
+ if (!/^(?:inline|file):sha256:[a-f0-9]{64}$/.test(recordedSourceIdentity)) errors.push("Clarification source must contain a valid 原始需求身份.");
48
+ else if (recordedSourceIdentity !== analysisSourceIdentity) errors.push("原始需求身份 must match Product Analysis.");
49
+ if (/(?:default-confirmed|confirmed-default)/.test(clarification.text)) errors.push("Requirement Clarification must not use default-confirmed or confirmed-default.");
47
50
 
48
51
  if (!questionBlocks.length) {
52
+ if (status !== "complete") errors.push("Compact no-clarification-required artifacts must be complete.");
49
53
  if (clarificationRounds !== 0) errors.push("Compact clarification must set clarification_rounds to 0.");
50
54
  if (analysisStatus !== "no-clarification-required") errors.push("Compact clarification is allowed only when Product Analysis is no-clarification-required.");
51
55
  const required = ["澄清结论", "来源", "合并结果"];
52
56
  assertHeadings(clarification.text, 2, required, errors, "Compact Requirement Clarification");
53
57
  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.");
58
+ const conclusion = section(clarification.text, 2, "澄清结论") ?? "";
59
+ if (!/状态[::]\s*no-clarification-required/.test(conclusion)) errors.push("Compact clarification must declare no-clarification-required.");
60
+ if (status === "complete" && !/^[ \t]*-[ \t]*内容补充[::][ \t]*用户已确认无需补充\s*$/m.test(conclusion)) {
61
+ errors.push("Complete compact clarification must record 内容补充:用户已确认无需补充.");
62
+ }
55
63
  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
64
  } else {
57
- if (clarificationRounds < 1 || clarificationRounds > 3) errors.push("Question-based clarification must set clarification_rounds from 1 to 3.");
65
+ if (clarificationRounds < 1) errors.push("Question-based clarification must set clarification_rounds to a positive integer.");
58
66
  if (analysisStatus !== "ready-for-clarification") errors.push("Question records require Product Analysis status ready-for-clarification.");
59
67
  assertHeadings(clarification.text, 2, ["澄清来源", "决策分支", "问题记录", "决策索引"], errors, "Requirement Clarification");
60
68
  const branchSection = section(clarification.text, 2, "决策分支") ?? "";
61
69
  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.");
70
+ if (branchIds.length < 1) errors.push("Question-based clarification must define at least one top-level BR-* branch.");
63
71
  if (status === "complete") {
64
72
  const unresolved = branchSection.split(/\r?\n/).filter((line) => /\|\s*BR-\d{3,}\s*\|/.test(line) && !/\|\s*resolved\s*\|\s*$/.test(line));
65
73
  if (unresolved.length) errors.push("Complete clarification requires every branch to be resolved.");
@@ -74,25 +82,28 @@ if (!questionBlocks.length) {
74
82
  const priority = field(block.text, "优先级");
75
83
  const impactScope = field(block.text, "影响范围");
76
84
  const answer = field(block.text, "用户回答");
85
+ const finalDecision = field(block.text, "最终决策");
77
86
  const decisionStatus = field(block.text, "状态");
78
87
  const decisionSource = field(block.text, "决策来源");
79
88
  if (!branchIds.includes(branch)) errors.push(`${id} 分支 must reference a defined BR-* branch.`);
80
89
  if (!Number.isInteger(round) || round < 1 || round > clarificationRounds) errors.push(`${id} 澄清轮次 must be between 1 and clarification_rounds.`);
81
90
  if (!["P0", "P1", "P2"].includes(priority)) errors.push(`${id} 优先级 must be P0, P1, or P2.`);
82
91
  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.`);
92
+ if (!["confirmed", "pending-blocking", "pending-non-blocking"].includes(decisionStatus)) errors.push(`${id} 状态 is invalid.`);
93
+ if (decisionSource !== "user") errors.push(`${id} 决策来源 must be user; source requirements and code facts must not be represented as Q-* decisions.`);
85
94
  const marker = field(block.text, "决策标记");
86
95
  if (marker !== `DEC-${id}`) errors.push(`${id} 决策标记 must be DEC-${id}.`);
87
96
  if (markers.has(marker)) errors.push(`Duplicate decision marker: ${marker}.`);
88
97
  markers.add(marker);
89
- if (decisionSource === "code-evidence" && !field(block.text, "代码证据")) errors.push(`${id} code-evidence decision requires 代码证据.`);
90
98
  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.`);
99
+ if (status === "complete" && (decisionStatus !== "confirmed" || decisionSource !== "user" || /^(?:未回答|待确认|无)$/.test(answer ?? ""))) errors.push(`${id} must be confirmed by an explicit user answer before clarification can be complete.`);
100
+ if (status === "complete" && /^(?:未形成|未回答|待确认|尚未确认|无)$/.test(finalDecision ?? "")) errors.push(`${id} must contain a formed final decision before clarification can be complete.`);
92
101
  if (status === "complete" && marker && !requirement.text.includes(marker)) errors.push(`${marker} is missing from Product Requirement decision traceability.`);
93
102
  if (!(section(clarification.text, 2, "决策索引") ?? "").includes(marker ?? "__missing__")) errors.push(`${marker ?? id} is missing from 决策索引.`);
94
103
  }
104
+ if (status === "complete" && !/^[ \t]*-[ \t]*共同理解[::][ \t]*已确认\s*$/m.test(section(clarification.text, 2, "决策索引") ?? "")) {
105
+ errors.push("Complete question-based clarification must record 共同理解:已确认 in 决策索引.");
106
+ }
95
107
  }
96
108
 
97
- if (status === "complete" && metadata(requirement.text, "requirement_status") !== "complete") errors.push("Complete clarification requires a complete Product Requirement.");
98
109
  printResult("Requirement Clarification", clarification.path, errors);
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync, statSync } from "node:fs";
2
- import { basename, dirname, join, resolve } from "node:path";
1
+ import { readFileSync } from "node:fs";
2
+ import { basename, dirname, resolve } from "node:path";
3
3
 
4
4
  export function loadMarkdown(fileArg, label = "artifact") {
5
5
  const path = resolve(fileArg);
@@ -14,6 +14,29 @@ export function frontmatter(text) {
14
14
  return text.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? "";
15
15
  }
16
16
 
17
+ export function validateFrontmatter(text, allowedKeys, errors, owner) {
18
+ const block = frontmatter(text);
19
+ if (!block) {
20
+ errors.push(`${owner} must contain YAML frontmatter.`);
21
+ return;
22
+ }
23
+ const keys = [];
24
+ for (const line of block.split(/\r?\n/)) {
25
+ if (!line.trim()) continue;
26
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/);
27
+ if (!match) {
28
+ errors.push(`${owner} frontmatter must contain only top-level scalar fields.`);
29
+ continue;
30
+ }
31
+ keys.push(match[1]);
32
+ }
33
+ for (const key of new Set(keys)) {
34
+ if (keys.filter((value) => value === key).length > 1) errors.push(`${owner} frontmatter contains duplicate field ${key}.`);
35
+ if (!allowedKeys.includes(key)) errors.push(`${owner} frontmatter contains unsupported field ${key}.`);
36
+ }
37
+ for (const key of allowedKeys) if (!keys.includes(key)) errors.push(`${owner} frontmatter is missing field ${key}.`);
38
+ }
39
+
17
40
  export function metadata(text, name) {
18
41
  const block = frontmatter(text);
19
42
  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -100,17 +123,21 @@ export function validateAcceptance(block, owner, errors) {
100
123
  if (!/(?:不得|不应|禁止|保留|恢复|重试|兜底|回滚|disabled)/.test(block)) {
101
124
  errors.push(`${owner} must include an observable protection or recovery result.`);
102
125
  }
126
+ if (/(?:功能正常|应正常展示|展示正确|接口可用|数据正确|符合预期|妥善处理|合理处理|无异常情况|没有需要处理的异常场景|无需处理失败)/.test(block)) {
127
+ errors.push(`${owner} must not use vague acceptance language.`);
128
+ }
103
129
  }
104
130
 
105
- export function containsUnresolvedBlockingPriority(block) {
106
- if (!block) return false;
107
- return block.split(/\r?\n/).slice(1).some((line) => {
108
- const plain = line.trim().replace(/^[-*+]\s*/, "").replace(/^\[[ xX]\]\s*/, "");
109
- if (!/\bP[01]\b/.test(plain)) return false;
110
- if (/^(?:不存在|没有|无)(?:任何)?(?:尚未|未)?(?:解决|确认|关闭|完成)?(?:的)?\s*P[01]\b/.test(plain)) return false;
111
- if (/^P[01]\b/.test(plain)) return true;
112
- return /(?:pending(?:-blocking)?|unresolved|未解决|未确认|待确认|未决|阻塞)/i.test(plain);
113
- });
131
+ export function validateNoImplementationLeakage(text, errors, owner = "Product Requirement") {
132
+ if (/\bKB-FACT-\d+\b/.test(text)) errors.push(`${owner} must not contain KB-FACT-* markers.`);
133
+ if (/\bCODE-FACT-\d+\b/.test(text)) errors.push(`${owner} must not contain CODE-FACT-* markers.`);
134
+ if (/(?:^|[\s`'(])(?:\.\/|\.\.\/)?(?:src|app|packages|lib|internal|cmd|pkg)\/[^\s`')]+/m.test(text)) {
135
+ errors.push(`${owner} must not contain repository file paths.`);
136
+ }
137
+ if (/\b[A-Z][A-Za-z0-9]*(?:Controller|Service|Repository|Handler|Middleware)\b/.test(text)) {
138
+ errors.push(`${owner} must not contain code-level class or component symbols.`);
139
+ }
140
+ if (/证据位置/.test(text)) errors.push(`${owner} must not contain 证据位置.`);
114
141
  }
115
142
 
116
143
  export function assertHeadings(text, level, required, errors, owner) {
@@ -132,23 +159,15 @@ export function printResult(label, path, errors) {
132
159
 
133
160
  export function validateArtifactLocation(artifact, errors, expectedName) {
134
161
  const requirementId = metadata(artifact.text, "requirement_id");
135
- const projectRootValue = metadata(artifact.text, "project_root");
136
162
  if (!requirementId || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(requirementId)) {
137
163
  errors.push("requirement_id must use lowercase letters, digits, and single hyphens.");
138
164
  return;
139
165
  }
140
- if (!projectRootValue) {
141
- errors.push("project_root is required.");
142
- return;
143
- }
144
- const projectRoot = resolve(dirname(artifact.path), projectRootValue);
145
- if (!existsSync(projectRoot) || !statSync(projectRoot).isDirectory()) {
146
- errors.push(`project_root does not resolve to an existing directory: ${projectRoot}`);
147
- return;
148
- }
149
- const expected = resolve(join(projectRoot, "docs", "product-analysis", requirementId));
150
- if (dirname(artifact.path) !== expected) {
151
- errors.push(`Artifact must be located under ${expected}.`);
166
+ const artifactDir = dirname(artifact.path);
167
+ const productAnalysisDir = dirname(artifactDir);
168
+ const docsDir = dirname(productAnalysisDir);
169
+ if (basename(artifactDir) !== requirementId || basename(productAnalysisDir) !== "product-analysis" || basename(docsDir) !== "docs") {
170
+ errors.push(`Artifact must be located under <project-root>/docs/product-analysis/${requirementId}.`);
152
171
  }
153
172
  if (expectedName && basename(artifact.path) !== expectedName) {
154
173
  errors.push(`Artifact filename must be ${expectedName}.`);
@@ -80,6 +80,13 @@ controller receipts:successful `open` → successful `find` → successful pos
80
80
  - 文件型证据:`screenshot` 与 `pdf` 必须使用 `--filename <file>`,且文件名由 controller 规范化到当前 case evidence 目录。截图一律写为 `playwright-cli screenshot --filename final.png`;有真实元素 ref 时写为 `playwright-cli screenshot e5 --filename final.png`。`pdf` 写为 `playwright-cli pdf --filename final.pdf`。`snapshot` 不带 filename 时只返回响应;需要文件时写为 `playwright-cli snapshot --filename snapshot.txt`。不得使用 `--path`、`--output`、`--file`,也不得把文件路径作为位置参数。
81
81
  - 对话框、上传、拖放和视口变化仅在 case 已给出所需前置条件时使用对应 allowlist 命令。
82
82
 
83
+ ## 持久化执行事实
84
+
85
+ 每个 browser case 必须在自身 evidenceDir 写入权威的 `execution.md` 与 `case-result.json`,最终报告只读取最后一次写入的文件:
86
+
87
+ - `case-result.json`:包含匹配的 `caseId`、最终 `status`(`passed`/`failed`/`blocked`)、`evidencePaths`;重跑时用整数 `rerunAttempt` 覆写同一文件,最终状态即报告状态。
88
+ - `execution.md`:当存在可记录的执行事实时,使用固定标题 `### 执行摘要` 与 `### 实际执行步骤`,以便有界确定性报告抽取器展示实际执行步骤与摘要。这两段为可选;缺失只形成 advisory,不会降级已通过状态。
89
+
83
90
  ## 阻塞处理
84
91
 
85
92
  当环境、数据、权限、时序、异常路径或响应内容无法用 RAG、冻结配置和 runtime allowlist 证明时,
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "产品依赖分析"
3
- short_description: "把故事、输出规范和验收标准映射到真实代码与 API 文档"
4
- default_prompt: "使用 $analyze-product-dependencies 将 Product Requirement 映射到代码,并按需生成 API 文档。"
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "产品需求分析"
3
- short_description: "分析并按需澄清需求,生成故事、输出规范和验收标准"
4
- default_prompt: "使用 $analyze-product-requirements 分析并按需澄清需求,生成完整 Product Requirement。"