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,243 @@
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.parseVisualAcRequirements = parseVisualAcRequirements;
7
+ exports.listPrdSourceImages = listPrdSourceImages;
8
+ exports.resolveBaselinePath = resolveBaselinePath;
9
+ exports.checkVisualRegression = checkVisualRegression;
10
+ /**
11
+ * L4 视觉回归门禁:05 visual AC + 基线图存在 + E2E toHaveScreenshot 绑定。
12
+ */
13
+ const promises_1 = __importDefault(require("fs/promises"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const e2eAssertionDepth_1 = require("./e2eAssertionDepth");
16
+ const fs_1 = require("./fs");
17
+ const testAssertionAnalysis_1 = require("./testAssertionAnalysis");
18
+ const IMAGE_REF_RE = /(?:source\/[\w/.-]+\.(?:png|jpe?g|webp)|visual-baseline\/[\w/.-]+\.(?:png|jpe?g|webp)|image_\d+\.(?:png|jpe?g|webp))/gi;
19
+ const DIFF_PERCENT_RE = /diff\s*[≤<=]\s*(\d+(?:\.\d+)?)\s*%/i;
20
+ const TO_HAVE_SCREENSHOT_RE = /\btoHaveScreenshot\s*\(/;
21
+ const PAGE_SCREENSHOT_ONLY_RE = /\bpage\.screenshot\s*\(/;
22
+ const UI_SCENARIO_KEYWORDS = /上传|点击|页面|按钮|弹窗|列表|表格|展示|抽屉|详情|编辑|导出|筛选|输入框|对话框|导航|toast|截图|布局/i;
23
+ /**
24
+ * 从验收标准提取基线图路径与 diff 阈值(如 diff≤2%)。
25
+ */
26
+ function parseVisualAcRequirements(items) {
27
+ var _a, _b, _c;
28
+ const out = [];
29
+ for (const item of items) {
30
+ if (!/visual/i.test((_a = item.testType) !== null && _a !== void 0 ? _a : ""))
31
+ continue;
32
+ const criterion = (_b = item.criterion) !== null && _b !== void 0 ? _b : "";
33
+ const baselineRefs = [...new Set(((_c = criterion.match(IMAGE_REF_RE)) !== null && _c !== void 0 ? _c : []).map((s) => s.trim()))];
34
+ const diffMatch = criterion.match(DIFF_PERCENT_RE);
35
+ const maxDiffPercent = diffMatch ? Number(diffMatch[1]) : null;
36
+ out.push({
37
+ acId: item.id,
38
+ criterion,
39
+ baselineRefs,
40
+ maxDiffPercent,
41
+ });
42
+ }
43
+ return out;
44
+ }
45
+ async function walkImages(dir, out) {
46
+ let entries;
47
+ try {
48
+ entries = await promises_1.default.readdir(dir, { withFileTypes: true });
49
+ }
50
+ catch {
51
+ return;
52
+ }
53
+ for (const ent of entries) {
54
+ const full = path_1.default.join(dir, ent.name);
55
+ if (ent.isDirectory()) {
56
+ await walkImages(full, out);
57
+ }
58
+ else if (/\.(png|jpe?g|webp)$/i.test(ent.name)) {
59
+ out.push(full);
60
+ }
61
+ }
62
+ }
63
+ /** PRD run 的 source/ 下是否存在原型截图 */
64
+ async function listPrdSourceImages(outputRoot) {
65
+ const sourceDir = path_1.default.join(outputRoot, "source");
66
+ const images = [];
67
+ await walkImages(sourceDir, images);
68
+ return images;
69
+ }
70
+ /**
71
+ * 将验收标准中的基线引用解析为磁盘绝对路径。
72
+ */
73
+ async function resolveBaselinePath(pkgRoot, outputRoot, ref, prdImages) {
74
+ const normalized = ref.replace(/\\/g, "/");
75
+ const candidates = [];
76
+ if (outputRoot) {
77
+ candidates.push(path_1.default.join(outputRoot, normalized));
78
+ candidates.push(path_1.default.join(outputRoot, "visual-baseline", path_1.default.basename(normalized)));
79
+ }
80
+ candidates.push(path_1.default.join(pkgRoot, normalized));
81
+ candidates.push(path_1.default.join(pkgRoot, "visual-baseline", path_1.default.basename(normalized)));
82
+ for (const c of candidates) {
83
+ const content = await (0, fs_1.readFileIfExists)(c);
84
+ if (content !== null)
85
+ return c;
86
+ }
87
+ if (/^image_\d+\./i.test(normalized)) {
88
+ const hit = prdImages.find((p) => path_1.default.basename(p) === path_1.default.basename(normalized));
89
+ if (hit)
90
+ return hit;
91
+ }
92
+ const byBase = prdImages.find((p) => p.endsWith(`/${path_1.default.basename(normalized)}`));
93
+ return byBase !== null && byBase !== void 0 ? byBase : null;
94
+ }
95
+ function e2eBlockHasVisualAssertion(blockBody) {
96
+ const body = (0, testAssertionAnalysis_1.stripComments)(blockBody);
97
+ return TO_HAVE_SCREENSHOT_RE.test(body);
98
+ }
99
+ function e2eReferencesBaseline(blockBody, baselineRefs) {
100
+ if (baselineRefs.length === 0)
101
+ return TO_HAVE_SCREENSHOT_RE.test((0, testAssertionAnalysis_1.stripComments)(blockBody));
102
+ const body = (0, testAssertionAnalysis_1.stripComments)(blockBody);
103
+ return baselineRefs.some((ref) => {
104
+ const base = path_1.default.basename(ref).replace(/\.(png|jpe?g|webp)$/i, "");
105
+ return body.includes(ref) || body.includes(base) || body.includes(`'${base}'`) || body.includes(`"${base}"`);
106
+ });
107
+ }
108
+ async function changeHasUiScenarios(pkgRoot, changeName) {
109
+ const specsDir = path_1.default.join(pkgRoot, "openspec/changes", changeName, "specs");
110
+ let entries;
111
+ try {
112
+ entries = await promises_1.default.readdir(specsDir, { withFileTypes: true });
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ for (const cap of entries) {
118
+ if (!cap.isDirectory())
119
+ continue;
120
+ const specPath = path_1.default.join(specsDir, cap.name, "spec.md");
121
+ const content = await (0, fs_1.readFileIfExists)(specPath);
122
+ if (!content)
123
+ continue;
124
+ const blocks = content.split(/^####\s+Scenario:/m);
125
+ for (let i = 1; i < blocks.length; i += 1) {
126
+ if (UI_SCENARIO_KEYWORDS.test(blocks[i]))
127
+ return true;
128
+ }
129
+ }
130
+ return false;
131
+ }
132
+ function hasVisualExempt(tasksContent) {
133
+ return /^VISUAL_REGRESSION_EXEMPT:/m.test(tasksContent);
134
+ }
135
+ /**
136
+ * 视觉回归静态门禁(deliver 前):基线图 + toHaveScreenshot 与 visual AC 绑定。
137
+ */
138
+ async function checkVisualRegression(pkgRoot, outputRoot, changeName, acItems) {
139
+ var _a, _b;
140
+ const checks = [];
141
+ const skip = process.env.POQUAN_SKIP_VISUAL_REGRESSION === "1" ||
142
+ process.env.OPSX_SKIP_VISUAL_REGRESSION === "1";
143
+ if (skip) {
144
+ checks.push({
145
+ name: "visual-regression",
146
+ ok: true,
147
+ detail: "SKIP: OPSX_SKIP_VISUAL_REGRESSION=1",
148
+ });
149
+ return checks;
150
+ }
151
+ const tasksContent = (_a = (await (0, fs_1.readFileIfExists)(path_1.default.join(pkgRoot, "openspec/changes", changeName, "tasks.md")))) !== null && _a !== void 0 ? _a : "";
152
+ if (hasVisualExempt(tasksContent)) {
153
+ checks.push({
154
+ name: "visual-regression",
155
+ ok: true,
156
+ detail: "VISUAL_REGRESSION_EXEMPT 已声明,跳过视觉回归门禁",
157
+ });
158
+ return checks;
159
+ }
160
+ const visualReqs = parseVisualAcRequirements(acItems);
161
+ const prdImages = outputRoot ? await listPrdSourceImages(outputRoot) : [];
162
+ const requiresUi = await changeHasUiScenarios(pkgRoot, changeName);
163
+ const specPaths = await (0, e2eAssertionDepth_1.collectChangeE2eSpecs)(pkgRoot, changeName);
164
+ if (prdImages.length > 0 && requiresUi && visualReqs.length === 0) {
165
+ checks.push({
166
+ name: "visual-ac-required",
167
+ ok: false,
168
+ detail: `PRD source 含 ${prdImages.length} 张原型图,但 05 无 visual 类型 AC;请补充基线对比验收项(如 AC-xx-v | visual | 基线 image_3.png)`,
169
+ });
170
+ return checks;
171
+ }
172
+ if (visualReqs.length === 0) {
173
+ checks.push({
174
+ name: "visual-regression",
175
+ ok: true,
176
+ detail: "无 visual 类型 AC,跳过视觉回归门禁",
177
+ });
178
+ return checks;
179
+ }
180
+ const failures = [];
181
+ let e2eBlob = "";
182
+ const e2eBlocks = [];
183
+ for (const rel of specPaths) {
184
+ const content = (_b = (await (0, fs_1.readFileIfExists)(path_1.default.join(pkgRoot, rel)))) !== null && _b !== void 0 ? _b : "";
185
+ e2eBlob += content;
186
+ for (const block of (0, testAssertionAnalysis_1.parseTestBlocks)(content)) {
187
+ e2eBlocks.push({ spec: rel, title: block.title, body: block.body });
188
+ }
189
+ }
190
+ if (specPaths.length === 0) {
191
+ failures.push("含 visual AC 但未绑定 e2e spec");
192
+ }
193
+ else if (!TO_HAVE_SCREENSHOT_RE.test(e2eBlob)) {
194
+ failures.push("e2e spec 须使用 toHaveScreenshot 做基线对比(禁止仅 page.screenshot 留证)");
195
+ }
196
+ for (const req of visualReqs) {
197
+ if (req.baselineRefs.length === 0) {
198
+ failures.push(`${req.acId}: visual AC 验收标准须写明基线路径(source/...png 或 visual-baseline/...png)`);
199
+ continue;
200
+ }
201
+ for (const ref of req.baselineRefs) {
202
+ const abs = await resolveBaselinePath(pkgRoot, outputRoot, ref, prdImages);
203
+ if (!abs) {
204
+ failures.push(`${req.acId}: 基线图不存在 ${ref}(请在 PRD run visual-baseline/ 或 source/ 放置)`);
205
+ }
206
+ }
207
+ const boundBlocks = e2eBlocks.filter((b) => (0, testAssertionAnalysis_1.testBlockReferencesAc)({ title: b.title, body: b.body, index: 0 }, req.acId) && e2eBlockHasVisualAssertion(b.body));
208
+ if (boundBlocks.length === 0) {
209
+ failures.push(`${req.acId}: 须在 e2e test 块内含 [${req.acId}] 且调用 toHaveScreenshot(建议 maxDiffPixelRatio≤${req.maxDiffPercent != null ? (req.maxDiffPercent / 100).toFixed(2) : "0.02"})`);
210
+ continue;
211
+ }
212
+ const baselineBound = boundBlocks.some((b) => e2eReferencesBaseline(b.body, req.baselineRefs));
213
+ if (!baselineBound && req.baselineRefs.length > 0) {
214
+ failures.push(`${req.acId}: toHaveScreenshot 须引用基线名 ${req.baselineRefs.map((r) => path_1.default.basename(r)).join("、")}`);
215
+ }
216
+ if (req.maxDiffPercent != null) {
217
+ const ratio = req.maxDiffPercent / 100;
218
+ const hasRatio = boundBlocks.some((b) => {
219
+ const body = (0, testAssertionAnalysis_1.stripComments)(b.body);
220
+ if (new RegExp(`maxDiffPixelRatio:\\s*${ratio}`).test(body))
221
+ return true;
222
+ if (ratio <= 0.02 && /maxDiffPixelRatio:\s*0\.0[12]/.test(body))
223
+ return true;
224
+ return false;
225
+ });
226
+ if (!hasRatio) {
227
+ failures.push(`${req.acId}: 建议 toHaveScreenshot 配置 maxDiffPixelRatio: ${ratio}(与验收标准 diff≤${req.maxDiffPercent}% 一致)`);
228
+ }
229
+ }
230
+ }
231
+ const screenshotOnlyNoCompare = PAGE_SCREENSHOT_ONLY_RE.test(e2eBlob) && !TO_HAVE_SCREENSHOT_RE.test(e2eBlob);
232
+ if (screenshotOnlyNoCompare) {
233
+ failures.push("存在 page.screenshot 但无 toHaveScreenshot,不能算作视觉回归");
234
+ }
235
+ checks.push({
236
+ name: "visual-regression",
237
+ ok: failures.length === 0,
238
+ detail: failures.length === 0
239
+ ? `视觉回归绑定通过(${visualReqs.length} 条 visual AC,spec: ${specPaths.join(", ") || "无"})`
240
+ : failures.join(" | "),
241
+ });
242
+ return checks;
243
+ }
@@ -82,6 +82,8 @@ async function startFlowScaffold(options) {
82
82
  await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "04-技术文档草稿.md"), techDocTemplate);
83
83
  const techDocFillGuide = await (0, templates_1.loadTemplateText)("prompts/04-tech-doc-fill-prompt.md");
84
84
  await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "04-技术文档生成指引.md"), techDocFillGuide);
85
+ const visualBaselineReadme = await (0, templates_1.loadTemplateText)("artifacts/visual-baseline-readme.template.md");
86
+ await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "visual-baseline/README.md"), visualBaselineReadme);
85
87
  // 5) Step5 openspec 提案入口(草稿)
86
88
  const openspecEntryTemplate = await (0, templates_1.loadTemplateText)("artifacts/06-openspec-提案草稿.template.md");
87
89
  await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "06-openspec-提案草稿.md"), openspecEntryTemplate);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-flow-kit",
3
- "version": "1.3.18",
3
+ "version": "1.3.22",
4
4
  "private": false,
5
5
  "description": "Cross-agent SDD automated development workflow kit (ADI/ADI-like).",
6
6
  "license": "MIT",
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AC 断言绑定门禁:P0/e2e AC 的验收标准字面量须出现在 test 块 expect 中。
4
+ *
5
+ * 用法:
6
+ * node scripts/assess-ac-assertion-binding.mjs --run-dir openspec/PRD/<runId>
7
+ * node scripts/assess-ac-assertion-binding.mjs --run-dir <path> --change <name>(可选)
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import {
13
+ checkAcAssertionBinding,
14
+ parseAcMatrixFrom05,
15
+ } from "./lib/ac-assertion-binding.mjs";
16
+
17
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
18
+ const TEST_FILE_RE = /\.(spec|test)\.(t|j)sx?$/i;
19
+
20
+ function parseArgs(argv) {
21
+ let runDir = "";
22
+ for (let i = 2; i < argv.length; i += 1) {
23
+ const a = argv[i];
24
+ if (a === "--run-dir" && argv[i + 1]) runDir = argv[++i];
25
+ else if (!a.startsWith("-") && !runDir) runDir = a;
26
+ }
27
+ return { runDir };
28
+ }
29
+
30
+ function fail(messages) {
31
+ console.error("[assess-ac-assertion-binding] FAIL");
32
+ for (const m of messages) console.error(` - ${m}`);
33
+ console.error(
34
+ " 修复: Red 阶段从 05「验收标准」复制完整文案到 expect;禁止 toContain('成功') 等模糊断言",
35
+ );
36
+ process.exit(1);
37
+ }
38
+
39
+ function walkTests(dir, out, relPrefix = "") {
40
+ if (!fs.existsSync(dir)) return;
41
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
42
+ if (ent.name === "node_modules" || ent.name === ".git" || ent.name === "dist") continue;
43
+ const full = path.join(dir, ent.name);
44
+ const rel = relPrefix ? `${relPrefix}/${ent.name}` : ent.name;
45
+ if (ent.isDirectory()) walkTests(full, out, rel);
46
+ else if (TEST_FILE_RE.test(ent.name)) {
47
+ out.push({ rel, content: fs.readFileSync(full, "utf8") });
48
+ }
49
+ }
50
+ }
51
+
52
+ function collectTestFiles() {
53
+ const files = [];
54
+ for (const r of ["e2e", "tests", "test", "__tests__", "playwright", "src"]) {
55
+ walkTests(path.join(ROOT, r), files, r);
56
+ }
57
+ return files;
58
+ }
59
+
60
+ function resolveRunDir(arg) {
61
+ if (!arg) return "";
62
+ if (path.isAbsolute(arg)) return arg;
63
+ const candidates = [
64
+ path.join(ROOT, arg),
65
+ path.join(ROOT, "openspec/PRD", arg),
66
+ path.join(ROOT, "frontend/openspec/PRD", arg),
67
+ ];
68
+ for (const c of candidates) {
69
+ if (fs.existsSync(path.join(c, "05-验收清单.md"))) return c;
70
+ }
71
+ return path.join(ROOT, arg);
72
+ }
73
+
74
+ function main() {
75
+ const skip = process.env.OPSX_SKIP_AC_ASSERTION_BINDING;
76
+ if (skip === "1") {
77
+ console.warn("[assess-ac-assertion-binding] SKIP");
78
+ process.exit(0);
79
+ }
80
+
81
+ const { runDir: runDirArg } = parseArgs(process.argv);
82
+ const runDir = resolveRunDir(runDirArg);
83
+ const ac05Path = path.join(runDir, "05-验收清单.md");
84
+ if (!fs.existsSync(ac05Path)) {
85
+ console.error("[assess-ac-assertion-binding] 缺少 --run-dir(须含 05-验收清单.md)");
86
+ process.exit(2);
87
+ }
88
+
89
+ const items = parseAcMatrixFrom05(fs.readFileSync(ac05Path, "utf8"));
90
+ if (items.length === 0) {
91
+ console.error("[assess-ac-assertion-binding] 05 未解析到 AC 行");
92
+ process.exit(2);
93
+ }
94
+
95
+ const testFiles = collectTestFiles();
96
+ const errors = checkAcAssertionBinding(items, testFiles);
97
+ if (errors.length > 0) fail(errors);
98
+
99
+ console.log(
100
+ `[assess-ac-assertion-binding] PASS run=${path.basename(runDir)} ac=${items.length} tests=${testFiles.length}`,
101
+ );
102
+ }
103
+
104
+ main();
@@ -81,7 +81,7 @@ function main() {
81
81
  const modRe = /config\/api\/modules\/([\w/-]+)/g;
82
82
  let m;
83
83
  while ((m = modRe.exec(blob)) !== null) {
84
- moduleRefs.add(m[1].replace(/\/index$/, ""));
84
+ moduleRefs.add(m[1].replace(/\/index$/, "").replace(/\/$/, ""));
85
85
  }
86
86
 
87
87
  if (moduleRefs.size === 0) {
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * API 字段级覆盖门禁(v1.3.20):fixture 样例 + 04 字段映射表 + 源码消费点。
4
+ *
5
+ * 用法:
6
+ * node scripts/assess-api-field-coverage.mjs --change <change-name> [--run-dir openspec/PRD/runId]
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { assessApiFieldCoverage } from "./lib/api-field-coverage.mjs";
12
+
13
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+
15
+ function parseArgs(argv) {
16
+ let change = "";
17
+ let runDir = "";
18
+ for (let i = 2; i < argv.length; i += 1) {
19
+ const a = argv[i];
20
+ if (a === "--change" && argv[i + 1]) change = argv[++i];
21
+ else if (a === "--run-dir" && argv[i + 1]) runDir = argv[++i];
22
+ else if (!a.startsWith("-") && !change) change = a;
23
+ }
24
+ return { change, runDir };
25
+ }
26
+
27
+ function resolveRunDir(arg) {
28
+ if (!arg) return "";
29
+ const candidates = [
30
+ path.isAbsolute(arg) ? arg : path.join(ROOT, arg),
31
+ path.join(ROOT, "openspec/PRD", arg),
32
+ path.join(ROOT, "frontend/openspec/PRD", arg),
33
+ ];
34
+ for (const c of candidates) {
35
+ if (fs.existsSync(path.join(c, "04-技术文档草稿.md"))) return c;
36
+ }
37
+ return candidates[0];
38
+ }
39
+
40
+ function fail(messages) {
41
+ console.error("[assess-api-field-coverage] FAIL");
42
+ for (const m of messages) console.error(` - ${m}`);
43
+ console.error(
44
+ " 修复: 1) 04 §4.3.1 补字段映射表 2) tests/fixtures/<module>/*.json 联调样例 3) caseTransform 单测加载 fixture",
45
+ );
46
+ process.exit(1);
47
+ }
48
+
49
+ function main() {
50
+ const skip =
51
+ process.env.POQUAN_SKIP_API_FIELD_COVERAGE ||
52
+ process.env.OPSX_SKIP_API_FIELD_COVERAGE;
53
+ if (skip === "1") {
54
+ console.warn("[assess-api-field-coverage] SKIP");
55
+ process.exit(0);
56
+ }
57
+
58
+ const { change, runDir: runDirArg } = parseArgs(process.argv);
59
+ if (!change) {
60
+ console.error("[assess-api-field-coverage] 缺少 --change <name>");
61
+ process.exit(2);
62
+ }
63
+
64
+ const changeDir = path.join(ROOT, "openspec/changes", change);
65
+ if (!fs.existsSync(changeDir)) {
66
+ console.error(`[assess-api-field-coverage] change 不存在: openspec/changes/${change}`);
67
+ process.exit(2);
68
+ }
69
+
70
+ let fieldDoc = "";
71
+ const runDir = resolveRunDir(runDirArg);
72
+ if (runDir && fs.existsSync(path.join(runDir, "04-技术文档草稿.md"))) {
73
+ fieldDoc += fs.readFileSync(path.join(runDir, "04-技术文档草稿.md"), "utf8");
74
+ }
75
+ const designPath = path.join(changeDir, "design.md");
76
+ if (fs.existsSync(designPath)) {
77
+ fieldDoc += fs.readFileSync(designPath, "utf8");
78
+ }
79
+
80
+ const { errors, passed } = assessApiFieldCoverage(ROOT, change, fieldDoc);
81
+ if (errors.length > 0) fail(errors);
82
+
83
+ console.log(
84
+ `[assess-api-field-coverage] PASS change=${change} ${passed.join("; ")}`,
85
+ );
86
+ }
87
+
88
+ main();
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 组件实质门禁:演示映射的生产组件须非空壳,子组件须被父组件引用。
4
+ *
5
+ * 用法: node scripts/assess-component-substance.mjs --change <change-name>
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import {
11
+ analyzeComponentSubstance,
12
+ checkChildComponentsImported,
13
+ } from "./lib/component-substance.mjs";
14
+ import { parseDemoReferenceSection } from "./lib/demo-reference-parse.mjs";
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
+
18
+ function parseArgs(argv) {
19
+ let change = "";
20
+ for (let i = 2; i < argv.length; i += 1) {
21
+ const a = argv[i];
22
+ if (a === "--change" && argv[i + 1]) change = argv[++i];
23
+ else if (!a.startsWith("-") && !change) change = a;
24
+ }
25
+ return { change };
26
+ }
27
+
28
+ function fail(messages) {
29
+ console.error("[assess-component-substance] FAIL");
30
+ for (const m of messages) console.error(` - ${m}`);
31
+ console.error(
32
+ " 修复: 按 design 映射实现真实子组件(template+API/emit),并在父 Drawer 中 import;禁止注释塞关键词",
33
+ );
34
+ process.exit(1);
35
+ }
36
+
37
+ async function main() {
38
+ const skip = process.env.OPSX_SKIP_COMPONENT_SUBSTANCE;
39
+ if (skip === "1") {
40
+ console.warn("[assess-component-substance] SKIP");
41
+ process.exit(0);
42
+ }
43
+
44
+ const { change } = parseArgs(process.argv);
45
+ if (!change) {
46
+ console.error("[assess-component-substance] 缺少 --change <name>");
47
+ process.exit(2);
48
+ }
49
+
50
+ const designPath = path.join(ROOT, "openspec/changes", change, "design.md");
51
+ if (!fs.existsSync(designPath)) {
52
+ console.error(`[assess-component-substance] design.md 不存在: ${change}`);
53
+ process.exit(2);
54
+ }
55
+
56
+ const design = fs.readFileSync(designPath, "utf8");
57
+ const parsed = parseDemoReferenceSection(design);
58
+ if (parsed.exempt || parsed.mappings.length === 0) {
59
+ console.log("[assess-component-substance] PASS(无演示映射)");
60
+ process.exit(0);
61
+ }
62
+
63
+ const errors = [];
64
+ for (const m of parsed.mappings) {
65
+ const rel = m.productionPath.replace(/^\.\//, "");
66
+ const abs = path.join(ROOT, rel);
67
+ if (!fs.existsSync(abs)) {
68
+ errors.push(`${m.moduleName}: 文件不存在 ${rel}`);
69
+ continue;
70
+ }
71
+ const content = fs.readFileSync(abs, "utf8");
72
+ const result = analyzeComponentSubstance(rel, content, m.interactionSignals);
73
+ if (!result.ok) {
74
+ errors.push(`${m.moduleName}(${rel}): ${result.issues.join("; ")}`);
75
+ }
76
+ }
77
+
78
+ const importFailures = await checkChildComponentsImported(ROOT, parsed.mappings);
79
+ errors.push(...importFailures);
80
+
81
+ if (errors.length > 0) fail(errors);
82
+ console.log(
83
+ `[assess-component-substance] PASS change=${change} modules=${parsed.mappings.length}`,
84
+ );
85
+ }
86
+
87
+ main();
@@ -8,6 +8,8 @@ import fs from "node:fs";
8
8
  import path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { analyzeE2eSpecDepth } from "./lib/test-assertion-analysis.mjs";
11
+ import { checkE2eDemoSignalClicks } from "./lib/e2e-demo-signal-clicks.mjs";
12
+ import { parseDemoReferenceSection } from "./lib/demo-reference-parse.mjs";
11
13
 
12
14
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13
15
  const CONFIG_PATH = path.join(ROOT, "e2e/e2e-scope.config.json");
@@ -136,6 +138,20 @@ function main() {
136
138
  }
137
139
  }
138
140
 
141
+ const designPath = path.join(changeDir, "design.md");
142
+ if (fs.existsSync(designPath) && specPaths.length > 0) {
143
+ const design = fs.readFileSync(designPath, "utf8");
144
+ const parsed = parseDemoReferenceSection(design);
145
+ if (!parsed.exempt && parsed.mappings.length > 0) {
146
+ let e2eBlob = "";
147
+ for (const rel of specPaths) {
148
+ e2eBlob += fs.readFileSync(path.join(ROOT, rel), "utf8");
149
+ }
150
+ const clickFailures = checkE2eDemoSignalClicks(design, e2eBlob);
151
+ for (const f of clickFailures) errors.push(f);
152
+ }
153
+ }
154
+
139
155
  if (errors.length > 0) fail(errors);
140
156
 
141
157
  console.log(
@@ -80,6 +80,13 @@ function main() {
80
80
  errors.push("缺少 §4.1 接口清单表格");
81
81
  }
82
82
 
83
+ if (
84
+ /\|\s*接口\s*\|\s*Method\s*\|/m.test(content) &&
85
+ !/\|\s*后端字段\s*\|\s*前端字段\s*\|/m.test(content)
86
+ ) {
87
+ errors.push("缺少 §4.3.1 响应字段映射表(| 后端字段 | 前端字段 |)");
88
+ }
89
+
83
90
  if (!/\|\s*演示模块\s*\|\s*演示文件\/组件\s*\|/m.test(content)) {
84
91
  errors.push("缺少 §2.4 演示模块映射表格");
85
92
  }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * L4 视觉回归门禁:05 visual AC + 基线图 + E2E toHaveScreenshot。
4
+ *
5
+ * 用法:
6
+ * node scripts/assess-visual-regression.mjs --change <name> --run-dir openspec/PRD/<runId>
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import {
12
+ assessVisualRegression,
13
+ parseAcMatrixFrom05,
14
+ } from "./lib/visual-regression.mjs";
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
+
18
+ function parseArgs(argv) {
19
+ let change = "";
20
+ let runDir = "";
21
+ for (let i = 2; i < argv.length; i += 1) {
22
+ const a = argv[i];
23
+ if (a === "--change" && argv[i + 1]) change = argv[++i];
24
+ else if (a === "--run-dir" && argv[i + 1]) runDir = argv[++i];
25
+ else if (!a.startsWith("-") && !change) change = a;
26
+ }
27
+ return { change, runDir };
28
+ }
29
+
30
+ function resolveRunDir(arg) {
31
+ if (!arg) return "";
32
+ const candidates = [
33
+ path.isAbsolute(arg) ? arg : path.join(ROOT, arg),
34
+ path.join(ROOT, "openspec/PRD", arg),
35
+ path.join(ROOT, "frontend/openspec/PRD", arg),
36
+ ];
37
+ for (const c of candidates) {
38
+ if (fs.existsSync(path.join(c, "05-验收清单.md"))) return c;
39
+ }
40
+ return "";
41
+ }
42
+
43
+ function fail(messages) {
44
+ console.error("[assess-visual-regression] FAIL");
45
+ for (const m of messages) console.error(` - ${m}`);
46
+ console.error(
47
+ " 修复: 05 增加 visual AC(含基线路径 + diff≤N%);e2e 使用 toHaveScreenshot;基线图放 source/ 或 visual-baseline/",
48
+ );
49
+ process.exit(1);
50
+ }
51
+
52
+ function main() {
53
+ const skip = process.env.OPSX_SKIP_VISUAL_REGRESSION;
54
+ if (skip === "1") {
55
+ console.warn("[assess-visual-regression] SKIP");
56
+ process.exit(0);
57
+ }
58
+
59
+ const { change, runDir: runDirArg } = parseArgs(process.argv);
60
+ if (!change) {
61
+ console.error("[assess-visual-regression] 缺少 --change <name>");
62
+ process.exit(2);
63
+ }
64
+
65
+ const changeDir = path.join(ROOT, "openspec/changes", change);
66
+ if (!fs.existsSync(changeDir)) {
67
+ console.error(`[assess-visual-regression] change 不存在: openspec/changes/${change}`);
68
+ process.exit(2);
69
+ }
70
+
71
+ const runDir = resolveRunDir(runDirArg);
72
+ if (!runDir) {
73
+ console.error("[assess-visual-regression] 缺少 --run-dir(须含 05-验收清单.md)");
74
+ process.exit(2);
75
+ }
76
+
77
+ const acItems = parseAcMatrixFrom05(fs.readFileSync(path.join(runDir, "05-验收清单.md"), "utf8"));
78
+ const { errors, visualReqs, exempt } = assessVisualRegression(ROOT, runDir, change, acItems);
79
+
80
+ if (exempt) {
81
+ console.log("[assess-visual-regression] PASS(豁免)");
82
+ process.exit(0);
83
+ }
84
+
85
+ if (errors.length > 0) fail(errors);
86
+
87
+ console.log(
88
+ `[assess-visual-regression] PASS change=${change} visualAc=${visualReqs.length}`,
89
+ );
90
+ }
91
+
92
+ main();