sdd-flow-kit 1.3.15 → 1.3.18

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 (41) hide show
  1. package/dist/cli.js +16 -0
  2. package/dist/core/apiFieldContract.js +90 -0
  3. package/dist/core/demoImplementationCoverage.js +242 -0
  4. package/dist/core/e2eAssertionDepth.js +233 -0
  5. package/dist/core/prdArtifacts.js +12 -1
  6. package/dist/core/prdCoverage.js +46 -9
  7. package/dist/core/prdSemanticDiff.js +7 -4
  8. package/dist/core/syncSourcePrd.js +181 -0
  9. package/dist/core/techDocQuality.js +87 -0
  10. package/dist/core/testAssertionAnalysis.js +213 -0
  11. package/dist/core/writeNext.js +1 -1
  12. package/dist/index.js +6 -1
  13. package/dist/steps/runGate.js +49 -5
  14. package/dist/steps/runSyncSourcePrd.js +38 -0
  15. package/dist/steps/runValidate.js +4 -2
  16. package/dist/steps/setupProject.js +3 -0
  17. package/dist/steps/startFlow.js +7 -2
  18. package/package.json +2 -1
  19. package/scripts/assert-active-openspec-change.sh +114 -0
  20. package/scripts/assess-api-field-contract.mjs +119 -0
  21. package/scripts/assess-demo-implementation-coverage.mjs +188 -0
  22. package/scripts/assess-e2e-assertion-depth.mjs +146 -0
  23. package/scripts/assess-playwright-e2e-coverage.mjs +244 -0
  24. package/scripts/assess-tech-doc-quality.mjs +91 -0
  25. package/scripts/install-git-hooks.sh +27 -0
  26. package/scripts/lib/test-assertion-analysis.mjs +134 -0
  27. package/scripts/resolve-playwright-e2e-scope.mjs +248 -0
  28. package/src/templates/artifacts/01-adi-doc-skill-/346/214/207/345/274/225.template.md +6 -2
  29. 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
  30. package/src/templates/artifacts/04-/346/212/200/346/234/257/346/226/207/346/241/243.template.md +14 -0
  31. package/src/templates/artifacts/05-/351/252/214/346/224/266/346/270/205/345/215/225.template.md +2 -2
  32. package/src/templates/artifacts/06-openspec-/346/217/220/346/241/210/350/215/211/347/250/277.template.md +2 -2
  33. package/src/templates/artifacts/07-opsx-auto-chain.template.md +2 -2
  34. 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
  35. package/src/templates/artifacts/design-demo-reference.template.md +24 -0
  36. package/src/templates/prompts/02-sdd-loop-prompt.md +25 -11
  37. package/src/templates/prompts/04-tech-doc-fill-prompt.md +71 -0
  38. package/src/templates/skills/confluence-doc.py.template +64 -25
  39. package/src/templates/skills/demo-reference-mandatory-workflow.md +77 -0
  40. package/src/templates/skills/doc-skill.SKILL.md.template +13 -6
  41. package/src/templates/skills/layered-testing-strategy.md +13 -6
@@ -0,0 +1,134 @@
1
+ /**
2
+ * 测试断言分析(与 src/core/testAssertionAnalysis.ts 保持语义一致,供 scripts 独立运行)。
3
+ */
4
+
5
+ const TEST_TITLE_RE = /(?:^|\n)\s*(?:test|it)(?:\.(?:only|skip))?\s*\(\s*(?:async\s*)?[`'"]([^`'"]*)[`'"]/g;
6
+ const EXPECT_RE = /\bexpect\s*\(/;
7
+ const E2E_DIR_RE = /(^|\/)(e2e|playwright)(\/|$)/i;
8
+ const REGISTRY_STUB_FILE_RE = /prd-atoms-coverage|prd-atoms-registry|semantic-registry/i;
9
+ const PLACEHOLDER_ONLY_RE = /getByPlaceholder|getByLabel|getByTestId/i;
10
+ const DATA_ASSERTION_RE =
11
+ /getByRole\s*\(\s*['"]cell|getByRole\s*\(\s*['"]row|toContainText|waitForResponse|\.n-data-table|data-table-tbody|data-table-body/i;
12
+ const HOOK_TITLE_RE = /^(?:beforeEach|afterEach|beforeAll|afterAll)$/i;
13
+
14
+ export function stripComments(content) {
15
+ let out = "";
16
+ let i = 0;
17
+ while (i < content.length) {
18
+ if (content[i] === "/" && content[i + 1] === "/") {
19
+ while (i < content.length && content[i] !== "\n") i += 1;
20
+ continue;
21
+ }
22
+ if (content[i] === "/" && content[i + 1] === "*") {
23
+ i += 2;
24
+ while (i < content.length && !(content[i] === "*" && content[i + 1] === "/")) i += 1;
25
+ i += 2;
26
+ continue;
27
+ }
28
+ out += content[i];
29
+ i += 1;
30
+ }
31
+ return out;
32
+ }
33
+
34
+ export function parseTestBlocks(content) {
35
+ const stripped = stripComments(content);
36
+ const blocks = [];
37
+ const re = new RegExp(TEST_TITLE_RE.source, "g");
38
+ let m;
39
+ const indices = [];
40
+ let idx = 0;
41
+ while ((m = re.exec(stripped)) !== null) {
42
+ indices.push({ title: m[1] ?? "", start: m.index, index: idx });
43
+ idx += 1;
44
+ }
45
+ for (let i = 0; i < indices.length; i += 1) {
46
+ const cur = indices[i];
47
+ const nextStart = indices[i + 1]?.start ?? stripped.length;
48
+ const slice = stripped.slice(cur.start, Math.min(cur.start + 4000, nextStart));
49
+ blocks.push({ title: cur.title, body: slice, index: cur.index });
50
+ }
51
+ return blocks;
52
+ }
53
+
54
+ export function isRegistryStubFile(relPath, content) {
55
+ const norm = relPath.replace(/\\/g, "/");
56
+ if (REGISTRY_STUB_FILE_RE.test(norm)) return true;
57
+ if (!E2E_DIR_RE.test(norm)) return false;
58
+ const blocks = parseTestBlocks(content);
59
+ if (blocks.length === 0) return false;
60
+ const allShallow = blocks.every((b) => {
61
+ const hasExpect = EXPECT_RE.test(b.body);
62
+ const onlyCount =
63
+ /expect\s*\([^)]*\)\s*\.toBeGreaterThan\s*\(\s*0\s*\)/.test(b.body) &&
64
+ !DATA_ASSERTION_RE.test(b.body) &&
65
+ !/\bpage\.goto\b/.test(b.body);
66
+ return !hasExpect || onlyCount;
67
+ });
68
+ return allShallow && blocks.length >= 3;
69
+ }
70
+
71
+ export function testBlockIsPlaceholderOnly(block) {
72
+ if (HOOK_TITLE_RE.test(block.title.trim())) return false;
73
+ if (!EXPECT_RE.test(block.body)) return false;
74
+ const hasPlaceholder = PLACEHOLDER_ONLY_RE.test(block.body);
75
+ const hasData = DATA_ASSERTION_RE.test(block.body);
76
+ const hasInteraction = /\b(?:click|dblclick|fill|press|selectOption)\b/.test(block.body);
77
+ return hasPlaceholder && !hasData && !hasInteraction;
78
+ }
79
+
80
+ export function analyzeE2eSpecDepth(relPath, content) {
81
+ const issues = [];
82
+ if (isRegistryStubFile(relPath, content)) {
83
+ return {
84
+ ok: false,
85
+ issues: ["伪覆盖登记文件,不能作为 change 级 E2E 验收"],
86
+ stats: { testCount: 0, testsWithExpect: 0, hasGoto: false, hasDataAssertion: false, hasInteraction: false },
87
+ };
88
+ }
89
+
90
+ const stripped = stripComments(content);
91
+ const blocks = parseTestBlocks(content);
92
+ const hookBlocks = blocks.filter((b) => HOOK_TITLE_RE.test(b.title.trim()));
93
+ const testBlocks = blocks.filter((b) => !HOOK_TITLE_RE.test(b.title.trim()));
94
+ const testsWithExpect = testBlocks.filter((b) => EXPECT_RE.test(b.body)).length;
95
+ const hasGoto = /\bpage\.goto\b/.test(stripped);
96
+ const hasDataInTests = testBlocks.some((b) => DATA_ASSERTION_RE.test(b.body));
97
+ const hasInteractionInTests = testBlocks.some((b) =>
98
+ /\b(?:click|dblclick|fill|press|selectOption)\b/.test(b.body),
99
+ );
100
+ const placeholderOnlyTests = testBlocks.filter((b) => testBlockIsPlaceholderOnly(b));
101
+
102
+ if (testBlocks.length === 0) issues.push("未解析到 test/it 用例(不含 hook)");
103
+ if (testsWithExpect === 0) issues.push("无含 expect( 的 test/it 块");
104
+ if (!hasGoto && !hookBlocks.some((b) => /\bpage\.goto\b/.test(b.body))) {
105
+ issues.push("缺少 page.goto(页面可达性未验证)");
106
+ }
107
+ if (placeholderOnlyTests.length > 0) {
108
+ issues.push(
109
+ `以下用例仅断言 placeholder/label: ${placeholderOnlyTests.map((t) => t.title).join(", ")}`,
110
+ );
111
+ }
112
+ if (!hasDataInTests && !hasInteractionInTests) {
113
+ issues.push("所有 test 块均缺少数据展示断言或交互操作");
114
+ }
115
+
116
+ const ok =
117
+ testBlocks.length > 0 &&
118
+ testsWithExpect > 0 &&
119
+ (hasGoto || hookBlocks.some((b) => /\bpage\.goto\b/.test(b.body))) &&
120
+ placeholderOnlyTests.length === 0 &&
121
+ (hasDataInTests || hasInteractionInTests);
122
+
123
+ return {
124
+ ok,
125
+ issues,
126
+ stats: {
127
+ testCount: testBlocks.length,
128
+ testsWithExpect,
129
+ hasGoto,
130
+ hasDataAssertion: hasDataInTests,
131
+ hasInteraction: hasInteractionInTests,
132
+ },
133
+ };
134
+ }
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 根据 OpenSpec change / 显式模块名解析 Playwright 运行范围。
4
+ *
5
+ * scope 三级(见 e2e/e2e-scope.config.json defaultScope):
6
+ * - change(默认):优先 change 文档中的 e2e/*.spec.ts;未写明则按 changeScopeFallback 回退(默认 module)
7
+ * - module:解析到的 e2e/<module>/ 整个目录
8
+ * - full:输出 FULL,由调用方跑全量 e2e/**
9
+ */
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
+ const CONFIG_PATH = path.join(ROOT, "e2e/e2e-scope.config.json");
16
+
17
+ const VALID_SCOPES = new Set(["change", "module", "full"]);
18
+
19
+ function loadConfig() {
20
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
21
+ }
22
+
23
+ function resolveDefaultScope(config) {
24
+ const fromEnv = process.env.OPSX_PLAYWRIGHT_SCOPE || process.env.POQUAN_PLAYWRIGHT_SCOPE;
25
+ if (fromEnv) return fromEnv;
26
+ return config.defaultScope || "change";
27
+ }
28
+
29
+ function parseArgs(argv) {
30
+ const config = loadConfig();
31
+ const out = {
32
+ change: "",
33
+ scope: resolveDefaultScope(config),
34
+ modules: "",
35
+ };
36
+ for (let i = 2; i < argv.length; i += 1) {
37
+ const a = argv[i];
38
+ if (a === "--change" && argv[i + 1]) {
39
+ out.change = argv[++i];
40
+ } else if (a === "--scope" && argv[i + 1]) {
41
+ out.scope = argv[++i];
42
+ } else if (a === "--modules" && argv[i + 1]) {
43
+ out.modules = argv[++i];
44
+ } else if (!a.startsWith("-") && !out.change) {
45
+ out.change = a;
46
+ }
47
+ }
48
+ const envModules = process.env.OPSX_E2E_MODULES || process.env.POQUAN_E2E_MODULES;
49
+ if (envModules) {
50
+ out.modules = envModules;
51
+ }
52
+ if (!VALID_SCOPES.has(out.scope)) {
53
+ console.error(
54
+ `[resolve-playwright-e2e-scope] 无效 scope="${out.scope}";允许: change | module | full`,
55
+ );
56
+ process.exit(2);
57
+ }
58
+ return out;
59
+ }
60
+
61
+ function findChangeDir(changeName) {
62
+ const active = path.join(ROOT, "openspec/changes", changeName);
63
+ if (fs.existsSync(active)) return active;
64
+ const archiveRoot = path.join(ROOT, "openspec/changes/archive");
65
+ if (!fs.existsSync(archiveRoot)) return null;
66
+ const hit = fs
67
+ .readdirSync(archiveRoot)
68
+ .filter((d) => d.endsWith(changeName) || d.includes(changeName))
69
+ .sort()
70
+ .pop();
71
+ return hit ? path.join(archiveRoot, hit) : null;
72
+ }
73
+
74
+ /** 从 change 文档中提取显式声明的 e2e spec 路径(tasks / proposal / design) */
75
+ function collectSpecPathsFromChangeDir(changeDir) {
76
+ const specs = new Set();
77
+ const specRe = /\be2e\/[\w/.-]+\.spec\.(?:ts|js)\b/g;
78
+ const files = ["tasks.md", "proposal.md", "design.md"];
79
+ for (const f of files) {
80
+ const fp = path.join(changeDir, f);
81
+ if (!fs.existsSync(fp)) continue;
82
+ const content = fs.readFileSync(fp, "utf8");
83
+ let m;
84
+ while ((m = specRe.exec(content)) !== null) {
85
+ const rel = m[0];
86
+ const abs = path.join(ROOT, rel);
87
+ if (fs.existsSync(abs)) specs.add(rel);
88
+ }
89
+ }
90
+ return [...specs].sort();
91
+ }
92
+
93
+ function resolveChangeSpecPaths(changeName, config) {
94
+ const paths = new Set();
95
+ const changeDir = findChangeDir(changeName);
96
+ if (changeDir) {
97
+ collectSpecPathsFromChangeDir(changeDir).forEach((p) => paths.add(p));
98
+ }
99
+ const overrides = config.changeNameToSpecs?.[changeName];
100
+ if (overrides) {
101
+ for (const raw of overrides) {
102
+ const rel = raw.startsWith("e2e/") ? raw : `e2e/${raw}`;
103
+ const abs = path.join(ROOT, rel);
104
+ if (fs.existsSync(abs)) paths.add(rel);
105
+ }
106
+ }
107
+ return [...paths].sort();
108
+ }
109
+
110
+ function collectCapabilities(changeDir) {
111
+ const caps = [];
112
+ const specsDir = path.join(changeDir, "specs");
113
+ if (!fs.existsSync(specsDir)) return caps;
114
+ for (const ent of fs.readdirSync(specsDir, { withFileTypes: true })) {
115
+ if (ent.isDirectory()) caps.push(ent.name);
116
+ }
117
+ return caps;
118
+ }
119
+
120
+ function scanFeatureDirs(text) {
121
+ const found = new Set();
122
+ const re = /src\/features\/([a-z0-9-]+)/gi;
123
+ let m;
124
+ while ((m = re.exec(text)) !== null) {
125
+ found.add(m[1]);
126
+ }
127
+ return [...found];
128
+ }
129
+
130
+ function matchChangeNameKeywords(changeName, config) {
131
+ const mods = new Set();
132
+ for (const { pattern, modules } of config.changeNameKeywords || []) {
133
+ if (new RegExp(pattern, "i").test(changeName)) {
134
+ modules.forEach((mod) => mods.add(mod));
135
+ }
136
+ }
137
+ return [...mods];
138
+ }
139
+
140
+ function modulesToPaths(moduleIds, config) {
141
+ const paths = new Set();
142
+ for (const id of moduleIds) {
143
+ const dirs = config.modules[id];
144
+ if (!dirs) continue;
145
+ dirs.forEach((p) => {
146
+ const abs = path.join(ROOT, p);
147
+ if (fs.existsSync(abs)) paths.add(p);
148
+ });
149
+ }
150
+ return [...paths].sort();
151
+ }
152
+
153
+ function resolveModules({ change, modules }, config) {
154
+ if (modules) {
155
+ return modules
156
+ .split(",")
157
+ .map((s) => s.trim())
158
+ .filter(Boolean);
159
+ }
160
+ const moduleSet = new Set();
161
+ if (change) {
162
+ const changeDir = findChangeDir(change);
163
+ if (changeDir) {
164
+ for (const cap of collectCapabilities(changeDir)) {
165
+ (config.capabilityToModules[cap] || []).forEach((m) => moduleSet.add(m));
166
+ }
167
+ const textFiles = ["proposal.md", "design.md", "tasks.md"];
168
+ let blob = "";
169
+ for (const f of textFiles) {
170
+ const fp = path.join(changeDir, f);
171
+ if (fs.existsSync(fp)) blob += `${fs.readFileSync(fp, "utf8")}\n`;
172
+ }
173
+ for (const feat of scanFeatureDirs(blob)) {
174
+ (config.featureDirToModules[feat] || []).forEach((m) => moduleSet.add(m));
175
+ }
176
+ }
177
+ matchChangeNameKeywords(change, config).forEach((m) => moduleSet.add(m));
178
+ }
179
+ return [...moduleSet];
180
+ }
181
+
182
+ /** 按 change / OPSX_E2E_MODULES / POQUAN_E2E_MODULES 解析模块 id 与 e2e 目录路径 */
183
+ function resolveModuleScope(args, config) {
184
+ const moduleIds = resolveModules(args, config);
185
+ const paths = modulesToPaths(moduleIds, config);
186
+ return { moduleIds, paths };
187
+ }
188
+
189
+ function emitModulePaths(args, config, logPrefix) {
190
+ const { moduleIds, paths } = resolveModuleScope(args, config);
191
+ if (paths.length === 0) {
192
+ console.error(
193
+ "[resolve-playwright-e2e-scope] 未能解析模块;请设置 OPSX_E2E_MODULES=auth,review / POQUAN_E2E_MODULES=auth,review 或 OPSX_PLAYWRIGHT_SCOPE=full",
194
+ );
195
+ process.exit(2);
196
+ }
197
+ process.stderr.write(
198
+ `${logPrefix} scope=module change=${args.change || "-"} modules=${moduleIds.join(",")} paths=${paths.join(" ")}\n`,
199
+ );
200
+ paths.forEach((p) => process.stdout.write(`${p}\n`));
201
+ }
202
+
203
+ function main() {
204
+ const config = loadConfig();
205
+ const args = parseArgs(process.argv);
206
+
207
+ if (args.scope === "full") {
208
+ process.stdout.write("FULL\n");
209
+ return;
210
+ }
211
+
212
+ if (args.scope === "change") {
213
+ if (!args.change) {
214
+ console.error(
215
+ "[resolve-playwright-e2e-scope] scope=change 需要 change 名;示例: node scripts/resolve-playwright-e2e-scope.mjs <change-name>",
216
+ );
217
+ process.exit(2);
218
+ }
219
+ const specPaths = resolveChangeSpecPaths(args.change, config);
220
+ if (specPaths.length > 0) {
221
+ process.stderr.write(
222
+ `[resolve-playwright-e2e-scope] scope=change(文档 spec) change=${args.change} specs=${specPaths.join(" ")}\n`,
223
+ );
224
+ specPaths.forEach((p) => process.stdout.write(`${p}\n`));
225
+ return;
226
+ }
227
+
228
+ const fallback = config.changeScopeFallback ?? "module";
229
+ if (fallback === "module") {
230
+ process.stderr.write(
231
+ `[resolve-playwright-e2e-scope] scope=change 未在 tasks/proposal/design 中找到 spec,` +
232
+ `已按 changeScopeFallback=module 回退模块目录(change=${args.change})\n`,
233
+ );
234
+ emitModulePaths(args, config, "[resolve-playwright-e2e-scope]");
235
+ return;
236
+ }
237
+
238
+ console.error(
239
+ `[resolve-playwright-e2e-scope] scope=change 未找到 e2e/*.spec.ts,且 changeScopeFallback=${fallback};` +
240
+ `可配置 changeNameToSpecs、将 changeScopeFallback 设为 module,或 OPSX_PLAYWRIGHT_SCOPE=module|full`,
241
+ );
242
+ process.exit(2);
243
+ }
244
+
245
+ emitModulePaths(args, config, "[resolve-playwright-e2e-scope]");
246
+ }
247
+
248
+ main();
@@ -7,9 +7,12 @@
7
7
  ```bash
8
8
  cd {{DOC_SKILL_REL}}
9
9
  export DOC_PRODUCT_PREFIX="{{PRODUCT_PREFIX}}"
10
+ export SDD_SYNC_SOURCE_DIR="{{SYNC_SOURCE_DIR_REL}}"
10
11
  python3 confluence-doc.py {{VERSION_EXAMPLE}}
11
12
  ```
12
13
 
14
+ 脚本成功后会自动将 `docs/<版本目录>/*.md` 与 `image/` 同步到本 run 的 `source/PRD.md` 与 `source/image/`(也可手动执行 `npx sdd-flow-kit sync-source-prd --run-id <runId>`)。
15
+
13
16
  版本号可传:`2.3.2`、`V2.3.2`、`ADI_V2.3.2`、`ADI-V2.3.2`(脚本会归一化)。
14
17
 
15
18
  2. 脚本会依次用关键词打开 **Confluence 专用搜索页** `dosearchsite.action?queryString=<关键词>`(非首页顶栏搜索),并依次尝试:`ADI_V*` → `ADI-V*` → `ADI_*` → `ADI-*` → `ADI_v*` → `ADI-v*` → `ADI V*` → `ADI_V*结算` → `V*` 等。
@@ -24,8 +27,9 @@ python3 confluence-doc.py {{VERSION_EXAMPLE}}
24
27
 
25
28
  5. **禁止**在 Confluence 脚本未跑完或未确认失败前,用本地 Word/原型替代 `source/PRD.md`。
26
29
 
27
- 6. 成功后把生成的 `docs/*.md` **主文档**复制/回填到本 run 目录:
28
- - `source/PRD.md`
30
+ 6. 同步结果(自动,无需手抄):
31
+ - `source/PRD.md`(含 `> 原型图片目录: image/` 元数据行)
32
+ - `source/image/`(正文图片,与 docs 版本目录一致)
29
33
 
30
34
  ## 直链兜底(可选,非默认)
31
35
 
@@ -23,6 +23,11 @@
23
23
  - `01-需求分析报告.md`
24
24
  - `02-改动点清单.md`
25
25
  - `03-待确认问题清单.md`
26
+ - 阶段 B 交付模式另产出:`05-验收清单.md`、**填写完成的** `04-技术文档草稿.md`(须符合 `04-技术文档生成指引.md`,禁止摘要版)
27
+
28
+ ## 04 技术文档(阶段 B 必读)
29
+ - 同 run 目录:`04-技术文档生成指引.md`
30
+ - 脚手架已生成完整模板 `04-技术文档草稿.md` → **在原文件上填内容,勿删章节**
26
31
 
27
32
  ## 输出话术(建议固定)
28
33
  - 提问模式:`SDD 模式:提问模式;矛盾点池状态:未闭合;仅更新 03,等待人工确认。`
@@ -92,6 +92,20 @@
92
92
  - 错误态提示口径(toast/inline):
93
93
  - 权限不足提示:
94
94
 
95
+ ### 2.4 演示代码参照(UI 类必填)
96
+
97
+ > 演示可为 **Vue / React / 其他**;生产实现须达到 **相同交互与功能**。Propose 时整段迁入 `design.md` 的 `## 演示代码参照`。
98
+
99
+ - 演示代码根路径:
100
+ - 技术栈:vue | react | 其他
101
+ - 生产 UI 库:
102
+
103
+ | 演示模块 | 演示文件/组件 | 计划生产组件路径 | 关键交互信号(验收时须在代码中出现) |
104
+ |---------|--------------|-----------------|-------------------------------------|
105
+ | | | | |
106
+
107
+ 纯后端/无 UI 演示:在 design 写 `DEMO_REFERENCE_EXEMPT: <原因>`
108
+
95
109
  ## 3. 前端状态与数据模型设计
96
110
 
97
111
  ### 3.1 领域模型(TypeScript)
@@ -11,10 +11,10 @@
11
11
  ## 填写规则
12
12
 
13
13
  1. **ID**:`AC-01`、`AC-02`… 全局唯一。
14
- 2. **验收标准**:必须可观测(DOM 顺序、完整 toast/tooltip 文案、E2E 路径、截图对比等)。
14
+ 2. **验收标准**:必须可观测(DOM 顺序、完整 toast/tooltip 文案、E2E 路径、截图对比等);**详情/弹窗类须写明演示模块名**(如「对齐演示 ProjectDetail §红票及其他」)。
15
15
  3. **测试类型**:`unit` | `component` | `e2e` | `visual` | `manual`(可组合,如 `unit+e2e`)。
16
16
  4. **优先级**:`P0`(交付阻断)/ `P1` / `P2`。
17
- 5. **映射**:propose 后在 `openspec/changes/<change>/tasks.md` 或 `spec.md` 中引用 `[AC-XX]`。
17
+ 5. **映射**:propose 后在 `openspec/changes/<change>/tasks.md` 或 `spec.md` 中引用 `[AC-XX]`;**tasks 须有「演示模块迁移」段,与 design 映射表一行一任务**。
18
18
  6. **Intentional 偏差**:与 PRD 有意不一致时,必须同步记入 `03-待确认问题清单.md` 并在下文登记。
19
19
 
20
20
  ## 验收项
@@ -8,7 +8,7 @@
8
8
  - [ ] `03-待确认问题清单.md` 已标注 **矛盾点池状态:已闭合**
9
9
  - [ ] `05-验收清单.md` 已标注 **定义状态:已定义**,且 P0 项齐全
10
10
  - [ ] `01-需求分析报告.md`、`02-改动点清单.md`、`04-技术文档草稿.md` 已生成(04 须引用 AC-ID)
11
- - [ ] `gate ac-ready` 通过(P0 已映射到 spec/tasks
11
+ - [ ] `gate ac-ready` 通过(P0 已映射到 spec/tasks,且 **design 含演示模块映射表**)
12
12
  - [ ] 用户已确认可进入提案阶段(阶段 B 闭环)
13
13
  - [ ] `autoChainOpsx` 为 true(默认)→ 与用户确认后**同一回合**自动 propose,无需再等「继续」
14
14
 
@@ -18,7 +18,7 @@
18
18
  - **验收 SSOT**:`05-验收清单.md`(**必须**)
19
19
  - 改动清单:`02-改动点清单.md`
20
20
  - 技术方案:`04-技术文档草稿.md`(引用 AC-ID)
21
- - UI/交互参考:(填写用户提供的路径,例如原型页面或设计稿目录)
21
+ - UI/交互参考:(**必填**)演示代码根路径 + 技术栈(vue/react);propose 时写入 `design.md` → `## 演示代码参照` 映射表(见 `design-demo-reference.template.md`)
22
22
 
23
23
  ## 执行命令(在 AI 工具中)
24
24
 
@@ -38,8 +38,8 @@
38
38
  ## UI 路径解析(按优先级)
39
39
 
40
40
  1. 用户本条消息给出的路径
41
- 2. `source/PRD.md` 中的「原型图片目录」行(如 `docs/adi-v232/`)
42
- 3. `docs/<product>-v<version无点>/` 或 `docs/<product>-<version>/` 目录(只读扫描)
41
+ 2. `source/PRD.md` 中的「原型图片目录」行(如 `source/image/` 或 `docs/adi-v232/image/`)
42
+ 3. `source/image/` 或 `docs/<product>-v<version无点>/image/`(只读扫描)
43
43
 
44
44
  缺失 UI 时:**仍继续** propose,在 `proposal.md` / `design.md` 注明「UI 待补」。
45
45
 
@@ -66,6 +66,8 @@
66
66
  - 「先沿用旧版,后续再改」— 兜底措辞
67
67
  - 「组件里应该有处理」— 无路径/行号/断言
68
68
  - 「基本满足」— 结论不在三选一内
69
+ - `src/views/foo/index.vue:1` — **仅文件首行,无具体组件/交互证据**
70
+ - `tests/.../prd-atoms-coverage.test.ts` — **注释登记 PRD-ID,非真实实现**
69
71
 
70
72
  ## 逐条对比(PRD 原子 × 代码)
71
73
 
@@ -0,0 +1,24 @@
1
+ ## 演示代码参照
2
+
3
+ > **UI 类 change 必填**。演示可为 Vue/React;生产实现须达到相同交互与功能,允许换 UI 库。
4
+ > 纯后端/无 UI 时在 change 文档首行写:`DEMO_REFERENCE_EXEMPT: <原因>`
5
+
6
+ - **演示代码根路径**:`(填写绝对或相对路径,如 /path/to/demo 或 docs/demo-v234)`
7
+ - **技术栈**:`vue` | `react` | `其他`
8
+ - **生产 UI 库**:`(如 Naive UI / Ant Design)`
9
+ - **迁移说明**:(可选)演示 Element Plus → 生产 Naive UI,布局与交互 SSOT 仍以演示为准
10
+
11
+ ### 演示模块 → 生产组件映射(门禁 SSOT)
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 | 红票及其他、添加记录、红票 |
18
+ | (填写) | | | |
19
+
20
+ ### 禁止的实现方式
21
+
22
+ - 禁止仅用单个 `*DetailDrawer.vue` 只读 `info-grid` 代替演示中的多区块/可编辑/弹窗。
23
+ - 禁止 tasks 只写「实现 DetailDrawer」而不拆演示模块。
24
+ - 禁止 08 报告证据仅写 `index.vue:1`。
@@ -46,7 +46,7 @@
46
46
  |------|------|
47
47
  | `01` | 业务概述、范围、风险(**不承担**完整验收) |
48
48
  | `05` | **唯一验收 SSOT**(从 `source/PRD.md` 逐条抽 AC) |
49
- | `04` | 技术方案(引用 AC-ID) |
49
+ | `04` | **可开发级**前端技术方案(**保留模板全部 11 章**,引用 AC-ID) |
50
50
  | `02` | 代码改动计划表 |
51
51
 
52
52
  #### 3. 文档模板要求
@@ -73,6 +73,19 @@
73
73
  * **格式**:表格形式(仅在“提问模式”下生成或更新)。
74
74
  * **字段**:问题类型 (断点/起点缺失/推测缺口) | 问题描述 | 代码位置 (如有) | 建议/待确认项。
75
75
 
76
+ **📄 04-技术文档草稿.md(与 `04-技术文档.template.md` 同结构,强制)**
77
+
78
+ > **必读**:同 run 目录下 `04-技术文档生成指引.md`。
79
+ > **禁止**用 bullet 摘要替换整份文件;**禁止**删减 `##` / `###` 章节标题。
80
+
81
+ * **填写方式**:Read 现有 `04-技术文档草稿.md`(脚手架已写入完整模板)→ **在原文件内逐节替换占位**,不得新建简化版。
82
+ * **输入**:`source/PRD.md`、`source/接口文档.md`、用户演示代码路径(Vue/React)、`05-验收清单.md`、仓库代码扫描结果。
83
+ * **必备章节**(标题须保留):文档元信息 → §1 改造范围(含 1.1–1.4)→ §2 页面与交互(含 **2.4 演示代码参照与映射表**)→ §3 状态模型 → §4 接口(含 **4.1 接口清单表**、4.3 字段映射/normalize)→ §5 表单校验 → §6 边界 → §7 **文件级**改造清单 → §8 风险表 → §9 发布回滚 → §10 测试验收 → §11 待确认。
84
+ * **篇幅**:填完后通常 **≥150 行**;§4.1 至少 5 条接口;UI 类 §2.4 至少 3 行演示映射。
85
+ * **AC 标注**:`[AC-XX]` 分散在各章节,勿只在文首堆叠。
86
+ * **详情/抽屉类**:须按演示代码拆子模块写入 §2.2 / §2.4 / §7(如 BD预估编辑、红票及其他、可编辑金额),**禁止**仅写单个 `*DetailDrawer`。
87
+ * **gate**:`docs-closed` 会校验 `tech-doc-structure`,不合格不得 propose。
88
+
76
89
  ---
77
90
 
78
91
  ### 🚦 核心指令 (Execution Logic)
@@ -100,19 +113,20 @@
100
113
 
101
114
  **任务(阶段 B 文档产出;`full-chain` 模式下不得在本路径结束回合)**:
102
115
  1. 更新 `03-待确认问题清单.md`,标注“**矛盾点池状态:已闭合**”,写入用户确认结论。
103
- 2. **从 `source/PRD.md` 生成 `05-验收清单.md`**(标注「定义状态:已定义」),再生成 `04-技术文档草稿.md`(引用 AC-ID)。
104
- 3. 生成最终版 `01-需求分析报告.md` `02-改动点清单.md`(清单中的路径是**计划**,不得在本回合修改这些文件)。
105
- 4. 确认 `02-改动点清单.md` 中仅包含代码文件改动,不涉及 `openspec/spec/` 内容。
106
- 5. **禁止** 在阶段 B 内修改 `src/` 等业务代码。
107
- 6. 更新 `.session-state.json`:`status: ready_for_propose`;若用户口令含执行模式,同步 `executionMode` / `strictCommands`。
108
- 7. 执行 `gate docs-closed` `phase advance --to docs-done`(exit 0 才继续)。
109
- 8. **若 `autoChainOpsx !== false` `executionMode !== staged-confirm`(默认 full-chain)**:**不得结束回合**,同一回合继续读 `07-opsx-自动串联指引.md`,执行 C→D→E 直至 `deliver` exit 0;**不要**要求用户再说「继续」。
110
- 9. 仅当 `autoChainOpsx === false` 时输出:`05/01/02/03/04 已更新。回复「继续」进入提案。`
111
- 10. **`executionMode=staged-confirm` 时**(含兼容口令「严格SOP,不可替代」):
116
+ 2. **从 `source/PRD.md` 生成 `05-验收清单.md`**(标注「定义状态:已定义」)。
117
+ 3. **填写 `04-技术文档草稿.md`**:先 Read 已有模板文件 + `04-技术文档生成指引.md`,**保留全部章节标题**,逐节填入 PRD/接口/演示/代码扫描内容;**禁止**重写为摘要版。
118
+ 4. 生成最终版 `01-需求分析报告.md` `02-改动点清单.md`(清单中的路径是**计划**,不得在本回合修改这些文件)。
119
+ 5. 确认 `02-改动点清单.md` 中仅包含代码文件改动,不涉及 `openspec/spec/` 内容。
120
+ 6. **禁止** 在阶段 B 内修改 `src/` 等业务代码。
121
+ 7. 更新 `.session-state.json`:`status: ready_for_propose`;若用户口令含执行模式,同步 `executionMode` / `strictCommands`。
122
+ 8. 执行 `gate docs-closed` `phase advance --to docs-done`(exit 0 才继续;失败则继续补全 04 直至 `tech-doc-structure` 通过)。
123
+ 9. **若 `autoChainOpsx !== false` `executionMode !== staged-confirm`(默认 full-chain)**:**不得结束回合**,同一回合继续读 `07-opsx-自动串联指引.md`,执行 C→D→E 直至 `deliver` exit 0;**不要**要求用户再说「继续」。
124
+ 10. 仅当 `autoChainOpsx === false` 时输出:`05/01/02/03/04 已更新。回复「继续」进入提案。`
125
+ 11. **`executionMode=staged-confirm` 时**(含兼容口令「严格SOP,不可替代」):
112
126
  - 每阶段执行前先输出:`本次计划命令清单(逐条)`
113
127
  - 每阶段执行后输出:`门禁结果 + 证据文件路径 + 下一步命令`
114
128
  - 等待用户确认后再进入下一阶段
115
- 11. **`strictCommands=true` 时**:禁止用等价命令替代 NEXT.md 中的 gate/phase 链路(可与 full-chain 组合,不强制每阶段暂停)
129
+ 12. **`strictCommands=true` 时**:禁止用等价命令替代 NEXT.md 中的 gate/phase 链路(可与 full-chain 组合,不强制每阶段暂停)
116
130
 
117
131
  ---
118
132
 
@@ -0,0 +1,71 @@
1
+ # 04-技术文档填写规范(阶段 B 强制)
2
+
3
+ > **SSOT 模板**:同目录 run 下已由脚手架生成的 `04-技术文档草稿.md`(来自 `04-技术文档.template.md`)。
4
+ > **禁止**用几行 bullet + `[AC-xx]` 替换整份文件;**必须保留模板全部章节标题**,仅在各节内填入真实内容。
5
+
6
+ ---
7
+
8
+ ## 填写方式(必须遵守)
9
+
10
+ 1. **先 Read** 当前 `04-技术文档草稿.md`(已是完整模板,约 11 章)。
11
+ 2. **在原文件上逐节替换**占位内容(`(填写)`、空表格行、示例行),**不得删除**任何 `##` / `###` 标题。
12
+ 3. 输入来源(按优先级):
13
+ - `source/PRD.md`(业务规则、交互、文案)
14
+ - `source/接口文档.md`(§4 接口清单、字段映射)
15
+ - 用户提供的**演示代码**路径(Vue/React 均可 → §2.4 映射表)
16
+ - `05-验收清单.md`(在各章标注 `[AC-XX]`,勿只在文首堆叠)
17
+ - 仓库现有代码(§1 现状、§7 改造清单)
18
+ 4. 填完后自检:行数建议 **≥150**;§4.1 至少 **5 行**接口表;§2.4 至少 **3 行**演示映射(UI 类需求)。
19
+ 5. 运行 `npx sdd-flow-kit gate --expect docs-closed`;若 `tech-doc-structure` 失败,继续补全缺失章节。
20
+
21
+ ---
22
+
23
+ ## 各章最低填写要求
24
+
25
+ ### 文档元信息
26
+ - 需求名称、版本、业务域、分支、关联 PRD/接口/演示路径/自测
27
+
28
+ ### §1 改造范围
29
+ - **1.1** 量化成功标准;**1.2** 列出全部页面/路由/弹窗;**1.3** Out of Scope(含 mock、后续迭代)
30
+ - 每条 In Scope 能力标注 `[AC-XX]`
31
+
32
+ ### §2 页面与交互设计
33
+ - **2.1** 权限/场景矩阵(有权限/无权限/只读等)
34
+ - **2.2** 至少 2 条用户路径(列表、详情、导入、编辑…)写清 Step 与成功/失败 toast **完整文案**
35
+ - **2.3** loading、空态、防重复提交
36
+ - **2.4 演示代码参照(UI 必填)**:根路径、技术栈、**映射表**(演示模块 → 计划生产组件 → 关键交互信号)
37
+
38
+ ### §3 状态与数据模型
39
+ - 核心 TypeScript 类型(列表行、详情 VO、表单草稿)
40
+ - remote / ui / 派生状态划分;详情打开/关闭清理
41
+
42
+ ### §4 接口与数据交互
43
+ - **4.1 表格**:接口路径 | Method | 用途 | 请求字段 | 响应字段 | 异常处理
44
+ - **4.3** 写明后端 SNAKE_CASE → 前端 camelCase normalize 位置(如 `config/api/modules/*/caseTransform`)
45
+ - 提交 payload 示例(JSON)
46
+
47
+ ### §5 表单与校验
48
+ - 关键弹窗/编辑字段校验矩阵(必填、格式、错误文案)
49
+
50
+ ### §6–§11
51
+ - 边界场景、文件级改造清单(**禁止**只写一个大组件名)、风险表、发布检查、测试要点与 05 AC 对应、待确认项(无则写「无」)
52
+
53
+ ---
54
+
55
+ ## 禁止的反模式(gate 会失败)
56
+
57
+ | 反模式 | 示例 |
58
+ |--------|------|
59
+ | 摘要替换 | 仅 7 条 `## N. xxx [AC-xx]` 列表 |
60
+ | 删章节 | 缺少 §3/§5/§6/§8/§9/§11 |
61
+ | 无接口表 | §4 只有一句话「封装 api/modules」 |
62
+ | 无演示映射 | UI 需求但 §2.4 为空 |
63
+ | 详情一笔带过 | 「FinanceDetailDrawer」不提子模块(BD预估、红票、可编辑金额) |
64
+
65
+ ---
66
+
67
+ ## 与下游衔接
68
+
69
+ - Propose 时:`design.md` **复制** §2.4 映射表 + §4 接口决策
70
+ - `tasks.md`:**一行映射表 → 至少一条**「演示模块迁移」任务
71
+ - 实现:**禁止**用单文件只读骨架代替映射表中的多个生产组件