sdd-flow-kit 1.3.14 → 1.3.18

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 (47) hide show
  1. package/dist/cli.js +97 -10
  2. package/dist/core/apiFieldContract.js +90 -0
  3. package/dist/core/demoImplementationCoverage.js +242 -0
  4. package/dist/core/e2eAssertionDepth.js +233 -0
  5. package/dist/core/prdArtifacts.js +12 -1
  6. package/dist/core/prdCoverage.js +46 -9
  7. package/dist/core/prdSemanticDiff.js +7 -4
  8. package/dist/core/syncSourcePrd.js +181 -0
  9. package/dist/core/techDocQuality.js +87 -0
  10. package/dist/core/testAssertionAnalysis.js +213 -0
  11. package/dist/core/versionIntent.js +65 -0
  12. package/dist/core/writeNext.js +1 -1
  13. package/dist/index.js +6 -1
  14. package/dist/steps/invokeFlow.js +5 -19
  15. package/dist/steps/runGate.js +49 -5
  16. package/dist/steps/runGuard.js +44 -0
  17. package/dist/steps/runSyncSourcePrd.js +38 -0
  18. package/dist/steps/runValidate.js +4 -2
  19. package/dist/steps/setupProject.js +31 -0
  20. package/dist/steps/startFlow.js +7 -2
  21. package/package.json +2 -1
  22. package/scripts/assert-active-openspec-change.sh +114 -0
  23. package/scripts/assess-api-field-contract.mjs +119 -0
  24. package/scripts/assess-demo-implementation-coverage.mjs +188 -0
  25. package/scripts/assess-e2e-assertion-depth.mjs +146 -0
  26. package/scripts/assess-playwright-e2e-coverage.mjs +244 -0
  27. package/scripts/assess-tech-doc-quality.mjs +91 -0
  28. package/scripts/install-git-hooks.sh +27 -0
  29. package/scripts/lib/test-assertion-analysis.mjs +134 -0
  30. package/scripts/resolve-playwright-e2e-scope.mjs +248 -0
  31. package/src/templates/artifacts/01-adi-doc-skill-/346/214/207/345/274/225.template.md +6 -2
  32. package/src/templates/artifacts/02-/351/234/200/346/261/202/345/210/206/346/236/220/346/217/220/347/244/272/350/257/215.template.md +5 -0
  33. package/src/templates/artifacts/04-/346/212/200/346/234/257/346/226/207/346/241/243.template.md +14 -0
  34. package/src/templates/artifacts/05-/351/252/214/346/224/266/346/270/205/345/215/225.template.md +2 -2
  35. package/src/templates/artifacts/06-agent-skill.template.md +3 -3
  36. package/src/templates/artifacts/06-openspec-/346/217/220/346/241/210/350/215/211/347/250/277.template.md +2 -2
  37. package/src/templates/artifacts/07-opsx-auto-chain.template.md +2 -2
  38. package/src/templates/artifacts/08-PRD/344/270/200/350/207/264/346/200/247/345/244/215/346/237/245/346/212/245/345/221/212.template.md +2 -0
  39. package/src/templates/artifacts/design-demo-reference.template.md +24 -0
  40. package/src/templates/prompts/02-sdd-loop-prompt.md +25 -11
  41. package/src/templates/prompts/04-tech-doc-fill-prompt.md +71 -0
  42. package/src/templates/rules/sdd-version-trigger.mdc.template +56 -0
  43. package/src/templates/skills/confluence-doc.py.template +64 -25
  44. package/src/templates/skills/demo-reference-mandatory-workflow.md +77 -0
  45. package/src/templates/skills/doc-skill.SKILL.md.template +13 -6
  46. package/src/templates/skills/layered-testing-strategy.md +13 -6
  47. package/src/templates/skills/version-trigger.SKILL.md.template +56 -0
package/dist/cli.js CHANGED
@@ -20,10 +20,13 @@ const runDeliver_1 = require("./steps/runDeliver");
20
20
  const runPrdReview_1 = require("./steps/runPrdReview");
21
21
  const runPrdDiff_1 = require("./steps/runPrdDiff");
22
22
  const runPrdRemediate_1 = require("./steps/runPrdRemediate");
23
+ const runSyncSourcePrd_1 = require("./steps/runSyncSourcePrd");
23
24
  const runChain_1 = require("./steps/runChain");
24
25
  const runSessionMode_1 = require("./steps/runSessionMode");
26
+ const runGuard_1 = require("./steps/runGuard");
25
27
  const bootstrap_1 = require("./core/bootstrap");
26
28
  const projectRoots_1 = require("./core/projectRoots");
29
+ const versionIntent_1 = require("./core/versionIntent");
27
30
  const ALLOWED_AGENTS = ["cursor", "claude-code", "codex", "openclaw", "custom"];
28
31
  function getArgValue(flag) {
29
32
  const idx = process_1.default.argv.indexOf(flag);
@@ -34,6 +37,42 @@ function getArgValue(flag) {
34
37
  return null;
35
38
  return v;
36
39
  }
40
+ function getFreeTextArgs(startIndex) {
41
+ const flagsWithValue = new Set([
42
+ "--agent",
43
+ "--project",
44
+ "--product",
45
+ "--version",
46
+ "--project-root",
47
+ "--projectRoot",
48
+ "--run-id",
49
+ "--runId",
50
+ "--change",
51
+ "--branch",
52
+ "--from",
53
+ "--to",
54
+ "--expect",
55
+ "--execution-mode",
56
+ "--executionMode",
57
+ "--text",
58
+ ]);
59
+ const parts = [];
60
+ const tokens = process_1.default.argv.slice(startIndex);
61
+ for (let i = 0; i < tokens.length; i += 1) {
62
+ const token = tokens[i];
63
+ if (token === "-y" || token === "--dry-run" || token === "--plan-json" || token === "--strict-commands") {
64
+ continue;
65
+ }
66
+ if (flagsWithValue.has(token)) {
67
+ i += 1;
68
+ continue;
69
+ }
70
+ if (token.startsWith("--"))
71
+ continue;
72
+ parts.push(token);
73
+ }
74
+ return parts.join(" ").trim();
75
+ }
37
76
  function printHelp() {
38
77
  // eslint-disable-next-line no-console
39
78
  console.log([
@@ -51,7 +90,8 @@ function printHelp() {
51
90
  "3) 生成骨架并进入可执行模式(推荐):",
52
91
  " npx sdd-flow-kit run --product ADI --version 2.3.2 --agent claude-code",
53
92
  "",
54
- "4) 一句话触发(可选能力):",
93
+ "4) 一句话触发(版本开发强制入口):",
94
+ " npx sdd-flow-kit guard \"开发 ADI_V2.3.4版本\" -y --agent cursor",
55
95
  " npx sdd-flow-kit invoke \"开发 XXX 1.2.3 需求\"",
56
96
  "",
57
97
  "5) 环境健检:",
@@ -68,6 +108,7 @@ function printHelp() {
68
108
  " npx sdd-flow-kit gate --project-root . --run-id <runId> --expect prd-coverage --change <name>",
69
109
  "",
70
110
  "10) PRD 一致性复查(最细粒度,交付前强制):",
111
+ " npx sdd-flow-kit sync-source-prd --project-root . --run-id <runId>",
71
112
  " npx sdd-flow-kit prd-review --project-root . --run-id <runId>",
72
113
  " # 在 AI 工具执行 openspec/PRD/<runId>/08-PRD一致性复查提示词.md",
73
114
  " npx sdd-flow-kit prd-diff --project-root . --run-id <runId>",
@@ -111,7 +152,7 @@ function printHelp() {
111
152
  ].join("\n"));
112
153
  }
113
154
  async function main() {
114
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
155
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
115
156
  const cmd = process_1.default.argv[2];
116
157
  if (!cmd || cmd === "--help" || cmd === "-h") {
117
158
  printHelp();
@@ -144,6 +185,7 @@ async function main() {
144
185
  cmd !== "install" &&
145
186
  cmd !== "doctor" &&
146
187
  cmd !== "ensure-opsx" &&
188
+ cmd !== "guard" &&
147
189
  cmd !== "invoke" &&
148
190
  cmd !== "gate" &&
149
191
  cmd !== "phase" &&
@@ -152,6 +194,7 @@ async function main() {
152
194
  cmd !== "prd-review" &&
153
195
  cmd !== "prd-diff" &&
154
196
  cmd !== "prd-remediate" &&
197
+ cmd !== "sync-source-prd" &&
155
198
  cmd !== "deliver" &&
156
199
  cmd !== "chain" &&
157
200
  cmd !== "session") {
@@ -169,7 +212,11 @@ async function main() {
169
212
  const dryRun = process_1.default.argv.includes("--dry-run");
170
213
  const planJson = process_1.default.argv.includes("--plan-json");
171
214
  const agentValue = (_g = getArgValue("--agent")) !== null && _g !== void 0 ? _g : "claude-code";
172
- const projectKind = ((_h = getArgValue("--project")) !== null && _h !== void 0 ? _h : "OMS");
215
+ const rawGuardText = cmd === "guard"
216
+ ? ((_h = getArgValue("--text")) !== null && _h !== void 0 ? _h : getFreeTextArgs(3)).trim()
217
+ : "";
218
+ const guardIntent = rawGuardText ? (0, versionIntent_1.parseVersionIntent)(rawGuardText) : null;
219
+ const projectKind = ((_k = (_j = getArgValue("--project")) !== null && _j !== void 0 ? _j : guardIntent === null || guardIntent === void 0 ? void 0 : guardIntent.product) !== null && _k !== void 0 ? _k : "OMS");
173
220
  if (!ALLOWED_AGENTS.includes(agentValue)) {
174
221
  // eslint-disable-next-line no-console
175
222
  console.error(`不支持的 --agent: ${agentValue}`);
@@ -189,6 +236,7 @@ async function main() {
189
236
  const bootstrapCommands = new Set([
190
237
  "doctor",
191
238
  "ensure-opsx",
239
+ "guard",
192
240
  "run",
193
241
  "invoke",
194
242
  "install",
@@ -200,6 +248,7 @@ async function main() {
200
248
  "prd-review",
201
249
  "prd-diff",
202
250
  "prd-remediate",
251
+ "sync-source-prd",
203
252
  "deliver",
204
253
  "chain",
205
254
  "session",
@@ -211,6 +260,36 @@ async function main() {
211
260
  projectKind: projectKind,
212
261
  });
213
262
  }
263
+ if (cmd === "guard") {
264
+ const text = rawGuardText;
265
+ if (!text) {
266
+ // eslint-disable-next-line no-console
267
+ console.error("guard 需要自然语言文本,例如:npx sdd-flow-kit guard \"开发 ADI_V2.3.4版本\" -y --agent cursor");
268
+ process_1.default.exitCode = 1;
269
+ return;
270
+ }
271
+ const result = await (0, runGuard_1.runGuard)({
272
+ text,
273
+ projectRoot,
274
+ branch,
275
+ yes,
276
+ dryRun,
277
+ agent,
278
+ });
279
+ // eslint-disable-next-line no-console
280
+ console.log(result.ok ? `✅ ${result.detail}` : `ℹ️ ${result.detail}`);
281
+ if (result.ok && result.nextPath) {
282
+ // eslint-disable-next-line no-console
283
+ console.log(`下一步:读取 ${result.nextPath} 与同目录 .session-state.json`);
284
+ }
285
+ if (planJson) {
286
+ // eslint-disable-next-line no-console
287
+ console.log(JSON.stringify({ command: "guard", projectRoot, dryRun, result }, null, 2));
288
+ }
289
+ if (!result.ok)
290
+ process_1.default.exitCode = 2;
291
+ return;
292
+ }
214
293
  if (cmd === "gate") {
215
294
  const expect = getArgValue("--expect");
216
295
  if (!expect || !GATE_EXPECTS.includes(expect)) {
@@ -277,6 +356,18 @@ async function main() {
277
356
  process_1.default.exitCode = 1;
278
357
  return;
279
358
  }
359
+ if (cmd === "sync-source-prd") {
360
+ const result = await (0, runSyncSourcePrd_1.runSyncSourcePrd)({
361
+ projectRoot,
362
+ runId,
363
+ dryRun,
364
+ });
365
+ // eslint-disable-next-line no-console
366
+ console.log(result.ok ? `✅ ${result.detail}` : `❌ ${result.detail}`);
367
+ if (!result.ok)
368
+ process_1.default.exitCode = 1;
369
+ return;
370
+ }
280
371
  if (cmd === "validate") {
281
372
  if (!changeName) {
282
373
  // eslint-disable-next-line no-console
@@ -376,7 +467,7 @@ async function main() {
376
467
  process_1.default.exitCode = 1;
377
468
  return;
378
469
  }
379
- const from = ((_j = getArgValue("--from")) !== null && _j !== void 0 ? _j : "docs-done");
470
+ const from = ((_l = getArgValue("--from")) !== null && _l !== void 0 ? _l : "docs-done");
380
471
  if (!CHAIN_FROM.includes(from)) {
381
472
  // eslint-disable-next-line no-console
382
473
  console.error(`chain 需要 --from ${CHAIN_FROM.join("|")}`);
@@ -405,7 +496,7 @@ async function main() {
405
496
  process_1.default.exitCode = 1;
406
497
  return;
407
498
  }
408
- const modeArg = (_k = getArgValue("--execution-mode")) !== null && _k !== void 0 ? _k : getArgValue("--executionMode");
499
+ const modeArg = (_m = getArgValue("--execution-mode")) !== null && _m !== void 0 ? _m : getArgValue("--executionMode");
409
500
  const textArg = getArgValue("--text");
410
501
  const strictCommands = process_1.default.argv.includes("--strict-commands");
411
502
  const result = await (0, runSessionMode_1.runSessionMode)({
@@ -474,11 +565,7 @@ async function main() {
474
565
  }
475
566
  if (!product || !version) {
476
567
  if (cmd === "invoke") {
477
- const raw = process_1.default.argv
478
- .slice(3)
479
- .filter((token) => !token.startsWith("--") && token !== "-y")
480
- .join(" ")
481
- .trim();
568
+ const raw = getFreeTextArgs(3);
482
569
  const fromFlag = getArgValue("--text");
483
570
  const text = (fromFlag !== null && fromFlag !== void 0 ? fromFlag : raw).trim();
484
571
  const parsed = (0, invokeFlow_1.parseInvokeText)(text);
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkApiFieldContract = checkApiFieldContract;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = require("./fs");
9
+ const API_MODULE_DIR_RE = /(^|\/)(src|frontend\/src)\/config\/api\/modules\//;
10
+ const NORMALIZE_MARKERS = /snakeToCamel|camelToSnake|normalizeFinanceApiResult|normalize\w*Api|caseTransform|toCamelCase|toSnakeCase/i;
11
+ const API_CLIENT_CALL_RE = /\b(?:apiClient|request|post|get)\s*[.(]/i;
12
+ /**
13
+ * 扫描 change 关联的 API 模块:对接后端时须在模块层做字段 normalize(防 SNAKE_CASE 漏映射)。
14
+ */
15
+ async function checkApiFieldContract(pkgRoot, changeName) {
16
+ const checks = [];
17
+ const skip = process.env.POQUAN_SKIP_API_FIELD_CONTRACT === "1" ||
18
+ process.env.OPSX_SKIP_API_FIELD_CONTRACT === "1";
19
+ if (skip) {
20
+ checks.push({
21
+ name: "api-field-contract",
22
+ ok: true,
23
+ detail: "SKIP: OPSX_SKIP_API_FIELD_CONTRACT=1",
24
+ });
25
+ return checks;
26
+ }
27
+ const changeDir = path_1.default.join(pkgRoot, "openspec/changes", changeName);
28
+ const artifactParts = ["tasks.md", "proposal.md", "design.md"];
29
+ let blob = "";
30
+ for (const f of artifactParts) {
31
+ const c = await (0, fs_1.readFileIfExists)(path_1.default.join(changeDir, f));
32
+ if (c)
33
+ blob += c;
34
+ }
35
+ const moduleRefs = new Set();
36
+ const modRe = /config\/api\/modules\/([\w/-]+)/g;
37
+ let m;
38
+ while ((m = modRe.exec(blob)) !== null) {
39
+ moduleRefs.add(m[1].replace(/\/index$/, ""));
40
+ }
41
+ if (moduleRefs.size === 0) {
42
+ checks.push({
43
+ name: "api-field-contract",
44
+ ok: true,
45
+ detail: "change 未引用 config/api/modules,跳过 API 字段契约检查",
46
+ });
47
+ return checks;
48
+ }
49
+ const failures = [];
50
+ const passed = [];
51
+ for (const mod of moduleRefs) {
52
+ const candidates = [
53
+ path_1.default.join(pkgRoot, "src/config/api/modules", mod, "index.ts"),
54
+ path_1.default.join(pkgRoot, "frontend/src/config/api/modules", mod, "index.ts"),
55
+ path_1.default.join(pkgRoot, "src/config/api/modules", `${mod}.ts`),
56
+ path_1.default.join(pkgRoot, "frontend/src/config/api/modules", `${mod}.ts`),
57
+ ];
58
+ let found = false;
59
+ for (const fp of candidates) {
60
+ const content = await (0, fs_1.readFileIfExists)(fp);
61
+ if (content === null)
62
+ continue;
63
+ found = true;
64
+ const rel = path_1.default.relative(pkgRoot, fp).replace(/\\/g, "/");
65
+ if (!API_MODULE_DIR_RE.test(`/${rel}`))
66
+ continue;
67
+ if (!API_CLIENT_CALL_RE.test(content)) {
68
+ passed.push(`${rel}(无 HTTP 调用,跳过)`);
69
+ continue;
70
+ }
71
+ if (!NORMALIZE_MARKERS.test(content)) {
72
+ failures.push(`${rel} 含 API 调用但未发现 snake/camel normalize(须防后端 SNAKE_CASE 漏映射)`);
73
+ }
74
+ else {
75
+ passed.push(rel);
76
+ }
77
+ }
78
+ if (!found) {
79
+ failures.push(`config/api/modules/${mod} 在 change 中引用但文件不存在`);
80
+ }
81
+ }
82
+ checks.push({
83
+ name: "api-field-contract",
84
+ ok: failures.length === 0,
85
+ detail: failures.length === 0
86
+ ? `API 字段契约通过: ${passed.join(", ") || "(无模块文件)"}`
87
+ : failures.join(" | "),
88
+ });
89
+ return checks;
90
+ }
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseDemoReferenceSection = parseDemoReferenceSection;
7
+ exports.checkDemoReferenceArtifacts = checkDemoReferenceArtifacts;
8
+ exports.checkDemoImplementationCoverage = checkDemoImplementationCoverage;
9
+ const promises_1 = __importDefault(require("fs/promises"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const fs_1 = require("./fs");
12
+ const testAssertionAnalysis_1 = require("./testAssertionAnalysis");
13
+ const DEMO_SECTION_RE = /^##\s*(演示代码参照|Demo Reference)\s*$/im;
14
+ const DEMO_EXEMPT_RE = /^DEMO_REFERENCE_EXEMPT:\s*(.+)$/m;
15
+ const MAPPING_HEADER_RE = /演示模块|Demo Module/i;
16
+ const UI_SCENARIO_KEYWORDS = /上传|点击|页面|按钮|弹窗|列表|表格|展示|抽屉|详情|编辑|导出|筛选|输入框|对话框|导航|toast|保存|删除|添加/i;
17
+ /**
18
+ * 从 Markdown 解析「演示代码参照」章节与模块映射表。
19
+ */
20
+ function parseDemoReferenceSection(content) {
21
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
22
+ const exemptMatch = content.match(DEMO_EXEMPT_RE);
23
+ const exempt = ((_a = exemptMatch === null || exemptMatch === void 0 ? void 0 : exemptMatch[1]) === null || _a === void 0 ? void 0 : _a.trim()) || null;
24
+ const sectionMatch = content.match(DEMO_SECTION_RE);
25
+ if (!sectionMatch || sectionMatch.index === undefined) {
26
+ return { hasSection: false, exempt, demoRoot: null, demoStack: null, mappings: [] };
27
+ }
28
+ const sectionBody = content.slice(sectionMatch.index);
29
+ const demoRoot = ((_c = (_b = sectionBody.match(/演示代码根路径[::]\s*`?([^`\n]+)`?/i)) === null || _b === void 0 ? void 0 : _b[1]) === null || _c === void 0 ? void 0 : _c.trim()) ||
30
+ ((_e = (_d = sectionBody.match(/Demo Root[::]\s*`?([^`\n]+)`?/i)) === null || _d === void 0 ? void 0 : _d[1]) === null || _e === void 0 ? void 0 : _e.trim()) ||
31
+ null;
32
+ const demoStack = ((_g = (_f = sectionBody.match(/技术栈[::]\s*`?(vue|react|其他)[^`\n]*`?/i)) === null || _f === void 0 ? void 0 : _f[1]) === null || _g === void 0 ? void 0 : _g.trim()) ||
33
+ ((_j = (_h = sectionBody.match(/Stack[::]\s*`?(vue|react|other)[^`\n]*`?/i)) === null || _h === void 0 ? void 0 : _h[1]) === null || _j === void 0 ? void 0 : _j.trim()) ||
34
+ null;
35
+ const mappings = [];
36
+ const lines = sectionBody.split("\n");
37
+ let inTable = false;
38
+ for (let i = 0; i < lines.length; i += 1) {
39
+ const line = lines[i].trim();
40
+ if (line.startsWith("|") && MAPPING_HEADER_RE.test(line)) {
41
+ inTable = true;
42
+ continue;
43
+ }
44
+ if (!inTable || !line.startsWith("|"))
45
+ continue;
46
+ if (/^\|\s*[-:]+/.test(line))
47
+ continue;
48
+ const cells = line
49
+ .split("|")
50
+ .map((c) => c.trim())
51
+ .filter((_, idx, arr) => idx > 0 && idx < arr.length - 1);
52
+ if (cells.length < 4)
53
+ continue;
54
+ if (/演示模块|Demo Module/i.test(cells[0]))
55
+ continue;
56
+ const signals = ((_k = cells[3]) !== null && _k !== void 0 ? _k : "")
57
+ .split(/[、,,|;;]/)
58
+ .map((s) => s.trim())
59
+ .filter((s) => s.length >= 2);
60
+ mappings.push({
61
+ moduleName: cells[0],
62
+ demoRef: cells[1],
63
+ productionPath: cells[2].replace(/\\/g, "/"),
64
+ interactionSignals: signals,
65
+ rowLine: i + 1,
66
+ });
67
+ }
68
+ return { hasSection: true, exempt, demoRoot, demoStack, mappings };
69
+ }
70
+ async function readChangeDesignAndTasks(pkgRoot, changeName) {
71
+ const changeDir = path_1.default.join(pkgRoot, "openspec/changes", changeName);
72
+ const design = await (0, fs_1.readFileIfExists)(path_1.default.join(changeDir, "design.md"));
73
+ const tasks = await (0, fs_1.readFileIfExists)(path_1.default.join(changeDir, "tasks.md"));
74
+ return { design, tasks };
75
+ }
76
+ async function changeHasUiScenarios(pkgRoot, changeName) {
77
+ const specsDir = path_1.default.join(pkgRoot, "openspec/changes", changeName, "specs");
78
+ let entries;
79
+ try {
80
+ entries = await promises_1.default.readdir(specsDir, { withFileTypes: true });
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ for (const cap of entries) {
86
+ if (!cap.isDirectory())
87
+ continue;
88
+ const specPath = path_1.default.join(specsDir, cap.name, "spec.md");
89
+ const content = await (0, fs_1.readFileIfExists)(specPath);
90
+ if (!content)
91
+ continue;
92
+ const blocks = content.split(/^####\s+Scenario:/m);
93
+ for (let i = 1; i < blocks.length; i += 1) {
94
+ if (UI_SCENARIO_KEYWORDS.test(blocks[i]))
95
+ return true;
96
+ }
97
+ }
98
+ return false;
99
+ }
100
+ /**
101
+ * Propose / AC 阶段:design 须声明演示参照与模块映射表;tasks 须含演示迁移段。
102
+ */
103
+ async function checkDemoReferenceArtifacts(pkgRoot, changeName) {
104
+ const checks = [];
105
+ const skip = process.env.POQUAN_SKIP_DEMO_REFERENCE === "1" ||
106
+ process.env.OPSX_SKIP_DEMO_REFERENCE === "1";
107
+ if (skip) {
108
+ checks.push({
109
+ name: "demo-reference-artifacts",
110
+ ok: true,
111
+ detail: "SKIP: OPSX_SKIP_DEMO_REFERENCE=1",
112
+ });
113
+ return checks;
114
+ }
115
+ const requiresUi = await changeHasUiScenarios(pkgRoot, changeName);
116
+ const { design, tasks } = await readChangeDesignAndTasks(pkgRoot, changeName);
117
+ if (!design) {
118
+ checks.push({
119
+ name: "demo-reference-design",
120
+ ok: !requiresUi,
121
+ detail: requiresUi
122
+ ? "UI change 缺少 design.md,无法声明演示代码参照"
123
+ : "无 UI Scenario,跳过演示参照",
124
+ });
125
+ return checks;
126
+ }
127
+ const parsed = parseDemoReferenceSection(design);
128
+ if (parsed.exempt) {
129
+ checks.push({
130
+ name: "demo-reference-design",
131
+ ok: true,
132
+ detail: `DEMO_REFERENCE_EXEMPT: ${parsed.exempt}`,
133
+ });
134
+ return checks;
135
+ }
136
+ if (!requiresUi) {
137
+ checks.push({
138
+ name: "demo-reference-design",
139
+ ok: true,
140
+ detail: "无 UI Scenario,不要求演示映射表",
141
+ });
142
+ return checks;
143
+ }
144
+ if (!parsed.hasSection) {
145
+ checks.push({
146
+ name: "demo-reference-design",
147
+ ok: false,
148
+ detail: "design.md 缺少 ## 演示代码参照(须映射演示模块→生产组件→关键交互)",
149
+ });
150
+ return checks;
151
+ }
152
+ if (!parsed.demoRoot) {
153
+ checks.push({
154
+ name: "demo-reference-root",
155
+ ok: false,
156
+ detail: "演示代码参照 未填写「演示代码根路径」(Vue/React 演示仓库或目录)",
157
+ });
158
+ }
159
+ else {
160
+ checks.push({
161
+ name: "demo-reference-root",
162
+ ok: true,
163
+ detail: `演示根路径: ${parsed.demoRoot}`,
164
+ });
165
+ }
166
+ if (parsed.mappings.length === 0) {
167
+ checks.push({
168
+ name: "demo-module-mapping",
169
+ ok: false,
170
+ detail: "演示模块映射表为空(至少须覆盖详情/弹窗等每个 UI 子模块一行)",
171
+ });
172
+ }
173
+ else {
174
+ checks.push({
175
+ name: "demo-module-mapping",
176
+ ok: true,
177
+ detail: `已登记 ${parsed.mappings.length} 个演示模块映射`,
178
+ });
179
+ }
180
+ const tasksText = tasks !== null && tasks !== void 0 ? tasks : "";
181
+ const hasMigrationSection = /##\s*演示模块迁移|演示模块迁移/.test(tasksText);
182
+ const missingInTasks = [];
183
+ for (const m of parsed.mappings) {
184
+ const prodBase = path_1.default.basename(m.productionPath);
185
+ if (!tasksText.includes(m.moduleName) && !tasksText.includes(prodBase)) {
186
+ missingInTasks.push(m.moduleName);
187
+ }
188
+ }
189
+ checks.push({
190
+ name: "demo-tasks-migration",
191
+ ok: hasMigrationSection && missingInTasks.length === 0,
192
+ detail: hasMigrationSection && missingInTasks.length === 0
193
+ ? "tasks.md 含演示模块迁移且覆盖全部映射行"
194
+ : `tasks 缺少演示迁移: section=${hasMigrationSection}, 未覆盖模块=${missingInTasks.join(", ") || "无"}`,
195
+ });
196
+ return checks;
197
+ }
198
+ /**
199
+ * Validate / Deliver:生产代码须实现映射表中的组件与关键交互信号。
200
+ */
201
+ async function checkDemoImplementationCoverage(pkgRoot, changeName) {
202
+ const checks = [];
203
+ const artifactChecks = await checkDemoReferenceArtifacts(pkgRoot, changeName);
204
+ const exempt = artifactChecks.some((c) => c.ok && c.detail.startsWith("DEMO_REFERENCE_EXEMPT"));
205
+ if (exempt || artifactChecks.some((c) => c.name === "demo-reference-design" && c.ok && c.detail.includes("跳过"))) {
206
+ checks.push(...artifactChecks.filter((c) => c.ok));
207
+ return checks;
208
+ }
209
+ const failedArtifact = artifactChecks.filter((c) => !c.ok);
210
+ if (failedArtifact.length > 0) {
211
+ checks.push(...failedArtifact);
212
+ return checks;
213
+ }
214
+ const { design } = await readChangeDesignAndTasks(pkgRoot, changeName);
215
+ const parsed = parseDemoReferenceSection(design !== null && design !== void 0 ? design : "");
216
+ const failures = [];
217
+ for (const m of parsed.mappings) {
218
+ const prodRel = m.productionPath.replace(/^\.\//, "");
219
+ const abs = path_1.default.join(pkgRoot, prodRel);
220
+ const content = await (0, fs_1.readFileIfExists)(abs);
221
+ if (content === null) {
222
+ failures.push(`${m.moduleName}: 生产文件不存在 ${prodRel}`);
223
+ continue;
224
+ }
225
+ const code = (0, testAssertionAnalysis_1.stripComments)(content);
226
+ const missingSignals = m.interactionSignals.filter((s) => !code.includes(s));
227
+ if (m.interactionSignals.length > 0 && missingSignals.length === m.interactionSignals.length) {
228
+ failures.push(`${m.moduleName}: ${prodRel} 未实现任何关键交互(${m.interactionSignals.join("、")})`);
229
+ }
230
+ else if (missingSignals.length > 0) {
231
+ failures.push(`${m.moduleName}: ${prodRel} 缺少交互信号: ${missingSignals.join("、")}`);
232
+ }
233
+ }
234
+ checks.push({
235
+ name: "demo-implementation-coverage",
236
+ ok: failures.length === 0,
237
+ detail: failures.length === 0
238
+ ? `演示模块已全部落地(${parsed.mappings.length} 项)`
239
+ : failures.join(" | "),
240
+ });
241
+ return checks;
242
+ }