@xenonbyte/da-vinci-workflow 0.1.18 → 0.1.20
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 +27 -0
- package/README.md +61 -4
- package/README.zh-CN.md +57 -6
- package/SKILL.md +11 -3
- package/commands/claude/dv/continue.md +4 -0
- package/commands/claude/dv/design.md +3 -2
- package/commands/codex/prompts/dv-continue.md +4 -0
- package/commands/codex/prompts/dv-design.md +3 -2
- package/commands/gemini/dv/continue.toml +4 -0
- package/commands/gemini/dv/design.toml +3 -2
- package/docs/codex-natural-language-usage.md +5 -0
- package/docs/dv-command-reference.md +4 -0
- package/docs/mode-use-cases.md +6 -4
- package/docs/pencil-rendering-workflow.md +79 -15
- package/docs/prompt-entrypoints.md +11 -1
- package/docs/prompt-presets/README.md +7 -4
- package/docs/prompt-presets/desktop-app.md +51 -7
- package/docs/prompt-presets/mobile-app.md +51 -7
- package/docs/prompt-presets/tablet-app.md +51 -7
- package/docs/prompt-presets/web-app.md +51 -7
- package/docs/visual-adapters.md +179 -1
- package/docs/visual-assist-presets/README.md +28 -5
- package/docs/visual-assist-presets/desktop-app.md +88 -2
- package/docs/visual-assist-presets/mobile-app.md +89 -2
- package/docs/visual-assist-presets/tablet-app.md +88 -2
- package/docs/visual-assist-presets/web-app.md +88 -2
- package/docs/workflow-examples.md +8 -3
- package/docs/workflow-overview.md +23 -0
- package/docs/zh-CN/codex-natural-language-usage.md +5 -0
- package/docs/zh-CN/dv-command-reference.md +4 -0
- package/docs/zh-CN/mode-use-cases.md +7 -5
- package/docs/zh-CN/pencil-rendering-workflow.md +79 -15
- package/docs/zh-CN/prompt-entrypoints.md +13 -1
- package/docs/zh-CN/prompt-presets/README.md +7 -4
- package/docs/zh-CN/prompt-presets/desktop-app.md +50 -7
- package/docs/zh-CN/prompt-presets/mobile-app.md +50 -7
- package/docs/zh-CN/prompt-presets/tablet-app.md +50 -7
- package/docs/zh-CN/prompt-presets/web-app.md +50 -7
- package/docs/zh-CN/visual-adapters.md +179 -1
- package/docs/zh-CN/visual-assist-presets/README.md +28 -5
- package/docs/zh-CN/visual-assist-presets/desktop-app.md +88 -1
- package/docs/zh-CN/visual-assist-presets/mobile-app.md +89 -2
- package/docs/zh-CN/visual-assist-presets/tablet-app.md +89 -2
- package/docs/zh-CN/visual-assist-presets/web-app.md +88 -1
- package/docs/zh-CN/workflow-examples.md +8 -3
- package/docs/zh-CN/workflow-overview.md +23 -0
- package/lib/audit.js +654 -0
- package/lib/pencil-lock.js +15 -4
- package/package.json +4 -1
- package/references/artifact-templates.md +57 -0
- package/references/checkpoints.md +44 -19
- package/references/modes.md +2 -2
- package/references/pencil-design-to-code.md +3 -3
- package/references/prompt-recipes.md +12 -2
- package/scripts/test-audit-context-delta.js +446 -0
- package/scripts/test-audit-design-supervisor.js +348 -0
- package/scripts/test-mode-consistency.js +134 -0
- package/scripts/test-pencil-lock.js +130 -0
package/lib/audit.js
CHANGED
|
@@ -45,6 +45,10 @@ function relativeTo(projectRoot, targetPath) {
|
|
|
45
45
|
return path.relative(projectRoot, targetPath) || ".";
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function escapeRegExp(value) {
|
|
49
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
50
|
+
}
|
|
51
|
+
|
|
48
52
|
function collectRegisteredPenPaths(projectRoot, designRegistryPath) {
|
|
49
53
|
const registryText = readTextIfExists(designRegistryPath);
|
|
50
54
|
const matches = registryText.match(/\.da-vinci\/designs\/[^\s`]+\.pen/g) || [];
|
|
@@ -117,6 +121,448 @@ function addMissingArtifacts(projectRoot, artifactPaths, targetList) {
|
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
function pushUnique(targetList, message) {
|
|
125
|
+
if (!targetList.includes(message)) {
|
|
126
|
+
targetList.push(message);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getMarkdownSection(text, heading) {
|
|
131
|
+
if (!text) {
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const escapedHeading = escapeRegExp(heading);
|
|
136
|
+
const headingPattern = new RegExp(`^##\\s+${escapedHeading}\\s*$`, "i");
|
|
137
|
+
const anyHeadingPattern = /^##\s+/;
|
|
138
|
+
const lines = String(text).replace(/\r\n?/g, "\n").split("\n");
|
|
139
|
+
const sectionLines = [];
|
|
140
|
+
let capturing = false;
|
|
141
|
+
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
if (capturing && anyHeadingPattern.test(line)) {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!capturing && headingPattern.test(line)) {
|
|
148
|
+
capturing = true;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (capturing) {
|
|
153
|
+
sectionLines.push(line);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return sectionLines.join("\n").trim();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeCheckpointLabel(value) {
|
|
161
|
+
return String(value || "")
|
|
162
|
+
.toLowerCase()
|
|
163
|
+
.replace(/`/g, "")
|
|
164
|
+
.replace(/[_-]+/g, " ")
|
|
165
|
+
.replace(/\s+/g, " ")
|
|
166
|
+
.trim();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseCheckpointStatusMap(markdownText) {
|
|
170
|
+
const section = getMarkdownSection(markdownText, "Checkpoint Status");
|
|
171
|
+
if (!section) {
|
|
172
|
+
return {};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const statuses = {};
|
|
176
|
+
const matches = section.matchAll(/(?:^|\n)\s*-\s*`?([^`:\n]+?)`?\s*:\s*(PASS|WARN|BLOCK)\b/gi);
|
|
177
|
+
for (const match of matches) {
|
|
178
|
+
const label = normalizeCheckpointLabel(match[1]);
|
|
179
|
+
if (!label) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
statuses[label] = String(match[2]).toUpperCase();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return statuses;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function hasContextDeltaExpectationSignals(markdownText) {
|
|
189
|
+
const text = String(markdownText || "");
|
|
190
|
+
return (
|
|
191
|
+
/##\s+(Checkpoint Status|MCP Runtime Gate)\b/i.test(text) ||
|
|
192
|
+
/(?:^|\n)\s*(?:[-*]\s*)?`?Context Delta Required`?\s*:\s*(?:true|yes|on|1)\b/i.test(text)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function parseSupersedesTokens(value) {
|
|
197
|
+
return String(value || "")
|
|
198
|
+
.split(/[,\n;]/)
|
|
199
|
+
.map((token) => token.trim())
|
|
200
|
+
.filter(Boolean);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizeTimeToken(value) {
|
|
204
|
+
const raw = String(value || "").trim();
|
|
205
|
+
if (!raw) {
|
|
206
|
+
return "";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const parseCandidates = [];
|
|
210
|
+
const rawWithT = raw.includes(" ") ? raw.replace(/\s+/, "T") : raw;
|
|
211
|
+
const hasExplicitTimezone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(rawWithT);
|
|
212
|
+
|
|
213
|
+
if (!hasExplicitTimezone && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(rawWithT)) {
|
|
214
|
+
parseCandidates.push(`${rawWithT}Z`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
parseCandidates.push(rawWithT);
|
|
218
|
+
parseCandidates.push(raw);
|
|
219
|
+
|
|
220
|
+
for (const candidate of parseCandidates) {
|
|
221
|
+
const timestamp = Date.parse(candidate);
|
|
222
|
+
if (!Number.isFinite(timestamp)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
return new Date(timestamp).toISOString();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return "";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getTimeReferenceKeys(value) {
|
|
232
|
+
const raw = String(value || "").trim();
|
|
233
|
+
if (!raw) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const keys = new Set([raw]);
|
|
238
|
+
|
|
239
|
+
if (raw.includes(" ")) {
|
|
240
|
+
keys.add(raw.replace(/\s+/, "T"));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (raw.endsWith(".000Z")) {
|
|
244
|
+
keys.add(raw.replace(".000Z", "Z"));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const normalized = normalizeTimeToken(raw);
|
|
248
|
+
if (normalized) {
|
|
249
|
+
keys.add(normalized);
|
|
250
|
+
if (normalized.endsWith(".000Z")) {
|
|
251
|
+
keys.add(normalized.replace(".000Z", "Z"));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return [...keys];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function buildContextDeltaReferenceIndex(entries) {
|
|
259
|
+
const referenceIndex = new Map();
|
|
260
|
+
|
|
261
|
+
function addReference(key, entryIndex) {
|
|
262
|
+
const normalizedKey = String(key || "").trim();
|
|
263
|
+
if (!normalizedKey) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const existing = referenceIndex.get(normalizedKey) || [];
|
|
268
|
+
existing.push(entryIndex);
|
|
269
|
+
referenceIndex.set(normalizedKey, existing);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
entries.forEach((entry, entryIndex) => {
|
|
273
|
+
if (entry.time) {
|
|
274
|
+
for (const timeKey of getTimeReferenceKeys(entry.time)) {
|
|
275
|
+
addReference(timeKey, entryIndex);
|
|
276
|
+
addReference(`time:${timeKey}`, entryIndex);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (entry.checkpointType && entry.time) {
|
|
281
|
+
const normalizedCheckpointType = normalizeCheckpointLabel(entry.checkpointType);
|
|
282
|
+
for (const timeKey of getTimeReferenceKeys(entry.time)) {
|
|
283
|
+
addReference(`${entry.checkpointType}@${timeKey}`, entryIndex);
|
|
284
|
+
addReference(`${normalizedCheckpointType}@${timeKey}`, entryIndex);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return referenceIndex;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function getSupersedesCandidateKeys(token) {
|
|
293
|
+
const trimmed = String(token || "").trim();
|
|
294
|
+
if (!trimmed) {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const keys = new Set([trimmed]);
|
|
299
|
+
const prefixedTimeMatch = trimmed.match(/^time\s*:\s*(.+)$/i);
|
|
300
|
+
if (prefixedTimeMatch) {
|
|
301
|
+
for (const timeKey of getTimeReferenceKeys(prefixedTimeMatch[1])) {
|
|
302
|
+
keys.add(timeKey);
|
|
303
|
+
keys.add(`time:${timeKey}`);
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
keys.add(`time:${trimmed}`);
|
|
307
|
+
for (const timeKey of getTimeReferenceKeys(trimmed)) {
|
|
308
|
+
keys.add(timeKey);
|
|
309
|
+
keys.add(`time:${timeKey}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const atIndex = trimmed.indexOf("@");
|
|
314
|
+
if (atIndex > 0 && atIndex < trimmed.length - 1) {
|
|
315
|
+
const checkpointType = trimmed.slice(0, atIndex).trim();
|
|
316
|
+
const time = trimmed.slice(atIndex + 1).trim();
|
|
317
|
+
if (checkpointType && time) {
|
|
318
|
+
const normalizedCheckpointType = normalizeCheckpointLabel(checkpointType);
|
|
319
|
+
for (const timeKey of getTimeReferenceKeys(time)) {
|
|
320
|
+
keys.add(`${checkpointType}@${timeKey}`);
|
|
321
|
+
keys.add(`${normalizedCheckpointType}@${timeKey}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return [...keys];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function resolveSupersedesReferenceIndices(token, referenceIndex) {
|
|
330
|
+
const indices = new Set();
|
|
331
|
+
for (const key of getSupersedesCandidateKeys(token)) {
|
|
332
|
+
const matches = referenceIndex.get(key);
|
|
333
|
+
if (!matches) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
for (const entryIndex of matches) {
|
|
337
|
+
indices.add(entryIndex);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return [...indices].sort((a, b) => a - b);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function inspectContextDelta(markdownText) {
|
|
344
|
+
const section = getMarkdownSection(markdownText, "Context Delta");
|
|
345
|
+
if (!section) {
|
|
346
|
+
return {
|
|
347
|
+
found: false,
|
|
348
|
+
hasConcreteEntry: false,
|
|
349
|
+
entries: [],
|
|
350
|
+
incompleteEntryCount: 0
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const lines = String(section).replace(/\r\n?/g, "\n").split("\n");
|
|
355
|
+
const entries = [];
|
|
356
|
+
let current = null;
|
|
357
|
+
|
|
358
|
+
function ensureCurrent() {
|
|
359
|
+
if (!current) {
|
|
360
|
+
current = {};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function flushCurrent() {
|
|
365
|
+
if (!current) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const hasAnyValue = [
|
|
370
|
+
current.time,
|
|
371
|
+
current.checkpointType,
|
|
372
|
+
current.goal,
|
|
373
|
+
current.decision,
|
|
374
|
+
current.constraints,
|
|
375
|
+
current.impact,
|
|
376
|
+
current.status,
|
|
377
|
+
current.nextAction,
|
|
378
|
+
current.supersedes
|
|
379
|
+
].some((value) => Boolean(value));
|
|
380
|
+
|
|
381
|
+
if (hasAnyValue) {
|
|
382
|
+
entries.push(current);
|
|
383
|
+
}
|
|
384
|
+
current = null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
for (const rawLine of lines) {
|
|
388
|
+
const line = rawLine.trim();
|
|
389
|
+
|
|
390
|
+
if (!line) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const timeMatch = line.match(/^-+\s*`?time`?\s*:\s*(.+)$/i);
|
|
395
|
+
if (timeMatch) {
|
|
396
|
+
flushCurrent();
|
|
397
|
+
current = {
|
|
398
|
+
time: timeMatch[1].trim()
|
|
399
|
+
};
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const checkpointTypeMatch = line.match(/^-+\s*`?checkpoint(?:[_ -]?type)?`?\s*:\s*(.+)$/i);
|
|
404
|
+
if (checkpointTypeMatch) {
|
|
405
|
+
ensureCurrent();
|
|
406
|
+
current.checkpointType = checkpointTypeMatch[1].trim();
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const goalMatch = line.match(/^-+\s*`?goal`?\s*:\s*(.+)$/i);
|
|
411
|
+
if (goalMatch) {
|
|
412
|
+
ensureCurrent();
|
|
413
|
+
current.goal = goalMatch[1].trim();
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const decisionMatch = line.match(/^-+\s*`?decision`?\s*:\s*(.+)$/i);
|
|
418
|
+
if (decisionMatch) {
|
|
419
|
+
ensureCurrent();
|
|
420
|
+
current.decision = decisionMatch[1].trim();
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const constraintsMatch = line.match(/^-+\s*`?constraints`?\s*:\s*(.+)$/i);
|
|
425
|
+
if (constraintsMatch) {
|
|
426
|
+
ensureCurrent();
|
|
427
|
+
current.constraints = constraintsMatch[1].trim();
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const impactMatch = line.match(/^-+\s*`?impact`?\s*:\s*(.+)$/i);
|
|
432
|
+
if (impactMatch) {
|
|
433
|
+
ensureCurrent();
|
|
434
|
+
current.impact = impactMatch[1].trim();
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const statusMatch = line.match(/^-+\s*`?status`?\s*:\s*(PASS|WARN|BLOCK)\b/i);
|
|
439
|
+
if (statusMatch) {
|
|
440
|
+
ensureCurrent();
|
|
441
|
+
current.status = String(statusMatch[1]).toUpperCase();
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const nextActionMatch = line.match(/^-+\s*`?next(?:[_ -]?action)?`?\s*:\s*(.+)$/i);
|
|
446
|
+
if (nextActionMatch) {
|
|
447
|
+
ensureCurrent();
|
|
448
|
+
current.nextAction = nextActionMatch[1].trim();
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const supersedesMatch = line.match(/^-+\s*`?supersedes`?\s*:\s*(.+)$/i);
|
|
453
|
+
if (supersedesMatch) {
|
|
454
|
+
ensureCurrent();
|
|
455
|
+
current.supersedes = supersedesMatch[1].trim();
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
flushCurrent();
|
|
461
|
+
|
|
462
|
+
const hasConcreteEntry = entries.some(
|
|
463
|
+
(entry) => entry.time || entry.checkpointType || entry.status || entry.decision || entry.nextAction
|
|
464
|
+
);
|
|
465
|
+
const incompleteEntryCount = entries.filter(
|
|
466
|
+
(entry) => !entry.time || !entry.checkpointType || !entry.status
|
|
467
|
+
).length;
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
found: true,
|
|
471
|
+
hasConcreteEntry,
|
|
472
|
+
entries,
|
|
473
|
+
incompleteEntryCount
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function getVisualAssistFieldValues(daVinciText, fieldName) {
|
|
478
|
+
const section = getMarkdownSection(daVinciText, "Visual Assist");
|
|
479
|
+
if (!section) {
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const fieldPattern = new RegExp(`^\\s*-\\s*${escapeRegExp(fieldName)}\\s*:\\s*(.*)$`, "i");
|
|
484
|
+
const nestedValuePattern = /^\s{2,}-\s*(.+?)\s*$/;
|
|
485
|
+
const nextFieldPattern = /^\s*-\s+[^:]+:\s*.*$/;
|
|
486
|
+
const values = [];
|
|
487
|
+
let capturing = false;
|
|
488
|
+
|
|
489
|
+
for (const rawLine of String(section).replace(/\r\n?/g, "\n").split("\n")) {
|
|
490
|
+
const fieldMatch = rawLine.match(fieldPattern);
|
|
491
|
+
if (!capturing && fieldMatch) {
|
|
492
|
+
capturing = true;
|
|
493
|
+
const inlineValue = (fieldMatch[1] || "").trim();
|
|
494
|
+
if (inlineValue) {
|
|
495
|
+
values.push(inlineValue);
|
|
496
|
+
}
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (capturing && nextFieldPattern.test(rawLine)) {
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (capturing) {
|
|
505
|
+
const nestedMatch = rawLine.match(nestedValuePattern);
|
|
506
|
+
if (nestedMatch) {
|
|
507
|
+
const value = nestedMatch[1].trim();
|
|
508
|
+
if (value) {
|
|
509
|
+
values.push(value);
|
|
510
|
+
}
|
|
511
|
+
} else if (rawLine.trim() === "") {
|
|
512
|
+
continue;
|
|
513
|
+
} else {
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return values;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function hasConfiguredDesignSupervisorReview(daVinciText) {
|
|
523
|
+
return getVisualAssistFieldValues(daVinciText, "Design-supervisor reviewers").length > 0;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function isDesignSupervisorReviewRequired(daVinciText) {
|
|
527
|
+
return getVisualAssistFieldValues(daVinciText, "Require Supervisor Review").some((value) =>
|
|
528
|
+
/^true$/i.test(String(value).trim())
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function inspectDesignSupervisorReview(pencilDesignText) {
|
|
533
|
+
const section = getMarkdownSection(pencilDesignText, "Design-Supervisor Review");
|
|
534
|
+
if (!section) {
|
|
535
|
+
return {
|
|
536
|
+
found: false,
|
|
537
|
+
status: null,
|
|
538
|
+
acceptedWarn: false,
|
|
539
|
+
hasIssueList: false,
|
|
540
|
+
hasRevisionOutcome: false
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const statusMatch = section.match(/(?:^|\n)\s*-\s*(?:Status|状态)\s*:\s*(PASS|WARN|BLOCK)\b/i);
|
|
545
|
+
const status = statusMatch ? statusMatch[1].toUpperCase() : null;
|
|
546
|
+
const issueListMatch = section.match(/(?:^|\n)\s*-\s*(?:Issue list|问题列表)\s*:\s*(.+)$/im);
|
|
547
|
+
const revisionOutcomeMatch = section.match(
|
|
548
|
+
/(?:^|\n)\s*-\s*(?:Revision outcome|修订结果)\s*:\s*(.+)$/im
|
|
549
|
+
);
|
|
550
|
+
const revisionOutcome = revisionOutcomeMatch ? revisionOutcomeMatch[1].trim() : "";
|
|
551
|
+
const acceptedWarn =
|
|
552
|
+
status === "WARN" &&
|
|
553
|
+
/(accepted|accepted with follow-up|accepted warning|warn accepted|接受|已接受|接受警告)/i.test(
|
|
554
|
+
revisionOutcome
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
found: true,
|
|
559
|
+
status,
|
|
560
|
+
acceptedWarn,
|
|
561
|
+
hasIssueList: Boolean(issueListMatch && issueListMatch[1].trim()),
|
|
562
|
+
hasRevisionOutcome: Boolean(revisionOutcome)
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
120
566
|
function auditProject(projectPathInput, options = {}) {
|
|
121
567
|
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
122
568
|
const mode = options.mode || "integrity";
|
|
@@ -125,6 +571,9 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
125
571
|
const changesDir = path.join(daVinciDir, "changes");
|
|
126
572
|
const designRegistryPath = path.join(daVinciDir, "design-registry.md");
|
|
127
573
|
const pencilSessionPath = getSessionStatePath(projectRoot);
|
|
574
|
+
const daVinciText = readTextIfExists(path.join(projectRoot, "DA-VINCI.md"));
|
|
575
|
+
const designSupervisorConfigured = hasConfiguredDesignSupervisorReview(daVinciText);
|
|
576
|
+
const designSupervisorRequired = isDesignSupervisorReviewRequired(daVinciText);
|
|
128
577
|
|
|
129
578
|
const failures = [];
|
|
130
579
|
const warnings = [];
|
|
@@ -186,6 +635,16 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
186
635
|
addMissingArtifacts(projectRoot, completionRequiredArtifacts.slice(1, 4), warnings);
|
|
187
636
|
}
|
|
188
637
|
|
|
638
|
+
if (designSupervisorRequired && !designSupervisorConfigured) {
|
|
639
|
+
const message =
|
|
640
|
+
"DA-VINCI.md sets `Require Supervisor Review: true` but no `Design-supervisor reviewers` are configured.";
|
|
641
|
+
if (mode === "completion") {
|
|
642
|
+
failures.push(message);
|
|
643
|
+
} else {
|
|
644
|
+
warnings.push(message);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
189
648
|
const daVinciFiles = listFilesRecursive(daVinciDir);
|
|
190
649
|
const misplacedExports = daVinciFiles.filter(
|
|
191
650
|
(filePath) => IMAGE_EXPORT_PATTERN.test(filePath) && !isAllowedExportPath(projectRoot, filePath)
|
|
@@ -348,6 +807,201 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
348
807
|
);
|
|
349
808
|
}
|
|
350
809
|
|
|
810
|
+
const pencilDesignPath = path.join(changeDir, "pencil-design.md");
|
|
811
|
+
if (designSupervisorConfigured && pathExists(pencilDesignPath)) {
|
|
812
|
+
const review = inspectDesignSupervisorReview(readTextIfExists(pencilDesignPath));
|
|
813
|
+
const enforceAsFailure =
|
|
814
|
+
mode === "completion" && scopedChangeDirs.includes(changeDir) && designSupervisorRequired;
|
|
815
|
+
|
|
816
|
+
if (!review.found) {
|
|
817
|
+
const message =
|
|
818
|
+
`DA-VINCI.md configures Design-supervisor reviewers, but ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
819
|
+
"does not record a `## Design-Supervisor Review` section.";
|
|
820
|
+
if (enforceAsFailure) {
|
|
821
|
+
failures.push(message);
|
|
822
|
+
} else {
|
|
823
|
+
warnings.push(message);
|
|
824
|
+
}
|
|
825
|
+
} else if (!review.status) {
|
|
826
|
+
const message =
|
|
827
|
+
`Design-supervisor review is recorded in ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
828
|
+
"but no PASS/WARN/BLOCK status was found.";
|
|
829
|
+
if (enforceAsFailure) {
|
|
830
|
+
failures.push(message);
|
|
831
|
+
} else {
|
|
832
|
+
warnings.push(message);
|
|
833
|
+
}
|
|
834
|
+
} else if (!review.hasIssueList || !review.hasRevisionOutcome) {
|
|
835
|
+
const missingFields = [
|
|
836
|
+
!review.hasIssueList ? "Issue list" : null,
|
|
837
|
+
!review.hasRevisionOutcome ? "Revision outcome" : null
|
|
838
|
+
]
|
|
839
|
+
.filter(Boolean)
|
|
840
|
+
.join(" and ");
|
|
841
|
+
const message =
|
|
842
|
+
`Design-supervisor review in ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
843
|
+
`is missing required field(s): ${missingFields}.`;
|
|
844
|
+
if (enforceAsFailure) {
|
|
845
|
+
failures.push(message);
|
|
846
|
+
} else {
|
|
847
|
+
warnings.push(message);
|
|
848
|
+
}
|
|
849
|
+
} else if (review.status === "BLOCK") {
|
|
850
|
+
const message =
|
|
851
|
+
`Design-supervisor review is still BLOCK in ${relativeTo(projectRoot, pencilDesignPath)}.`;
|
|
852
|
+
if (enforceAsFailure) {
|
|
853
|
+
failures.push(message);
|
|
854
|
+
} else {
|
|
855
|
+
warnings.push(message);
|
|
856
|
+
}
|
|
857
|
+
} else if (review.status === "WARN" && !review.acceptedWarn) {
|
|
858
|
+
const message =
|
|
859
|
+
`Design-supervisor review is WARN in ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
860
|
+
"but the warning was not explicitly accepted.";
|
|
861
|
+
if (enforceAsFailure) {
|
|
862
|
+
failures.push(message);
|
|
863
|
+
} else {
|
|
864
|
+
warnings.push(message);
|
|
865
|
+
}
|
|
866
|
+
} else {
|
|
867
|
+
notes.push(
|
|
868
|
+
`Detected design-supervisor review status ${review.status} in ${relativeTo(projectRoot, pencilDesignPath)}.`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const tasksPath = path.join(changeDir, "tasks.md");
|
|
874
|
+
const verificationPath = path.join(changeDir, "verification.md");
|
|
875
|
+
const contextArtifacts = [pencilDesignPath, tasksPath, verificationPath]
|
|
876
|
+
.filter((artifactPath) => pathExists(artifactPath))
|
|
877
|
+
.map((artifactPath) => ({
|
|
878
|
+
path: artifactPath,
|
|
879
|
+
text: readTextIfExists(artifactPath)
|
|
880
|
+
}));
|
|
881
|
+
const contextCandidates = contextArtifacts.filter((artifact) =>
|
|
882
|
+
hasContextDeltaExpectationSignals(artifact.text)
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
if (contextCandidates.length > 0) {
|
|
886
|
+
const contextInspections = contextCandidates.map((artifact) => ({
|
|
887
|
+
path: artifact.path,
|
|
888
|
+
inspection: inspectContextDelta(artifact.text),
|
|
889
|
+
checkpointStatuses: parseCheckpointStatusMap(artifact.text)
|
|
890
|
+
}));
|
|
891
|
+
|
|
892
|
+
const hasAnyConcreteContextDelta = contextInspections.some(
|
|
893
|
+
(item) => item.inspection.hasConcreteEntry
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
if (!hasAnyConcreteContextDelta) {
|
|
897
|
+
pushUnique(
|
|
898
|
+
warnings,
|
|
899
|
+
`Checkpoint-bearing artifacts in ${changeRel} do not record any concrete \`## Context Delta\` entries.`
|
|
900
|
+
);
|
|
901
|
+
} else {
|
|
902
|
+
notes.push(`Detected context-delta recovery notes in ${changeRel}.`);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
for (const item of contextInspections) {
|
|
906
|
+
if (!item.inspection.found || item.inspection.incompleteEntryCount === 0) {
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
pushUnique(
|
|
911
|
+
warnings,
|
|
912
|
+
`${relativeTo(projectRoot, item.path)} has ${item.inspection.incompleteEntryCount} context-delta entr` +
|
|
913
|
+
`${item.inspection.incompleteEntryCount === 1 ? "y" : "ies"} missing one of: time/checkpoint_type/status.`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
for (const item of contextInspections) {
|
|
918
|
+
if (!item.inspection.found || item.inspection.entries.length === 0) {
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const referenceIndex = buildContextDeltaReferenceIndex(item.inspection.entries);
|
|
923
|
+
|
|
924
|
+
item.inspection.entries.forEach((entry, entryIndex) => {
|
|
925
|
+
if (!entry.supersedes) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const entryLabel = entry.time ? `time \`${entry.time}\`` : `entry #${entryIndex + 1}`;
|
|
930
|
+
const supersedesTokens = parseSupersedesTokens(entry.supersedes);
|
|
931
|
+
|
|
932
|
+
if (supersedesTokens.length === 0) {
|
|
933
|
+
pushUnique(
|
|
934
|
+
warnings,
|
|
935
|
+
`${relativeTo(projectRoot, item.path)} has ${entryLabel} with an empty \`supersedes\` reference.`
|
|
936
|
+
);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
for (const token of supersedesTokens) {
|
|
941
|
+
const resolvedIndices = resolveSupersedesReferenceIndices(token, referenceIndex);
|
|
942
|
+
|
|
943
|
+
if (resolvedIndices.length === 0) {
|
|
944
|
+
pushUnique(
|
|
945
|
+
warnings,
|
|
946
|
+
`${relativeTo(projectRoot, item.path)} has ${entryLabel} superseding \`${token}\`, ` +
|
|
947
|
+
"but no referenced context-delta entry exists in the same artifact."
|
|
948
|
+
);
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const hasOlderMatch = resolvedIndices.some((resolvedIndex) => resolvedIndex < entryIndex);
|
|
953
|
+
if (!hasOlderMatch) {
|
|
954
|
+
pushUnique(
|
|
955
|
+
warnings,
|
|
956
|
+
`${relativeTo(projectRoot, item.path)} has ${entryLabel} superseding \`${token}\`, ` +
|
|
957
|
+
"but the reference does not point to an earlier context-delta entry."
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (mode === "completion" && scopedChangeDirs.includes(changeDir)) {
|
|
965
|
+
for (const item of contextInspections) {
|
|
966
|
+
if (!item.inspection.hasConcreteEntry || Object.keys(item.checkpointStatuses).length === 0) {
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const entriesByType = new Map();
|
|
971
|
+
for (const entry of item.inspection.entries) {
|
|
972
|
+
if (!entry.checkpointType || !entry.status) {
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
const checkpointType = normalizeCheckpointLabel(entry.checkpointType);
|
|
976
|
+
const existing = entriesByType.get(checkpointType) || [];
|
|
977
|
+
existing.push(entry);
|
|
978
|
+
entriesByType.set(checkpointType, existing);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
for (const [checkpointType, entries] of entriesByType.entries()) {
|
|
982
|
+
const currentStatus = item.checkpointStatuses[checkpointType];
|
|
983
|
+
if (!currentStatus || entries.length === 0) {
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const latestEntry = entries[entries.length - 1];
|
|
988
|
+
if (currentStatus === latestEntry.status) {
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const statusHistory = entries.map((entry) => entry.status).join(" -> ");
|
|
993
|
+
pushUnique(
|
|
994
|
+
warnings,
|
|
995
|
+
`Context-delta/checkpoint status mismatch in ${relativeTo(projectRoot, item.path)} for ` +
|
|
996
|
+
`\`${latestEntry.checkpointType}\`: latest context-delta status is ${latestEntry.status}, ` +
|
|
997
|
+
`current checkpoint status is ${currentStatus} (history: ${statusHistory}). ` +
|
|
998
|
+
"This may indicate stale checkpoint status or stale context-delta notes."
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
351
1005
|
const specDirs = listChildDirs(path.join(changeDir, "specs"));
|
|
352
1006
|
for (const specDir of specDirs) {
|
|
353
1007
|
const specFile = path.join(specDir, "spec.md");
|