sdd-flow-kit 1.3.15 → 1.3.21

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 (60) hide show
  1. package/dist/cli.js +16 -0
  2. package/dist/core/acAssertionBinding.js +180 -0
  3. package/dist/core/apiFieldContract.js +90 -0
  4. package/dist/core/apiFieldCoverage.js +335 -0
  5. package/dist/core/componentSubstance.js +172 -0
  6. package/dist/core/demoImplementationCoverage.js +244 -0
  7. package/dist/core/e2eAssertionDepth.js +237 -0
  8. package/dist/core/e2eDemoSignalClicks.js +90 -0
  9. package/dist/core/prdArtifacts.js +12 -1
  10. package/dist/core/prdCoverage.js +52 -36
  11. package/dist/core/prdSemanticDiff.js +7 -4
  12. package/dist/core/snakeCamel.js +93 -0
  13. package/dist/core/syncSourcePrd.js +181 -0
  14. package/dist/core/techDocQuality.js +91 -0
  15. package/dist/core/testAssertionAnalysis.js +213 -0
  16. package/dist/core/visualRegression.js +243 -0
  17. package/dist/core/writeNext.js +1 -1
  18. package/dist/index.js +6 -1
  19. package/dist/steps/runGate.js +49 -5
  20. package/dist/steps/runSyncSourcePrd.js +38 -0
  21. package/dist/steps/runValidate.js +4 -2
  22. package/dist/steps/setupProject.js +3 -0
  23. package/dist/steps/startFlow.js +9 -2
  24. package/package.json +2 -1
  25. package/scripts/assert-active-openspec-change.sh +114 -0
  26. package/scripts/assess-ac-assertion-binding.mjs +104 -0
  27. package/scripts/assess-api-field-contract.mjs +119 -0
  28. package/scripts/assess-api-field-coverage.mjs +88 -0
  29. package/scripts/assess-component-substance.mjs +87 -0
  30. package/scripts/assess-demo-implementation-coverage.mjs +188 -0
  31. package/scripts/assess-e2e-assertion-depth.mjs +162 -0
  32. package/scripts/assess-playwright-e2e-coverage.mjs +244 -0
  33. package/scripts/assess-tech-doc-quality.mjs +98 -0
  34. package/scripts/assess-visual-regression.mjs +92 -0
  35. package/scripts/install-git-hooks.sh +27 -0
  36. package/scripts/lib/ac-assertion-binding.mjs +119 -0
  37. package/scripts/lib/api-field-coverage.mjs +238 -0
  38. package/scripts/lib/component-substance.mjs +117 -0
  39. package/scripts/lib/demo-reference-parse.mjs +55 -0
  40. package/scripts/lib/e2e-demo-signal-clicks.mjs +43 -0
  41. package/scripts/lib/snake-camel.mjs +74 -0
  42. package/scripts/lib/test-assertion-analysis.mjs +148 -0
  43. package/scripts/lib/visual-regression.mjs +191 -0
  44. package/scripts/resolve-playwright-e2e-scope.mjs +248 -0
  45. package/src/templates/artifacts/01-adi-doc-skill-/346/214/207/345/274/225.template.md +6 -2
  46. 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
  47. package/src/templates/artifacts/04-/346/212/200/346/234/257/346/226/207/346/241/243.template.md +22 -0
  48. package/src/templates/artifacts/05-/351/252/214/346/224/266/346/270/205/345/215/225.template.md +4 -3
  49. package/src/templates/artifacts/06-openspec-/346/217/220/346/241/210/350/215/211/347/250/277.template.md +2 -2
  50. package/src/templates/artifacts/07-opsx-auto-chain.template.md +2 -2
  51. 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
  52. package/src/templates/artifacts/design-demo-reference.template.md +24 -0
  53. package/src/templates/artifacts/tests-fixtures-readme.template.md +19 -0
  54. package/src/templates/artifacts/visual-baseline-readme.template.md +28 -0
  55. package/src/templates/prompts/02-sdd-loop-prompt.md +25 -11
  56. package/src/templates/prompts/04-tech-doc-fill-prompt.md +76 -0
  57. package/src/templates/skills/confluence-doc.py.template +64 -25
  58. package/src/templates/skills/demo-reference-mandatory-workflow.md +79 -0
  59. package/src/templates/skills/doc-skill.SKILL.md.template +13 -6
  60. package/src/templates/skills/layered-testing-strategy.md +15 -6
@@ -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
+ }
@@ -0,0 +1,244 @@
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 componentSubstance_1 = require("./componentSubstance");
12
+ const fs_1 = require("./fs");
13
+ const testAssertionAnalysis_1 = require("./testAssertionAnalysis");
14
+ const DEMO_SECTION_RE = /^##\s*(演示代码参照|Demo Reference)\s*$/im;
15
+ const DEMO_EXEMPT_RE = /^DEMO_REFERENCE_EXEMPT:\s*(.+)$/m;
16
+ const MAPPING_HEADER_RE = /演示模块|Demo Module/i;
17
+ const UI_SCENARIO_KEYWORDS = /上传|点击|页面|按钮|弹窗|列表|表格|展示|抽屉|详情|编辑|导出|筛选|输入框|对话框|导航|toast|保存|删除|添加/i;
18
+ /**
19
+ * 从 Markdown 解析「演示代码参照」章节与模块映射表。
20
+ */
21
+ function parseDemoReferenceSection(content) {
22
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
23
+ const exemptMatch = content.match(DEMO_EXEMPT_RE);
24
+ const exempt = ((_a = exemptMatch === null || exemptMatch === void 0 ? void 0 : exemptMatch[1]) === null || _a === void 0 ? void 0 : _a.trim()) || null;
25
+ const sectionMatch = content.match(DEMO_SECTION_RE);
26
+ if (!sectionMatch || sectionMatch.index === undefined) {
27
+ return { hasSection: false, exempt, demoRoot: null, demoStack: null, mappings: [] };
28
+ }
29
+ const sectionBody = content.slice(sectionMatch.index);
30
+ 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()) ||
31
+ ((_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()) ||
32
+ null;
33
+ 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()) ||
34
+ ((_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()) ||
35
+ null;
36
+ const mappings = [];
37
+ const lines = sectionBody.split("\n");
38
+ let inTable = false;
39
+ for (let i = 0; i < lines.length; i += 1) {
40
+ const line = lines[i].trim();
41
+ if (line.startsWith("|") && MAPPING_HEADER_RE.test(line)) {
42
+ inTable = true;
43
+ continue;
44
+ }
45
+ if (!inTable || !line.startsWith("|"))
46
+ continue;
47
+ if (/^\|\s*[-:]+/.test(line))
48
+ continue;
49
+ const cells = line
50
+ .split("|")
51
+ .map((c) => c.trim())
52
+ .filter((_, idx, arr) => idx > 0 && idx < arr.length - 1);
53
+ if (cells.length < 4)
54
+ continue;
55
+ if (/演示模块|Demo Module/i.test(cells[0]))
56
+ continue;
57
+ const signals = ((_k = cells[3]) !== null && _k !== void 0 ? _k : "")
58
+ .split(/[、,,|;;]/)
59
+ .map((s) => s.trim())
60
+ .filter((s) => s.length >= 2);
61
+ mappings.push({
62
+ moduleName: cells[0],
63
+ demoRef: cells[1],
64
+ productionPath: cells[2].replace(/\\/g, "/"),
65
+ interactionSignals: signals,
66
+ rowLine: i + 1,
67
+ });
68
+ }
69
+ return { hasSection: true, exempt, demoRoot, demoStack, mappings };
70
+ }
71
+ async function readChangeDesignAndTasks(pkgRoot, changeName) {
72
+ const changeDir = path_1.default.join(pkgRoot, "openspec/changes", changeName);
73
+ const design = await (0, fs_1.readFileIfExists)(path_1.default.join(changeDir, "design.md"));
74
+ const tasks = await (0, fs_1.readFileIfExists)(path_1.default.join(changeDir, "tasks.md"));
75
+ return { design, tasks };
76
+ }
77
+ async function changeHasUiScenarios(pkgRoot, changeName) {
78
+ const specsDir = path_1.default.join(pkgRoot, "openspec/changes", changeName, "specs");
79
+ let entries;
80
+ try {
81
+ entries = await promises_1.default.readdir(specsDir, { withFileTypes: true });
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ for (const cap of entries) {
87
+ if (!cap.isDirectory())
88
+ continue;
89
+ const specPath = path_1.default.join(specsDir, cap.name, "spec.md");
90
+ const content = await (0, fs_1.readFileIfExists)(specPath);
91
+ if (!content)
92
+ continue;
93
+ const blocks = content.split(/^####\s+Scenario:/m);
94
+ for (let i = 1; i < blocks.length; i += 1) {
95
+ if (UI_SCENARIO_KEYWORDS.test(blocks[i]))
96
+ return true;
97
+ }
98
+ }
99
+ return false;
100
+ }
101
+ /**
102
+ * Propose / AC 阶段:design 须声明演示参照与模块映射表;tasks 须含演示迁移段。
103
+ */
104
+ async function checkDemoReferenceArtifacts(pkgRoot, changeName) {
105
+ const checks = [];
106
+ const skip = process.env.POQUAN_SKIP_DEMO_REFERENCE === "1" ||
107
+ process.env.OPSX_SKIP_DEMO_REFERENCE === "1";
108
+ if (skip) {
109
+ checks.push({
110
+ name: "demo-reference-artifacts",
111
+ ok: true,
112
+ detail: "SKIP: OPSX_SKIP_DEMO_REFERENCE=1",
113
+ });
114
+ return checks;
115
+ }
116
+ const requiresUi = await changeHasUiScenarios(pkgRoot, changeName);
117
+ const { design, tasks } = await readChangeDesignAndTasks(pkgRoot, changeName);
118
+ if (!design) {
119
+ checks.push({
120
+ name: "demo-reference-design",
121
+ ok: !requiresUi,
122
+ detail: requiresUi
123
+ ? "UI change 缺少 design.md,无法声明演示代码参照"
124
+ : "无 UI Scenario,跳过演示参照",
125
+ });
126
+ return checks;
127
+ }
128
+ const parsed = parseDemoReferenceSection(design);
129
+ if (parsed.exempt) {
130
+ checks.push({
131
+ name: "demo-reference-design",
132
+ ok: true,
133
+ detail: `DEMO_REFERENCE_EXEMPT: ${parsed.exempt}`,
134
+ });
135
+ return checks;
136
+ }
137
+ if (!requiresUi) {
138
+ checks.push({
139
+ name: "demo-reference-design",
140
+ ok: true,
141
+ detail: "无 UI Scenario,不要求演示映射表",
142
+ });
143
+ return checks;
144
+ }
145
+ if (!parsed.hasSection) {
146
+ checks.push({
147
+ name: "demo-reference-design",
148
+ ok: false,
149
+ detail: "design.md 缺少 ## 演示代码参照(须映射演示模块→生产组件→关键交互)",
150
+ });
151
+ return checks;
152
+ }
153
+ if (!parsed.demoRoot) {
154
+ checks.push({
155
+ name: "demo-reference-root",
156
+ ok: false,
157
+ detail: "演示代码参照 未填写「演示代码根路径」(Vue/React 演示仓库或目录)",
158
+ });
159
+ }
160
+ else {
161
+ checks.push({
162
+ name: "demo-reference-root",
163
+ ok: true,
164
+ detail: `演示根路径: ${parsed.demoRoot}`,
165
+ });
166
+ }
167
+ if (parsed.mappings.length === 0) {
168
+ checks.push({
169
+ name: "demo-module-mapping",
170
+ ok: false,
171
+ detail: "演示模块映射表为空(至少须覆盖详情/弹窗等每个 UI 子模块一行)",
172
+ });
173
+ }
174
+ else {
175
+ checks.push({
176
+ name: "demo-module-mapping",
177
+ ok: true,
178
+ detail: `已登记 ${parsed.mappings.length} 个演示模块映射`,
179
+ });
180
+ }
181
+ const tasksText = tasks !== null && tasks !== void 0 ? tasks : "";
182
+ const hasMigrationSection = /##\s*演示模块迁移|演示模块迁移/.test(tasksText);
183
+ const missingInTasks = [];
184
+ for (const m of parsed.mappings) {
185
+ const prodBase = path_1.default.basename(m.productionPath);
186
+ if (!tasksText.includes(m.moduleName) && !tasksText.includes(prodBase)) {
187
+ missingInTasks.push(m.moduleName);
188
+ }
189
+ }
190
+ checks.push({
191
+ name: "demo-tasks-migration",
192
+ ok: hasMigrationSection && missingInTasks.length === 0,
193
+ detail: hasMigrationSection && missingInTasks.length === 0
194
+ ? "tasks.md 含演示模块迁移且覆盖全部映射行"
195
+ : `tasks 缺少演示迁移: section=${hasMigrationSection}, 未覆盖模块=${missingInTasks.join(", ") || "无"}`,
196
+ });
197
+ return checks;
198
+ }
199
+ /**
200
+ * Validate / Deliver:生产代码须实现映射表中的组件与关键交互信号。
201
+ */
202
+ async function checkDemoImplementationCoverage(pkgRoot, changeName) {
203
+ const checks = [];
204
+ const artifactChecks = await checkDemoReferenceArtifacts(pkgRoot, changeName);
205
+ const exempt = artifactChecks.some((c) => c.ok && c.detail.startsWith("DEMO_REFERENCE_EXEMPT"));
206
+ if (exempt || artifactChecks.some((c) => c.name === "demo-reference-design" && c.ok && c.detail.includes("跳过"))) {
207
+ checks.push(...artifactChecks.filter((c) => c.ok));
208
+ return checks;
209
+ }
210
+ const failedArtifact = artifactChecks.filter((c) => !c.ok);
211
+ if (failedArtifact.length > 0) {
212
+ checks.push(...failedArtifact);
213
+ return checks;
214
+ }
215
+ const { design } = await readChangeDesignAndTasks(pkgRoot, changeName);
216
+ const parsed = parseDemoReferenceSection(design !== null && design !== void 0 ? design : "");
217
+ const failures = [];
218
+ for (const m of parsed.mappings) {
219
+ const prodRel = m.productionPath.replace(/^\.\//, "");
220
+ const abs = path_1.default.join(pkgRoot, prodRel);
221
+ const content = await (0, fs_1.readFileIfExists)(abs);
222
+ if (content === null) {
223
+ failures.push(`${m.moduleName}: 生产文件不存在 ${prodRel}`);
224
+ continue;
225
+ }
226
+ const code = (0, testAssertionAnalysis_1.stripComments)(content);
227
+ const missingSignals = m.interactionSignals.filter((s) => !code.includes(s));
228
+ if (m.interactionSignals.length > 0 && missingSignals.length === m.interactionSignals.length) {
229
+ failures.push(`${m.moduleName}: ${prodRel} 未实现任何关键交互(${m.interactionSignals.join("、")})`);
230
+ }
231
+ else if (missingSignals.length > 0) {
232
+ failures.push(`${m.moduleName}: ${prodRel} 缺少交互信号: ${missingSignals.join("、")}`);
233
+ }
234
+ }
235
+ checks.push({
236
+ name: "demo-implementation-coverage",
237
+ ok: failures.length === 0,
238
+ detail: failures.length === 0
239
+ ? `演示模块已全部落地(${parsed.mappings.length} 项)`
240
+ : failures.join(" | "),
241
+ });
242
+ checks.push(...(await (0, componentSubstance_1.checkComponentSubstance)(pkgRoot, parsed.mappings)));
243
+ return checks;
244
+ }
@@ -0,0 +1,237 @@
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.collectChangeE2eSpecs = collectChangeE2eSpecs;
7
+ exports.changeHasUiScenarios = changeHasUiScenarios;
8
+ exports.checkE2eAssertionDepth = checkE2eAssertionDepth;
9
+ const promises_1 = __importDefault(require("fs/promises"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const acMatrix_1 = require("./acMatrix");
12
+ const fs_1 = require("./fs");
13
+ const testAssertionAnalysis_1 = require("./testAssertionAnalysis");
14
+ const e2eDemoSignalClicks_1 = require("./e2eDemoSignalClicks");
15
+ const E2E_SPEC_RE = /\be2e\/[\w/.-]+\.spec\.(?:ts|js)\b/g;
16
+ const UI_SCENARIO_KEYWORDS = /上传|点击|页面|按钮|弹窗|列表|表格|展示|抽屉|详情|导出|筛选|输入框|对话框|导航|toast|截图/i;
17
+ async function readChangeArtifacts(changeDir) {
18
+ const files = ["tasks.md", "proposal.md", "design.md"];
19
+ let blob = "";
20
+ for (const f of files) {
21
+ const content = await (0, fs_1.readFileIfExists)(path_1.default.join(changeDir, f));
22
+ if (content)
23
+ blob += `${content}\n`;
24
+ }
25
+ return blob;
26
+ }
27
+ /** 从 change 文档收集绑定的 e2e spec(须文件存在) */
28
+ async function collectChangeE2eSpecs(pkgRoot, changeName) {
29
+ var _a;
30
+ const changeDir = path_1.default.join(pkgRoot, "openspec/changes", changeName);
31
+ const specs = new Set();
32
+ const artifactBlob = await readChangeArtifacts(changeDir);
33
+ let m;
34
+ const re = new RegExp(E2E_SPEC_RE.source, "g");
35
+ while ((m = re.exec(artifactBlob)) !== null) {
36
+ const rel = m[0];
37
+ const abs = path_1.default.join(pkgRoot, rel);
38
+ const content = await (0, fs_1.readFileIfExists)(abs);
39
+ if (content !== null)
40
+ specs.add(rel);
41
+ }
42
+ const configPath = path_1.default.join(pkgRoot, "e2e/e2e-scope.config.json");
43
+ const configRaw = await (0, fs_1.readFileIfExists)(configPath);
44
+ if (configRaw) {
45
+ try {
46
+ const config = JSON.parse(configRaw);
47
+ const overrides = (_a = config.changeNameToSpecs) === null || _a === void 0 ? void 0 : _a[changeName];
48
+ if (overrides) {
49
+ for (const raw of overrides) {
50
+ const rel = raw.startsWith("e2e/") ? raw : `e2e/${raw}`;
51
+ const content = await (0, fs_1.readFileIfExists)(path_1.default.join(pkgRoot, rel));
52
+ if (content !== null)
53
+ specs.add(rel);
54
+ }
55
+ }
56
+ }
57
+ catch {
58
+ // ignore invalid config
59
+ }
60
+ }
61
+ return [...specs].sort();
62
+ }
63
+ /** delta specs 是否含 UI 交互类 Scenario */
64
+ async function changeHasUiScenarios(pkgRoot, changeName) {
65
+ const specsDir = path_1.default.join(pkgRoot, "openspec/changes", changeName, "specs");
66
+ let entries;
67
+ try {
68
+ entries = await promises_1.default.readdir(specsDir, { withFileTypes: true });
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ for (const cap of entries) {
74
+ if (!cap.isDirectory())
75
+ continue;
76
+ const specPath = path_1.default.join(specsDir, cap.name, "spec.md");
77
+ const content = await (0, fs_1.readFileIfExists)(specPath);
78
+ if (!content)
79
+ continue;
80
+ const blocks = content.split(/^####\s+Scenario:/m);
81
+ for (let i = 1; i < blocks.length; i += 1) {
82
+ if (UI_SCENARIO_KEYWORDS.test(blocks[i]))
83
+ return true;
84
+ }
85
+ }
86
+ return false;
87
+ }
88
+ function hasPlaywrightExempt(tasksContent) {
89
+ return /^PLAYWRIGHT_E2E_EXEMPT:/m.test(tasksContent);
90
+ }
91
+ /**
92
+ * E2E 断言深度门禁:change 级 spec 须含页面可达、数据/交互断言;e2e 类型 AC 须在有效 test 块内映射。
93
+ */
94
+ async function checkE2eAssertionDepth(pkgRoot, outputRoot, changeName) {
95
+ var _a, _b;
96
+ const checks = [];
97
+ const skip = process.env.POQUAN_SKIP_E2E_ASSERTION_DEPTH === "1" ||
98
+ process.env.OPSX_SKIP_E2E_ASSERTION_DEPTH === "1";
99
+ if (skip) {
100
+ checks.push({
101
+ name: "e2e-assertion-depth",
102
+ ok: true,
103
+ detail: "SKIP: OPSX_SKIP_E2E_ASSERTION_DEPTH=1",
104
+ });
105
+ return checks;
106
+ }
107
+ const changeDir = path_1.default.join(pkgRoot, "openspec/changes", changeName);
108
+ const changeExists = await (0, fs_1.readFileIfExists)(path_1.default.join(changeDir, "tasks.md"));
109
+ if (changeExists === null) {
110
+ checks.push({
111
+ name: "e2e-assertion-depth",
112
+ ok: true,
113
+ detail: `活跃 change 不存在,跳过深度检查: ${changeName}`,
114
+ });
115
+ return checks;
116
+ }
117
+ const tasksContent = (_a = (await (0, fs_1.readFileIfExists)(path_1.default.join(changeDir, "tasks.md")))) !== null && _a !== void 0 ? _a : "";
118
+ if (hasPlaywrightExempt(tasksContent)) {
119
+ checks.push({
120
+ name: "e2e-assertion-depth",
121
+ ok: true,
122
+ detail: "PLAYWRIGHT_E2E_EXEMPT 已声明,跳过 E2E 深度检查",
123
+ });
124
+ return checks;
125
+ }
126
+ const requiresUiE2e = await changeHasUiScenarios(pkgRoot, changeName);
127
+ const specPaths = await collectChangeE2eSpecs(pkgRoot, changeName);
128
+ if (!requiresUiE2e && specPaths.length === 0) {
129
+ checks.push({
130
+ name: "e2e-assertion-depth",
131
+ ok: true,
132
+ detail: "无 UI Scenario 且未绑定 change 级 e2e spec,跳过深度检查",
133
+ });
134
+ return checks;
135
+ }
136
+ if (requiresUiE2e && specPaths.length === 0) {
137
+ checks.push({
138
+ name: "e2e-assertion-depth",
139
+ ok: false,
140
+ detail: "含 UI Scenario 但未绑定 e2e/*.spec.ts,无法做断言深度验收",
141
+ });
142
+ return checks;
143
+ }
144
+ const depthFailures = [];
145
+ for (const rel of specPaths) {
146
+ const abs = path_1.default.join(pkgRoot, rel);
147
+ const content = (_b = (await (0, fs_1.readFileIfExists)(abs))) !== null && _b !== void 0 ? _b : "";
148
+ const depth = (0, testAssertionAnalysis_1.analyzeE2eSpecDepth)(rel, content);
149
+ if (!depth.ok) {
150
+ depthFailures.push(`${rel}: ${depth.issues.join("; ")}`);
151
+ }
152
+ }
153
+ checks.push({
154
+ name: "e2e-assertion-depth",
155
+ ok: depthFailures.length === 0,
156
+ detail: depthFailures.length === 0
157
+ ? `change 级 E2E 深度通过(${specPaths.join(", ")})`
158
+ : `E2E 断言过浅: ${depthFailures.join(" | ")}`,
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);
163
+ const acParsed = await (0, acMatrix_1.parseAcMatrix)(outputRoot);
164
+ if (acParsed.ok) {
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 : ""); });
166
+ await checkE2eAcDepth(pkgRoot, e2eAcs, checks);
167
+ }
168
+ return checks;
169
+ }
170
+ /** 校验声明 e2e 的 AC 是否在有效 test 块内有映射 */
171
+ async function checkE2eAcDepth(pkgRoot, acs, checks) {
172
+ var _a, _b;
173
+ const e2eRoots = ["e2e", "playwright"];
174
+ const e2eFiles = [];
175
+ for (const root of e2eRoots) {
176
+ const dir = path_1.default.join(pkgRoot, root);
177
+ let entries;
178
+ try {
179
+ entries = await promises_1.default.readdir(dir, { withFileTypes: true });
180
+ }
181
+ catch {
182
+ continue;
183
+ }
184
+ for (const ent of entries) {
185
+ if (!ent.isFile() || !/\.spec\.(t|j)sx?$/.test(ent.name))
186
+ continue;
187
+ const abs = path_1.default.join(dir, ent.name);
188
+ const content = (_a = (await (0, fs_1.readFileIfExists)(abs))) !== null && _a !== void 0 ? _a : "";
189
+ e2eFiles.push({ rel: `${root}/${ent.name}`, content });
190
+ }
191
+ await walkE2eSubdirs(dir, root, e2eFiles);
192
+ }
193
+ for (const ac of acs) {
194
+ const e2eRequired = /e2e/i.test((_b = ac.testType) !== null && _b !== void 0 ? _b : "");
195
+ if (!e2eRequired)
196
+ continue;
197
+ const hits = e2eFiles.filter((f) => (0, testAssertionAnalysis_1.hasMeaningfulAcCoverageInFile)(f.rel, f.content, ac.id).covered);
198
+ checks.push({
199
+ name: `ac-e2e-depth-${ac.id}`,
200
+ ok: hits.length > 0,
201
+ detail: hits.length > 0
202
+ ? `${ac.id} 在有效 E2E test 块内有 expect 断言: ${hits.map((h) => h.rel).join(", ")}`
203
+ : `${ac.id} 测试类型含 e2e,但 e2e/ 下无含 [${ac.id}] 且带 expect 的 test/it 块`,
204
+ });
205
+ }
206
+ }
207
+ async function walkE2eSubdirs(dir, relPrefix, out) {
208
+ var _a;
209
+ let entries;
210
+ try {
211
+ entries = await promises_1.default.readdir(dir, { withFileTypes: true });
212
+ }
213
+ catch {
214
+ return;
215
+ }
216
+ for (const ent of entries) {
217
+ if (!ent.isDirectory())
218
+ continue;
219
+ const sub = path_1.default.join(dir, ent.name);
220
+ const subRel = `${relPrefix}/${ent.name}`;
221
+ let files;
222
+ try {
223
+ files = await promises_1.default.readdir(sub, { withFileTypes: true });
224
+ }
225
+ catch {
226
+ continue;
227
+ }
228
+ for (const f of files) {
229
+ if (!f.isFile() || !/\.spec\.(t|j)sx?$/.test(f.name))
230
+ continue;
231
+ const abs = path_1.default.join(sub, f.name);
232
+ const content = (_a = (await (0, fs_1.readFileIfExists)(abs))) !== null && _a !== void 0 ? _a : "";
233
+ out.push({ rel: `${subRel}/${f.name}`, content });
234
+ }
235
+ await walkE2eSubdirs(sub, subRel, out);
236
+ }
237
+ }