@xenonbyte/da-vinci-workflow 0.1.26 → 0.2.2
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/CHANGELOG.md +31 -0
- package/README.md +28 -65
- package/README.zh-CN.md +28 -65
- package/bin/da-vinci-tui.js +8 -0
- package/commands/claude/dv/continue.md +5 -0
- package/commands/codex/prompts/dv-continue.md +6 -1
- package/commands/gemini/dv/continue.toml +5 -0
- package/commands/templates/dv-continue.shared.md +33 -0
- package/docs/dv-command-reference.md +35 -0
- package/docs/execution-chain-migration.md +46 -0
- package/docs/execution-chain-plan.md +125 -0
- package/docs/prompt-entrypoints.md +8 -0
- package/docs/skill-usage.md +217 -0
- package/docs/workflow-examples.md +10 -0
- package/docs/workflow-overview.md +26 -0
- package/docs/zh-CN/dv-command-reference.md +35 -0
- package/docs/zh-CN/execution-chain-migration.md +46 -0
- package/docs/zh-CN/prompt-entrypoints.md +8 -0
- package/docs/zh-CN/skill-usage.md +217 -0
- package/docs/zh-CN/workflow-examples.md +10 -0
- package/docs/zh-CN/workflow-overview.md +26 -0
- package/lib/artifact-parsers.js +120 -0
- package/lib/audit.js +61 -0
- package/lib/cli.js +351 -13
- package/lib/diff-spec.js +242 -0
- package/lib/execution-signals.js +136 -0
- package/lib/lint-bindings.js +143 -0
- package/lib/lint-spec.js +408 -0
- package/lib/lint-tasks.js +176 -0
- package/lib/planning-parsers.js +567 -0
- package/lib/scaffold.js +193 -0
- package/lib/scope-check.js +603 -0
- package/lib/sidecars.js +369 -0
- package/lib/supervisor-review.js +28 -3
- package/lib/utils.js +10 -2
- package/lib/verify.js +652 -0
- package/lib/workflow-contract.js +107 -0
- package/lib/workflow-persisted-state.js +297 -0
- package/lib/workflow-state.js +785 -0
- package/package.json +13 -3
- package/references/artifact-templates.md +26 -0
- package/references/checkpoints.md +14 -0
- package/references/modes.md +10 -0
- package/tui/catalog.js +1190 -0
- package/tui/index.js +727 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { getMarkdownSection } = require("./audit-parsers");
|
|
3
|
+
const { pathExists, readTextIfExists } = require("./utils");
|
|
4
|
+
const { STATUS } = require("./workflow-contract");
|
|
5
|
+
const {
|
|
6
|
+
parseListItems,
|
|
7
|
+
unique,
|
|
8
|
+
resolveChangeDir,
|
|
9
|
+
parseRuntimeSpecs
|
|
10
|
+
} = require("./planning-parsers");
|
|
11
|
+
|
|
12
|
+
const PAGE_NAME_PATTERN = /`([^`]+)`|([a-z0-9][a-z0-9 \-/]{1,80}?)\s+pages?\b/gi;
|
|
13
|
+
const STATE_SYNONYMS = {
|
|
14
|
+
success: ["success", "succeed", "succeeds", "succeeded"],
|
|
15
|
+
error: ["error", "fail", "fails", "failed", "failure"],
|
|
16
|
+
loading: ["loading", "load", "spinner"]
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function normalizeWhitespace(value) {
|
|
20
|
+
return String(value || "")
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[`*_~]/g, "")
|
|
23
|
+
.replace(/\s+/g, " ")
|
|
24
|
+
.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizePageKey(value) {
|
|
28
|
+
return normalizeWhitespace(value)
|
|
29
|
+
.replace(/\b(page|pages|screen|screens)\b/g, "")
|
|
30
|
+
.replace(/\s+/g, " ")
|
|
31
|
+
.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeStateKey(value) {
|
|
35
|
+
const raw = String(value || "").split(":")[0];
|
|
36
|
+
return normalizeWhitespace(raw)
|
|
37
|
+
.replace(/\bstate\b/g, "")
|
|
38
|
+
.replace(/\s+/g, " ")
|
|
39
|
+
.trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function registerName(map, key, label) {
|
|
43
|
+
if (!key) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!map.has(key)) {
|
|
47
|
+
map.set(key, String(label || key).trim());
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractPageCandidatesFromItem(item) {
|
|
52
|
+
const text = String(item || "");
|
|
53
|
+
const pages = [];
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = PAGE_NAME_PATTERN.exec(text)) !== null) {
|
|
56
|
+
const pageName = String(match[1] || match[2] || "").trim();
|
|
57
|
+
if (pageName) {
|
|
58
|
+
pages.push(pageName);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (/homepage/i.test(text)) {
|
|
62
|
+
pages.push("Homepage");
|
|
63
|
+
}
|
|
64
|
+
if (/product detail/i.test(text)) {
|
|
65
|
+
pages.push("Product Detail");
|
|
66
|
+
}
|
|
67
|
+
return unique(pages);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseProposal(text) {
|
|
71
|
+
const scopeSection = getMarkdownSection(text, "Scope");
|
|
72
|
+
const nonGoalsSection = getMarkdownSection(text, "Non-Goals");
|
|
73
|
+
const scopeItems = parseListItems(scopeSection);
|
|
74
|
+
const nonGoalItems = parseListItems(nonGoalsSection);
|
|
75
|
+
|
|
76
|
+
const scopePages = unique(scopeItems.flatMap((item) => extractPageCandidatesFromItem(item)));
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
scopeItems,
|
|
80
|
+
nonGoalItems,
|
|
81
|
+
scopePages
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parsePageMap(text) {
|
|
86
|
+
const canonicalSection = getMarkdownSection(text, "Canonical Pages");
|
|
87
|
+
const statesPerPageSection = getMarkdownSection(text, "States Per Page");
|
|
88
|
+
|
|
89
|
+
const canonicalItems = parseListItems(canonicalSection);
|
|
90
|
+
const pageNames = [];
|
|
91
|
+
for (const item of canonicalItems) {
|
|
92
|
+
const first = String(item || "").split("->")[0];
|
|
93
|
+
const cleaned = String(first || "")
|
|
94
|
+
.replace(/`/g, "")
|
|
95
|
+
.trim();
|
|
96
|
+
if (!cleaned) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
pageNames.push(cleaned);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const statesPerPageItems = parseListItems(statesPerPageSection);
|
|
103
|
+
const stateNames = [];
|
|
104
|
+
for (const item of statesPerPageItems) {
|
|
105
|
+
const stateSlice = String(item || "").split(":").slice(1).join(":").trim();
|
|
106
|
+
if (!stateSlice) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
for (const statePart of stateSlice.split(/[;,/]| and /i)) {
|
|
110
|
+
const normalized = String(statePart || "").trim();
|
|
111
|
+
if (!normalized) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
stateNames.push(normalized);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
pages: unique(pageNames),
|
|
120
|
+
states: unique(stateNames),
|
|
121
|
+
canonicalItems,
|
|
122
|
+
statesPerPageItems
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseSpecs(changeDir, projectRoot) {
|
|
127
|
+
const specRecords = parseRuntimeSpecs(changeDir, projectRoot);
|
|
128
|
+
const stateNames = [];
|
|
129
|
+
const combinedTextChunks = [];
|
|
130
|
+
const records = [];
|
|
131
|
+
|
|
132
|
+
for (const record of specRecords) {
|
|
133
|
+
const text = record.text || "";
|
|
134
|
+
const parsed = record.parsed || {
|
|
135
|
+
missingSections: [],
|
|
136
|
+
emptySections: [],
|
|
137
|
+
sections: {
|
|
138
|
+
states: { items: [] }
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const relativePath = record.path || "";
|
|
142
|
+
const states = parsed.sections.states.items.map((item) => {
|
|
143
|
+
const value = String(item || "").split(":")[0].trim();
|
|
144
|
+
return value || item;
|
|
145
|
+
});
|
|
146
|
+
stateNames.push(...states);
|
|
147
|
+
combinedTextChunks.push(text);
|
|
148
|
+
records.push({
|
|
149
|
+
path: relativePath,
|
|
150
|
+
missingSections: parsed.missingSections,
|
|
151
|
+
emptySections: parsed.emptySections,
|
|
152
|
+
states
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
files: specRecords.map((item) => item.path),
|
|
158
|
+
records,
|
|
159
|
+
states: unique(stateNames),
|
|
160
|
+
combinedText: combinedTextChunks.join("\n")
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parsePencilDesign(text) {
|
|
165
|
+
const statesSection = getMarkdownSection(text, "States Represented");
|
|
166
|
+
const screensSection = getMarkdownSection(text, "Screens");
|
|
167
|
+
const stateItems = parseListItems(statesSection);
|
|
168
|
+
const screenItems = parseListItems(screensSection);
|
|
169
|
+
const states = [];
|
|
170
|
+
const pages = [];
|
|
171
|
+
|
|
172
|
+
for (const item of stateItems) {
|
|
173
|
+
const afterColon = String(item || "").split(":").slice(1).join(":").trim();
|
|
174
|
+
if (afterColon) {
|
|
175
|
+
for (const token of afterColon.split(/[;,/]| and /i)) {
|
|
176
|
+
const candidate = String(token || "").trim();
|
|
177
|
+
if (candidate) {
|
|
178
|
+
states.push(candidate);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
states.push(item);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const item of screenItems) {
|
|
187
|
+
const screenName = String(item || "").split("->")[0].replace(/`/g, "").trim();
|
|
188
|
+
if (screenName) {
|
|
189
|
+
pages.push(screenName);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
states: unique(states),
|
|
195
|
+
pages: unique(pages)
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseTasks(text) {
|
|
200
|
+
const normalizedText = normalizeWhitespace(text);
|
|
201
|
+
const taskGroups = [];
|
|
202
|
+
const checklistItems = [];
|
|
203
|
+
|
|
204
|
+
const lines = String(text || "").replace(/\r\n?/g, "\n").split("\n");
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
const groupMatch = line.match(/^\s{0,3}##\s+(\d+(?:\.\d+)*)\.\s+(.+)$/);
|
|
207
|
+
if (groupMatch) {
|
|
208
|
+
taskGroups.push({
|
|
209
|
+
id: groupMatch[1],
|
|
210
|
+
title: String(groupMatch[2] || "").trim()
|
|
211
|
+
});
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const checklistMatch = line.match(/^\s*-\s*\[[ xX]\]\s+(.+)$/);
|
|
216
|
+
if (checklistMatch) {
|
|
217
|
+
const item = String(checklistMatch[1] || "").trim();
|
|
218
|
+
if (item) {
|
|
219
|
+
checklistItems.push(item);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const mentionedPages = unique(checklistItems.flatMap((item) => extractPageCandidatesFromItem(item)));
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
text,
|
|
228
|
+
normalizedText,
|
|
229
|
+
taskGroups,
|
|
230
|
+
checklistItems,
|
|
231
|
+
mentionedPages
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function expandStateCoverageTokens(stateKey) {
|
|
236
|
+
const normalized = normalizeStateKey(stateKey);
|
|
237
|
+
if (!normalized) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
return STATE_SYNONYMS[normalized] || [normalized];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function containsAnyToken(source, tokens) {
|
|
244
|
+
const normalizedSource = normalizeWhitespace(source);
|
|
245
|
+
return (tokens || []).some((token) => normalizedSource.includes(normalizeWhitespace(token)));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildScopeResultEnvelope(projectRoot, strict) {
|
|
249
|
+
return {
|
|
250
|
+
status: STATUS.PASS,
|
|
251
|
+
failures: [],
|
|
252
|
+
warnings: [],
|
|
253
|
+
notes: [],
|
|
254
|
+
projectRoot,
|
|
255
|
+
changeId: null,
|
|
256
|
+
strict,
|
|
257
|
+
coverage: null,
|
|
258
|
+
matrix: {
|
|
259
|
+
pages: [],
|
|
260
|
+
states: []
|
|
261
|
+
},
|
|
262
|
+
summary: {
|
|
263
|
+
pages: 0,
|
|
264
|
+
states: 0
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function resolveChange(projectRoot, requestedChangeId, failures, notes) {
|
|
270
|
+
const resolved = resolveChangeDir(projectRoot, requestedChangeId);
|
|
271
|
+
failures.push(...resolved.failures);
|
|
272
|
+
notes.push(...resolved.notes);
|
|
273
|
+
return resolved.changeDir;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function finalizeResult(result) {
|
|
277
|
+
const hasFindings = result.failures.length > 0 || result.warnings.length > 0;
|
|
278
|
+
if (!hasFindings) {
|
|
279
|
+
result.status = STATUS.PASS;
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (result.strict) {
|
|
284
|
+
result.status = STATUS.BLOCK;
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
result.status = STATUS.WARN;
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function analyzeScopePropagation(result, proposal, pageMap, specs, pencilDesign, tasks) {
|
|
293
|
+
const pageNames = new Map();
|
|
294
|
+
const stateNames = new Map();
|
|
295
|
+
|
|
296
|
+
const proposalPageKeys = new Set();
|
|
297
|
+
for (const page of proposal.scopePages) {
|
|
298
|
+
const key = normalizePageKey(page);
|
|
299
|
+
if (!key) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
proposalPageKeys.add(key);
|
|
303
|
+
registerName(pageNames, key, page);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const pageMapPageKeys = new Set();
|
|
307
|
+
for (const page of pageMap.pages) {
|
|
308
|
+
const key = normalizePageKey(page);
|
|
309
|
+
if (!key) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
pageMapPageKeys.add(key);
|
|
313
|
+
registerName(pageNames, key, page);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const pencilPageKeys = new Set();
|
|
317
|
+
for (const page of pencilDesign.pages) {
|
|
318
|
+
const key = normalizePageKey(page);
|
|
319
|
+
if (!key) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
pencilPageKeys.add(key);
|
|
323
|
+
registerName(pageNames, key, page);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const taskPageKeys = new Set();
|
|
327
|
+
for (const page of tasks.mentionedPages) {
|
|
328
|
+
const key = normalizePageKey(page);
|
|
329
|
+
if (!key) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
taskPageKeys.add(key);
|
|
333
|
+
registerName(pageNames, key, page);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const scopeText = proposal.scopeItems.join(" ");
|
|
337
|
+
const specText = specs.combinedText;
|
|
338
|
+
const tasksText = tasks.text;
|
|
339
|
+
|
|
340
|
+
const allPageKeys = unique([
|
|
341
|
+
...proposalPageKeys,
|
|
342
|
+
...pageMapPageKeys,
|
|
343
|
+
...pencilPageKeys,
|
|
344
|
+
...taskPageKeys
|
|
345
|
+
]);
|
|
346
|
+
for (const key of allPageKeys) {
|
|
347
|
+
const tokens = STATE_SYNONYMS[key] || [key];
|
|
348
|
+
const row = {
|
|
349
|
+
key,
|
|
350
|
+
label: pageNames.get(key) || key,
|
|
351
|
+
proposal: proposalPageKeys.has(key),
|
|
352
|
+
pageMap: pageMapPageKeys.has(key),
|
|
353
|
+
spec: containsAnyToken(specText, tokens),
|
|
354
|
+
pencil: pencilPageKeys.has(key),
|
|
355
|
+
tasks: containsAnyToken(tasksText, tokens) || taskPageKeys.has(key)
|
|
356
|
+
};
|
|
357
|
+
result.matrix.pages.push(row);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const specStateKeys = new Set();
|
|
361
|
+
for (const state of specs.states) {
|
|
362
|
+
const key = normalizeStateKey(state);
|
|
363
|
+
if (!key) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
specStateKeys.add(key);
|
|
367
|
+
registerName(stateNames, key, state);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const pageMapStateKeys = new Set();
|
|
371
|
+
for (const state of pageMap.states) {
|
|
372
|
+
const key = normalizeStateKey(state);
|
|
373
|
+
if (!key) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
pageMapStateKeys.add(key);
|
|
377
|
+
registerName(stateNames, key, state);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const pencilStateKeys = new Set();
|
|
381
|
+
for (const state of pencilDesign.states) {
|
|
382
|
+
const key = normalizeStateKey(state);
|
|
383
|
+
if (!key) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
pencilStateKeys.add(key);
|
|
387
|
+
registerName(stateNames, key, state);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const taskStateKeys = new Set();
|
|
391
|
+
const taskText = tasks.text;
|
|
392
|
+
for (const key of specStateKeys) {
|
|
393
|
+
if (containsAnyToken(taskText, expandStateCoverageTokens(key))) {
|
|
394
|
+
taskStateKeys.add(key);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const allStateKeys = unique([...specStateKeys, ...pageMapStateKeys, ...pencilStateKeys, ...taskStateKeys]);
|
|
399
|
+
for (const key of allStateKeys) {
|
|
400
|
+
const tokens = expandStateCoverageTokens(key);
|
|
401
|
+
const row = {
|
|
402
|
+
key,
|
|
403
|
+
label: stateNames.get(key) || key,
|
|
404
|
+
spec: specStateKeys.has(key),
|
|
405
|
+
pageMap: pageMapStateKeys.has(key),
|
|
406
|
+
pencil: pencilStateKeys.has(key),
|
|
407
|
+
tasks: containsAnyToken(taskText, tokens) || taskStateKeys.has(key)
|
|
408
|
+
};
|
|
409
|
+
result.matrix.states.push(row);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
result.summary.pages = result.matrix.pages.length;
|
|
413
|
+
result.summary.states = result.matrix.states.length;
|
|
414
|
+
|
|
415
|
+
for (const row of result.matrix.pages) {
|
|
416
|
+
if (row.pageMap && !row.proposal) {
|
|
417
|
+
result.warnings.push(
|
|
418
|
+
`Page propagation gap: \`${row.label}\` is in page-map but missing from proposal scope.`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
if (row.proposal && !row.pageMap) {
|
|
422
|
+
result.warnings.push(
|
|
423
|
+
`Page propagation gap: \`${row.label}\` is in proposal scope but missing from page-map.`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
if (row.pageMap && !row.spec) {
|
|
427
|
+
result.warnings.push(`Page propagation gap: \`${row.label}\` is in page-map but missing from specs.`);
|
|
428
|
+
}
|
|
429
|
+
if (row.pageMap && !row.tasks) {
|
|
430
|
+
result.warnings.push(`Page propagation gap: \`${row.label}\` is in page-map but missing from tasks.`);
|
|
431
|
+
}
|
|
432
|
+
if (row.tasks && !row.proposal && !row.pageMap) {
|
|
433
|
+
result.warnings.push(`Overscoped task surface: \`${row.label}\` appears in tasks but is outside proposal/page-map scope.`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
for (const row of result.matrix.states) {
|
|
438
|
+
if (row.spec && !row.pageMap) {
|
|
439
|
+
result.warnings.push(
|
|
440
|
+
`State propagation gap: \`${row.label}\` is in specs but missing from page-map states.`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
if (row.spec && !row.pencil) {
|
|
444
|
+
result.warnings.push(
|
|
445
|
+
`State propagation gap: \`${row.label}\` is in specs but missing from pencil-design states.`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
if (row.spec && !row.tasks) {
|
|
449
|
+
result.warnings.push(`State propagation gap: \`${row.label}\` is in specs but missing from tasks.`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const scopeSet = new Set(proposal.scopeItems.map((item) => normalizeWhitespace(item)));
|
|
454
|
+
const nonGoalOverlap = proposal.nonGoalItems
|
|
455
|
+
.map((item) => normalizeWhitespace(item))
|
|
456
|
+
.filter((item) => scopeSet.has(item));
|
|
457
|
+
for (const value of unique(nonGoalOverlap)) {
|
|
458
|
+
result.warnings.push(`Contradictory scope signal: \`${value}\` appears in both Scope and Non-Goals.`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
result.coverage = {
|
|
462
|
+
proposal: {
|
|
463
|
+
scopeItems: proposal.scopeItems,
|
|
464
|
+
nonGoalItems: proposal.nonGoalItems,
|
|
465
|
+
pages: proposal.scopePages
|
|
466
|
+
},
|
|
467
|
+
pageMap: {
|
|
468
|
+
pages: pageMap.pages,
|
|
469
|
+
states: pageMap.states
|
|
470
|
+
},
|
|
471
|
+
specs: {
|
|
472
|
+
files: specs.files,
|
|
473
|
+
states: specs.states
|
|
474
|
+
},
|
|
475
|
+
pencilDesign: {
|
|
476
|
+
pages: pencilDesign.pages,
|
|
477
|
+
states: pencilDesign.states
|
|
478
|
+
},
|
|
479
|
+
tasks: {
|
|
480
|
+
groups: tasks.taskGroups,
|
|
481
|
+
checklistItems: tasks.checklistItems,
|
|
482
|
+
pages: tasks.mentionedPages
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function runScopeCheck(projectPathInput, options = {}) {
|
|
488
|
+
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
489
|
+
const strict = options.strict === true;
|
|
490
|
+
const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
|
|
491
|
+
const result = buildScopeResultEnvelope(projectRoot, strict);
|
|
492
|
+
|
|
493
|
+
if (!pathExists(projectRoot)) {
|
|
494
|
+
result.failures.push(`Project path does not exist: ${projectRoot}`);
|
|
495
|
+
result.notes.push("scope-check defaults to advisory mode; pass `--strict` to block on findings.");
|
|
496
|
+
result.notes.push(
|
|
497
|
+
"Promotion criteria: turn on `--strict` in CI after scope-check warning baselines are stable and reviewed."
|
|
498
|
+
);
|
|
499
|
+
return finalizeResult(result);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const changeDir = resolveChange(projectRoot, requestedChangeId, result.failures, result.notes);
|
|
503
|
+
if (!changeDir) {
|
|
504
|
+
result.notes.push("scope-check defaults to advisory mode; pass `--strict` to block on findings.");
|
|
505
|
+
result.notes.push(
|
|
506
|
+
"Promotion criteria: turn on `--strict` in CI after scope-check warning baselines are stable and reviewed."
|
|
507
|
+
);
|
|
508
|
+
return finalizeResult(result);
|
|
509
|
+
}
|
|
510
|
+
result.changeId = path.basename(changeDir);
|
|
511
|
+
|
|
512
|
+
const proposalPath = path.join(changeDir, "proposal.md");
|
|
513
|
+
const pageMapPath = path.join(projectRoot, ".da-vinci", "page-map.md");
|
|
514
|
+
const fallbackPageMapPath = path.join(changeDir, "page-map.md");
|
|
515
|
+
const pencilDesignPath = path.join(changeDir, "pencil-design.md");
|
|
516
|
+
const tasksPath = path.join(changeDir, "tasks.md");
|
|
517
|
+
|
|
518
|
+
const proposalText = readTextIfExists(proposalPath);
|
|
519
|
+
const pageMapText = readTextIfExists(pageMapPath) || readTextIfExists(fallbackPageMapPath);
|
|
520
|
+
const pencilDesignText = readTextIfExists(pencilDesignPath);
|
|
521
|
+
const tasksText = readTextIfExists(tasksPath);
|
|
522
|
+
|
|
523
|
+
if (!proposalText) {
|
|
524
|
+
result.failures.push("Missing `proposal.md` for scope-check.");
|
|
525
|
+
}
|
|
526
|
+
if (!pageMapText) {
|
|
527
|
+
result.failures.push("Missing `.da-vinci/page-map.md` (or change-local fallback) for scope-check.");
|
|
528
|
+
}
|
|
529
|
+
if (!pencilDesignText) {
|
|
530
|
+
result.failures.push("Missing `pencil-design.md` for scope-check state propagation.");
|
|
531
|
+
}
|
|
532
|
+
if (!tasksText) {
|
|
533
|
+
result.failures.push("Missing `tasks.md` for scope-check propagation.");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const specs = parseSpecs(changeDir, projectRoot);
|
|
537
|
+
if (specs.files.length === 0) {
|
|
538
|
+
result.failures.push("Missing runtime spec files under `.da-vinci/changes/<change-id>/specs/*/spec.md`.");
|
|
539
|
+
}
|
|
540
|
+
for (const record of specs.records) {
|
|
541
|
+
if (record.missingSections.length > 0 || record.emptySections.length > 0) {
|
|
542
|
+
result.warnings.push(
|
|
543
|
+
`Spec shape warning: \`${record.path}\` has incomplete runtime sections; scope-check coverage may be degraded.`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (result.failures.length === 0) {
|
|
549
|
+
const proposal = parseProposal(proposalText);
|
|
550
|
+
const pageMap = parsePageMap(pageMapText);
|
|
551
|
+
const pencilDesign = parsePencilDesign(pencilDesignText);
|
|
552
|
+
const tasks = parseTasks(tasksText);
|
|
553
|
+
analyzeScopePropagation(result, proposal, pageMap, specs, pencilDesign, tasks);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
result.warnings = unique(result.warnings);
|
|
557
|
+
result.notes = unique(result.notes);
|
|
558
|
+
result.notes.push("scope-check defaults to advisory mode; pass `--strict` to block on findings.");
|
|
559
|
+
result.notes.push(
|
|
560
|
+
"Promotion criteria: turn on `--strict` in CI after scope-check warning baselines are stable and reviewed."
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
return finalizeResult(result);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function formatScopeCheckReport(result) {
|
|
567
|
+
const lines = [
|
|
568
|
+
"Da Vinci scope-check",
|
|
569
|
+
`Project: ${result.projectRoot}`,
|
|
570
|
+
`Change: ${result.changeId || "(not selected)"}`,
|
|
571
|
+
`Strict mode: ${result.strict ? "yes" : "no"}`,
|
|
572
|
+
`Status: ${result.status}`,
|
|
573
|
+
`Coverage rows: pages=${result.summary.pages}, states=${result.summary.states}`
|
|
574
|
+
];
|
|
575
|
+
|
|
576
|
+
if (result.failures.length > 0) {
|
|
577
|
+
lines.push("", "Failures:");
|
|
578
|
+
for (const failure of result.failures) {
|
|
579
|
+
lines.push(`- ${failure}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (result.warnings.length > 0) {
|
|
584
|
+
lines.push("", "Warnings:");
|
|
585
|
+
for (const warning of result.warnings) {
|
|
586
|
+
lines.push(`- ${warning}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (result.notes.length > 0) {
|
|
591
|
+
lines.push("", "Notes:");
|
|
592
|
+
for (const note of result.notes) {
|
|
593
|
+
lines.push(`- ${note}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return lines.join("\n");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
module.exports = {
|
|
601
|
+
runScopeCheck,
|
|
602
|
+
formatScopeCheckReport
|
|
603
|
+
};
|