sdd-flow-kit 1.3.6 → 1.3.11
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/README.md +33 -0
- package/dist/cli.js +66 -0
- package/dist/core/agentRemediateInvoke.js +188 -0
- package/dist/core/playwrightEnsure.js +142 -0
- package/dist/core/prdCoverage.js +398 -0
- package/dist/core/prdParse.js +88 -0
- package/dist/core/prdRemediate.js +300 -0
- package/dist/core/prdRemediateState.js +135 -0
- package/dist/core/prdSemanticDiff.js +365 -0
- package/dist/core/sessionState.js +2 -0
- package/dist/core/writeNext.js +5 -0
- package/dist/steps/runDeliver.js +13 -0
- package/dist/steps/runGate.js +34 -1
- package/dist/steps/runPhaseAdvance.js +4 -0
- package/dist/steps/runPrdDiff.js +71 -0
- package/dist/steps/runPrdRemediate.js +49 -0
- package/dist/steps/runPrdReview.js +73 -0
- package/dist/steps/runValidate.js +78 -52
- package/dist/steps/startFlow.js +5 -0
- package/package.json +1 -1
- package/src/templates/artifacts/06-agent-skill.template.md +2 -2
- package/src/templates/artifacts/07-opsx-auto-chain.template.md +11 -1
- 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 +93 -0
- package/src/templates/prompts/08-prd-consistency-review-prompt.md +49 -0
- package/src/templates/rules/sdd-no-implement-until-apply.mdc.template +6 -2
|
@@ -0,0 +1,398 @@
|
|
|
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.findTestFiles = findTestFiles;
|
|
7
|
+
exports.parseReviewReportTable = parseReviewReportTable;
|
|
8
|
+
exports.writePrdAtomsManifest = writePrdAtomsManifest;
|
|
9
|
+
exports.loadPrdAtomsFromManifest = loadPrdAtomsFromManifest;
|
|
10
|
+
exports.checkPrdCoverage = checkPrdCoverage;
|
|
11
|
+
exports.writePrdCoveragePassMarker = writePrdCoveragePassMarker;
|
|
12
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const acMatrix_1 = require("./acMatrix");
|
|
15
|
+
const fs_1 = require("./fs");
|
|
16
|
+
const prdParse_1 = require("./prdParse");
|
|
17
|
+
const prdSemanticDiff_1 = require("./prdSemanticDiff");
|
|
18
|
+
const VALID_VERDICTS = ["一致", "不一致-按PRD修复", "待人工确认"];
|
|
19
|
+
/** 禁止在证据/结论中出现的揣摩或兜底措辞 */
|
|
20
|
+
const FORBIDDEN_SPECULATION = [
|
|
21
|
+
"推测",
|
|
22
|
+
"兜底",
|
|
23
|
+
"暂定",
|
|
24
|
+
"可能",
|
|
25
|
+
"大概",
|
|
26
|
+
"应该可以",
|
|
27
|
+
"或沿用",
|
|
28
|
+
"默认处理",
|
|
29
|
+
"先这样",
|
|
30
|
+
"暂时",
|
|
31
|
+
"估计",
|
|
32
|
+
"猜测",
|
|
33
|
+
"或许",
|
|
34
|
+
];
|
|
35
|
+
const TEST_FILE_RE = /\.(spec|test)\.(t|j)sx?$/i;
|
|
36
|
+
const E2E_DIR_RE = /(^|\/)(e2e|playwright)(\/|$)/i;
|
|
37
|
+
async function walkFiles(dir, out) {
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = await promises_1.default.readdir(dir, { withFileTypes: true });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for (const ent of entries) {
|
|
46
|
+
if (ent.name === "node_modules" || ent.name === ".git" || ent.name === "dist")
|
|
47
|
+
continue;
|
|
48
|
+
const full = path_1.default.join(dir, ent.name);
|
|
49
|
+
if (ent.isDirectory()) {
|
|
50
|
+
await walkFiles(full, out);
|
|
51
|
+
}
|
|
52
|
+
else if (TEST_FILE_RE.test(ent.name)) {
|
|
53
|
+
out.push(full);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function findTestFiles(pkgRoot) {
|
|
58
|
+
const roots = ["e2e", "tests", "test", "__tests__", "playwright", "src"];
|
|
59
|
+
const files = [];
|
|
60
|
+
for (const r of roots) {
|
|
61
|
+
await walkFiles(path_1.default.join(pkgRoot, r), files);
|
|
62
|
+
}
|
|
63
|
+
return [...new Set(files)];
|
|
64
|
+
}
|
|
65
|
+
function parseReviewReportTable(content) {
|
|
66
|
+
var _a, _b, _c, _d, _e, _f;
|
|
67
|
+
const rows = [];
|
|
68
|
+
for (const line of content.split("\n")) {
|
|
69
|
+
const trimmed = line.trim();
|
|
70
|
+
if (!trimmed.startsWith("|"))
|
|
71
|
+
continue;
|
|
72
|
+
if (/^\|\s*[-:]+/.test(trimmed))
|
|
73
|
+
continue;
|
|
74
|
+
if (/^\|\s*PRD-ID\s*\|/i.test(trimmed))
|
|
75
|
+
continue;
|
|
76
|
+
const cells = trimmed
|
|
77
|
+
.split("|")
|
|
78
|
+
.map((c) => c.trim())
|
|
79
|
+
.filter((_, i, arr) => i > 0 && i < arr.length - 1);
|
|
80
|
+
if (cells.length < 5)
|
|
81
|
+
continue;
|
|
82
|
+
const prdId = (_b = (_a = cells[0]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : "";
|
|
83
|
+
if (!/^PRD-\d{3,}$/.test(prdId))
|
|
84
|
+
continue;
|
|
85
|
+
rows.push({
|
|
86
|
+
prdId,
|
|
87
|
+
prdText: (_c = cells[1]) !== null && _c !== void 0 ? _c : "",
|
|
88
|
+
codeEvidence: (_d = cells[2]) !== null && _d !== void 0 ? _d : "",
|
|
89
|
+
verdict: (_e = cells[3]) !== null && _e !== void 0 ? _e : "",
|
|
90
|
+
note: (_f = cells[4]) !== null && _f !== void 0 ? _f : "",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return rows;
|
|
94
|
+
}
|
|
95
|
+
function hasForbiddenSpeculation(text) {
|
|
96
|
+
for (const word of FORBIDDEN_SPECULATION) {
|
|
97
|
+
if (text.includes(word))
|
|
98
|
+
return word;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
async function writePrdAtomsManifest(outputRoot, atoms) {
|
|
103
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, ".prd-review-atoms.json"), `${JSON.stringify({ count: atoms.length, atoms }, null, 2)}\n`);
|
|
104
|
+
}
|
|
105
|
+
async function loadPrdAtomsFromManifest(outputRoot) {
|
|
106
|
+
var _a;
|
|
107
|
+
const raw = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, ".prd-review-atoms.json"));
|
|
108
|
+
if (!raw)
|
|
109
|
+
return [];
|
|
110
|
+
try {
|
|
111
|
+
const j = JSON.parse(raw);
|
|
112
|
+
return (_a = j.atoms) !== null && _a !== void 0 ? _a : [];
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function checkAcTestMapping(items, testContents) {
|
|
119
|
+
var _a;
|
|
120
|
+
const checks = [];
|
|
121
|
+
// P0 全量 + 所有声明 e2e 的 AC(含 P1/P2 边缘场景)
|
|
122
|
+
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
|
+
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
|
+
const e2eRequired = /e2e/i.test((_a = ac.testType) !== null && _a !== void 0 ? _a : "");
|
|
127
|
+
const e2eHits = hits.filter((h) => E2E_DIR_RE.test(h.file.replace(/\\/g, "/")));
|
|
128
|
+
if (hits.length === 0) {
|
|
129
|
+
checks.push({
|
|
130
|
+
name: `ac-test-${ac.id}`,
|
|
131
|
+
ok: false,
|
|
132
|
+
detail: `${ac.id}(${ac.priority}) 未在测试文件中找到 [${ac.id}] 映射`,
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (e2eRequired && e2eHits.length === 0) {
|
|
137
|
+
checks.push({
|
|
138
|
+
name: `ac-e2e-${ac.id}`,
|
|
139
|
+
ok: false,
|
|
140
|
+
detail: `${ac.id} 测试类型含 e2e,但 e2e/ 或 playwright/ 下无对应测试`,
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
checks.push({
|
|
145
|
+
name: `ac-test-${ac.id}`,
|
|
146
|
+
ok: true,
|
|
147
|
+
detail: e2eRequired
|
|
148
|
+
? `${ac.id} 已映射(含 e2e: ${e2eHits.map((h) => path_1.default.basename(h.file)).join(", ")})`
|
|
149
|
+
: `${ac.id} 已映射: ${hits.map((h) => path_1.default.basename(h.file)).join(", ")}`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return checks;
|
|
153
|
+
}
|
|
154
|
+
function checkSemanticDiffItems(semantic) {
|
|
155
|
+
const checks = [];
|
|
156
|
+
checks.push({
|
|
157
|
+
name: "semantic-diff",
|
|
158
|
+
ok: semantic.ok,
|
|
159
|
+
detail: semantic.ok
|
|
160
|
+
? `语义 diff 通过(matched=${semantic.summary.matched}, unverifiable=${semantic.summary.unverifiable})`
|
|
161
|
+
: `语义 diff 未通过: missing=${semantic.summary.missing}, partial=${semantic.summary.partial}, reportMismatch=${semantic.summary.reportMismatch}, missingTest=${semantic.summary.missingTest}, missingE2e=${semantic.summary.missingE2e}`,
|
|
162
|
+
});
|
|
163
|
+
const mismatches = semantic.items.filter((i) => i.status === "report_mismatch").slice(0, 6);
|
|
164
|
+
if (mismatches.length > 0) {
|
|
165
|
+
checks.push({
|
|
166
|
+
name: "semantic-report-align",
|
|
167
|
+
ok: false,
|
|
168
|
+
detail: `08 报告与脚本 diff 不一致: ${mismatches.map((m) => `${m.atomId}(${m.detail})`).join("; ")}`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
checks.push({
|
|
173
|
+
name: "semantic-report-align",
|
|
174
|
+
ok: true,
|
|
175
|
+
detail: "08 报告结论与脚本语义 diff 一致",
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const missingCode = semantic.items.filter((i) => i.status === "missing_in_code").slice(0, 6);
|
|
179
|
+
if (missingCode.length > 0) {
|
|
180
|
+
checks.push({
|
|
181
|
+
name: "semantic-code-presence",
|
|
182
|
+
ok: false,
|
|
183
|
+
detail: `PRD 字面量在代码中缺失: ${missingCode.map((m) => { var _a; return `${m.atomId}「${(_a = m.missingSignals[0]) !== null && _a !== void 0 ? _a : m.atomText.slice(0, 20)}」`; }).join("; ")}`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
checks.push({
|
|
188
|
+
name: "semantic-code-presence",
|
|
189
|
+
ok: true,
|
|
190
|
+
detail: "可验证 PRD 字面量均在代码中出现",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const missingTests = semantic.items
|
|
194
|
+
.filter((i) => i.requiresAutomatedTest && !i.hasAutomatedTest)
|
|
195
|
+
.slice(0, 8);
|
|
196
|
+
if (missingTests.length > 0) {
|
|
197
|
+
checks.push({
|
|
198
|
+
name: "semantic-test-coverage",
|
|
199
|
+
ok: false,
|
|
200
|
+
detail: `须自动化测试但未覆盖: ${missingTests.map((m) => m.atomId).join(", ")}(须在 e2e/ 或测试中引用 PRD-ID 或 PRD 字面量)`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
checks.push({
|
|
205
|
+
name: "semantic-test-coverage",
|
|
206
|
+
ok: true,
|
|
207
|
+
detail: `全部 ${semantic.summary.requiresTest} 条可验证 PRD 均有自动化测试引用`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const missingE2e = semantic.items
|
|
211
|
+
.filter((i) => i.requiresAutomatedTest && !i.hasE2eTest)
|
|
212
|
+
.slice(0, 8);
|
|
213
|
+
if (missingE2e.length > 0) {
|
|
214
|
+
checks.push({
|
|
215
|
+
name: "semantic-e2e-coverage",
|
|
216
|
+
ok: false,
|
|
217
|
+
detail: `须 E2E 但未覆盖: ${missingE2e.map((m) => m.atomId).join(", ")}(在 e2e/ 中引用 PRD-ID 或断言 PRD 字面量)`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
checks.push({
|
|
222
|
+
name: "semantic-e2e-coverage",
|
|
223
|
+
ok: true,
|
|
224
|
+
detail: "全部可验证 PRD 条目均有 E2E 测试引用",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const badEvidence = semantic.items
|
|
228
|
+
.filter((i) => i.reportVerdict === "一致" && !i.reportEvidenceValid && i.signals.length > 0)
|
|
229
|
+
.slice(0, 6);
|
|
230
|
+
if (badEvidence.length > 0) {
|
|
231
|
+
checks.push({
|
|
232
|
+
name: "evidence-verified",
|
|
233
|
+
ok: false,
|
|
234
|
+
detail: `08 证据无法核对: ${badEvidence.map((m) => m.atomId).join(", ")}(路径不存在或行内无 PRD 字面量)`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
checks.push({
|
|
239
|
+
name: "evidence-verified",
|
|
240
|
+
ok: true,
|
|
241
|
+
detail: "08 报告中「一致」项的证据路径与字面量可机械核对",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return checks;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* gate prd-coverage:PRD 最细粒度条目全覆盖 + 08 复查报告合规 + P0 AC 测试映射。
|
|
248
|
+
*/
|
|
249
|
+
async function checkPrdCoverage(outputRoot, pkgRoot) {
|
|
250
|
+
const checks = [];
|
|
251
|
+
const prdContent = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, "source", "PRD.md"));
|
|
252
|
+
if (!prdContent) {
|
|
253
|
+
checks.push({ name: "prd-source", ok: false, detail: "缺少 source/PRD.md" });
|
|
254
|
+
return { ok: false, checks };
|
|
255
|
+
}
|
|
256
|
+
let atoms = await loadPrdAtomsFromManifest(outputRoot);
|
|
257
|
+
if (atoms.length === 0) {
|
|
258
|
+
atoms = (0, prdParse_1.parsePrdAtoms)(prdContent);
|
|
259
|
+
if (atoms.length > 0) {
|
|
260
|
+
await writePrdAtomsManifest(outputRoot, atoms);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (atoms.length === 0) {
|
|
264
|
+
checks.push({ name: "prd-atoms", ok: false, detail: "PRD 未解析到任何原子条目" });
|
|
265
|
+
return { ok: false, checks };
|
|
266
|
+
}
|
|
267
|
+
checks.push({
|
|
268
|
+
name: "prd-atoms",
|
|
269
|
+
ok: true,
|
|
270
|
+
detail: `PRD 原子条目 ${atoms.length} 条(最细粒度)`,
|
|
271
|
+
});
|
|
272
|
+
const reportPath = path_1.default.join(outputRoot, "08-PRD一致性复查报告.md");
|
|
273
|
+
const reportContent = await (0, fs_1.readFileIfExists)(reportPath);
|
|
274
|
+
if (!reportContent || reportContent.trim().length < 200) {
|
|
275
|
+
checks.push({
|
|
276
|
+
name: "review-report",
|
|
277
|
+
ok: false,
|
|
278
|
+
detail: "缺少 08-PRD一致性复查报告.md,请先执行: npx sdd-flow-kit prd-review",
|
|
279
|
+
});
|
|
280
|
+
return { ok: false, checks };
|
|
281
|
+
}
|
|
282
|
+
if (!/复查状态[::]\s*[`「]*复查完成/.test(reportContent)) {
|
|
283
|
+
checks.push({
|
|
284
|
+
name: "review-status",
|
|
285
|
+
ok: false,
|
|
286
|
+
detail: "08 报告未标注「复查状态:复查完成」",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
checks.push({ name: "review-status", ok: true, detail: "复查状态:复查完成" });
|
|
291
|
+
}
|
|
292
|
+
const reportRows = parseReviewReportTable(reportContent);
|
|
293
|
+
const rowById = new Map(reportRows.map((r) => [r.prdId, r]));
|
|
294
|
+
const missingInReport = atoms.filter((a) => !rowById.has(a.id));
|
|
295
|
+
if (missingInReport.length > 0) {
|
|
296
|
+
checks.push({
|
|
297
|
+
name: "report-coverage",
|
|
298
|
+
ok: false,
|
|
299
|
+
detail: `08 报告缺少 ${missingInReport.length} 条 PRD 条目: ${missingInReport
|
|
300
|
+
.slice(0, 6)
|
|
301
|
+
.map((a) => a.id)
|
|
302
|
+
.join(", ")}${missingInReport.length > 6 ? "..." : ""}`,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
checks.push({
|
|
307
|
+
name: "report-coverage",
|
|
308
|
+
ok: true,
|
|
309
|
+
detail: `08 报告已覆盖全部 ${atoms.length} 条 PRD 原子条目`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const invalidVerdicts = [];
|
|
313
|
+
const needFix = [];
|
|
314
|
+
const needHuman = [];
|
|
315
|
+
const speculationHits = [];
|
|
316
|
+
const emptyEvidence = [];
|
|
317
|
+
for (const atom of atoms) {
|
|
318
|
+
const row = rowById.get(atom.id);
|
|
319
|
+
if (!row)
|
|
320
|
+
continue;
|
|
321
|
+
if (!VALID_VERDICTS.includes(row.verdict)) {
|
|
322
|
+
invalidVerdicts.push(atom.id);
|
|
323
|
+
}
|
|
324
|
+
if (row.verdict === "不一致-按PRD修复")
|
|
325
|
+
needFix.push(atom.id);
|
|
326
|
+
if (row.verdict === "待人工确认")
|
|
327
|
+
needHuman.push(atom.id);
|
|
328
|
+
const evidence = row.codeEvidence.trim();
|
|
329
|
+
if (!evidence || evidence.length < 8 || /(填写)|待填写|TBD/i.test(evidence)) {
|
|
330
|
+
emptyEvidence.push(atom.id);
|
|
331
|
+
}
|
|
332
|
+
const combined = `${row.codeEvidence} ${row.note} ${row.verdict}`;
|
|
333
|
+
const forbidden = hasForbiddenSpeculation(combined);
|
|
334
|
+
if (forbidden)
|
|
335
|
+
speculationHits.push(`${atom.id}(${forbidden})`);
|
|
336
|
+
}
|
|
337
|
+
checks.push({
|
|
338
|
+
name: "verdict-valid",
|
|
339
|
+
ok: invalidVerdicts.length === 0,
|
|
340
|
+
detail: invalidVerdicts.length === 0
|
|
341
|
+
? "全部结论为:一致 | 不一致-按PRD修复 | 待人工确认"
|
|
342
|
+
: `无效结论: ${invalidVerdicts.join(", ")}`,
|
|
343
|
+
});
|
|
344
|
+
checks.push({
|
|
345
|
+
name: "no-speculation",
|
|
346
|
+
ok: speculationHits.length === 0,
|
|
347
|
+
detail: speculationHits.length === 0
|
|
348
|
+
? "未发现揣摩/兜底措辞"
|
|
349
|
+
: `含禁止措辞: ${speculationHits.slice(0, 8).join(", ")}`,
|
|
350
|
+
});
|
|
351
|
+
checks.push({
|
|
352
|
+
name: "code-evidence",
|
|
353
|
+
ok: emptyEvidence.length === 0,
|
|
354
|
+
detail: emptyEvidence.length === 0
|
|
355
|
+
? "每条均有可核对代码证据(文件路径+行号/选择器/断言)"
|
|
356
|
+
: `缺少代码证据: ${emptyEvidence.slice(0, 8).join(", ")}`,
|
|
357
|
+
});
|
|
358
|
+
checks.push({
|
|
359
|
+
name: "no-prd-mismatch-open",
|
|
360
|
+
ok: needFix.length === 0,
|
|
361
|
+
detail: needFix.length === 0
|
|
362
|
+
? "无未修复的 PRD 不一致项"
|
|
363
|
+
: `仍须按 PRD 修复: ${needFix.join(", ")}`,
|
|
364
|
+
});
|
|
365
|
+
checks.push({
|
|
366
|
+
name: "no-human-pending",
|
|
367
|
+
ok: needHuman.length === 0,
|
|
368
|
+
detail: needHuman.length === 0
|
|
369
|
+
? "无待人工确认项"
|
|
370
|
+
: `待人工确认: ${needHuman.join(", ")}(见 08 报告「待人工确认汇总」)`,
|
|
371
|
+
});
|
|
372
|
+
// ---- 脚本级 PRD 语义 diff(独立于 AI 填表) ----
|
|
373
|
+
const reportRowMap = new Map(reportRows.map((r) => [r.prdId, { verdict: r.verdict, codeEvidence: r.codeEvidence }]));
|
|
374
|
+
const semantic = await (0, prdSemanticDiff_1.runPrdSemanticDiff)({ pkgRoot, atoms, reportRows: reportRowMap });
|
|
375
|
+
await (0, prdSemanticDiff_1.writeSemanticDiffArtifacts)(outputRoot, semantic);
|
|
376
|
+
checks.push(...checkSemanticDiffItems(semantic));
|
|
377
|
+
const acParsed = await (0, acMatrix_1.parseAcMatrix)(outputRoot);
|
|
378
|
+
if (!acParsed.ok) {
|
|
379
|
+
checks.push({ name: "ac-matrix", ok: false, detail: acParsed.detail });
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
checks.push({ name: "ac-matrix", ok: true, detail: acParsed.detail });
|
|
383
|
+
const testFiles = await findTestFiles(pkgRoot);
|
|
384
|
+
const testContents = await Promise.all(testFiles.map(async (f) => {
|
|
385
|
+
var _a;
|
|
386
|
+
return ({
|
|
387
|
+
file: f,
|
|
388
|
+
content: (_a = (await (0, fs_1.readFileIfExists)(f))) !== null && _a !== void 0 ? _a : "",
|
|
389
|
+
});
|
|
390
|
+
}));
|
|
391
|
+
checks.push(...checkAcTestMapping(acParsed.items, testContents));
|
|
392
|
+
}
|
|
393
|
+
const ok = checks.every((c) => c.ok);
|
|
394
|
+
return { ok, checks };
|
|
395
|
+
}
|
|
396
|
+
async function writePrdCoveragePassMarker(outputRoot, payload) {
|
|
397
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, ".prd-coverage-pass.json"), `${JSON.stringify(payload, null, 2)}\n`);
|
|
398
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parsePrdAtoms = parsePrdAtoms;
|
|
4
|
+
function isTableSeparator(line) {
|
|
5
|
+
const cells = line
|
|
6
|
+
.split("|")
|
|
7
|
+
.map((c) => c.trim())
|
|
8
|
+
.filter((_, i, arr) => i > 0 && i < arr.length - 1);
|
|
9
|
+
return cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 将 PRD Markdown 拆成最细粒度原子条目(标题 / 列表项 / 表格行 / 实质段落)。
|
|
13
|
+
*/
|
|
14
|
+
function parsePrdAtoms(content) {
|
|
15
|
+
const lines = content.split("\n");
|
|
16
|
+
const atoms = [];
|
|
17
|
+
const sectionStack = [];
|
|
18
|
+
let counter = 0;
|
|
19
|
+
let inCodeFence = false;
|
|
20
|
+
for (let i = 0; i < lines.length; i++) {
|
|
21
|
+
const raw = lines[i];
|
|
22
|
+
const trimmed = raw.trim();
|
|
23
|
+
if (trimmed.startsWith("```")) {
|
|
24
|
+
inCodeFence = !inCodeFence;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (inCodeFence || !trimmed || trimmed.startsWith("!["))
|
|
28
|
+
continue;
|
|
29
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
30
|
+
if (headingMatch) {
|
|
31
|
+
const level = headingMatch[1].length;
|
|
32
|
+
const title = headingMatch[2].trim();
|
|
33
|
+
sectionStack.length = level;
|
|
34
|
+
sectionStack[level - 1] = title;
|
|
35
|
+
counter += 1;
|
|
36
|
+
atoms.push({
|
|
37
|
+
id: `PRD-${String(counter).padStart(3, "0")}`,
|
|
38
|
+
sectionPath: sectionStack.filter(Boolean).join(" > "),
|
|
39
|
+
line: i + 1,
|
|
40
|
+
text: title,
|
|
41
|
+
kind: "heading",
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const listMatch = trimmed.match(/^(?:\d+\.|[-*+])\s+(.+)$/);
|
|
46
|
+
if (listMatch && listMatch[1].trim().length >= 4) {
|
|
47
|
+
counter += 1;
|
|
48
|
+
atoms.push({
|
|
49
|
+
id: `PRD-${String(counter).padStart(3, "0")}`,
|
|
50
|
+
sectionPath: sectionStack.filter(Boolean).join(" > "),
|
|
51
|
+
line: i + 1,
|
|
52
|
+
text: listMatch[1].trim(),
|
|
53
|
+
kind: "list",
|
|
54
|
+
});
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (trimmed.startsWith("|")) {
|
|
58
|
+
if (isTableSeparator(trimmed))
|
|
59
|
+
continue;
|
|
60
|
+
const cells = trimmed
|
|
61
|
+
.split("|")
|
|
62
|
+
.map((c) => c.trim())
|
|
63
|
+
.filter((_, idx, arr) => idx > 0 && idx < arr.length - 1);
|
|
64
|
+
if (cells.length >= 1 && cells.some((c) => c.length > 0)) {
|
|
65
|
+
counter += 1;
|
|
66
|
+
atoms.push({
|
|
67
|
+
id: `PRD-${String(counter).padStart(3, "0")}`,
|
|
68
|
+
sectionPath: sectionStack.filter(Boolean).join(" > "),
|
|
69
|
+
line: i + 1,
|
|
70
|
+
text: cells.join(" | "),
|
|
71
|
+
kind: "table-row",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (trimmed.length >= 12 && !trimmed.startsWith("|")) {
|
|
77
|
+
counter += 1;
|
|
78
|
+
atoms.push({
|
|
79
|
+
id: `PRD-${String(counter).padStart(3, "0")}`,
|
|
80
|
+
sectionPath: sectionStack.filter(Boolean).join(" > "),
|
|
81
|
+
line: i + 1,
|
|
82
|
+
text: trimmed,
|
|
83
|
+
kind: "paragraph",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return atoms;
|
|
88
|
+
}
|