sdd-flow-kit 1.3.6 → 1.3.12
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 +41 -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/01-adi-doc-skill-/346/214/207/345/274/225.template.md +10 -6
- 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
- package/src/templates/skills/confluence-doc.py.template +49 -13
- package/src/templates/skills/doc-skill.SKILL.md.template +9 -3
|
@@ -0,0 +1,365 @@
|
|
|
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.extractVerifiableSignals = extractVerifiableSignals;
|
|
7
|
+
exports.classifyPrdAtom = classifyPrdAtom;
|
|
8
|
+
exports.atomRequiresAutomatedTest = atomRequiresAutomatedTest;
|
|
9
|
+
exports.findSourceFiles = findSourceFiles;
|
|
10
|
+
exports.findAllTestFiles = findAllTestFiles;
|
|
11
|
+
exports.buildCodeCorpus = buildCodeCorpus;
|
|
12
|
+
exports.searchSignalInCorpus = searchSignalInCorpus;
|
|
13
|
+
exports.parseEvidenceRefs = parseEvidenceRefs;
|
|
14
|
+
exports.verifyEvidenceRefs = verifyEvidenceRefs;
|
|
15
|
+
exports.runPrdSemanticDiff = runPrdSemanticDiff;
|
|
16
|
+
exports.renderSemanticDiffReportMarkdown = renderSemanticDiffReportMarkdown;
|
|
17
|
+
exports.writeSemanticDiffArtifacts = writeSemanticDiffArtifacts;
|
|
18
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
19
|
+
const path_1 = __importDefault(require("path"));
|
|
20
|
+
const fs_1 = require("./fs");
|
|
21
|
+
const SOURCE_EXT = /\.(vue|tsx?|jsx?|json|css|less|scss)$/i;
|
|
22
|
+
const TEST_EXT = /\.(spec|test)\.(t|j)sx?$/i;
|
|
23
|
+
const E2E_DIR_RE = /(^|\/)(e2e|playwright)(\/|$)/i;
|
|
24
|
+
const UI_KEYWORDS = /按钮|toast|提示|文案|placeholder|tooltip|列|字段|筛选项|弹窗|对话框|表单|输入框|下拉|标签页|tab|列表|导出|导入|审核|禁用|必填|校验|message\.|getByText|getByRole/i;
|
|
25
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "coverage", "openspec"]);
|
|
26
|
+
/** 从 PRD 文本提取可机械比对的字面量(引号内文案、toast 等) */
|
|
27
|
+
function extractVerifiableSignals(text) {
|
|
28
|
+
var _a;
|
|
29
|
+
const signals = [];
|
|
30
|
+
const patterns = [
|
|
31
|
+
/[「『]([^」』]{2,120})[」』]/g,
|
|
32
|
+
/"([^"]{2,120})"/g,
|
|
33
|
+
/'([^']{2,120})'/g,
|
|
34
|
+
/`([^`]{2,120})`/g,
|
|
35
|
+
];
|
|
36
|
+
for (const re of patterns) {
|
|
37
|
+
for (const m of text.matchAll(re)) {
|
|
38
|
+
const s = ((_a = m[1]) !== null && _a !== void 0 ? _a : "").trim();
|
|
39
|
+
if (s.length >= 2)
|
|
40
|
+
signals.push(s);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...new Set(signals)];
|
|
44
|
+
}
|
|
45
|
+
function classifyPrdAtom(atom) {
|
|
46
|
+
const signals = extractVerifiableSignals(atom.text);
|
|
47
|
+
if (signals.length > 0)
|
|
48
|
+
return "ui-verifiable";
|
|
49
|
+
if (UI_KEYWORDS.test(atom.text) && atom.text.length >= 6)
|
|
50
|
+
return "behavior-verifiable";
|
|
51
|
+
if (atom.kind === "heading")
|
|
52
|
+
return "structural";
|
|
53
|
+
if (atom.text.length < 12)
|
|
54
|
+
return "structural";
|
|
55
|
+
return "narrative";
|
|
56
|
+
}
|
|
57
|
+
function atomRequiresAutomatedTest(classification, signals) {
|
|
58
|
+
return classification === "ui-verifiable" || classification === "behavior-verifiable" || signals.length > 0;
|
|
59
|
+
}
|
|
60
|
+
async function walkDir(dir, filter, out) {
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = await promises_1.default.readdir(dir, { withFileTypes: true });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
for (const ent of entries) {
|
|
69
|
+
if (SKIP_DIRS.has(ent.name))
|
|
70
|
+
continue;
|
|
71
|
+
const full = path_1.default.join(dir, ent.name);
|
|
72
|
+
if (ent.isDirectory()) {
|
|
73
|
+
await walkDir(full, filter, out);
|
|
74
|
+
}
|
|
75
|
+
else if (filter.test(ent.name)) {
|
|
76
|
+
out.push(full);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function findSourceFiles(pkgRoot) {
|
|
81
|
+
const roots = ["src", "frontend/src", "app", "pages", "components", "lib"];
|
|
82
|
+
const files = [];
|
|
83
|
+
for (const r of roots) {
|
|
84
|
+
await walkDir(path_1.default.join(pkgRoot, r), SOURCE_EXT, files);
|
|
85
|
+
}
|
|
86
|
+
return [...new Set(files)];
|
|
87
|
+
}
|
|
88
|
+
async function findAllTestFiles(pkgRoot) {
|
|
89
|
+
const roots = ["e2e", "tests", "test", "__tests__", "playwright", "src", "frontend/src"];
|
|
90
|
+
const files = [];
|
|
91
|
+
for (const r of roots) {
|
|
92
|
+
await walkDir(path_1.default.join(pkgRoot, r), TEST_EXT, files);
|
|
93
|
+
}
|
|
94
|
+
return [...new Set(files)];
|
|
95
|
+
}
|
|
96
|
+
async function buildCodeCorpus(pkgRoot) {
|
|
97
|
+
const sourceFiles = await findSourceFiles(pkgRoot);
|
|
98
|
+
const testFiles = await findAllTestFiles(pkgRoot);
|
|
99
|
+
const all = [...new Set([...sourceFiles, ...testFiles])];
|
|
100
|
+
const entries = [];
|
|
101
|
+
for (const file of all) {
|
|
102
|
+
const content = await (0, fs_1.readFileIfExists)(file);
|
|
103
|
+
if (content === null)
|
|
104
|
+
continue;
|
|
105
|
+
const rel = path_1.default.relative(pkgRoot, file).replace(/\\/g, "/");
|
|
106
|
+
entries.push({
|
|
107
|
+
file,
|
|
108
|
+
rel,
|
|
109
|
+
content,
|
|
110
|
+
lines: content.split("\n"),
|
|
111
|
+
isTest: TEST_EXT.test(file),
|
|
112
|
+
isE2e: E2E_DIR_RE.test(rel),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return entries;
|
|
116
|
+
}
|
|
117
|
+
function searchSignalInCorpus(corpus, signal) {
|
|
118
|
+
var _a;
|
|
119
|
+
const hits = [];
|
|
120
|
+
if (signal.length < 2)
|
|
121
|
+
return hits;
|
|
122
|
+
for (const entry of corpus) {
|
|
123
|
+
const idx = entry.content.indexOf(signal);
|
|
124
|
+
if (idx === -1)
|
|
125
|
+
continue;
|
|
126
|
+
const line = entry.content.slice(0, idx).split("\n").length;
|
|
127
|
+
const snippet = ((_a = entry.lines[line - 1]) !== null && _a !== void 0 ? _a : "").trim().slice(0, 200);
|
|
128
|
+
hits.push({ file: entry.rel, line, snippet });
|
|
129
|
+
if (hits.length >= 5)
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
return hits;
|
|
133
|
+
}
|
|
134
|
+
/** 解析 08 报告证据列中的 file:line */
|
|
135
|
+
function parseEvidenceRefs(evidence) {
|
|
136
|
+
const refs = [];
|
|
137
|
+
const re = /([A-Za-z0-9_./@-]+\.(?:vue|tsx?|jsx?|spec\.tsx?|test\.tsx?)):(\d+)/g;
|
|
138
|
+
for (const m of evidence.matchAll(re)) {
|
|
139
|
+
refs.push({ rel: m[1].replace(/^\.\//, ""), line: Number(m[2]) });
|
|
140
|
+
}
|
|
141
|
+
return refs;
|
|
142
|
+
}
|
|
143
|
+
async function verifyEvidenceRefs(pkgRoot, evidence, signals) {
|
|
144
|
+
var _a;
|
|
145
|
+
const refs = parseEvidenceRefs(evidence);
|
|
146
|
+
if (refs.length === 0) {
|
|
147
|
+
return { ok: false, detail: "证据中未解析到 file:line 路径" };
|
|
148
|
+
}
|
|
149
|
+
for (const ref of refs) {
|
|
150
|
+
const abs = path_1.default.join(pkgRoot, ref.rel);
|
|
151
|
+
const content = await (0, fs_1.readFileIfExists)(abs);
|
|
152
|
+
if (content === null) {
|
|
153
|
+
return { ok: false, detail: `证据路径不存在: ${ref.rel}` };
|
|
154
|
+
}
|
|
155
|
+
const lines = content.split("\n");
|
|
156
|
+
const lineText = (_a = lines[ref.line - 1]) !== null && _a !== void 0 ? _a : "";
|
|
157
|
+
if (!lineText.trim()) {
|
|
158
|
+
return { ok: false, detail: `${ref.rel}:${ref.line} 为空行` };
|
|
159
|
+
}
|
|
160
|
+
if (signals.length > 0) {
|
|
161
|
+
const anySignalOnLine = signals.some((s) => lineText.includes(s) || evidence.includes(s));
|
|
162
|
+
const anySignalInFile = signals.some((s) => content.includes(s));
|
|
163
|
+
if (!anySignalOnLine && !anySignalInFile) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
detail: `${ref.rel}:${ref.line} 未包含 PRD 字面量「${signals[0]}」等可核对内容`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { ok: true, detail: "证据路径与字面量可核对" };
|
|
172
|
+
}
|
|
173
|
+
function hasTestCoverageForAtom(atomId, signals, testCorpus) {
|
|
174
|
+
let hasTest = false;
|
|
175
|
+
let hasE2e = false;
|
|
176
|
+
for (const entry of testCorpus) {
|
|
177
|
+
const hasId = entry.content.includes(atomId) || entry.content.includes(`[${atomId}]`);
|
|
178
|
+
const hasSignal = signals.some((s) => s.length >= 4 && entry.content.includes(s));
|
|
179
|
+
if (hasId || hasSignal) {
|
|
180
|
+
hasTest = true;
|
|
181
|
+
if (entry.isE2e)
|
|
182
|
+
hasE2e = true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { hasTest, hasE2e };
|
|
186
|
+
}
|
|
187
|
+
function reconcileStatusWithReport(semanticStatus, verdict, classification, signals) {
|
|
188
|
+
if (!verdict)
|
|
189
|
+
return { consistent: false, detail: "08 报告无此行" };
|
|
190
|
+
if (verdict === "一致") {
|
|
191
|
+
if (semanticStatus === "matched" || semanticStatus === "unverifiable") {
|
|
192
|
+
return { consistent: true, detail: "08 一致且语义匹配" };
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
consistent: false,
|
|
196
|
+
detail: `08 标「一致」但脚本 diff=${semanticStatus}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (verdict === "不一致-按PRD修复") {
|
|
200
|
+
if (semanticStatus === "missing_in_code" || semanticStatus === "partial") {
|
|
201
|
+
return { consistent: true, detail: "08 不一致与脚本 diff 吻合" };
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
consistent: false,
|
|
205
|
+
detail: `08 标「不一致」但脚本 diff=${semanticStatus}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (verdict === "待人工确认") {
|
|
209
|
+
if (semanticStatus === "unverifiable" ||
|
|
210
|
+
(classification === "behavior-verifiable" && signals.length === 0)) {
|
|
211
|
+
return { consistent: true, detail: "待人工确认与不可机械验证一致" };
|
|
212
|
+
}
|
|
213
|
+
if (semanticStatus === "missing_in_code" || semanticStatus === "partial") {
|
|
214
|
+
return { consistent: true, detail: "待人工确认:脚本发现缺口,等待人工裁定" };
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
consistent: false,
|
|
218
|
+
detail: `08 标「待人工确认」但脚本已完全匹配(diff=${semanticStatus}),应标「一致」`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return { consistent: false, detail: `非法结论: ${verdict}` };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* PRD × 代码语义 diff:提取字面量、搜索代码库、校验 08 证据、检查测试覆盖。
|
|
225
|
+
*/
|
|
226
|
+
async function runPrdSemanticDiff(args) {
|
|
227
|
+
var _a;
|
|
228
|
+
const corpus = await buildCodeCorpus(args.pkgRoot);
|
|
229
|
+
const testCorpus = corpus.filter((c) => c.isTest);
|
|
230
|
+
const items = [];
|
|
231
|
+
for (const atom of args.atoms) {
|
|
232
|
+
const classification = classifyPrdAtom(atom);
|
|
233
|
+
const signals = extractVerifiableSignals(atom.text);
|
|
234
|
+
const requiresAutomatedTest = atomRequiresAutomatedTest(classification, signals);
|
|
235
|
+
const report = args.reportRows.get(atom.id);
|
|
236
|
+
const verdict = (_a = report === null || report === void 0 ? void 0 : report.verdict) !== null && _a !== void 0 ? _a : null;
|
|
237
|
+
let codeHits = [];
|
|
238
|
+
const missingSignals = [];
|
|
239
|
+
for (const signal of signals) {
|
|
240
|
+
const hits = searchSignalInCorpus(corpus.filter((c) => !c.isTest), signal);
|
|
241
|
+
if (hits.length === 0) {
|
|
242
|
+
missingSignals.push(signal);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
codeHits = codeHits.concat(hits);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
let status;
|
|
249
|
+
if (classification === "structural") {
|
|
250
|
+
status = "unverifiable";
|
|
251
|
+
}
|
|
252
|
+
else if (signals.length === 0 && classification === "narrative") {
|
|
253
|
+
status = "unverifiable";
|
|
254
|
+
}
|
|
255
|
+
else if (signals.length === 0 && classification === "behavior-verifiable") {
|
|
256
|
+
const { hasTest } = hasTestCoverageForAtom(atom.id, signals, testCorpus);
|
|
257
|
+
status = hasTest ? "matched" : "missing_in_code";
|
|
258
|
+
}
|
|
259
|
+
else if (missingSignals.length === 0 && signals.length > 0) {
|
|
260
|
+
status = "matched";
|
|
261
|
+
}
|
|
262
|
+
else if (missingSignals.length > 0 && missingSignals.length < signals.length) {
|
|
263
|
+
status = "partial";
|
|
264
|
+
}
|
|
265
|
+
else if (signals.length > 0) {
|
|
266
|
+
status = "missing_in_code";
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
status = "unverifiable";
|
|
270
|
+
}
|
|
271
|
+
const { hasTest, hasE2e } = hasTestCoverageForAtom(atom.id, signals, testCorpus);
|
|
272
|
+
let reportEvidenceValid = false;
|
|
273
|
+
if (report === null || report === void 0 ? void 0 : report.codeEvidence) {
|
|
274
|
+
const ev = await verifyEvidenceRefs(args.pkgRoot, report.codeEvidence, signals);
|
|
275
|
+
reportEvidenceValid = ev.ok;
|
|
276
|
+
}
|
|
277
|
+
const reconcile = reconcileStatusWithReport(status, verdict, classification, signals);
|
|
278
|
+
let reportConsistent = reconcile.consistent;
|
|
279
|
+
if (verdict === "一致" && !reportEvidenceValid && signals.length > 0) {
|
|
280
|
+
reportConsistent = false;
|
|
281
|
+
}
|
|
282
|
+
if (!reportConsistent && verdict) {
|
|
283
|
+
status = "report_mismatch";
|
|
284
|
+
}
|
|
285
|
+
const humanPending = verdict === "待人工确认";
|
|
286
|
+
const waiveTestRequirement = humanPending && classification !== "ui-verifiable";
|
|
287
|
+
items.push({
|
|
288
|
+
atomId: atom.id,
|
|
289
|
+
atomText: atom.text,
|
|
290
|
+
sectionPath: atom.sectionPath,
|
|
291
|
+
classification,
|
|
292
|
+
signals,
|
|
293
|
+
status,
|
|
294
|
+
codeHits: codeHits.slice(0, 8),
|
|
295
|
+
missingSignals,
|
|
296
|
+
requiresAutomatedTest: requiresAutomatedTest && !waiveTestRequirement,
|
|
297
|
+
hasAutomatedTest: hasTest,
|
|
298
|
+
hasE2eTest: hasE2e,
|
|
299
|
+
reportVerdict: verdict,
|
|
300
|
+
reportEvidenceValid,
|
|
301
|
+
reportConsistent,
|
|
302
|
+
detail: reconcile.detail,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const openHuman = items.filter((i) => i.reportVerdict === "待人工确认").length;
|
|
306
|
+
const summary = {
|
|
307
|
+
total: items.length,
|
|
308
|
+
matched: items.filter((i) => i.status === "matched").length,
|
|
309
|
+
partial: items.filter((i) => i.status === "partial").length,
|
|
310
|
+
missing: items.filter((i) => i.status === "missing_in_code").length,
|
|
311
|
+
unverifiable: items.filter((i) => i.status === "unverifiable").length,
|
|
312
|
+
reportMismatch: items.filter((i) => i.status === "report_mismatch").length,
|
|
313
|
+
requiresTest: items.filter((i) => i.requiresAutomatedTest).length,
|
|
314
|
+
missingTest: items.filter((i) => i.requiresAutomatedTest && !i.hasAutomatedTest).length,
|
|
315
|
+
missingE2e: items.filter((i) => i.requiresAutomatedTest && !i.hasE2eTest).length,
|
|
316
|
+
};
|
|
317
|
+
const ok = summary.missing === 0 &&
|
|
318
|
+
summary.partial === 0 &&
|
|
319
|
+
summary.reportMismatch === 0 &&
|
|
320
|
+
summary.missingTest === 0 &&
|
|
321
|
+
summary.missingE2e === 0 &&
|
|
322
|
+
openHuman === 0;
|
|
323
|
+
return { ok, items, summary };
|
|
324
|
+
}
|
|
325
|
+
function renderSemanticDiffReportMarkdown(result) {
|
|
326
|
+
const lines = [
|
|
327
|
+
"# 09-PRD语义diff报告(脚本自动生成)",
|
|
328
|
+
"",
|
|
329
|
+
"> 本报告由 `sdd-flow-kit prd-diff` 机械生成,用于校验 PRD 字面量是否在代码/测试中出现。",
|
|
330
|
+
"> **不揣摩**:无法提取字面量的条目标为 `unverifiable`,须在 08 报告中标 `待人工确认`。",
|
|
331
|
+
"",
|
|
332
|
+
"## 摘要",
|
|
333
|
+
"",
|
|
334
|
+
`- 总条目: ${result.summary.total}`,
|
|
335
|
+
`- 代码匹配 matched: ${result.summary.matched}`,
|
|
336
|
+
`- 部分匹配 partial: ${result.summary.partial}`,
|
|
337
|
+
`- 代码缺失 missing_in_code: ${result.summary.missing}`,
|
|
338
|
+
`- 不可机械验证 unverifiable: ${result.summary.unverifiable}`,
|
|
339
|
+
`- 08 报告与脚本不一致 report_mismatch: ${result.summary.reportMismatch}`,
|
|
340
|
+
`- 须自动化测试: ${result.summary.requiresTest},缺测试: ${result.summary.missingTest},缺 E2E: ${result.summary.missingE2e}`,
|
|
341
|
+
"",
|
|
342
|
+
"## 逐条 diff",
|
|
343
|
+
"",
|
|
344
|
+
"| PRD-ID | 分类 | 状态 | PRD 信号 | 代码命中 | 缺测试 | 08 一致 | 说明 |",
|
|
345
|
+
"|--------|------|------|---------|---------|--------|---------|------|",
|
|
346
|
+
];
|
|
347
|
+
for (const item of result.items) {
|
|
348
|
+
const signals = item.signals.slice(0, 2).join(";") || "—";
|
|
349
|
+
const hits = item.codeHits.length > 0
|
|
350
|
+
? item.codeHits
|
|
351
|
+
.slice(0, 2)
|
|
352
|
+
.map((h) => `${h.file}:${h.line}`)
|
|
353
|
+
.join(";")
|
|
354
|
+
: "—";
|
|
355
|
+
const missTest = item.requiresAutomatedTest && !item.hasAutomatedTest ? "是" : "—";
|
|
356
|
+
const ok08 = item.reportConsistent ? "是" : "否";
|
|
357
|
+
lines.push(`| ${item.atomId} | ${item.classification} | ${item.status} | ${signals.replace(/\|/g, "/")} | ${hits} | ${missTest} | ${ok08} | ${item.detail.replace(/\|/g, "/")} |`);
|
|
358
|
+
}
|
|
359
|
+
lines.push("");
|
|
360
|
+
return lines.join("\n");
|
|
361
|
+
}
|
|
362
|
+
async function writeSemanticDiffArtifacts(outputRoot, result) {
|
|
363
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, ".prd-semantic-diff.json"), `${JSON.stringify(result, null, 2)}\n`);
|
|
364
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "09-PRD语义diff报告.md"), renderSemanticDiffReportMarkdown(result));
|
|
365
|
+
}
|
|
@@ -33,6 +33,7 @@ function defaultSession(args) {
|
|
|
33
33
|
createdAtISO: new Date().toISOString(),
|
|
34
34
|
implGitBaseline: null,
|
|
35
35
|
validateRetryCount: 0,
|
|
36
|
+
prdFixRetryCount: 0,
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
/** 将旧版 session(无 phase 字段)迁移为当前结构 */
|
|
@@ -53,5 +54,6 @@ function normalizeSession(raw, outputRoot) {
|
|
|
53
54
|
createdAtISO: String((_e = raw.createdAtISO) !== null && _e !== void 0 ? _e : new Date().toISOString()),
|
|
54
55
|
implGitBaseline: raw.implGitBaseline ? String(raw.implGitBaseline) : null,
|
|
55
56
|
validateRetryCount: typeof raw.validateRetryCount === "number" ? raw.validateRetryCount : 0,
|
|
57
|
+
prdFixRetryCount: typeof raw.prdFixRetryCount === "number" ? raw.prdFixRetryCount : 0,
|
|
56
58
|
};
|
|
57
59
|
}
|
package/dist/core/writeNext.js
CHANGED
|
@@ -63,6 +63,11 @@ async function writeInitialNext(outputRoot, projectRoot, runId) {
|
|
|
63
63
|
"",
|
|
64
64
|
"## 6. 阶段 E — 验收签收与交付",
|
|
65
65
|
"```bash",
|
|
66
|
+
`npx sdd-flow-kit prd-review --project-root "${pr}" --run-id ${runId}`,
|
|
67
|
+
"# 在 AI 工具执行 08-PRD一致性复查提示词.md(最细粒度逐条对比,禁止揣摩)",
|
|
68
|
+
`npx sdd-flow-kit prd-diff --project-root "${pr}" --run-id ${runId}`,
|
|
69
|
+
"# 若失败:每项最多 3 次自动修复(附上次错误摘要);validate 内置;亦可手动 prd-remediate --change <name>",
|
|
70
|
+
`npx sdd-flow-kit gate --project-root "${pr}" --run-id ${runId} --expect prd-coverage`,
|
|
66
71
|
`npx sdd-flow-kit validate --project-root "${pr}" --run-id ${runId} --change <kebab-name>`,
|
|
67
72
|
`npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to validate-done --change <kebab-name>`,
|
|
68
73
|
"# 在 05 中将 P0 标为已验收,签收状态改为已签收",
|
package/dist/steps/runDeliver.js
CHANGED
|
@@ -42,6 +42,19 @@ async function runDeliver(options) {
|
|
|
42
42
|
detail: "ac-signed gate 未通过,禁止交付(请在 05-验收清单.md 完成 P0 签收)",
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
const prdCoverageGate = await (0, runGate_1.runGate)({
|
|
46
|
+
projectRoot: options.projectRoot,
|
|
47
|
+
runId: loaded.resolved.runId,
|
|
48
|
+
expect: "prd-coverage",
|
|
49
|
+
change: options.change,
|
|
50
|
+
});
|
|
51
|
+
if (!prdCoverageGate.ok) {
|
|
52
|
+
(0, runGate_1.printGateResult)(prdCoverageGate);
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
detail: "prd-coverage gate 未通过,禁止交付(请完成 08-PRD一致性复查报告.md)",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
45
58
|
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
46
59
|
const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
|
|
47
60
|
const agent = options.agent || "cursor";
|
package/dist/steps/runGate.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.runGate = runGate;
|
|
|
7
7
|
exports.printGateResult = printGateResult;
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const prdArtifacts_1 = require("../core/prdArtifacts");
|
|
10
|
+
const prdCoverage_1 = require("../core/prdCoverage");
|
|
10
11
|
const gitDiffGuard_1 = require("../core/gitDiffGuard");
|
|
11
12
|
const acMatrix_1 = require("../core/acMatrix");
|
|
12
13
|
const projectRoots_1 = require("../core/projectRoots");
|
|
@@ -179,6 +180,15 @@ async function runGate(options) {
|
|
|
179
180
|
checks.push({ name: "acceptance-checklist-signed", ...signed });
|
|
180
181
|
return pass(checks, options.expect, runId);
|
|
181
182
|
}
|
|
183
|
+
if (options.expect === "prd-coverage") {
|
|
184
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
185
|
+
const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
|
|
186
|
+
const coverage = await (0, prdCoverage_1.checkPrdCoverage)(outputRoot, pkgRoot);
|
|
187
|
+
for (const c of coverage.checks) {
|
|
188
|
+
checks.push({ name: c.name, ok: c.ok, detail: c.detail });
|
|
189
|
+
}
|
|
190
|
+
return pass(checks, options.expect, runId);
|
|
191
|
+
}
|
|
182
192
|
if (options.expect === "propose-ready") {
|
|
183
193
|
const changeName = await readActiveChange(options.projectRoot, session, options.change);
|
|
184
194
|
if (!changeName) {
|
|
@@ -287,7 +297,30 @@ async function runGate(options) {
|
|
|
287
297
|
break;
|
|
288
298
|
}
|
|
289
299
|
}
|
|
290
|
-
// 2)
|
|
300
|
+
// 2) PRD 一致性复查签收
|
|
301
|
+
const prdMarker = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, ".prd-coverage-pass.json"));
|
|
302
|
+
if (prdMarker) {
|
|
303
|
+
try {
|
|
304
|
+
const j = JSON.parse(prdMarker);
|
|
305
|
+
const ok = !j.change || j.change === changeName;
|
|
306
|
+
checks.push({
|
|
307
|
+
name: "prd-coverage-pass",
|
|
308
|
+
ok,
|
|
309
|
+
detail: ok ? "PRD 一致性复查已通过" : "prd-coverage-pass.json 与 change 不匹配",
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
checks.push({ name: "prd-coverage-pass", ok: false, detail: ".prd-coverage-pass.json 解析失败" });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
checks.push({
|
|
318
|
+
name: "prd-coverage-pass",
|
|
319
|
+
ok: false,
|
|
320
|
+
detail: "缺少 .prd-coverage-pass.json,请先 gate prd-coverage 或 validate",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
// 3) 验收报告签收文件
|
|
291
324
|
const marker = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, ".validate-pass.json"));
|
|
292
325
|
if (marker) {
|
|
293
326
|
try {
|
|
@@ -172,6 +172,10 @@ async function runPhaseAdvance(options) {
|
|
|
172
172
|
"在 AI 工具中执行:`/opsx-apply`(必须先完成 tasks.md 全部 [x])",
|
|
173
173
|
"完成后验收:",
|
|
174
174
|
"```bash",
|
|
175
|
+
`npx sdd-flow-kit prd-review --project-root ${options.projectRoot} --run-id ${runId}`,
|
|
176
|
+
"# AI 执行 08-PRD一致性复查提示词.md(最细粒度,禁止揣摩)",
|
|
177
|
+
`npx sdd-flow-kit prd-diff --project-root ${options.projectRoot} --run-id ${runId}`,
|
|
178
|
+
`npx sdd-flow-kit gate --project-root ${options.projectRoot} --run-id ${runId} --expect prd-coverage`,
|
|
175
179
|
`npx sdd-flow-kit validate --project-root ${options.projectRoot} --run-id ${runId} --change ${changeName}`,
|
|
176
180
|
`npx sdd-flow-kit phase advance --project-root ${options.projectRoot} --run-id ${runId} --to validate-done --change ${changeName}`,
|
|
177
181
|
"```",
|
|
@@ -0,0 +1,71 @@
|
|
|
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.runPrdDiff = runPrdDiff;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const resolveRun_1 = require("./resolveRun");
|
|
9
|
+
const fs_1 = require("../core/fs");
|
|
10
|
+
const prdParse_1 = require("../core/prdParse");
|
|
11
|
+
const prdCoverage_1 = require("../core/prdCoverage");
|
|
12
|
+
const projectRoots_1 = require("../core/projectRoots");
|
|
13
|
+
const prdSemanticDiff_1 = require("../core/prdSemanticDiff");
|
|
14
|
+
/**
|
|
15
|
+
* 脚本级 PRD × 代码语义 diff(不依赖 AI 填表结论)。
|
|
16
|
+
*/
|
|
17
|
+
async function runPrdDiff(options) {
|
|
18
|
+
const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
|
|
19
|
+
if (!(loaded === null || loaded === void 0 ? void 0 : loaded.resolved)) {
|
|
20
|
+
return { ok: false, detail: "未找到 PRD run", semantic: null };
|
|
21
|
+
}
|
|
22
|
+
const outputRoot = loaded.resolved.outputRoot;
|
|
23
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
24
|
+
const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
|
|
25
|
+
const prdContent = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, "source", "PRD.md"));
|
|
26
|
+
if (!prdContent) {
|
|
27
|
+
return { ok: false, detail: "缺少 source/PRD.md", semantic: null };
|
|
28
|
+
}
|
|
29
|
+
let atoms = await (0, prdCoverage_1.loadPrdAtomsFromManifest)(outputRoot);
|
|
30
|
+
if (atoms.length === 0)
|
|
31
|
+
atoms = (0, prdParse_1.parsePrdAtoms)(prdContent);
|
|
32
|
+
const reportContent = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, "08-PRD一致性复查报告.md"));
|
|
33
|
+
const reportRows = new Map();
|
|
34
|
+
if (reportContent) {
|
|
35
|
+
for (const row of (0, prdCoverage_1.parseReviewReportTable)(reportContent)) {
|
|
36
|
+
reportRows.set(row.prdId, { verdict: row.verdict, codeEvidence: row.codeEvidence });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (options.dryRun) {
|
|
40
|
+
return {
|
|
41
|
+
ok: true,
|
|
42
|
+
detail: `dry-run: would semantic-diff ${atoms.length} atoms in ${pkgRoot}`,
|
|
43
|
+
semantic: null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const semantic = await (0, prdSemanticDiff_1.runPrdSemanticDiff)({ pkgRoot, atoms, reportRows });
|
|
47
|
+
await (0, prdSemanticDiff_1.writeSemanticDiffArtifacts)(outputRoot, semantic);
|
|
48
|
+
if (!semantic.ok) {
|
|
49
|
+
const parts = [];
|
|
50
|
+
if (semantic.summary.missing > 0)
|
|
51
|
+
parts.push(`代码缺失 ${semantic.summary.missing}`);
|
|
52
|
+
if (semantic.summary.partial > 0)
|
|
53
|
+
parts.push(`部分匹配 ${semantic.summary.partial}`);
|
|
54
|
+
if (semantic.summary.reportMismatch > 0)
|
|
55
|
+
parts.push(`08不一致 ${semantic.summary.reportMismatch}`);
|
|
56
|
+
if (semantic.summary.missingTest > 0)
|
|
57
|
+
parts.push(`缺自动化测试 ${semantic.summary.missingTest}`);
|
|
58
|
+
if (semantic.summary.missingE2e > 0)
|
|
59
|
+
parts.push(`缺E2E ${semantic.summary.missingE2e}`);
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
detail: `PRD 语义 diff 未通过: ${parts.join(", ")}。见 09-PRD语义diff报告.md`,
|
|
63
|
+
semantic,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
detail: `PRD 语义 diff 通过(${semantic.summary.total} 条,matched=${semantic.summary.matched})`,
|
|
69
|
+
semantic,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runPrdRemediate = runPrdRemediate;
|
|
4
|
+
const resolveRun_1 = require("./resolveRun");
|
|
5
|
+
const projectRoots_1 = require("../core/projectRoots");
|
|
6
|
+
const runPrdDiff_1 = require("./runPrdDiff");
|
|
7
|
+
const prdRemediate_1 = require("../core/prdRemediate");
|
|
8
|
+
const sessionState_1 = require("../core/sessionState");
|
|
9
|
+
/**
|
|
10
|
+
* PRD 不一致自动修复:按 09 报告调 /opsx-apply 修复,每项最多 3 次,再 prd-diff;失败项 escalated 到人工确认清单。
|
|
11
|
+
*/
|
|
12
|
+
async function runPrdRemediate(options) {
|
|
13
|
+
var _a;
|
|
14
|
+
const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
|
|
15
|
+
if (!(loaded === null || loaded === void 0 ? void 0 : loaded.resolved)) {
|
|
16
|
+
return { ok: false, detail: "未找到 PRD run", attempts: 0, escalated: false, needUserConfirm: false };
|
|
17
|
+
}
|
|
18
|
+
if (!options.change) {
|
|
19
|
+
return { ok: false, detail: "需要 --change <name>", attempts: 0, escalated: false, needUserConfirm: false };
|
|
20
|
+
}
|
|
21
|
+
const outputRoot = loaded.resolved.outputRoot;
|
|
22
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
23
|
+
const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
|
|
24
|
+
const result = await (0, prdRemediate_1.runPrdRemediateLoop)({
|
|
25
|
+
projectRoot: pkgRoot,
|
|
26
|
+
outputRoot,
|
|
27
|
+
runId: loaded.resolved.runId,
|
|
28
|
+
change: options.change,
|
|
29
|
+
agent: options.agent,
|
|
30
|
+
dryRun: options.dryRun,
|
|
31
|
+
maxAttempts: (_a = options.maxAttempts) !== null && _a !== void 0 ? _a : prdRemediate_1.MAX_PRD_FIX_RETRIES,
|
|
32
|
+
runDiff: async () => {
|
|
33
|
+
const diff = await (0, runPrdDiff_1.runPrdDiff)({
|
|
34
|
+
projectRoot: options.projectRoot,
|
|
35
|
+
runId: loaded.resolved.runId,
|
|
36
|
+
dryRun: false,
|
|
37
|
+
});
|
|
38
|
+
return { ok: diff.ok, semantic: diff.semantic };
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
if (loaded.session) {
|
|
42
|
+
await (0, sessionState_1.writeSession)(outputRoot, {
|
|
43
|
+
...loaded.session,
|
|
44
|
+
prdFixRetryCount: result.attempts,
|
|
45
|
+
status: result.ok ? "validate_in_progress" : result.escalated ? "awaiting_user_confirmation" : "apply_in_progress",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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.runPrdReview = runPrdReview;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const templates_1 = require("../core/templates");
|
|
9
|
+
const fs_1 = require("../core/fs");
|
|
10
|
+
const prdParse_1 = require("../core/prdParse");
|
|
11
|
+
const prdCoverage_1 = require("../core/prdCoverage");
|
|
12
|
+
const resolveRun_1 = require("./resolveRun");
|
|
13
|
+
const prdCoverage_2 = require("../core/prdCoverage");
|
|
14
|
+
function renderReportSkeleton(atoms, template) {
|
|
15
|
+
const tableRows = atoms
|
|
16
|
+
.map((a) => `| ${a.id} | ${a.text.replace(/\|/g, "\\|").slice(0, 120)} | (文件:行号 或 选择器 + 断言原文) | 待填写 | ${a.sectionPath} L${a.line} |`)
|
|
17
|
+
.join("\n");
|
|
18
|
+
return template
|
|
19
|
+
.replace("{{ATOM_COUNT}}", String(atoms.length))
|
|
20
|
+
.replace("{{REVIEW_TABLE_ROWS}}", tableRows)
|
|
21
|
+
.replace("{{GENERATED_AT}}", new Date().toISOString());
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 从 PRD 生成最细粒度原子清单,并初始化/刷新 08-PRD一致性复查报告.md 骨架。
|
|
25
|
+
*/
|
|
26
|
+
async function runPrdReview(options) {
|
|
27
|
+
const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
|
|
28
|
+
if (!(loaded === null || loaded === void 0 ? void 0 : loaded.resolved)) {
|
|
29
|
+
return { ok: false, detail: "未找到 PRD run", atomCount: 0 };
|
|
30
|
+
}
|
|
31
|
+
const outputRoot = loaded.resolved.outputRoot;
|
|
32
|
+
const prdPath = path_1.default.join(outputRoot, "source", "PRD.md");
|
|
33
|
+
const prdContent = await (0, fs_1.readFileIfExists)(prdPath);
|
|
34
|
+
if (!prdContent) {
|
|
35
|
+
return { ok: false, detail: "缺少 source/PRD.md", atomCount: 0 };
|
|
36
|
+
}
|
|
37
|
+
const atoms = (0, prdParse_1.parsePrdAtoms)(prdContent);
|
|
38
|
+
if (atoms.length === 0) {
|
|
39
|
+
return { ok: false, detail: "PRD 未解析到原子条目", atomCount: 0 };
|
|
40
|
+
}
|
|
41
|
+
if (options.dryRun) {
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
detail: `dry-run: 将生成 ${atoms.length} 条 PRD 原子与 08 报告骨架`,
|
|
45
|
+
atomCount: atoms.length,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
await (0, prdCoverage_1.writePrdAtomsManifest)(outputRoot, atoms);
|
|
49
|
+
const reportPath = path_1.default.join(outputRoot, "08-PRD一致性复查报告.md");
|
|
50
|
+
const existing = await (0, fs_1.readFileIfExists)(reportPath);
|
|
51
|
+
const reviewComplete = existing && /复查状态[::]\s*[`「]*复查完成/.test(existing);
|
|
52
|
+
if (!existing || !reviewComplete) {
|
|
53
|
+
const template = await (0, templates_1.loadTemplateText)("artifacts/08-PRD一致性复查报告.template.md");
|
|
54
|
+
const promptTemplate = await (0, templates_1.loadTemplateText)("prompts/08-prd-consistency-review-prompt.md");
|
|
55
|
+
const body = renderReportSkeleton(atoms, template);
|
|
56
|
+
await (0, fs_1.writeTextFileEnsuredDir)(reportPath, body);
|
|
57
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "08-PRD一致性复查提示词.md"), promptTemplate.replace(/\{\{ATOM_COUNT\}\}/g, String(atoms.length)));
|
|
58
|
+
}
|
|
59
|
+
const coverage = await (0, prdCoverage_2.checkPrdCoverage)(outputRoot, options.projectRoot);
|
|
60
|
+
if (coverage.ok) {
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
detail: `PRD 一致性复查通过(${atoms.length} 条原子条目)`,
|
|
64
|
+
atomCount: atoms.length,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const failed = coverage.checks.filter((c) => !c.ok).map((c) => c.name);
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
detail: `已生成/更新 08 报告骨架(${atoms.length} 条)。待完成复查: ${failed.join(", ")}。请执行 08-PRD一致性复查提示词.md 后重试 gate prd-coverage`,
|
|
71
|
+
atomCount: atoms.length,
|
|
72
|
+
};
|
|
73
|
+
}
|