sdd-flow-kit 1.3.18 → 1.3.22

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 (38) hide show
  1. package/dist/cli.js +42 -13
  2. package/dist/core/acAssertionBinding.js +180 -0
  3. package/dist/core/apiFieldContract.js +1 -1
  4. package/dist/core/apiFieldCoverage.js +335 -0
  5. package/dist/core/componentSubstance.js +172 -0
  6. package/dist/core/demoImplementationCoverage.js +2 -0
  7. package/dist/core/e2eAssertionDepth.js +4 -0
  8. package/dist/core/e2eDemoSignalClicks.js +90 -0
  9. package/dist/core/inferProject.js +26 -0
  10. package/dist/core/prdCoverage.js +13 -34
  11. package/dist/core/snakeCamel.js +93 -0
  12. package/dist/core/techDocQuality.js +4 -0
  13. package/dist/core/visualRegression.js +243 -0
  14. package/dist/steps/startFlow.js +2 -0
  15. package/package.json +1 -1
  16. package/scripts/assess-ac-assertion-binding.mjs +104 -0
  17. package/scripts/assess-api-field-contract.mjs +1 -1
  18. package/scripts/assess-api-field-coverage.mjs +88 -0
  19. package/scripts/assess-component-substance.mjs +87 -0
  20. package/scripts/assess-e2e-assertion-depth.mjs +16 -0
  21. package/scripts/assess-tech-doc-quality.mjs +7 -0
  22. package/scripts/assess-visual-regression.mjs +92 -0
  23. package/scripts/lib/ac-assertion-binding.mjs +119 -0
  24. package/scripts/lib/api-field-coverage.mjs +238 -0
  25. package/scripts/lib/component-substance.mjs +117 -0
  26. package/scripts/lib/demo-reference-parse.mjs +55 -0
  27. package/scripts/lib/e2e-demo-signal-clicks.mjs +43 -0
  28. package/scripts/lib/snake-camel.mjs +74 -0
  29. package/scripts/lib/test-assertion-analysis.mjs +14 -0
  30. package/scripts/lib/visual-regression.mjs +191 -0
  31. package/src/templates/artifacts/04-/346/212/200/346/234/257/346/226/207/346/241/243.template.md +8 -0
  32. package/src/templates/artifacts/05-/351/252/214/346/224/266/346/270/205/345/215/225.template.md +3 -2
  33. package/src/templates/artifacts/design-demo-reference.template.md +5 -5
  34. package/src/templates/artifacts/tests-fixtures-readme.template.md +19 -0
  35. package/src/templates/artifacts/visual-baseline-readme.template.md +28 -0
  36. package/src/templates/prompts/04-tech-doc-fill-prompt.md +7 -2
  37. package/src/templates/skills/demo-reference-mandatory-workflow.md +3 -1
  38. package/src/templates/skills/layered-testing-strategy.md +5 -3
@@ -0,0 +1,172 @@
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.countEffectiveLines = countEffectiveLines;
7
+ exports.signalPresentInComponent = signalPresentInComponent;
8
+ exports.analyzeComponentSubstance = analyzeComponentSubstance;
9
+ exports.checkChildComponentsImported = checkChildComponentsImported;
10
+ exports.checkComponentSubstance = checkComponentSubstance;
11
+ /**
12
+ * 组件实质门禁:防止演示迁移用空壳 .vue/.tsx + 注释塞关键词过关。
13
+ */
14
+ const path_1 = __importDefault(require("path"));
15
+ const fs_1 = require("./fs");
16
+ const testAssertionAnalysis_1 = require("./testAssertionAnalysis");
17
+ const MIN_EFFECTIVE_LINES = 40;
18
+ const VUE_TEMPLATE_RE = /<template[^>]*>([\s\S]*?)<\/template>/i;
19
+ const INTERACTIVE_CONTROL_RE = /<(?:n-button|a-button|el-button|NButton|AButton|ElButton|button|n-input|a-input|el-input|input|n-data-table|a-table|el-table|NDataTable|ATable|n-drawer|a-drawer)[\s/>]/i;
20
+ const API_BEHAVIOR_RE = /from\s+['"][^'"]*(?:config\/api|api\/modules)|useMutation|useQuery|Api\s*\(|financePost|financeGet/i;
21
+ const EMIT_HANDLER_RE = /defineEmits|emit\s*\(|@\w+\s*=\s*["']|onClick|onSubmit/i;
22
+ /** 子组件路径启发式(Section/Modal 等须被父组件引用) */
23
+ const CHILD_COMPONENT_RE = /(?:Section|Modal|Panel|Form)\.vue$/i;
24
+ const PARENT_CONTAINER_RE = /(?:Drawer|index)\.vue$/i;
25
+ /**
26
+ * 统计去注释后的非空行数,用于识别空壳文件。
27
+ */
28
+ function countEffectiveLines(content) {
29
+ const stripped = (0, testAssertionAnalysis_1.stripComments)(content);
30
+ return stripped.split("\n").filter((l) => l.trim().length > 0).length;
31
+ }
32
+ /**
33
+ * 交互信号须出现在 template 或 script(非注释),禁止只在注释中登记。
34
+ */
35
+ function signalPresentInComponent(content, signal) {
36
+ var _a, _b;
37
+ if (!signal || signal.length < 2)
38
+ return true;
39
+ const stripped = (0, testAssertionAnalysis_1.stripComments)(content);
40
+ const template = (_b = (_a = stripped.match(VUE_TEMPLATE_RE)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : "";
41
+ const script = stripped.replace(VUE_TEMPLATE_RE, "");
42
+ return template.includes(signal) || script.includes(signal);
43
+ }
44
+ /**
45
+ * 单文件组件实质分析(Vue SFC / TSX)。
46
+ */
47
+ function analyzeComponentSubstance(relPath, content, interactionSignals) {
48
+ var _a, _b;
49
+ const issues = [];
50
+ const stripped = (0, testAssertionAnalysis_1.stripComments)(content);
51
+ const effectiveLines = countEffectiveLines(content);
52
+ if (effectiveLines < MIN_EFFECTIVE_LINES) {
53
+ issues.push(`有效代码行 ${effectiveLines} < ${MIN_EFFECTIVE_LINES}(疑似空壳)`);
54
+ }
55
+ const isVue = /\.vue$/i.test(relPath);
56
+ const isTsx = /\.tsx$/i.test(relPath);
57
+ if (isVue) {
58
+ const template = (_b = (_a = content.match(VUE_TEMPLATE_RE)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : "";
59
+ if (!template.trim()) {
60
+ issues.push("缺少 <template> 区块");
61
+ }
62
+ else if (!INTERACTIVE_CONTROL_RE.test(template)) {
63
+ issues.push("template 中未发现 button/input/table 等可交互控件");
64
+ }
65
+ }
66
+ else if (isTsx) {
67
+ if (!INTERACTIVE_CONTROL_RE.test(stripped) && !/<[A-Z]\w*/.test(stripped)) {
68
+ issues.push("tsx 中未发现可交互控件或业务组件");
69
+ }
70
+ }
71
+ const hasBehavior = API_BEHAVIOR_RE.test(stripped) || EMIT_HANDLER_RE.test(stripped);
72
+ if (!hasBehavior && interactionSignals.length > 0) {
73
+ issues.push("script 中未发现 API 调用或 emit/事件处理(疑似只读骨架)");
74
+ }
75
+ const meaningfulSignals = interactionSignals.filter((s) => s.length >= 2);
76
+ const missingSignals = meaningfulSignals.filter((s) => !signalPresentInComponent(content, s));
77
+ if (meaningfulSignals.length > 0 && missingSignals.length === meaningfulSignals.length) {
78
+ issues.push(`交互信号均未出现在 template/script: ${meaningfulSignals.join("、")}`);
79
+ }
80
+ else if (missingSignals.length > 0) {
81
+ issues.push(`缺少交互信号: ${missingSignals.join("、")}`);
82
+ }
83
+ return { ok: issues.length === 0, issues };
84
+ }
85
+ /**
86
+ * 子组件须被映射表中的容器组件(Drawer/index)或其它生产文件 import。
87
+ */
88
+ async function checkChildComponentsImported(pkgRoot, mappings) {
89
+ const failures = [];
90
+ const paths = mappings.map((m) => m.productionPath.replace(/^\.\//, ""));
91
+ for (const m of mappings) {
92
+ const rel = m.productionPath.replace(/^\.\//, "");
93
+ if (!CHILD_COMPONENT_RE.test(rel))
94
+ continue;
95
+ if (PARENT_CONTAINER_RE.test(rel))
96
+ continue;
97
+ const base = path_1.default.basename(rel, path_1.default.extname(rel));
98
+ let imported = false;
99
+ for (const otherRel of paths) {
100
+ if (otherRel === rel)
101
+ continue;
102
+ const content = await (0, fs_1.readFileIfExists)(path_1.default.join(pkgRoot, otherRel));
103
+ if (!content)
104
+ continue;
105
+ if (new RegExp(`import\\s+\\w+\\s+from\\s+['"][^'"]*${base}['"]`).test(content) ||
106
+ content.includes(`<${base}`) ||
107
+ content.includes(`<${base} `)) {
108
+ imported = true;
109
+ break;
110
+ }
111
+ }
112
+ if (!imported) {
113
+ failures.push(`${m.moduleName}: ${rel} 未被父组件 import/挂载`);
114
+ }
115
+ }
116
+ return failures;
117
+ }
118
+ /**
119
+ * Validate/Deliver:演示映射对应的生产组件须通过实质检查。
120
+ */
121
+ async function checkComponentSubstance(pkgRoot, mappings) {
122
+ var _a;
123
+ const checks = [];
124
+ const skip = process.env.POQUAN_SKIP_COMPONENT_SUBSTANCE === "1" ||
125
+ process.env.OPSX_SKIP_COMPONENT_SUBSTANCE === "1";
126
+ if (skip) {
127
+ checks.push({
128
+ name: "component-substance",
129
+ ok: true,
130
+ detail: "SKIP: OPSX_SKIP_COMPONENT_SUBSTANCE=1",
131
+ });
132
+ return checks;
133
+ }
134
+ if (mappings.length === 0) {
135
+ checks.push({
136
+ name: "component-substance",
137
+ ok: true,
138
+ detail: "无演示映射,跳过组件实质检查",
139
+ });
140
+ return checks;
141
+ }
142
+ const failures = [];
143
+ for (const m of mappings) {
144
+ const rel = m.productionPath.replace(/^\.\//, "");
145
+ const abs = path_1.default.join(pkgRoot, rel);
146
+ const content = await (0, fs_1.readFileIfExists)(abs);
147
+ if (content === null) {
148
+ failures.push({ moduleName: m.moduleName, path: rel, reason: "文件不存在" });
149
+ continue;
150
+ }
151
+ const result = analyzeComponentSubstance(rel, content, m.interactionSignals);
152
+ if (!result.ok) {
153
+ failures.push({
154
+ moduleName: m.moduleName,
155
+ path: rel,
156
+ reason: result.issues.join("; "),
157
+ });
158
+ }
159
+ }
160
+ const importFailures = await checkChildComponentsImported(pkgRoot, mappings);
161
+ for (const msg of importFailures) {
162
+ failures.push({ moduleName: (_a = msg.split(":")[0]) !== null && _a !== void 0 ? _a : "?", path: "", reason: msg });
163
+ }
164
+ checks.push({
165
+ name: "component-substance",
166
+ ok: failures.length === 0,
167
+ detail: failures.length === 0
168
+ ? `组件实质检查通过(${mappings.length} 个映射组件)`
169
+ : failures.map((f) => `${f.moduleName}(${f.path}): ${f.reason}`).join(" | "),
170
+ });
171
+ return checks;
172
+ }
@@ -8,6 +8,7 @@ exports.checkDemoReferenceArtifacts = checkDemoReferenceArtifacts;
8
8
  exports.checkDemoImplementationCoverage = checkDemoImplementationCoverage;
9
9
  const promises_1 = __importDefault(require("fs/promises"));
10
10
  const path_1 = __importDefault(require("path"));
11
+ const componentSubstance_1 = require("./componentSubstance");
11
12
  const fs_1 = require("./fs");
12
13
  const testAssertionAnalysis_1 = require("./testAssertionAnalysis");
13
14
  const DEMO_SECTION_RE = /^##\s*(演示代码参照|Demo Reference)\s*$/im;
@@ -238,5 +239,6 @@ async function checkDemoImplementationCoverage(pkgRoot, changeName) {
238
239
  ? `演示模块已全部落地(${parsed.mappings.length} 项)`
239
240
  : failures.join(" | "),
240
241
  });
242
+ checks.push(...(await (0, componentSubstance_1.checkComponentSubstance)(pkgRoot, parsed.mappings)));
241
243
  return checks;
242
244
  }
@@ -11,6 +11,7 @@ const path_1 = __importDefault(require("path"));
11
11
  const acMatrix_1 = require("./acMatrix");
12
12
  const fs_1 = require("./fs");
13
13
  const testAssertionAnalysis_1 = require("./testAssertionAnalysis");
14
+ const e2eDemoSignalClicks_1 = require("./e2eDemoSignalClicks");
14
15
  const E2E_SPEC_RE = /\be2e\/[\w/.-]+\.spec\.(?:ts|js)\b/g;
15
16
  const UI_SCENARIO_KEYWORDS = /上传|点击|页面|按钮|弹窗|列表|表格|展示|抽屉|详情|导出|筛选|输入框|对话框|导航|toast|截图/i;
16
17
  async function readChangeArtifacts(changeDir) {
@@ -156,6 +157,9 @@ async function checkE2eAssertionDepth(pkgRoot, outputRoot, changeName) {
156
157
  ? `change 级 E2E 深度通过(${specPaths.join(", ")})`
157
158
  : `E2E 断言过浅: ${depthFailures.join(" | ")}`,
158
159
  });
160
+ const designContent = await (0, fs_1.readFileIfExists)(path_1.default.join(pkgRoot, "openspec/changes", changeName, "design.md"));
161
+ const demoClickCheck = await (0, e2eDemoSignalClicks_1.checkE2eDemoSignalClicks)(pkgRoot, designContent, specPaths);
162
+ checks.push(demoClickCheck);
159
163
  const acParsed = await (0, acMatrix_1.parseAcMatrix)(outputRoot);
160
164
  if (acParsed.ok) {
161
165
  const e2eAcs = acParsed.items.filter((i) => { var _a; return /^P0$/i.test(i.priority.trim()) || /e2e/i.test((_a = i.testType) !== null && _a !== void 0 ? _a : ""); });
@@ -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.e2eExercisesSignal = e2eExercisesSignal;
7
+ exports.checkE2eDemoSignalClicks = checkE2eDemoSignalClicks;
8
+ /**
9
+ * E2E 演示信号点击门禁:design 映射表中的可点击交互信号须在 change 级 e2e 中有 click/getByRole。
10
+ */
11
+ const path_1 = __importDefault(require("path"));
12
+ const demoImplementationCoverage_1 = require("./demoImplementationCoverage");
13
+ const fs_1 = require("./fs");
14
+ /** 须在 E2E 中验证点击的信号特征 */
15
+ const CLICKABLE_SIGNAL_RE = /编辑|保存|删除|新增|导入|拉取|提交|确认|取消|打开|修改|添加/i;
16
+ function escapeRegExp(s) {
17
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18
+ }
19
+ /**
20
+ * 判断 E2E 源码是否对某交互信号做了点击类操作。
21
+ */
22
+ function e2eExercisesSignal(e2eContent, signal) {
23
+ if (!signal || signal.length < 2)
24
+ return true;
25
+ const stripped = e2eContent.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
26
+ if (!stripped.includes(signal))
27
+ return false;
28
+ const esc = escapeRegExp(signal);
29
+ const clickPatterns = [
30
+ new RegExp(`\\.click\\s*\\([^)]*${esc}`),
31
+ new RegExp(`getByRole\\s*\\(\\s*['"]button['"][^)]*${esc}`),
32
+ new RegExp(`getByRole\\s*\\(\\s*['"][^'"]+['"][^)]*name:\\s*[^)]*${esc}`),
33
+ new RegExp(`getByText\\s*\\(\\s*['"\`]${esc}`),
34
+ new RegExp(`getByText\\s*\\(\\s*[^)]*${esc}`),
35
+ new RegExp(`locator\\s*\\([^)]*${esc}[^)]*\\)\\.click`),
36
+ ];
37
+ return clickPatterns.some((re) => re.test(stripped));
38
+ }
39
+ /**
40
+ * UI 类 change:映射表中可点击信号须在绑定的 e2e spec 中出现 click/getByRole。
41
+ */
42
+ async function checkE2eDemoSignalClicks(pkgRoot, designContent, specPaths) {
43
+ var _a;
44
+ const skip = process.env.POQUAN_SKIP_E2E_DEMO_CLICKS === "1" ||
45
+ process.env.OPSX_SKIP_E2E_DEMO_CLICKS === "1";
46
+ if (skip) {
47
+ return {
48
+ name: "e2e-demo-signal-clicks",
49
+ ok: true,
50
+ detail: "SKIP: OPSX_SKIP_E2E_DEMO_CLICKS=1",
51
+ };
52
+ }
53
+ const parsed = (0, demoImplementationCoverage_1.parseDemoReferenceSection)(designContent !== null && designContent !== void 0 ? designContent : "");
54
+ if (parsed.exempt || parsed.mappings.length === 0) {
55
+ return {
56
+ name: "e2e-demo-signal-clicks",
57
+ ok: true,
58
+ detail: "无演示映射或已豁免,跳过 E2E 点击检查",
59
+ };
60
+ }
61
+ if (specPaths.length === 0) {
62
+ return {
63
+ name: "e2e-demo-signal-clicks",
64
+ ok: false,
65
+ detail: "有演示映射但未绑定 e2e spec,无法校验交互点击",
66
+ };
67
+ }
68
+ let e2eBlob = "";
69
+ for (const rel of specPaths) {
70
+ const content = (_a = (await (0, fs_1.readFileIfExists)(path_1.default.join(pkgRoot, rel)))) !== null && _a !== void 0 ? _a : "";
71
+ e2eBlob += `\n// ${rel}\n${content}`;
72
+ }
73
+ const failures = [];
74
+ for (const m of parsed.mappings) {
75
+ const clickable = m.interactionSignals.filter((s) => CLICKABLE_SIGNAL_RE.test(s));
76
+ if (clickable.length === 0)
77
+ continue;
78
+ const uncovered = clickable.filter((s) => !e2eExercisesSignal(e2eBlob, s));
79
+ if (uncovered.length === clickable.length) {
80
+ failures.push(`${m.moduleName}: E2E 须 click/getByRole/getByText 覆盖信号「${clickable.join("、")}」`);
81
+ }
82
+ }
83
+ return {
84
+ name: "e2e-demo-signal-clicks",
85
+ ok: failures.length === 0,
86
+ detail: failures.length === 0
87
+ ? `E2E 已覆盖演示映射中的可点击信号(${specPaths.join(", ")})`
88
+ : failures.join(" | "),
89
+ };
90
+ }
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.PRODUCT_PREFIX_BY_KIND = exports.DOC_SKILL_BY_KIND = exports.ALLOWED_PROJECTS = void 0;
7
7
  exports.productToKind = productToKind;
8
+ exports.resolveProjectKindInput = resolveProjectKindInput;
8
9
  exports.resolveDocSkillDir = resolveDocSkillDir;
9
10
  exports.inferProjectKind = inferProjectKind;
10
11
  const promises_1 = __importDefault(require("fs/promises"));
@@ -38,6 +39,31 @@ function productToKind(product) {
38
39
  return "BreakX";
39
40
  return "OMS";
40
41
  }
42
+ /**
43
+ * 将 CLI `--project` 入参解析为规范项目类型。
44
+ * 支持精确名、别名(adtools / ad-tools)及带空格写法;无法识别时返回 null(避免 productToKind 默认回落 OMS)。
45
+ */
46
+ function resolveProjectKindInput(raw) {
47
+ const value = raw.trim();
48
+ if (!value)
49
+ return null;
50
+ if (exports.ALLOWED_PROJECTS.includes(value)) {
51
+ return value;
52
+ }
53
+ const upper = value.toUpperCase();
54
+ const compact = upper.replace(/[\s_-]+/g, "");
55
+ if (upper === "ADI" || upper.includes("ADINSIGHT"))
56
+ return "ADI";
57
+ if (upper === "OMS" || upper.includes("OPERATION"))
58
+ return "OMS";
59
+ if (value === "欢盟" || upper === "HUAN" || upper === "HUANMENG" || compact === "HM")
60
+ return "欢盟";
61
+ if (upper.includes("AD TOOLS") || compact === "ADTOOLS")
62
+ return "AD Tools";
63
+ if (compact === "BREAKX" || upper.includes("POQUAN") || value.includes("破圈"))
64
+ return "BreakX";
65
+ return null;
66
+ }
41
67
  /** 从 projectRoot 向上查找 docs/*-doc-skill(适配 frontend 子目录 + 根目录 docs) */
42
68
  async function resolveDocSkillDir(projectRoot, kind) {
43
69
  const dirName = exports.DOC_SKILL_BY_KIND[kind];
@@ -15,10 +15,12 @@ const acMatrix_1 = require("./acMatrix");
15
15
  const fs_1 = require("./fs");
16
16
  const prdParse_1 = require("./prdParse");
17
17
  const prdSemanticDiff_1 = require("./prdSemanticDiff");
18
- const testAssertionAnalysis_1 = require("./testAssertionAnalysis");
19
18
  const e2eAssertionDepth_1 = require("./e2eAssertionDepth");
20
19
  const apiFieldContract_1 = require("./apiFieldContract");
20
+ const apiFieldCoverage_1 = require("./apiFieldCoverage");
21
+ const visualRegression_1 = require("./visualRegression");
21
22
  const demoImplementationCoverage_1 = require("./demoImplementationCoverage");
23
+ const acAssertionBinding_1 = require("./acAssertionBinding");
22
24
  const VALID_VERDICTS = ["一致", "不一致-按PRD修复", "待人工确认"];
23
25
  /** 禁止在证据/结论中出现的揣摩或兜底措辞 */
24
26
  const FORBIDDEN_SPECULATION = [
@@ -37,7 +39,6 @@ const FORBIDDEN_SPECULATION = [
37
39
  "或许",
38
40
  ];
39
41
  const TEST_FILE_RE = /\.(spec|test)\.(t|j)sx?$/i;
40
- const E2E_DIR_RE = /(^|\/)(e2e|playwright)(\/|$)/i;
41
42
  async function walkFiles(dir, out) {
42
43
  let entries;
43
44
  try {
@@ -136,38 +137,12 @@ async function loadPrdAtomsFromManifest(outputRoot) {
136
137
  }
137
138
  }
138
139
  function checkAcTestMapping(items, testContents) {
139
- var _a;
140
- const checks = [];
141
- const mustCover = items.filter((i) => { var _a; return /^P0$/i.test(i.priority.trim()) || /e2e/i.test((_a = i.testType) !== null && _a !== void 0 ? _a : ""); });
142
- for (const ac of mustCover) {
143
- const e2eRequired = /e2e/i.test((_a = ac.testType) !== null && _a !== void 0 ? _a : "");
144
- const hits = testContents.filter((t) => (0, testAssertionAnalysis_1.hasMeaningfulAcCoverageInFile)(t.rel, t.content, ac.id).covered);
145
- const e2eHits = hits.filter((h) => E2E_DIR_RE.test(h.rel));
146
- if (hits.length === 0) {
147
- checks.push({
148
- name: `ac-test-${ac.id}`,
149
- ok: false,
150
- detail: `${ac.id}(${ac.priority}) 未在有效 test/it 块中找到 [${ac.id}] + expect 映射(注释登记不计入)`,
151
- });
152
- continue;
153
- }
154
- if (e2eRequired && e2eHits.length === 0) {
155
- checks.push({
156
- name: `ac-e2e-${ac.id}`,
157
- ok: false,
158
- detail: `${ac.id} 测试类型含 e2e,但 e2e/ 或 playwright/ 下无含 expect 的对应测试`,
159
- });
160
- continue;
161
- }
162
- checks.push({
163
- name: `ac-test-${ac.id}`,
164
- ok: true,
165
- detail: e2eRequired
166
- ? `${ac.id} 已映射(含 e2e: ${e2eHits.map((h) => path_1.default.basename(h.rel)).join(", ")})`
167
- : `${ac.id} 已映射: ${hits.map((h) => path_1.default.basename(h.rel)).join(", ")}`,
168
- });
169
- }
170
- return checks;
140
+ const bindingChecks = (0, acAssertionBinding_1.checkAcAssertionBinding)(items, testContents.map((t) => ({ rel: t.rel, content: t.content })));
141
+ return bindingChecks.map((c) => ({
142
+ name: c.name,
143
+ ok: c.ok,
144
+ detail: c.detail,
145
+ }));
171
146
  }
172
147
  function checkSemanticDiffItems(semantic) {
173
148
  const checks = [];
@@ -421,10 +396,14 @@ async function checkPrdCoverage(outputRoot, pkgRoot, options) {
421
396
  });
422
397
  }));
423
398
  checks.push(...checkAcTestMapping(acParsed.items, testContents));
399
+ if (options === null || options === void 0 ? void 0 : options.changeName) {
400
+ checks.push(...(await (0, visualRegression_1.checkVisualRegression)(pkgRoot, outputRoot, options.changeName, acParsed.items)));
401
+ }
424
402
  }
425
403
  if (options === null || options === void 0 ? void 0 : options.changeName) {
426
404
  checks.push(...(await (0, e2eAssertionDepth_1.checkE2eAssertionDepth)(pkgRoot, outputRoot, options.changeName)));
427
405
  checks.push(...(await (0, apiFieldContract_1.checkApiFieldContract)(pkgRoot, options.changeName)));
406
+ checks.push(...(await (0, apiFieldCoverage_1.checkApiFieldCoverage)(pkgRoot, options.changeName, outputRoot)));
428
407
  checks.push(...(await (0, demoImplementationCoverage_1.checkDemoImplementationCoverage)(pkgRoot, options.changeName)));
429
408
  }
430
409
  const ok = checks.every((c) => c.ok);
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ /**
3
+ * snake_case ↔ camelCase 深转换(与前端 caseTransform 语义一致,供门禁脚本使用)。
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.snakeKeyToCamel = snakeKeyToCamel;
7
+ exports.snakeToCamelDeep = snakeToCamelDeep;
8
+ exports.hasSnakeCaseKeys = hasSnakeCaseKeys;
9
+ exports.collectLeafCamelKeys = collectLeafCamelKeys;
10
+ exports.extractFirstListRow = extractFirstListRow;
11
+ function snakeKeyToCamel(key) {
12
+ return key.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
13
+ }
14
+ function snakeToCamelDeep(input) {
15
+ if (input === null || input === undefined) {
16
+ return input;
17
+ }
18
+ if (Array.isArray(input)) {
19
+ return input.map((item) => snakeToCamelDeep(item));
20
+ }
21
+ if (typeof input === "object") {
22
+ const result = {};
23
+ for (const [key, value] of Object.entries(input)) {
24
+ result[snakeKeyToCamel(key)] = snakeToCamelDeep(value);
25
+ }
26
+ return result;
27
+ }
28
+ return input;
29
+ }
30
+ /** 对象是否含 snake_case 键(判定为后端样例 fixture) */
31
+ function hasSnakeCaseKeys(input) {
32
+ if (input === null || typeof input !== "object")
33
+ return false;
34
+ if (Array.isArray(input)) {
35
+ return input.some((item) => hasSnakeCaseKeys(item));
36
+ }
37
+ for (const key of Object.keys(input)) {
38
+ if (key.includes("_"))
39
+ return true;
40
+ if (hasSnakeCaseKeys(input[key]))
41
+ return true;
42
+ }
43
+ return false;
44
+ }
45
+ /** 收集对象所有叶子路径的 camelCase 键名(末级 key) */
46
+ function collectLeafCamelKeys(input, out = new Set()) {
47
+ if (input === null || input === undefined)
48
+ return out;
49
+ if (Array.isArray(input)) {
50
+ for (const item of input)
51
+ collectLeafCamelKeys(item, out);
52
+ return out;
53
+ }
54
+ if (typeof input === "object") {
55
+ const obj = input;
56
+ const keys = Object.keys(obj);
57
+ if (keys.length === 0)
58
+ return out;
59
+ const allPrimitive = keys.every((k) => obj[k] === null || ["string", "number", "boolean"].includes(typeof obj[k]));
60
+ if (allPrimitive) {
61
+ for (const k of keys)
62
+ out.add(k);
63
+ return out;
64
+ }
65
+ for (const k of keys)
66
+ collectLeafCamelKeys(obj[k], out);
67
+ return out;
68
+ }
69
+ return out;
70
+ }
71
+ /** 从常见分页响应结构取首条列表行 */
72
+ function extractFirstListRow(payload) {
73
+ var _a, _b;
74
+ const normalized = snakeToCamelDeep(payload);
75
+ const candidates = [
76
+ normalized,
77
+ normalized.data,
78
+ normalized.records,
79
+ (_a = normalized.data) === null || _a === void 0 ? void 0 : _a.records,
80
+ (_b = normalized.data) === null || _b === void 0 ? void 0 : _b.items,
81
+ normalized.items,
82
+ normalized.list,
83
+ ];
84
+ for (const c of candidates) {
85
+ if (Array.isArray(c) && c.length > 0 && typeof c[0] === "object" && c[0] !== null) {
86
+ return c[0];
87
+ }
88
+ }
89
+ if (typeof normalized === "object" && normalized !== null && !Array.isArray(normalized)) {
90
+ return normalized;
91
+ }
92
+ return null;
93
+ }
@@ -24,6 +24,7 @@ exports.REQUIRED_TECH_DOC_SECTIONS = [
24
24
  const MIN_LINES = 150;
25
25
  const MIN_H3_UNDER_SCOPE = 2;
26
26
  const API_TABLE_RE = /\|\s*接口\s*\|\s*Method\s*\|/m;
27
+ const FIELD_MAPPING_TABLE_RE = /\|\s*后端字段\s*\|\s*前端字段\s*\|/m;
27
28
  const DEMO_TABLE_RE = /\|\s*演示模块\s*\|\s*演示文件\/组件\s*\|/m;
28
29
  const AC_REF_RE = /\[AC-\d{2,}\]/g;
29
30
  /** 典型「摘要版」反模式:仅若干条顶层 [AC-xx] bullet,无三级标题 */
@@ -59,6 +60,9 @@ function checkTechDocStructure(content) {
59
60
  if (!API_TABLE_RE.test(content)) {
60
61
  missingSections.push("4.1 接口清单表格(| 接口 | Method |)");
61
62
  }
63
+ if (API_TABLE_RE.test(content) && !FIELD_MAPPING_TABLE_RE.test(content)) {
64
+ missingSections.push("4.3.1 响应字段映射表(| 后端字段 | 前端字段 |)");
65
+ }
62
66
  if (!DEMO_TABLE_RE.test(content)) {
63
67
  missingSections.push("2.4 演示模块映射表格(| 演示模块 | 演示文件/组件 |)");
64
68
  }