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,213 @@
1
+ "use strict";
2
+ /**
3
+ * 测试断言深度分析:用于 PRD 语义覆盖、AC 映射与 E2E 深度门禁。
4
+ * 禁止仅靠注释登记 PRD/AC;要求 test/it 块内存在 expect 等有效断言。
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.stripComments = stripComments;
8
+ exports.parseTestBlocks = parseTestBlocks;
9
+ exports.isRegistryStubFile = isRegistryStubFile;
10
+ exports.testBlockHasExpect = testBlockHasExpect;
11
+ exports.testBlockReferencesAc = testBlockReferencesAc;
12
+ exports.testBlockReferencesAtom = testBlockReferencesAtom;
13
+ exports.hasMeaningfulAtomCoverageInFile = hasMeaningfulAtomCoverageInFile;
14
+ exports.hasMeaningfulAcCoverageInFile = hasMeaningfulAcCoverageInFile;
15
+ exports.testBlockIsPlaceholderOnly = testBlockIsPlaceholderOnly;
16
+ exports.analyzeE2eSpecDepth = analyzeE2eSpecDepth;
17
+ const TEST_TITLE_RE = /(?:^|\n)\s*(?:test|it)(?:\.(?:only|skip))?\s*\(\s*(?:async\s*)?[`'"]([^`'"]*)[`'"]/g;
18
+ const EXPECT_RE = /\bexpect\s*\(/;
19
+ const E2E_DIR_RE = /(^|\/)(e2e|playwright)(\/|$)/i;
20
+ /** 仅用于登记 PRD 原子、无真实 UI 断言的伪覆盖文件 */
21
+ const REGISTRY_STUB_FILE_RE = /prd-atoms-coverage|prd-atoms-registry|semantic-registry/i;
22
+ /** 浅层 E2E:仅有占位符可见性断言 */
23
+ const PLACEHOLDER_ONLY_RE = /getByPlaceholder|getByLabel|getByTestId/i;
24
+ const DATA_ASSERTION_RE = /getByRole\s*\(\s*['"]cell|getByRole\s*\(\s*['"]row|toContainText|waitForResponse|\.n-data-table|data-table-tbody|data-table-body/i;
25
+ const HOOK_TITLE_RE = /^(?:beforeEach|afterEach|beforeAll|afterAll)$/i;
26
+ /** 移除行注释与块注释,避免注释中的 PRD 或 AC 标识误计为覆盖 */
27
+ function stripComments(content) {
28
+ let out = "";
29
+ let i = 0;
30
+ while (i < content.length) {
31
+ if (content[i] === "/" && content[i + 1] === "/") {
32
+ while (i < content.length && content[i] !== "\n")
33
+ i += 1;
34
+ continue;
35
+ }
36
+ if (content[i] === "/" && content[i + 1] === "*") {
37
+ i += 2;
38
+ while (i < content.length && !(content[i] === "*" && content[i + 1] === "/"))
39
+ i += 1;
40
+ i += 2;
41
+ continue;
42
+ }
43
+ out += content[i];
44
+ i += 1;
45
+ }
46
+ return out;
47
+ }
48
+ /** 解析 test/it 块(标题 + 回调体近似区间) */
49
+ function parseTestBlocks(content) {
50
+ var _a, _b, _c;
51
+ const stripped = stripComments(content);
52
+ const blocks = [];
53
+ const re = new RegExp(TEST_TITLE_RE.source, "g");
54
+ let m;
55
+ const indices = [];
56
+ let idx = 0;
57
+ while ((m = re.exec(stripped)) !== null) {
58
+ indices.push({ title: (_a = m[1]) !== null && _a !== void 0 ? _a : "", start: m.index, index: idx });
59
+ idx += 1;
60
+ }
61
+ for (let i = 0; i < indices.length; i += 1) {
62
+ const cur = indices[i];
63
+ const nextStart = (_c = (_b = indices[i + 1]) === null || _b === void 0 ? void 0 : _b.start) !== null && _c !== void 0 ? _c : stripped.length;
64
+ const slice = stripped.slice(cur.start, Math.min(cur.start + 4000, nextStart));
65
+ blocks.push({ title: cur.title, body: slice, index: cur.index });
66
+ }
67
+ return blocks;
68
+ }
69
+ /** 是否为 PRD 原子登记类伪覆盖文件 */
70
+ function isRegistryStubFile(relPath, content) {
71
+ const norm = relPath.replace(/\\/g, "/");
72
+ if (REGISTRY_STUB_FILE_RE.test(norm))
73
+ return true;
74
+ if (!E2E_DIR_RE.test(norm))
75
+ return false;
76
+ const blocks = parseTestBlocks(content);
77
+ if (blocks.length === 0)
78
+ return false;
79
+ const allShallow = blocks.every((b) => {
80
+ const hasExpect = EXPECT_RE.test(b.body);
81
+ const onlyCount = /expect\s*\([^)]*\)\s*\.toBeGreaterThan\s*\(\s*0\s*\)/.test(b.body) &&
82
+ !DATA_ASSERTION_RE.test(b.body) &&
83
+ !/\bpage\.goto\b/.test(b.body);
84
+ return !hasExpect || onlyCount;
85
+ });
86
+ return allShallow && blocks.length >= 3;
87
+ }
88
+ function testBlockHasExpect(block) {
89
+ return EXPECT_RE.test(block.body);
90
+ }
91
+ /** test 块是否引用 AC(标题或块内显式标注) */
92
+ function testBlockReferencesAc(block, acId) {
93
+ const bracket = `[${acId}]`;
94
+ const upper = acId.toUpperCase();
95
+ return (block.title.includes(bracket) ||
96
+ block.title.toUpperCase().includes(upper) ||
97
+ block.body.includes(bracket));
98
+ }
99
+ /** test 块是否引用 PRD 原子或可核对信号 */
100
+ function testBlockReferencesAtom(block, atomId, signals) {
101
+ const hasId = block.title.includes(atomId) ||
102
+ block.title.includes(`[${atomId}]`) ||
103
+ block.body.includes(atomId);
104
+ if (hasId)
105
+ return true;
106
+ return signals.some((s) => s.length >= 4 && (block.title.includes(s) || block.body.includes(s)));
107
+ }
108
+ /**
109
+ * 判断单条测试文件是否对 PRD 原子有「有效」自动化覆盖(非注释、非 stub、含 expect)。
110
+ */
111
+ function hasMeaningfulAtomCoverageInFile(relPath, content, atomId, signals) {
112
+ if (isRegistryStubFile(relPath, content)) {
113
+ return { covered: false, hasE2e: false };
114
+ }
115
+ const isE2e = E2E_DIR_RE.test(relPath);
116
+ const blocks = parseTestBlocks(content);
117
+ for (const block of blocks) {
118
+ if (!testBlockHasExpect(block))
119
+ continue;
120
+ if (!testBlockReferencesAtom(block, atomId, signals))
121
+ continue;
122
+ return { covered: true, hasE2e: isE2e };
123
+ }
124
+ return { covered: false, hasE2e: false };
125
+ }
126
+ /**
127
+ * 判断单条测试文件是否对 AC 有有效覆盖(test 块内 [AC-XX] + expect)。
128
+ */
129
+ function hasMeaningfulAcCoverageInFile(relPath, content, acId) {
130
+ if (isRegistryStubFile(relPath, content)) {
131
+ return { covered: false, isE2e: false };
132
+ }
133
+ const isE2e = E2E_DIR_RE.test(relPath);
134
+ const blocks = parseTestBlocks(content);
135
+ for (const block of blocks) {
136
+ if (!testBlockReferencesAc(block, acId))
137
+ continue;
138
+ if (!testBlockHasExpect(block))
139
+ continue;
140
+ return { covered: true, isE2e };
141
+ }
142
+ return { covered: false, isE2e: false };
143
+ }
144
+ function testBlockIsPlaceholderOnly(block) {
145
+ if (HOOK_TITLE_RE.test(block.title.trim()))
146
+ return false;
147
+ if (!EXPECT_RE.test(block.body))
148
+ return false;
149
+ const hasPlaceholder = PLACEHOLDER_ONLY_RE.test(block.body);
150
+ const hasData = DATA_ASSERTION_RE.test(block.body);
151
+ const hasInteraction = /\b(?:click|dblclick|fill|press|selectOption)\b/.test(block.body);
152
+ return hasPlaceholder && !hasData && !hasInteraction;
153
+ }
154
+ /**
155
+ * 评估单个 E2E spec 的断言深度(L3 最低要求)。
156
+ * 每个非 hook 的 test 块不得仅断言 placeholder;至少一个 test 块须含数据或交互断言。
157
+ */
158
+ function analyzeE2eSpecDepth(relPath, content) {
159
+ const issues = [];
160
+ if (isRegistryStubFile(relPath, content)) {
161
+ return {
162
+ ok: false,
163
+ issues: ["伪覆盖登记文件,不能作为 change 级 E2E 验收"],
164
+ stats: {
165
+ testCount: 0,
166
+ testsWithExpect: 0,
167
+ hasGoto: false,
168
+ hasDataAssertion: false,
169
+ hasInteraction: false,
170
+ },
171
+ };
172
+ }
173
+ const stripped = stripComments(content);
174
+ const blocks = parseTestBlocks(content);
175
+ const hookBlocks = blocks.filter((b) => HOOK_TITLE_RE.test(b.title.trim()));
176
+ const testBlocks = blocks.filter((b) => !HOOK_TITLE_RE.test(b.title.trim()));
177
+ const testsWithExpect = testBlocks.filter((b) => testBlockHasExpect(b)).length;
178
+ const hasGoto = /\bpage\.goto\b/.test(stripped);
179
+ const hasDataInTests = testBlocks.some((b) => DATA_ASSERTION_RE.test(b.body));
180
+ const hasInteractionInTests = testBlocks.some((b) => /\b(?:click|dblclick|fill|press|selectOption)\b/.test(b.body));
181
+ const placeholderOnlyTests = testBlocks.filter((b) => testBlockIsPlaceholderOnly(b));
182
+ if (testBlocks.length === 0) {
183
+ issues.push("未解析到 test/it 用例(不含 hook)");
184
+ }
185
+ if (testsWithExpect === 0) {
186
+ issues.push("无含 expect( 的 test/it 块");
187
+ }
188
+ if (!hasGoto && !hookBlocks.some((b) => /\bpage\.goto\b/.test(b.body))) {
189
+ issues.push("缺少 page.goto(页面可达性未验证)");
190
+ }
191
+ if (placeholderOnlyTests.length > 0) {
192
+ issues.push(`以下用例仅断言 placeholder/label,缺少数据或交互: ${placeholderOnlyTests.map((t) => t.title).join(", ")}`);
193
+ }
194
+ if (!hasDataInTests && !hasInteractionInTests) {
195
+ issues.push("所有 test 块均缺少数据展示断言(cell/row/toContainText/waitForResponse)或交互操作");
196
+ }
197
+ const ok = testBlocks.length > 0 &&
198
+ testsWithExpect > 0 &&
199
+ (hasGoto || hookBlocks.some((b) => /\bpage\.goto\b/.test(b.body))) &&
200
+ placeholderOnlyTests.length === 0 &&
201
+ (hasDataInTests || hasInteractionInTests);
202
+ return {
203
+ ok,
204
+ issues,
205
+ stats: {
206
+ testCount: testBlocks.length,
207
+ testsWithExpect,
208
+ hasGoto,
209
+ hasDataAssertion: hasDataInTests,
210
+ hasInteraction: hasInteractionInTests,
211
+ },
212
+ };
213
+ }
@@ -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
+ }
@@ -35,7 +35,7 @@ async function writeInitialNext(outputRoot, projectRoot, runId) {
35
35
  "| `tasks.md` | AC 的可执行映射(`[AC-XX]`) |",
36
36
  "",
37
37
  "## 1. Step1 — 拉取 PRD",
38
- "按 `01-获取需求文档指引.md` 执行 confluence 脚本;脚本和 gate 会拒绝 `技术文档`/`后端`/`详细设计` 等非 PRD 页面,然后:",
38
+ "按 `01-获取需求文档指引.md` 执行 confluence 脚本(设置 `SDD_SYNC_SOURCE_DIR` 后自动同步到 `source/PRD.md` + `source/image/`);脚本和 gate 会拒绝 `技术文档`/`后端`/`详细设计` 等非 PRD 页面,然后:",
39
39
  "```bash",
40
40
  `npx sdd-flow-kit gate --project-root "${pr}" --run-id ${runId} --expect prd-fetched`,
41
41
  `npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to after-prd`,
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadRunSession = exports.resolveRunDir = exports.runDeliver = exports.runPropose = exports.runPhaseAdvance = exports.printGateResult = exports.runGate = exports.toRunOptions = exports.parseInvokeText = exports.runDoctor = exports.setupProject = exports.runFlow = exports.startFlowScaffold = void 0;
3
+ exports.loadRunSession = exports.resolveRunDir = exports.tryAutoSyncSourcePrd = exports.syncSourcePrdFromDocs = exports.runSyncSourcePrd = exports.runDeliver = exports.runPropose = exports.runPhaseAdvance = exports.printGateResult = exports.runGate = exports.toRunOptions = exports.parseInvokeText = exports.runDoctor = exports.setupProject = exports.runFlow = exports.startFlowScaffold = void 0;
4
4
  var startFlow_1 = require("./steps/startFlow");
5
5
  Object.defineProperty(exports, "startFlowScaffold", { enumerable: true, get: function () { return startFlow_1.startFlowScaffold; } });
6
6
  var runFlow_1 = require("./steps/runFlow");
@@ -21,6 +21,11 @@ var runPropose_1 = require("./steps/runPropose");
21
21
  Object.defineProperty(exports, "runPropose", { enumerable: true, get: function () { return runPropose_1.runPropose; } });
22
22
  var runDeliver_1 = require("./steps/runDeliver");
23
23
  Object.defineProperty(exports, "runDeliver", { enumerable: true, get: function () { return runDeliver_1.runDeliver; } });
24
+ var runSyncSourcePrd_1 = require("./steps/runSyncSourcePrd");
25
+ Object.defineProperty(exports, "runSyncSourcePrd", { enumerable: true, get: function () { return runSyncSourcePrd_1.runSyncSourcePrd; } });
26
+ var syncSourcePrd_1 = require("./core/syncSourcePrd");
27
+ Object.defineProperty(exports, "syncSourcePrdFromDocs", { enumerable: true, get: function () { return syncSourcePrd_1.syncSourcePrdFromDocs; } });
28
+ Object.defineProperty(exports, "tryAutoSyncSourcePrd", { enumerable: true, get: function () { return syncSourcePrd_1.tryAutoSyncSourcePrd; } });
24
29
  var resolveRun_1 = require("./steps/resolveRun");
25
30
  Object.defineProperty(exports, "resolveRunDir", { enumerable: true, get: function () { return resolveRun_1.resolveRunDir; } });
26
31
  Object.defineProperty(exports, "loadRunSession", { enumerable: true, get: function () { return resolveRun_1.loadRunSession; } });
@@ -7,11 +7,14 @@ exports.runGate = runGate;
7
7
  exports.printGateResult = printGateResult;
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const prdArtifacts_1 = require("../core/prdArtifacts");
10
+ const syncSourcePrd_1 = require("../core/syncSourcePrd");
10
11
  const prdCoverage_1 = require("../core/prdCoverage");
11
12
  const gitDiffGuard_1 = require("../core/gitDiffGuard");
12
13
  const acMatrix_1 = require("../core/acMatrix");
13
14
  const projectRoots_1 = require("../core/projectRoots");
14
15
  const openspecGate_1 = require("../core/openspecGate");
16
+ const demoImplementationCoverage_1 = require("../core/demoImplementationCoverage");
17
+ const techDocQuality_1 = require("../core/techDocQuality");
15
18
  const resolveRun_1 = require("./resolveRun");
16
19
  const fs_1 = require("../core/fs");
17
20
  function fail(checks, expect, runId) {
@@ -73,7 +76,7 @@ async function resolveImplRoots(projectRoot) {
73
76
  * 脚本门禁:失败时 process.exitCode=1,成功 exit 0。
74
77
  */
75
78
  async function runGate(options) {
76
- var _a, _b, _c;
79
+ var _a, _b, _c, _d, _e, _f, _g;
77
80
  const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
78
81
  const checks = [];
79
82
  if (!loaded || !loaded.resolved) {
@@ -85,7 +88,27 @@ async function runGate(options) {
85
88
  const runId = resolved.runId;
86
89
  const implRoots = await resolveImplRoots(options.projectRoot);
87
90
  if (options.expect === "prd-fetched") {
88
- const prd = await (0, prdArtifacts_1.isPrdFetched)(outputRoot);
91
+ let prd = await (0, prdArtifacts_1.isPrdFetched)(outputRoot);
92
+ if (!prd.ok) {
93
+ const product = (_b = (_a = session === null || session === void 0 ? void 0 : session.product) !== null && _a !== void 0 ? _a : runId.split("-")[1]) !== null && _b !== void 0 ? _b : "ADI";
94
+ const version = (_d = (_c = session === null || session === void 0 ? void 0 : session.version) !== null && _c !== void 0 ? _c : runId.split("-").slice(2).join("-")) !== null && _d !== void 0 ? _d : "";
95
+ const sync = await (0, syncSourcePrd_1.tryAutoSyncSourcePrd)({
96
+ projectRoot: options.projectRoot,
97
+ outputRoot,
98
+ product,
99
+ version,
100
+ });
101
+ if (sync.attempted) {
102
+ checks.push({
103
+ name: "source-prd-sync",
104
+ ok: sync.ok,
105
+ detail: sync.detail,
106
+ });
107
+ }
108
+ if (sync.ok) {
109
+ prd = await (0, prdArtifacts_1.isPrdFetched)(outputRoot);
110
+ }
111
+ }
89
112
  checks.push({ name: "prd-fetched", ...prd });
90
113
  return pass(checks, options.expect, runId);
91
114
  }
@@ -105,6 +128,14 @@ async function runGate(options) {
105
128
  checks.push({ name: "questions-closed", ...closed });
106
129
  const docs = await (0, prdArtifacts_1.areAnalysisDocsReady)(outputRoot);
107
130
  checks.push({ name: "analysis-docs", ...docs });
131
+ const techRaw = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, "04-技术文档草稿.md"));
132
+ if (process.env.OPSX_SKIP_TECH_DOC_QUALITY !== "1") {
133
+ const techStruct = (0, techDocQuality_1.checkTechDocStructure)(techRaw !== null && techRaw !== void 0 ? techRaw : "");
134
+ checks.push({ name: "tech-doc-structure", ok: techStruct.ok, detail: techStruct.detail });
135
+ }
136
+ else {
137
+ checks.push({ name: "tech-doc-structure", ok: true, detail: "OPSX_SKIP_TECH_DOC_QUALITY=1 已跳过" });
138
+ }
108
139
  const acDefined = await (0, prdArtifacts_1.isAcceptanceChecklistDefined)(outputRoot);
109
140
  checks.push({ name: "acceptance-checklist-defined", ...acDefined });
110
141
  if (session && session.docModeOnly === false) {
@@ -173,6 +204,11 @@ async function runGate(options) {
173
204
  detail: `openspec/changes/${changeName} 不存在,无法校验 AC 映射`,
174
205
  });
175
206
  }
207
+ const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
208
+ const demoArtifactChecks = await (0, demoImplementationCoverage_1.checkDemoReferenceArtifacts)(pkgRoot, changeName);
209
+ for (const c of demoArtifactChecks) {
210
+ checks.push({ name: c.name, ok: c.ok, detail: c.detail });
211
+ }
176
212
  return pass(checks, options.expect, runId);
177
213
  }
178
214
  if (options.expect === "ac-signed") {
@@ -183,7 +219,10 @@ async function runGate(options) {
183
219
  if (options.expect === "prd-coverage") {
184
220
  const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
185
221
  const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
186
- const coverage = await (0, prdCoverage_1.checkPrdCoverage)(outputRoot, pkgRoot);
222
+ const changeName = await readActiveChange(options.projectRoot, session, options.change);
223
+ const coverage = await (0, prdCoverage_1.checkPrdCoverage)(outputRoot, pkgRoot, {
224
+ changeName: changeName !== null && changeName !== void 0 ? changeName : undefined,
225
+ });
187
226
  for (const c of coverage.checks) {
188
227
  checks.push({ name: c.name, ok: c.ok, detail: c.detail });
189
228
  }
@@ -211,6 +250,11 @@ async function runGate(options) {
211
250
  found = true;
212
251
  const st = (0, openspecGate_1.checkOpenspecApplyReady)(root, changeName);
213
252
  checks.push({ name: "openspec-apply-ready", ok: st.applyReady, detail: st.detail });
253
+ const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
254
+ const demoArtifactChecks = await (0, demoImplementationCoverage_1.checkDemoReferenceArtifacts)(pkgRoot, changeName);
255
+ for (const c of demoArtifactChecks) {
256
+ checks.push({ name: c.name, ok: c.ok, detail: c.detail });
257
+ }
214
258
  break;
215
259
  }
216
260
  }
@@ -330,8 +374,8 @@ async function runGate(options) {
330
374
  name: "validate-report",
331
375
  ok,
332
376
  detail: ok
333
- ? `验收通过 (pipeline=${(_a = j.pipelineShortExitCode) !== null && _a !== void 0 ? _a : "?"}, e2e=${(_b = j.e2eExitCode) !== null && _b !== void 0 ? _b : "?"})`
334
- : `验收结果: ${(_c = j.verdict) !== null && _c !== void 0 ? _c : "unknown"}`,
377
+ ? `验收通过 (pipeline=${(_e = j.pipelineShortExitCode) !== null && _e !== void 0 ? _e : "?"}, e2e=${(_f = j.e2eExitCode) !== null && _f !== void 0 ? _f : "?"})`
378
+ : `验收结果: ${(_g = j.verdict) !== null && _g !== void 0 ? _g : "unknown"}`,
335
379
  });
336
380
  }
337
381
  catch {
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runSyncSourcePrd = runSyncSourcePrd;
4
+ const syncSourcePrd_1 = require("../core/syncSourcePrd");
5
+ const resolveRun_1 = require("./resolveRun");
6
+ /**
7
+ * CLI:将 docs 版本目录 PRD 同步到 openspec run 的 source/。
8
+ */
9
+ async function runSyncSourcePrd(options) {
10
+ var _a, _b, _c, _d;
11
+ const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
12
+ if (!(loaded === null || loaded === void 0 ? void 0 : loaded.resolved)) {
13
+ return { ok: false, detail: "未找到 openspec/PRD/<runId> 目录", runId: null };
14
+ }
15
+ const { resolved, session } = loaded;
16
+ const parts = resolved.runId.split("-");
17
+ const product = (_b = (_a = session === null || session === void 0 ? void 0 : session.product) !== null && _a !== void 0 ? _a : parts[1]) !== null && _b !== void 0 ? _b : "ADI";
18
+ const version = (_d = (_c = session === null || session === void 0 ? void 0 : session.version) !== null && _c !== void 0 ? _c : parts.slice(2).join("-")) !== null && _d !== void 0 ? _d : "";
19
+ if (options.dryRun) {
20
+ return {
21
+ ok: true,
22
+ detail: `dry-run: 将把 docs 版本 PRD 同步到 ${resolved.outputRoot}/source/`,
23
+ runId: resolved.runId,
24
+ };
25
+ }
26
+ const result = await (0, syncSourcePrd_1.syncSourcePrdFromDocs)({
27
+ projectRoot: options.projectRoot,
28
+ outputRoot: resolved.outputRoot,
29
+ product,
30
+ version,
31
+ });
32
+ return {
33
+ ok: result.ok,
34
+ detail: result.detail,
35
+ runId: resolved.runId,
36
+ imageCount: result.imageCount,
37
+ };
38
+ }
@@ -114,7 +114,9 @@ async function runValidate(options) {
114
114
  if (remediateLoop.escalated) {
115
115
  needUserConfirmFromRemediate = true;
116
116
  }
117
- let prdCoverage = await (0, prdCoverage_1.checkPrdCoverage)(loaded.resolved.outputRoot, pkgRoot);
117
+ let prdCoverage = await (0, prdCoverage_1.checkPrdCoverage)(loaded.resolved.outputRoot, pkgRoot, {
118
+ changeName: options.change,
119
+ });
118
120
  if (!prdCoverage.ok) {
119
121
  const failed = prdCoverage.checks.filter((c) => !c.ok).map((c) => `${c.name}: ${c.detail}`);
120
122
  const payload = {
@@ -210,7 +212,7 @@ async function runValidate(options) {
210
212
  }
211
213
  return {
212
214
  ok: true,
213
- detail: "验收通过(prd-coverage + lint+type+unit + 强制 e2e),可进入交付",
215
+ detail: "验收通过(prd-coverage 含 E2E 深度与 API 契约 + lint+type+unit + 强制 e2e),可进入交付",
214
216
  needUserConfirm: false,
215
217
  };
216
218
  }