@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +61 -4
  3. package/README.zh-CN.md +57 -6
  4. package/SKILL.md +11 -3
  5. package/commands/claude/dv/continue.md +4 -0
  6. package/commands/claude/dv/design.md +3 -2
  7. package/commands/codex/prompts/dv-continue.md +4 -0
  8. package/commands/codex/prompts/dv-design.md +3 -2
  9. package/commands/gemini/dv/continue.toml +4 -0
  10. package/commands/gemini/dv/design.toml +3 -2
  11. package/docs/codex-natural-language-usage.md +5 -0
  12. package/docs/dv-command-reference.md +4 -0
  13. package/docs/mode-use-cases.md +6 -4
  14. package/docs/pencil-rendering-workflow.md +79 -15
  15. package/docs/prompt-entrypoints.md +11 -1
  16. package/docs/prompt-presets/README.md +7 -4
  17. package/docs/prompt-presets/desktop-app.md +51 -7
  18. package/docs/prompt-presets/mobile-app.md +51 -7
  19. package/docs/prompt-presets/tablet-app.md +51 -7
  20. package/docs/prompt-presets/web-app.md +51 -7
  21. package/docs/visual-adapters.md +179 -1
  22. package/docs/visual-assist-presets/README.md +28 -5
  23. package/docs/visual-assist-presets/desktop-app.md +88 -2
  24. package/docs/visual-assist-presets/mobile-app.md +89 -2
  25. package/docs/visual-assist-presets/tablet-app.md +88 -2
  26. package/docs/visual-assist-presets/web-app.md +88 -2
  27. package/docs/workflow-examples.md +8 -3
  28. package/docs/workflow-overview.md +23 -0
  29. package/docs/zh-CN/codex-natural-language-usage.md +5 -0
  30. package/docs/zh-CN/dv-command-reference.md +4 -0
  31. package/docs/zh-CN/mode-use-cases.md +7 -5
  32. package/docs/zh-CN/pencil-rendering-workflow.md +79 -15
  33. package/docs/zh-CN/prompt-entrypoints.md +13 -1
  34. package/docs/zh-CN/prompt-presets/README.md +7 -4
  35. package/docs/zh-CN/prompt-presets/desktop-app.md +50 -7
  36. package/docs/zh-CN/prompt-presets/mobile-app.md +50 -7
  37. package/docs/zh-CN/prompt-presets/tablet-app.md +50 -7
  38. package/docs/zh-CN/prompt-presets/web-app.md +50 -7
  39. package/docs/zh-CN/visual-adapters.md +179 -1
  40. package/docs/zh-CN/visual-assist-presets/README.md +28 -5
  41. package/docs/zh-CN/visual-assist-presets/desktop-app.md +88 -1
  42. package/docs/zh-CN/visual-assist-presets/mobile-app.md +89 -2
  43. package/docs/zh-CN/visual-assist-presets/tablet-app.md +89 -2
  44. package/docs/zh-CN/visual-assist-presets/web-app.md +88 -1
  45. package/docs/zh-CN/workflow-examples.md +8 -3
  46. package/docs/zh-CN/workflow-overview.md +23 -0
  47. package/lib/audit.js +654 -0
  48. package/lib/pencil-lock.js +15 -4
  49. package/package.json +4 -1
  50. package/references/artifact-templates.md +57 -0
  51. package/references/checkpoints.md +44 -19
  52. package/references/modes.md +2 -2
  53. package/references/pencil-design-to-code.md +3 -3
  54. package/references/prompt-recipes.md +12 -2
  55. package/scripts/test-audit-context-delta.js +446 -0
  56. package/scripts/test-audit-design-supervisor.js +348 -0
  57. package/scripts/test-mode-consistency.js +134 -0
  58. 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");