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.
- package/dist/cli.js +16 -0
- package/dist/core/apiFieldContract.js +90 -0
- package/dist/core/demoImplementationCoverage.js +242 -0
- package/dist/core/e2eAssertionDepth.js +233 -0
- package/dist/core/prdArtifacts.js +12 -1
- package/dist/core/prdCoverage.js +46 -9
- package/dist/core/prdSemanticDiff.js +7 -4
- package/dist/core/syncSourcePrd.js +181 -0
- package/dist/core/techDocQuality.js +87 -0
- package/dist/core/testAssertionAnalysis.js +213 -0
- package/dist/core/writeNext.js +1 -1
- package/dist/index.js +6 -1
- package/dist/steps/runGate.js +49 -5
- package/dist/steps/runSyncSourcePrd.js +38 -0
- package/dist/steps/runValidate.js +4 -2
- package/dist/steps/setupProject.js +3 -0
- package/dist/steps/startFlow.js +7 -2
- package/package.json +2 -1
- package/scripts/assert-active-openspec-change.sh +114 -0
- package/scripts/assess-api-field-contract.mjs +119 -0
- package/scripts/assess-demo-implementation-coverage.mjs +188 -0
- package/scripts/assess-e2e-assertion-depth.mjs +146 -0
- package/scripts/assess-playwright-e2e-coverage.mjs +244 -0
- package/scripts/assess-tech-doc-quality.mjs +91 -0
- package/scripts/install-git-hooks.sh +27 -0
- package/scripts/lib/test-assertion-analysis.mjs +134 -0
- package/scripts/resolve-playwright-e2e-scope.mjs +248 -0
- package/src/templates/artifacts/01-adi-doc-skill-/346/214/207/345/274/225.template.md +6 -2
- 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
- package/src/templates/artifacts/04-/346/212/200/346/234/257/346/226/207/346/241/243.template.md +14 -0
- package/src/templates/artifacts/05-/351/252/214/346/224/266/346/270/205/345/215/225.template.md +2 -2
- package/src/templates/artifacts/06-openspec-/346/217/220/346/241/210/350/215/211/347/250/277.template.md +2 -2
- package/src/templates/artifacts/07-opsx-auto-chain.template.md +2 -2
- 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
- package/src/templates/artifacts/design-demo-reference.template.md +24 -0
- package/src/templates/prompts/02-sdd-loop-prompt.md +25 -11
- package/src/templates/prompts/04-tech-doc-fill-prompt.md +71 -0
- package/src/templates/skills/confluence-doc.py.template +64 -25
- package/src/templates/skills/demo-reference-mandatory-workflow.md +77 -0
- package/src/templates/skills/doc-skill.SKILL.md.template +13 -6
- package/src/templates/skills/layered-testing-strategy.md +13 -6
package/dist/core/prdCoverage.js
CHANGED
|
@@ -15,6 +15,10 @@ const acMatrix_1 = require("./acMatrix");
|
|
|
15
15
|
const fs_1 = require("./fs");
|
|
16
16
|
const prdParse_1 = require("./prdParse");
|
|
17
17
|
const prdSemanticDiff_1 = require("./prdSemanticDiff");
|
|
18
|
+
const testAssertionAnalysis_1 = require("./testAssertionAnalysis");
|
|
19
|
+
const e2eAssertionDepth_1 = require("./e2eAssertionDepth");
|
|
20
|
+
const apiFieldContract_1 = require("./apiFieldContract");
|
|
21
|
+
const demoImplementationCoverage_1 = require("./demoImplementationCoverage");
|
|
18
22
|
const VALID_VERDICTS = ["一致", "不一致-按PRD修复", "待人工确认"];
|
|
19
23
|
/** 禁止在证据/结论中出现的揣摩或兜底措辞 */
|
|
20
24
|
const FORBIDDEN_SPECULATION = [
|
|
@@ -99,6 +103,22 @@ function hasForbiddenSpeculation(text) {
|
|
|
99
103
|
}
|
|
100
104
|
return null;
|
|
101
105
|
}
|
|
106
|
+
/** 08 报告中的无效/敷衍证据(如 index.vue:1、prd-atoms 注释覆盖) */
|
|
107
|
+
function isWeakCodeEvidence(evidence) {
|
|
108
|
+
const e = evidence.trim();
|
|
109
|
+
if (!e)
|
|
110
|
+
return "空证据";
|
|
111
|
+
if (/:(1|2)\s*(\||$)/.test(e) && !/expect\(|getByText|getByRole|message\.|toast/.test(e)) {
|
|
112
|
+
return "证据仅指向文件首行,无具体交互/断言";
|
|
113
|
+
}
|
|
114
|
+
if (/prd-atoms-coverage|semantic-registry/i.test(e)) {
|
|
115
|
+
return "伪覆盖登记文件不能作为实现证据";
|
|
116
|
+
}
|
|
117
|
+
if (/index\.vue:1|index\.tsx:1/i.test(e) && e.split(/[;;]/).length < 2) {
|
|
118
|
+
return "禁止用列表页首行代替详情子模块证据";
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
102
122
|
async function writePrdAtomsManifest(outputRoot, atoms) {
|
|
103
123
|
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, ".prd-review-atoms.json"), `${JSON.stringify({ count: atoms.length, atoms }, null, 2)}\n`);
|
|
104
124
|
}
|
|
@@ -118,18 +138,16 @@ async function loadPrdAtomsFromManifest(outputRoot) {
|
|
|
118
138
|
function checkAcTestMapping(items, testContents) {
|
|
119
139
|
var _a;
|
|
120
140
|
const checks = [];
|
|
121
|
-
// P0 全量 + 所有声明 e2e 的 AC(含 P1/P2 边缘场景)
|
|
122
141
|
const mustCover = items.filter((i) => { var _a; return /^P0$/i.test(i.priority.trim()) || /e2e/i.test((_a = i.testType) !== null && _a !== void 0 ? _a : ""); });
|
|
123
142
|
for (const ac of mustCover) {
|
|
124
|
-
const bracket = `[${ac.id}]`;
|
|
125
|
-
const hits = testContents.filter((t) => t.content.includes(bracket) || t.content.includes(ac.id));
|
|
126
143
|
const e2eRequired = /e2e/i.test((_a = ac.testType) !== null && _a !== void 0 ? _a : "");
|
|
127
|
-
const
|
|
144
|
+
const hits = testContents.filter((t) => (0, testAssertionAnalysis_1.hasMeaningfulAcCoverageInFile)(t.rel, t.content, ac.id).covered);
|
|
145
|
+
const e2eHits = hits.filter((h) => E2E_DIR_RE.test(h.rel));
|
|
128
146
|
if (hits.length === 0) {
|
|
129
147
|
checks.push({
|
|
130
148
|
name: `ac-test-${ac.id}`,
|
|
131
149
|
ok: false,
|
|
132
|
-
detail: `${ac.id}(${ac.priority})
|
|
150
|
+
detail: `${ac.id}(${ac.priority}) 未在有效 test/it 块中找到 [${ac.id}] + expect 映射(注释登记不计入)`,
|
|
133
151
|
});
|
|
134
152
|
continue;
|
|
135
153
|
}
|
|
@@ -137,7 +155,7 @@ function checkAcTestMapping(items, testContents) {
|
|
|
137
155
|
checks.push({
|
|
138
156
|
name: `ac-e2e-${ac.id}`,
|
|
139
157
|
ok: false,
|
|
140
|
-
detail: `${ac.id} 测试类型含 e2e,但 e2e/ 或 playwright/
|
|
158
|
+
detail: `${ac.id} 测试类型含 e2e,但 e2e/ 或 playwright/ 下无含 expect 的对应测试`,
|
|
141
159
|
});
|
|
142
160
|
continue;
|
|
143
161
|
}
|
|
@@ -145,8 +163,8 @@ function checkAcTestMapping(items, testContents) {
|
|
|
145
163
|
name: `ac-test-${ac.id}`,
|
|
146
164
|
ok: true,
|
|
147
165
|
detail: e2eRequired
|
|
148
|
-
? `${ac.id} 已映射(含 e2e: ${e2eHits.map((h) => path_1.default.basename(h.
|
|
149
|
-
: `${ac.id} 已映射: ${hits.map((h) => path_1.default.basename(h.
|
|
166
|
+
? `${ac.id} 已映射(含 e2e: ${e2eHits.map((h) => path_1.default.basename(h.rel)).join(", ")})`
|
|
167
|
+
: `${ac.id} 已映射: ${hits.map((h) => path_1.default.basename(h.rel)).join(", ")}`,
|
|
150
168
|
});
|
|
151
169
|
}
|
|
152
170
|
return checks;
|
|
@@ -246,7 +264,7 @@ function checkSemanticDiffItems(semantic) {
|
|
|
246
264
|
/**
|
|
247
265
|
* gate prd-coverage:PRD 最细粒度条目全覆盖 + 08 复查报告合规 + P0 AC 测试映射。
|
|
248
266
|
*/
|
|
249
|
-
async function checkPrdCoverage(outputRoot, pkgRoot) {
|
|
267
|
+
async function checkPrdCoverage(outputRoot, pkgRoot, options) {
|
|
250
268
|
const checks = [];
|
|
251
269
|
const prdContent = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, "source", "PRD.md"));
|
|
252
270
|
if (!prdContent) {
|
|
@@ -314,6 +332,7 @@ async function checkPrdCoverage(outputRoot, pkgRoot) {
|
|
|
314
332
|
const needHuman = [];
|
|
315
333
|
const speculationHits = [];
|
|
316
334
|
const emptyEvidence = [];
|
|
335
|
+
const weakEvidence = [];
|
|
317
336
|
for (const atom of atoms) {
|
|
318
337
|
const row = rowById.get(atom.id);
|
|
319
338
|
if (!row)
|
|
@@ -329,6 +348,11 @@ async function checkPrdCoverage(outputRoot, pkgRoot) {
|
|
|
329
348
|
if (!evidence || evidence.length < 8 || /(填写)|待填写|TBD/i.test(evidence)) {
|
|
330
349
|
emptyEvidence.push(atom.id);
|
|
331
350
|
}
|
|
351
|
+
if (row.verdict === "一致") {
|
|
352
|
+
const weak = isWeakCodeEvidence(evidence);
|
|
353
|
+
if (weak)
|
|
354
|
+
weakEvidence.push(`${atom.id}(${weak})`);
|
|
355
|
+
}
|
|
332
356
|
const combined = `${row.codeEvidence} ${row.note} ${row.verdict}`;
|
|
333
357
|
const forbidden = hasForbiddenSpeculation(combined);
|
|
334
358
|
if (forbidden)
|
|
@@ -355,6 +379,13 @@ async function checkPrdCoverage(outputRoot, pkgRoot) {
|
|
|
355
379
|
? "每条均有可核对代码证据(文件路径+行号/选择器/断言)"
|
|
356
380
|
: `缺少代码证据: ${emptyEvidence.slice(0, 8).join(", ")}`,
|
|
357
381
|
});
|
|
382
|
+
checks.push({
|
|
383
|
+
name: "code-evidence-quality",
|
|
384
|
+
ok: weakEvidence.length === 0,
|
|
385
|
+
detail: weakEvidence.length === 0
|
|
386
|
+
? "未发现敷衍证据(index.vue:1 / prd-atoms 登记等)"
|
|
387
|
+
: `证据质量不足: ${weakEvidence.slice(0, 8).join(", ")}`,
|
|
388
|
+
});
|
|
358
389
|
checks.push({
|
|
359
390
|
name: "no-prd-mismatch-open",
|
|
360
391
|
ok: needFix.length === 0,
|
|
@@ -385,11 +416,17 @@ async function checkPrdCoverage(outputRoot, pkgRoot) {
|
|
|
385
416
|
var _a;
|
|
386
417
|
return ({
|
|
387
418
|
file: f,
|
|
419
|
+
rel: path_1.default.relative(pkgRoot, f).replace(/\\/g, "/"),
|
|
388
420
|
content: (_a = (await (0, fs_1.readFileIfExists)(f))) !== null && _a !== void 0 ? _a : "",
|
|
389
421
|
});
|
|
390
422
|
}));
|
|
391
423
|
checks.push(...checkAcTestMapping(acParsed.items, testContents));
|
|
392
424
|
}
|
|
425
|
+
if (options === null || options === void 0 ? void 0 : options.changeName) {
|
|
426
|
+
checks.push(...(await (0, e2eAssertionDepth_1.checkE2eAssertionDepth)(pkgRoot, outputRoot, options.changeName)));
|
|
427
|
+
checks.push(...(await (0, apiFieldContract_1.checkApiFieldContract)(pkgRoot, options.changeName)));
|
|
428
|
+
checks.push(...(await (0, demoImplementationCoverage_1.checkDemoImplementationCoverage)(pkgRoot, options.changeName)));
|
|
429
|
+
}
|
|
393
430
|
const ok = checks.every((c) => c.ok);
|
|
394
431
|
return { ok, checks };
|
|
395
432
|
}
|
|
@@ -18,6 +18,7 @@ exports.writeSemanticDiffArtifacts = writeSemanticDiffArtifacts;
|
|
|
18
18
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
19
19
|
const path_1 = __importDefault(require("path"));
|
|
20
20
|
const fs_1 = require("./fs");
|
|
21
|
+
const testAssertionAnalysis_1 = require("./testAssertionAnalysis");
|
|
21
22
|
const SOURCE_EXT = /\.(vue|tsx?|jsx?|json|css|less|scss)$/i;
|
|
22
23
|
const TEST_EXT = /\.(spec|test)\.(t|j)sx?$/i;
|
|
23
24
|
const E2E_DIR_RE = /(^|\/)(e2e|playwright)(\/|$)/i;
|
|
@@ -170,15 +171,17 @@ async function verifyEvidenceRefs(pkgRoot, evidence, signals) {
|
|
|
170
171
|
}
|
|
171
172
|
return { ok: true, detail: "证据路径与字面量可核对" };
|
|
172
173
|
}
|
|
174
|
+
/** PRD 原子须有 test/it 块内 expect 断言;禁止注释或 prd-atoms 登记类伪覆盖 */
|
|
173
175
|
function hasTestCoverageForAtom(atomId, signals, testCorpus) {
|
|
174
176
|
let hasTest = false;
|
|
175
177
|
let hasE2e = false;
|
|
176
178
|
for (const entry of testCorpus) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
179
|
+
if (!entry.isTest)
|
|
180
|
+
continue;
|
|
181
|
+
const { covered, hasE2e: fileE2e } = (0, testAssertionAnalysis_1.hasMeaningfulAtomCoverageInFile)(entry.rel, entry.content, atomId, signals);
|
|
182
|
+
if (covered) {
|
|
180
183
|
hasTest = true;
|
|
181
|
-
if (
|
|
184
|
+
if (fileE2e)
|
|
182
185
|
hasE2e = true;
|
|
183
186
|
}
|
|
184
187
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
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.buildVersionSlug = buildVersionSlug;
|
|
7
|
+
exports.resolveDocsVersionDir = resolveDocsVersionDir;
|
|
8
|
+
exports.findMainPrdMarkdown = findMainPrdMarkdown;
|
|
9
|
+
exports.ensurePrdImageDirMetadata = ensurePrdImageDirMetadata;
|
|
10
|
+
exports.syncSourcePrdFromDocs = syncSourcePrdFromDocs;
|
|
11
|
+
exports.tryAutoSyncSourcePrd = tryAutoSyncSourcePrd;
|
|
12
|
+
exports.resolveSyncSourceDirRel = resolveSyncSourceDirRel;
|
|
13
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const fs_1 = require("./fs");
|
|
16
|
+
const inferProject_1 = require("./inferProject");
|
|
17
|
+
const IMAGE_SUBDIR = "image";
|
|
18
|
+
const PRD_PLACEHOLDER_MARKERS = ["请把《PRD》", "占位", "placeholder"];
|
|
19
|
+
/**
|
|
20
|
+
* 与 confluence-doc.py 的 version_slug 保持一致:1.2.3 → adi-v123。
|
|
21
|
+
*/
|
|
22
|
+
function buildVersionSlug(productPrefix, version) {
|
|
23
|
+
const normalized = version
|
|
24
|
+
.trim()
|
|
25
|
+
.replace(/^[A-Z]+[-_]?V?/i, "")
|
|
26
|
+
.replace(/^V/i, "");
|
|
27
|
+
return `${productPrefix.toLowerCase()}-v${normalized.replace(/\./g, "")}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 解析 docs 下当前版本的 PRD 目录(含主文档与 image/)。
|
|
31
|
+
*/
|
|
32
|
+
async function resolveDocsVersionDir(projectRoot, product, version) {
|
|
33
|
+
const kind = (0, inferProject_1.productToKind)(product);
|
|
34
|
+
const prefix = inferProject_1.PRODUCT_PREFIX_BY_KIND[kind];
|
|
35
|
+
const slug = buildVersionSlug(prefix, version);
|
|
36
|
+
const docSkill = await (0, inferProject_1.resolveDocSkillDir)(projectRoot, kind);
|
|
37
|
+
const docsRoot = path_1.default.dirname(docSkill.absPath);
|
|
38
|
+
const versionDir = path_1.default.join(docsRoot, slug);
|
|
39
|
+
try {
|
|
40
|
+
const st = await promises_1.default.stat(versionDir);
|
|
41
|
+
if (!st.isDirectory())
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return { docsRoot, versionDir, slug };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 在版本目录根下选取体积最大的 .md 作为主 PRD(排除子目录)。
|
|
51
|
+
*/
|
|
52
|
+
async function findMainPrdMarkdown(versionDir) {
|
|
53
|
+
let entries;
|
|
54
|
+
try {
|
|
55
|
+
entries = await promises_1.default.readdir(versionDir);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const mdFiles = entries.filter((name) => name.toLowerCase().endsWith(".md"));
|
|
61
|
+
if (mdFiles.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
let best = null;
|
|
64
|
+
for (const name of mdFiles) {
|
|
65
|
+
const full = path_1.default.join(versionDir, name);
|
|
66
|
+
const st = await promises_1.default.stat(full);
|
|
67
|
+
if (!st.isFile())
|
|
68
|
+
continue;
|
|
69
|
+
if (!best || st.size > best.size) {
|
|
70
|
+
best = { name, size: st.size };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return best ? path_1.default.join(versionDir, best.name) : null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 在 PRD 头部补充原型图片相对路径,供 OPSX UI 解析使用。
|
|
77
|
+
*/
|
|
78
|
+
function ensurePrdImageDirMetadata(content) {
|
|
79
|
+
if (/原型图片目录/.test(content)) {
|
|
80
|
+
return content;
|
|
81
|
+
}
|
|
82
|
+
const lines = content.split("\n");
|
|
83
|
+
const titleIdx = lines.findIndex((line) => line.trim().startsWith("# "));
|
|
84
|
+
const insertAt = titleIdx >= 0 ? titleIdx + 1 : 0;
|
|
85
|
+
const meta = `> 原型图片目录: ${IMAGE_SUBDIR}/`;
|
|
86
|
+
lines.splice(insertAt, 0, "", meta);
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
async function copyDirRecursive(srcDir, destDir) {
|
|
90
|
+
await promises_1.default.mkdir(destDir, { recursive: true });
|
|
91
|
+
const entries = await promises_1.default.readdir(srcDir, { withFileTypes: true });
|
|
92
|
+
let count = 0;
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const srcPath = path_1.default.join(srcDir, entry.name);
|
|
95
|
+
const destPath = path_1.default.join(destDir, entry.name);
|
|
96
|
+
if (entry.isDirectory()) {
|
|
97
|
+
count += await copyDirRecursive(srcPath, destPath);
|
|
98
|
+
}
|
|
99
|
+
else if (entry.isFile()) {
|
|
100
|
+
await promises_1.default.copyFile(srcPath, destPath);
|
|
101
|
+
count += 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return count;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 将 docs/<版本目录> 下的 PRD 主文档与 image/ 同步到 openspec run 的 source/。
|
|
108
|
+
*/
|
|
109
|
+
async function syncSourcePrdFromDocs(options) {
|
|
110
|
+
const resolved = await resolveDocsVersionDir(options.projectRoot, options.product, options.version);
|
|
111
|
+
if (!resolved) {
|
|
112
|
+
const slug = buildVersionSlug(inferProject_1.PRODUCT_PREFIX_BY_KIND[(0, inferProject_1.productToKind)(options.product)], options.version);
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
attempted: true,
|
|
116
|
+
detail: `未找到 docs 版本目录(期望 docs/${slug}/,请先执行 confluence-doc.py)`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const mainMd = await findMainPrdMarkdown(resolved.versionDir);
|
|
120
|
+
if (!mainMd) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
attempted: true,
|
|
124
|
+
detail: `docs/${resolved.slug}/ 下无 PRD 主文档(*.md)`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const sourceDir = path_1.default.join(options.outputRoot, "source");
|
|
128
|
+
const destPrd = path_1.default.join(sourceDir, "PRD.md");
|
|
129
|
+
const raw = await promises_1.default.readFile(mainMd, "utf8");
|
|
130
|
+
if (raw.trim().length < 80) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
attempted: true,
|
|
134
|
+
detail: `docs 主文档过短: ${path_1.default.basename(mainMd)}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const content = ensurePrdImageDirMetadata(raw);
|
|
138
|
+
await (0, fs_1.writeTextFileEnsuredDir)(destPrd, content);
|
|
139
|
+
const srcImageDir = path_1.default.join(resolved.versionDir, IMAGE_SUBDIR);
|
|
140
|
+
const destImageDir = path_1.default.join(sourceDir, IMAGE_SUBDIR);
|
|
141
|
+
let imageCount = 0;
|
|
142
|
+
try {
|
|
143
|
+
const st = await promises_1.default.stat(srcImageDir);
|
|
144
|
+
if (st.isDirectory()) {
|
|
145
|
+
await promises_1.default.rm(destImageDir, { recursive: true, force: true });
|
|
146
|
+
imageCount = await copyDirRecursive(srcImageDir, destImageDir);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// 无图片目录不阻断同步
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
attempted: true,
|
|
155
|
+
detail: `已同步 ${path_1.default.basename(mainMd)} → source/PRD.md${imageCount > 0 ? `,${imageCount} 张图片 → source/${IMAGE_SUBDIR}/` : "(无图片)"}`,
|
|
156
|
+
sourcePrdPath: destPrd,
|
|
157
|
+
imageCount,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* gate 前自动同步:source/PRD.md 仍为占位且 docs 版本目录已存在时尝试回填。
|
|
162
|
+
*/
|
|
163
|
+
async function tryAutoSyncSourcePrd(options) {
|
|
164
|
+
const existing = await (0, fs_1.readFileIfExists)(path_1.default.join(options.outputRoot, "source", "PRD.md"));
|
|
165
|
+
if (existing && !PRD_PLACEHOLDER_MARKERS.some((m) => existing.includes(m)) && existing.trim().length >= 80) {
|
|
166
|
+
return { ok: true, attempted: false, detail: "source/PRD.md 已回填,跳过同步" };
|
|
167
|
+
}
|
|
168
|
+
return syncSourcePrdFromDocs(options);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 从 run 目录相对 doc-skill 脚本目录的 source 路径(供 confluence-doc.py 环境变量)。
|
|
172
|
+
*/
|
|
173
|
+
async function resolveSyncSourceDirRel(projectRoot, sourceDir, product) {
|
|
174
|
+
const kind = (0, inferProject_1.productToKind)(product);
|
|
175
|
+
const docSkill = await (0, inferProject_1.resolveDocSkillDir)(projectRoot, kind);
|
|
176
|
+
const rel = path_1.default.relative(docSkill.absPath, sourceDir).replace(/\\/g, "/");
|
|
177
|
+
if (!rel || rel.startsWith("..")) {
|
|
178
|
+
return rel;
|
|
179
|
+
}
|
|
180
|
+
return `./${rel}`;
|
|
181
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 04-技术文档草稿 结构质量门禁:与 artifacts/04-技术文档.template.md 对齐,禁止摘要版替换完整模板。
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.REQUIRED_TECH_DOC_SECTIONS = void 0;
|
|
7
|
+
exports.checkTechDocStructure = checkTechDocStructure;
|
|
8
|
+
/** 与模板一致的必备二级章节(允许「## 1. 改造范围」类带后缀标题) */
|
|
9
|
+
exports.REQUIRED_TECH_DOC_SECTIONS = [
|
|
10
|
+
{ id: "meta", pattern: /##\s*文档元信息/m, label: "文档元信息" },
|
|
11
|
+
{ id: "scope", pattern: /##\s*1\.\s*改造范围/m, label: "1. 改造范围" },
|
|
12
|
+
{ id: "ui", pattern: /##\s*2\.\s*页面与交互设计/m, label: "2. 页面与交互设计" },
|
|
13
|
+
{ id: "demo", pattern: /###\s*2\.4\s*演示代码参照/m, label: "2.4 演示代码参照" },
|
|
14
|
+
{ id: "state", pattern: /##\s*3\.\s*前端状态与数据模型设计/m, label: "3. 前端状态与数据模型设计" },
|
|
15
|
+
{ id: "api", pattern: /##\s*4\.\s*接口与数据交互设计/m, label: "4. 接口与数据交互设计" },
|
|
16
|
+
{ id: "form", pattern: /##\s*5\.\s*表单与校验设计/m, label: "5. 表单与校验设计" },
|
|
17
|
+
{ id: "edge", pattern: /##\s*6\.\s*异常、边界与降级方案/m, label: "6. 异常、边界与降级方案" },
|
|
18
|
+
{ id: "files", pattern: /##\s*7\.\s*改造清单/m, label: "7. 改造清单" },
|
|
19
|
+
{ id: "risk", pattern: /##\s*8\.\s*风险点与应对策略/m, label: "8. 风险点与应对策略" },
|
|
20
|
+
{ id: "release", pattern: /##\s*9\.\s*发布、灰度与回滚/m, label: "9. 发布、灰度与回滚" },
|
|
21
|
+
{ id: "test", pattern: /##\s*10\.\s*测试与验收要点/m, label: "10. 测试与验收要点" },
|
|
22
|
+
{ id: "pending", pattern: /##\s*11\.\s*待确认事项/m, label: "11. 待确认事项" },
|
|
23
|
+
];
|
|
24
|
+
const MIN_LINES = 150;
|
|
25
|
+
const MIN_H3_UNDER_SCOPE = 2;
|
|
26
|
+
const API_TABLE_RE = /\|\s*接口\s*\|\s*Method\s*\|/m;
|
|
27
|
+
const DEMO_TABLE_RE = /\|\s*演示模块\s*\|\s*演示文件\/组件\s*\|/m;
|
|
28
|
+
const AC_REF_RE = /\[AC-\d{2,}\]/g;
|
|
29
|
+
/** 典型「摘要版」反模式:仅若干条顶层 [AC-xx] bullet,无三级标题 */
|
|
30
|
+
const SUMMARY_ANTIPATTERN_RE = /^#\s*04-技术文档草稿\s*\n+##\s*文档元信息[\s\S]{0,800}##\s*7\.\s*权限/m;
|
|
31
|
+
/**
|
|
32
|
+
* 校验 04 技术文档是否按模板填满(非摘要替换版)。
|
|
33
|
+
*/
|
|
34
|
+
function checkTechDocStructure(content) {
|
|
35
|
+
var _a, _b, _c, _d;
|
|
36
|
+
const missingSections = [];
|
|
37
|
+
const warnings = [];
|
|
38
|
+
if (!content || content.trim().length < 80) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
detail: "04-技术文档草稿.md 缺失或过短",
|
|
42
|
+
missingSections: exports.REQUIRED_TECH_DOC_SECTIONS.map((s) => s.label),
|
|
43
|
+
warnings: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const lines = content.split("\n");
|
|
47
|
+
if (lines.length < MIN_LINES) {
|
|
48
|
+
warnings.push(`行数 ${lines.length} < ${MIN_LINES}(模板级文档通常 ≥150 行)`);
|
|
49
|
+
}
|
|
50
|
+
for (const sec of exports.REQUIRED_TECH_DOC_SECTIONS) {
|
|
51
|
+
if (!sec.pattern.test(content)) {
|
|
52
|
+
missingSections.push(sec.label);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const h3InScope = ((_b = (_a = content.match(/##\s*1\.\s*改造范围[\s\S]*?(?=##\s*2\.)/m)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "").match(/^###\s+/gm);
|
|
56
|
+
if (!h3InScope || h3InScope.length < MIN_H3_UNDER_SCOPE) {
|
|
57
|
+
missingSections.push("1.x 改造目标/In Scope 等三级小节");
|
|
58
|
+
}
|
|
59
|
+
if (!API_TABLE_RE.test(content)) {
|
|
60
|
+
missingSections.push("4.1 接口清单表格(| 接口 | Method |)");
|
|
61
|
+
}
|
|
62
|
+
if (!DEMO_TABLE_RE.test(content)) {
|
|
63
|
+
missingSections.push("2.4 演示模块映射表格(| 演示模块 | 演示文件/组件 |)");
|
|
64
|
+
}
|
|
65
|
+
const acRefs = (_c = content.match(AC_REF_RE)) !== null && _c !== void 0 ? _c : [];
|
|
66
|
+
if (acRefs.length < 3) {
|
|
67
|
+
warnings.push("文中 [AC-XX] 引用过少(建议在各章节分散标注)");
|
|
68
|
+
}
|
|
69
|
+
if (SUMMARY_ANTIPATTERN_RE.test(content) && lines.length < 80) {
|
|
70
|
+
missingSections.push("禁止摘要版:须保留模板全部 11 章并逐节填写");
|
|
71
|
+
}
|
|
72
|
+
const placeholderHeavy = ((_d = content.match(/(填写)|待填写|TBD|示例:/g)) !== null && _d !== void 0 ? _d : []).length > 25 &&
|
|
73
|
+
lines.length < MIN_LINES;
|
|
74
|
+
if (placeholderHeavy) {
|
|
75
|
+
warnings.push("占位符过多且篇幅过短,疑似未按 PRD/接口/演示填充");
|
|
76
|
+
}
|
|
77
|
+
const ok = missingSections.length === 0 && lines.length >= MIN_LINES;
|
|
78
|
+
const detail = ok
|
|
79
|
+
? `04 技术文档结构合格(${lines.length} 行,${exports.REQUIRED_TECH_DOC_SECTIONS.length} 章齐全)`
|
|
80
|
+
: `04 技术文档不符合模板: 缺 ${missingSections.join("、")}${warnings.length ? `;${warnings.join(";")}` : ""}`;
|
|
81
|
+
return {
|
|
82
|
+
ok,
|
|
83
|
+
detail,
|
|
84
|
+
missingSections,
|
|
85
|
+
warnings,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/core/writeNext.js
CHANGED
|
@@ -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
|
|
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`,
|