sdd-flow-kit 1.3.18 → 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 (36) hide show
  1. package/dist/core/acAssertionBinding.js +180 -0
  2. package/dist/core/apiFieldContract.js +1 -1
  3. package/dist/core/apiFieldCoverage.js +335 -0
  4. package/dist/core/componentSubstance.js +172 -0
  5. package/dist/core/demoImplementationCoverage.js +2 -0
  6. package/dist/core/e2eAssertionDepth.js +4 -0
  7. package/dist/core/e2eDemoSignalClicks.js +90 -0
  8. package/dist/core/prdCoverage.js +13 -34
  9. package/dist/core/snakeCamel.js +93 -0
  10. package/dist/core/techDocQuality.js +4 -0
  11. package/dist/core/visualRegression.js +243 -0
  12. package/dist/steps/startFlow.js +2 -0
  13. package/package.json +1 -1
  14. package/scripts/assess-ac-assertion-binding.mjs +104 -0
  15. package/scripts/assess-api-field-contract.mjs +1 -1
  16. package/scripts/assess-api-field-coverage.mjs +88 -0
  17. package/scripts/assess-component-substance.mjs +87 -0
  18. package/scripts/assess-e2e-assertion-depth.mjs +16 -0
  19. package/scripts/assess-tech-doc-quality.mjs +7 -0
  20. package/scripts/assess-visual-regression.mjs +92 -0
  21. package/scripts/lib/ac-assertion-binding.mjs +119 -0
  22. package/scripts/lib/api-field-coverage.mjs +238 -0
  23. package/scripts/lib/component-substance.mjs +117 -0
  24. package/scripts/lib/demo-reference-parse.mjs +55 -0
  25. package/scripts/lib/e2e-demo-signal-clicks.mjs +43 -0
  26. package/scripts/lib/snake-camel.mjs +74 -0
  27. package/scripts/lib/test-assertion-analysis.mjs +14 -0
  28. package/scripts/lib/visual-regression.mjs +191 -0
  29. package/src/templates/artifacts/04-/346/212/200/346/234/257/346/226/207/346/241/243.template.md +8 -0
  30. package/src/templates/artifacts/05-/351/252/214/346/224/266/346/270/205/345/215/225.template.md +3 -2
  31. package/src/templates/artifacts/design-demo-reference.template.md +5 -5
  32. package/src/templates/artifacts/tests-fixtures-readme.template.md +19 -0
  33. package/src/templates/artifacts/visual-baseline-readme.template.md +28 -0
  34. package/src/templates/prompts/04-tech-doc-fill-prompt.md +7 -2
  35. package/src/templates/skills/demo-reference-mandatory-workflow.md +3 -1
  36. package/src/templates/skills/layered-testing-strategy.md +5 -3
@@ -0,0 +1,191 @@
1
+ /**
2
+ * L4 视觉回归分析(与 src/core/visualRegression.ts 语义一致)。
3
+ */
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { parseTestBlocks, stripComments, testBlockReferencesAc } from "./test-assertion-analysis.mjs";
7
+
8
+ const IMAGE_REF_RE =
9
+ /(?:source\/[\w/.-]+\.(?:png|jpe?g|webp)|visual-baseline\/[\w/.-]+\.(?:png|jpe?g|webp)|image_\d+\.(?:png|jpe?g|webp))/gi;
10
+ const DIFF_PERCENT_RE = /diff\s*[≤<=]\s*(\d+(?:\.\d+)?)\s*%/i;
11
+ const TO_HAVE_SCREENSHOT_RE = /\btoHaveScreenshot\s*\(/;
12
+ const PAGE_SCREENSHOT_ONLY_RE = /\bpage\.screenshot\s*\(/;
13
+
14
+ export function parseAcMatrixFrom05(content) {
15
+ const items = [];
16
+ for (const line of content.split("\n")) {
17
+ const trimmed = line.trim();
18
+ if (!trimmed.startsWith("|")) continue;
19
+ if (/^\|\s*[-:]+/.test(trimmed)) continue;
20
+ if (/^\|\s*ID\s*\|/i.test(trimmed)) continue;
21
+ const cells = trimmed
22
+ .split("|")
23
+ .map((c) => c.trim())
24
+ .filter((_, i, arr) => i > 0 && i < arr.length - 1);
25
+ if (cells.length < 5) continue;
26
+ const id = (cells[0] ?? "").toUpperCase();
27
+ if (!/^AC-\d{2,}/.test(id)) continue;
28
+ items.push({
29
+ id,
30
+ criterion: cells[2] ?? "",
31
+ testType: cells[3] ?? "",
32
+ priority: cells[4] ?? "",
33
+ });
34
+ }
35
+ return items;
36
+ }
37
+
38
+ export function parseVisualAcRequirements(items) {
39
+ const out = [];
40
+ for (const item of items) {
41
+ if (!/visual/i.test(item.testType ?? "")) continue;
42
+ const criterion = item.criterion ?? "";
43
+ const baselineRefs = [...new Set((criterion.match(IMAGE_REF_RE) ?? []).map((s) => s.trim()))];
44
+ const diffMatch = criterion.match(DIFF_PERCENT_RE);
45
+ out.push({
46
+ acId: item.id,
47
+ criterion,
48
+ baselineRefs,
49
+ maxDiffPercent: diffMatch ? Number(diffMatch[1]) : null,
50
+ });
51
+ }
52
+ return out;
53
+ }
54
+
55
+ function walkImages(dir, out) {
56
+ if (!fs.existsSync(dir)) return;
57
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
58
+ const full = path.join(dir, ent.name);
59
+ if (ent.isDirectory()) walkImages(full, out);
60
+ else if (/\.(png|jpe?g|webp)$/i.test(ent.name)) out.push(full);
61
+ }
62
+ }
63
+
64
+ export function listPrdSourceImages(outputRoot) {
65
+ const images = [];
66
+ walkImages(path.join(outputRoot, "source"), images);
67
+ return images;
68
+ }
69
+
70
+ export function resolveBaselinePath(pkgRoot, outputRoot, ref, prdImages) {
71
+ const normalized = ref.replace(/\\/g, "/");
72
+ const candidates = [];
73
+ if (outputRoot) {
74
+ candidates.push(path.join(outputRoot, normalized));
75
+ candidates.push(path.join(outputRoot, "visual-baseline", path.basename(normalized)));
76
+ }
77
+ candidates.push(path.join(pkgRoot, normalized));
78
+ candidates.push(path.join(pkgRoot, "visual-baseline", path.basename(normalized)));
79
+ for (const c of candidates) {
80
+ if (fs.existsSync(c)) return c;
81
+ }
82
+ if (/^image_\d+\./i.test(normalized)) {
83
+ const hit = prdImages.find((p) => path.basename(p) === path.basename(normalized));
84
+ if (hit) return hit;
85
+ }
86
+ return prdImages.find((p) => p.endsWith(`/${path.basename(normalized)}`)) ?? null;
87
+ }
88
+
89
+ function e2eBlockHasVisualAssertion(body) {
90
+ return TO_HAVE_SCREENSHOT_RE.test(stripComments(body));
91
+ }
92
+
93
+ function e2eReferencesBaseline(body, baselineRefs) {
94
+ const stripped = stripComments(body);
95
+ if (baselineRefs.length === 0) return TO_HAVE_SCREENSHOT_RE.test(stripped);
96
+ return baselineRefs.some((ref) => {
97
+ const base = path.basename(ref).replace(/\.(png|jpe?g|webp)$/i, "");
98
+ return (
99
+ stripped.includes(ref) ||
100
+ stripped.includes(base) ||
101
+ stripped.includes(`'${base}'`) ||
102
+ stripped.includes(`"${base}"`)
103
+ );
104
+ });
105
+ }
106
+
107
+ export function collectChangeE2eSpecs(root, changeName) {
108
+ const changeDir = path.join(root, "openspec/changes", changeName);
109
+ const specs = new Set();
110
+ let blob = "";
111
+ for (const f of ["tasks.md", "proposal.md", "design.md"]) {
112
+ const fp = path.join(changeDir, f);
113
+ if (!fs.existsSync(fp)) continue;
114
+ blob += fs.readFileSync(fp, "utf8");
115
+ }
116
+ const re = /\be2e\/[\w/.-]+\.spec\.(?:ts|js)\b/g;
117
+ let m;
118
+ while ((m = re.exec(blob)) !== null) {
119
+ const rel = m[0];
120
+ if (fs.existsSync(path.join(root, rel))) specs.add(rel);
121
+ }
122
+ return [...specs].sort();
123
+ }
124
+
125
+ export function assessVisualRegression(root, outputRoot, changeName, acItems) {
126
+ const errors = [];
127
+ const visualReqs = parseVisualAcRequirements(acItems);
128
+ const prdImages = outputRoot ? listPrdSourceImages(outputRoot) : [];
129
+ const specPaths = collectChangeE2eSpecs(root, changeName);
130
+
131
+ const tasksPath = path.join(root, "openspec/changes", changeName, "tasks.md");
132
+ if (fs.existsSync(tasksPath) && /^VISUAL_REGRESSION_EXEMPT:/m.test(fs.readFileSync(tasksPath, "utf8"))) {
133
+ return { errors: [], visualReqs, exempt: true };
134
+ }
135
+
136
+ if (prdImages.length > 0 && visualReqs.length === 0) {
137
+ errors.push(
138
+ `PRD source 含 ${prdImages.length} 张原型图,但 05 无 visual AC;请补充基线对比验收项`,
139
+ );
140
+ return { errors, visualReqs, exempt: false };
141
+ }
142
+
143
+ if (visualReqs.length === 0) return { errors: [], visualReqs, exempt: false };
144
+
145
+ let e2eBlob = "";
146
+ const e2eBlocks = [];
147
+ for (const rel of specPaths) {
148
+ const content = fs.readFileSync(path.join(root, rel), "utf8");
149
+ e2eBlob += content;
150
+ for (const block of parseTestBlocks(content)) {
151
+ e2eBlocks.push({ spec: rel, title: block.title, body: block.body });
152
+ }
153
+ }
154
+
155
+ if (specPaths.length === 0) errors.push("含 visual AC 但未绑定 e2e spec");
156
+ else if (!TO_HAVE_SCREENSHOT_RE.test(e2eBlob)) {
157
+ errors.push("e2e 须使用 toHaveScreenshot(禁止仅 page.screenshot)");
158
+ }
159
+
160
+ for (const req of visualReqs) {
161
+ if (req.baselineRefs.length === 0) {
162
+ errors.push(`${req.acId}: visual AC 须写明基线路径`);
163
+ continue;
164
+ }
165
+ for (const ref of req.baselineRefs) {
166
+ if (!resolveBaselinePath(root, outputRoot, ref, prdImages)) {
167
+ errors.push(`${req.acId}: 基线图不存在 ${ref}`);
168
+ }
169
+ }
170
+ const bound = e2eBlocks.filter(
171
+ (b) =>
172
+ testBlockReferencesAc({ title: b.title, body: b.body, index: 0 }, req.acId) &&
173
+ e2eBlockHasVisualAssertion(b.body),
174
+ );
175
+ if (bound.length === 0) {
176
+ errors.push(`${req.acId}: e2e 须含 [${req.acId}] + toHaveScreenshot`);
177
+ continue;
178
+ }
179
+ if (!bound.some((b) => e2eReferencesBaseline(b.body, req.baselineRefs))) {
180
+ errors.push(
181
+ `${req.acId}: toHaveScreenshot 须引用 ${req.baselineRefs.map((r) => path.basename(r)).join("、")}`,
182
+ );
183
+ }
184
+ }
185
+
186
+ if (PAGE_SCREENSHOT_ONLY_RE.test(e2eBlob) && !TO_HAVE_SCREENSHOT_RE.test(e2eBlob)) {
187
+ errors.push("仅有 page.screenshot 无 toHaveScreenshot");
188
+ }
189
+
190
+ return { errors, visualReqs, exempt: false };
191
+ }
@@ -158,6 +158,14 @@ type ExampleEntity = {
158
158
 
159
159
  ### 4.3 响应数据映射(API -> UI)
160
160
 
161
+ #### 4.3.1 响应字段映射表(门禁 SSOT,对接后端须逐字段登记)
162
+
163
+ | 后端字段 | 前端字段 | 展示位置 | 兜底值 |
164
+ | --- | --- | --- | --- |
165
+ | `subject_id` | `subjectId` | 列表第1列 | `-` |
166
+ | `subject_name` | `subjectName` | 列表项目名称 | `-` |
167
+ | (填写) | (填写) | (填写) | `-` |
168
+
161
169
  - 展示字段组装规则:
162
170
  - 兜底值处理:
163
171
  - 时间/金额/百分比格式化规则:
@@ -11,8 +11,8 @@
11
11
  ## 填写规则
12
12
 
13
13
  1. **ID**:`AC-01`、`AC-02`… 全局唯一。
14
- 2. **验收标准**:必须可观测(DOM 顺序、完整 toast/tooltip 文案、E2E 路径、截图对比等);**详情/弹窗类须写明演示模块名**(如「对齐演示 ProjectDetail §红票及其他」)。
15
- 3. **测试类型**:`unit` | `component` | `e2e` | `visual` | `manual`(可组合,如 `unit+e2e`)。
14
+ 2. **验收标准**:必须可观测(DOM 顺序、完整 toast/tooltip 文案、E2E 路径、**截图基线对比**等);含 PRD 原型图时须写 `source/.../image_N.png` 或 `visual-baseline/xxx.png` 与 `diff≤2%`。
15
+ 3. **测试类型**:`unit` | `component` | `e2e` | `visual` | `manual`(可组合,如 `unit+e2e+visual`)。**PRD source 含 `.png` 原型图时,至少 1 条 P0/P1 `visual` AC**。
16
16
  4. **优先级**:`P0`(交付阻断)/ `P1` / `P2`。
17
17
  5. **映射**:propose 后在 `openspec/changes/<change>/tasks.md` 或 `spec.md` 中引用 `[AC-XX]`;**tasks 须有「演示模块迁移」段,与 design 映射表一行一任务**。
18
18
  6. **Intentional 偏差**:与 PRD 有意不一致时,必须同步记入 `03-待确认问题清单.md` 并在下文登记。
@@ -23,6 +23,7 @@
23
23
  |----|---------|-------------------|---------|--------|-------------------|---------|
24
24
  | AC-01 | PRD §(填写) | (填写可观测断言,toast/tooltip 须一字不差) | unit | P0 | 待映射 | 待验收 |
25
25
  | AC-02 | PRD §(填写) | (填写) | e2e | P1 | 待映射 | 待验收 |
26
+ | AC-02-V | PRD §(填写) | 列表页与基线 `source/.../image_1.png` 布局一致(diff≤2%) | visual+e2e | P1 | e2e/*.spec.ts | 待验收 |
26
27
 
27
28
  ## Intentional 偏差登记
28
29
 
@@ -10,11 +10,11 @@
10
10
 
11
11
  ### 演示模块 → 生产组件映射(门禁 SSOT)
12
12
 
13
- | 演示模块 | 演示文件/组件 | 生产组件路径 | 关键交互信号(须在生产代码出现) |
14
- |---------|--------------|-------------|--------------------------------|
15
- | (示例)列表页 | FinancialOverview.vue | src/views/finance/index.vue | 请输入项目ID或名称、拉取欢聚最新数据 |
16
- | (示例)详情-BD预估 | ProjectDetail.vue | src/views/finance/components/BdEstimateSection.vue | 编辑预估、BD预计开票日期、距BD预计 |
17
- | (示例)详情-红票 | ProjectDetail.vue | src/views/finance/components/RedInvoiceSection.vue | 红票及其他、添加记录、红票 |
13
+ | 演示模块 | 演示文件/组件 | 生产组件路径 | 关键交互信号(须在生产代码出现) | E2E 点击绑定(getByText/click) |
14
+ |---------|--------------|-------------|--------------------------------|--------------------------------|
15
+ | (示例)列表页 | FinancialOverview.vue | src/views/finance/index.vue | 请输入项目ID或名称、拉取欢聚最新数据 | getByPlaceholder('请输入项目ID或名称') |
16
+ | (示例)详情-BD预估 | ProjectDetail.vue | src/views/finance/components/BdEstimateSection.vue | 编辑预估、BD预计开票日期 | getByRole('button', { name: '编辑预估' }).click() |
17
+ | (示例)详情-红票 | ProjectDetail.vue | src/views/finance/components/RedInvoiceSection.vue | 红票及其他、添加记录 | getByText('添加记录').click() |
18
18
  | (填写) | | | |
19
19
 
20
20
  ### 禁止的实现方式
@@ -0,0 +1,19 @@
1
+ # API Fixture 样例(v1.3.20 门禁)
2
+
3
+ 将**联调/抓包**得到的真实后端 JSON 放在此目录(保持 **snake_case** 键名)。
4
+
5
+ ## 命名建议
6
+
7
+ - `finance-subject-page.json` — 列表分页响应
8
+ - `finance-subject-info.json` — 详情 subject-info
9
+ - `finance-subject-detail.json` — 详情 subject-detail
10
+
11
+ ## 要求
12
+
13
+ 1. 须含 `_` 分隔的后端字段(如 `subject_id`),禁止手写全 camelCase 空对象
14
+ 2. 配套单测:`tests/**/caseTransform.test.ts` 加载本目录 JSON,`snakeToCamelDeep` 后 `expect` 关键 camelCase 字段
15
+ 3. 04 技术文档 §4.3.1 字段映射表须与 fixture 字段一致
16
+
17
+ ## 门禁
18
+
19
+ `assess-api-field-coverage.mjs` 在 deliver 阶段校验。
@@ -0,0 +1,28 @@
1
+ # visual-baseline(L4 视觉回归基线)
2
+
3
+ > 由 `sdd-flow-kit` 在 PRD run 脚手架时创建。与 `source/` 下 PRD 原型图配合使用。
4
+
5
+ ## 用途
6
+
7
+ - 存放 **裁剪后的 UI 基线截图**(可选;也可直接引用 `source/**/image_N.png`)
8
+ - `05-验收清单.md` 中 `visual` 类型 AC 须在「验收标准」写明基线路径,例如:
9
+ - `列表页与基线 source/adi-v234/image_1.png 布局一致(diff≤2%)`
10
+ - `详情抽屉与 visual-baseline/finance-detail.png 一致(diff≤2%)`
11
+
12
+ ## E2E 要求(v1.3.21 门禁)
13
+
14
+ ```typescript
15
+ await expect(page).toHaveScreenshot("finance-report-list.png", {
16
+ maxDiffPixelRatio: 0.02,
17
+ });
18
+ ```
19
+
20
+ - 须与 `[AC-xx]` 同处一个 test 块
21
+ - **禁止**仅用 `page.screenshot()` 代替 `toHaveScreenshot`
22
+ - Playwright 快照目录默认:`e2e/finance/*.spec.ts-snapshots/` 或与基线名一致
23
+
24
+ ## 门禁
25
+
26
+ `assess-visual-regression.mjs` 在 deliver 阶段校验。
27
+
28
+ 豁免:`tasks.md` 写 `VISUAL_REGRESSION_EXEMPT: <原因>` 或 `OPSX_SKIP_VISUAL_REGRESSION=1`
@@ -41,13 +41,18 @@
41
41
 
42
42
  ### §4 接口与数据交互
43
43
  - **4.1 表格**:接口路径 | Method | 用途 | 请求字段 | 响应字段 | 异常处理
44
- - **4.3** 写明后端 SNAKE_CASE 前端 camelCase normalize 位置(如 `config/api/modules/*/caseTransform`)
44
+ - **4.3** 写明 normalize 位置 + **4.3.1 字段映射表**(| 后端字段 | 前端字段 |);`tests/fixtures/<module>/*.json` 放联调抓包样例
45
45
  - 提交 payload 示例(JSON)
46
46
 
47
47
  ### §5 表单与校验
48
48
  - 关键弹窗/编辑字段校验矩阵(必填、格式、错误文案)
49
49
 
50
- ### §6–§11
50
+ ### §2.5 视觉基线(PRD 含原型图时必填)
51
+
52
+ - 基线图路径:`source/.../image_N.png` 或 `visual-baseline/xxx.png`
53
+ - 在 `05-验收清单.md` 增加 `visual` 类型 AC,写明 `diff≤2%`
54
+ - E2E 使用 `toHaveScreenshot`,禁止仅 `page.screenshot`
55
+
51
56
  - 边界场景、文件级改造清单(**禁止**只写一个大组件名)、风险表、发布检查、测试要点与 05 AC 对应、待确认项(无则写「无」)
52
57
 
53
58
  ---
@@ -57,7 +57,9 @@
57
57
  ## 阶段 E — Validate / Deliver
58
58
 
59
59
  - `assess-demo-implementation-coverage.mjs`:映射表中的生产文件存在,且含「关键交互信号」
60
- - `assess-e2e-assertion-depth.mjs`:E2E 须点到演示中的主按钮/区块(非 placeholder-only)
60
+ - `assess-component-substance.mjs`(v1.3.19):组件非空壳(≥40 有效行、template 含控件、API/emit);Section/Modal 须被父组件 import
61
+ - `assess-ac-assertion-binding.mjs`(v1.3.19):P0 AC 的 05 验收标准字面量须出现在 test expect 中;禁止纯 manual P0
62
+ - `assess-e2e-assertion-depth.mjs`:E2E 须数据/交互断言;**可点击信号**须在 spec 中有 click/getByText(v1.3.19)
61
63
  - 08 报告证据须指向 **具体组件文件 + 行号 + 交互文案**
62
64
 
63
65
  ## 05 验收清单
@@ -6,11 +6,13 @@
6
6
 
7
7
  | 层级 | 覆盖范围 | 典型断言 | 缺失风险 | 门禁 |
8
8
  |------|---------|---------|---------|------|
9
- | **L1 能力** | API、状态机、mapper、脏数据 | 接口封装、枚举映射、mutation 参数、**snake/camel normalize** | 主流程不通、列表空数据 | `assess-api-field-contract.mjs` + unit |
9
+ | **L1 能力** | API、状态机、mapper、脏数据 | 接口封装、枚举映射、mutation 参数、**snake/camel normalize** + **fixture 字段覆盖** | 主流程不通、列表空数据 | `assess-api-field-contract.mjs` + `assess-api-field-coverage.mjs` + unit |
10
10
  | **L2 交互契约** | 列序、disabled、toast、tooltip、placeholder | **完整字符串**(禁止 `toContain('成功')`) | PRD 细节漏项 | AC 映射须 test 块内 `expect` |
11
11
  | **L3 路径** | 每页面/每主链路一条 E2E | 登录 → 列表有数据 → 点详情 → **演示映射中的按钮/区块** | 只 smoke placeholder | `assess-e2e-assertion-depth.mjs` |
12
- | **L3.5 演示迁移** | 演示模块 → 生产组件 1:1 | design 映射表每行在生产代码有「关键交互信号」 | 单文件只读骨架代替演示 | `assess-demo-implementation-coverage.mjs` |
13
- | **L4 视觉** | 原型图 / Figma / `playwright-pic` | 列位置、紫色摘要、布局两行限制 | UI 与原型偏差 | 人工 + 截图 |
12
+ | **L3.5 演示迁移** | 演示模块 → 生产组件 1:1 | design 映射表每行在生产代码有「关键交互信号」+ **非空壳组件** | 单文件只读骨架代替演示 | `assess-demo-implementation-coverage.mjs` + `assess-component-substance.mjs` |
13
+ | **L3.6 AC 断言绑定** | P0 验收标准字面量 | test expect 05 原文(禁止模糊 toContain('成功')) | PRD 文案漏测 | `assess-ac-assertion-binding.mjs` |
14
+ | **L3.7 E2E 演示点击** | 映射表中可点击信号 | spec 中 click/getByText 覆盖「编辑/保存/导入」等 | 有按钮无点击 | `assess-e2e-assertion-depth.mjs`(含 demo signal) |
15
+ | **L4 视觉** | 原型图 / PRD 截图 / `visual-baseline` | `toHaveScreenshot` + 基线图 diff≤N% | UI 与原型偏差 | `assess-visual-regression.mjs`(v1.3.21) |
14
16
 
15
17
  ## 禁止伪覆盖
16
18