@xenonbyte/da-vinci-workflow 0.2.2 → 0.2.4

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 (72) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +49 -14
  3. package/README.zh-CN.md +169 -14
  4. package/commands/claude/dv/breakdown.md +8 -0
  5. package/commands/claude/dv/build.md +16 -0
  6. package/commands/claude/dv/continue.md +4 -0
  7. package/commands/claude/dv/design.md +5 -2
  8. package/commands/claude/dv/tasks.md +14 -0
  9. package/commands/claude/dv/verify.md +11 -0
  10. package/commands/codex/prompts/dv-breakdown.md +8 -0
  11. package/commands/codex/prompts/dv-build.md +16 -0
  12. package/commands/codex/prompts/dv-continue.md +4 -0
  13. package/commands/codex/prompts/dv-design.md +5 -2
  14. package/commands/codex/prompts/dv-tasks.md +14 -0
  15. package/commands/codex/prompts/dv-verify.md +10 -0
  16. package/commands/gemini/dv/breakdown.toml +8 -0
  17. package/commands/gemini/dv/build.toml +16 -0
  18. package/commands/gemini/dv/continue.toml +4 -0
  19. package/commands/gemini/dv/design.toml +5 -2
  20. package/commands/gemini/dv/tasks.toml +14 -0
  21. package/commands/gemini/dv/verify.toml +10 -0
  22. package/commands/templates/dv-continue.shared.md +4 -0
  23. package/docs/discipline-and-orchestration-upgrade.md +83 -0
  24. package/docs/dv-command-reference.md +61 -2
  25. package/docs/execution-chain-migration.md +23 -0
  26. package/docs/execution-chain-plan.md +10 -3
  27. package/docs/mode-use-cases.md +2 -1
  28. package/docs/pencil-rendering-workflow.md +15 -12
  29. package/docs/prompt-entrypoints.md +5 -0
  30. package/docs/prompt-presets/README.md +1 -1
  31. package/docs/prompt-presets/desktop-app.md +3 -3
  32. package/docs/prompt-presets/mobile-app.md +3 -3
  33. package/docs/prompt-presets/tablet-app.md +3 -3
  34. package/docs/prompt-presets/web-app.md +3 -3
  35. package/docs/skill-usage.md +61 -38
  36. package/docs/workflow-examples.md +16 -13
  37. package/docs/workflow-overview.md +19 -0
  38. package/docs/zh-CN/dv-command-reference.md +59 -2
  39. package/docs/zh-CN/execution-chain-migration.md +23 -0
  40. package/docs/zh-CN/mode-use-cases.md +2 -1
  41. package/docs/zh-CN/pencil-rendering-workflow.md +15 -12
  42. package/docs/zh-CN/prompt-entrypoints.md +5 -0
  43. package/docs/zh-CN/prompt-presets/README.md +1 -1
  44. package/docs/zh-CN/prompt-presets/desktop-app.md +3 -3
  45. package/docs/zh-CN/prompt-presets/mobile-app.md +3 -3
  46. package/docs/zh-CN/prompt-presets/tablet-app.md +3 -3
  47. package/docs/zh-CN/prompt-presets/web-app.md +3 -3
  48. package/docs/zh-CN/skill-usage.md +61 -38
  49. package/docs/zh-CN/workflow-examples.md +15 -13
  50. package/docs/zh-CN/workflow-overview.md +19 -0
  51. package/examples/greenfield-spec-markupflow/.da-vinci/state/execution-signals/demo__lint-tasks.json +16 -0
  52. package/lib/audit-parsers.js +166 -10
  53. package/lib/audit.js +3 -26
  54. package/lib/cli.js +156 -2
  55. package/lib/design-source-registry.js +146 -0
  56. package/lib/execution-profile.js +143 -0
  57. package/lib/execution-signals.js +19 -1
  58. package/lib/lint-tasks.js +86 -2
  59. package/lib/planning-parsers.js +255 -18
  60. package/lib/save-current-design.js +790 -0
  61. package/lib/supervisor-review.js +3 -2
  62. package/lib/task-execution.js +160 -0
  63. package/lib/task-review.js +197 -0
  64. package/lib/verify.js +152 -1
  65. package/lib/workflow-bootstrap.js +2 -13
  66. package/lib/workflow-persisted-state.js +3 -1
  67. package/lib/workflow-state.js +503 -33
  68. package/lib/worktree-preflight.js +214 -0
  69. package/package.json +1 -1
  70. package/references/artifact-templates.md +56 -6
  71. package/tui/catalog.js +103 -0
  72. package/tui/index.js +2274 -418
@@ -67,7 +67,8 @@ function resolveCodexBinaryPath(explicitPath) {
67
67
  }
68
68
 
69
69
  function normalizePositiveInt(value, fallback, minimum = 1, maximum = Number.POSITIVE_INFINITY) {
70
- const parsed = Number.parseInt(String(value || ""), 10);
70
+ const raw = value === undefined || value === null ? "" : String(value).trim();
71
+ const parsed = Number.parseInt(raw, 10);
71
72
  if (!Number.isFinite(parsed)) {
72
73
  return fallback;
73
74
  }
@@ -416,7 +417,7 @@ function extractMcpRuntimeGateStatus(markdownText) {
416
417
  }
417
418
 
418
419
  const statusMatch = String(section).match(
419
- /(?:^|\n)\s*-\s*(?:Status|状态)\s*:\s*`?(PASS|WARN|BLOCK)`?\b/i
420
+ /(?:^|\n)\s*-\s*(?:Final runtime gate status|Status|状态)\s*:\s*`?(PASS|WARN|BLOCK)`?\b/i
420
421
  );
421
422
  return statusMatch ? String(statusMatch[1]).toUpperCase() : "";
422
423
  }
@@ -0,0 +1,160 @@
1
+ const path = require("path");
2
+ const { resolveChangeDir, unique } = require("./planning-parsers");
3
+ const { writeExecutionSignal } = require("./execution-signals");
4
+
5
+ const VALID_IMPLEMENTER_STATUSES = new Set([
6
+ "DONE",
7
+ "DONE_WITH_CONCERNS",
8
+ "NEEDS_CONTEXT",
9
+ "BLOCKED"
10
+ ]);
11
+
12
+ function normalizeStatus(value) {
13
+ const normalized = String(value || "").trim().toUpperCase();
14
+ if (!VALID_IMPLEMENTER_STATUSES.has(normalized)) {
15
+ throw new Error(
16
+ "`task-execution --status` must be one of DONE, DONE_WITH_CONCERNS, NEEDS_CONTEXT, BLOCKED."
17
+ );
18
+ }
19
+ return normalized;
20
+ }
21
+
22
+ function normalizeList(value) {
23
+ if (Array.isArray(value)) {
24
+ return unique(value.map((item) => String(item || "").trim()).filter(Boolean));
25
+ }
26
+ return unique(
27
+ String(value || "")
28
+ .split(/[,\n;]/)
29
+ .map((item) => item.trim())
30
+ .filter(Boolean)
31
+ );
32
+ }
33
+
34
+ function normalizeTaskGroupId(value) {
35
+ const normalized = String(value || "").trim();
36
+ if (!normalized) {
37
+ throw new Error("`task-execution` requires `--task-group <id>`.");
38
+ }
39
+ return normalized;
40
+ }
41
+
42
+ function mapImplementerStatusToSignal(status) {
43
+ if (status === "DONE") {
44
+ return "PASS";
45
+ }
46
+ if (status === "DONE_WITH_CONCERNS" || status === "NEEDS_CONTEXT") {
47
+ return "WARN";
48
+ }
49
+ return "BLOCK";
50
+ }
51
+
52
+ function buildSurface(taskGroupId) {
53
+ const safeId = String(taskGroupId || "")
54
+ .trim()
55
+ .replace(/[^A-Za-z0-9._-]+/g, "_");
56
+ return `task-execution.${safeId}`;
57
+ }
58
+
59
+ function normalizeTaskExecutionEnvelope(input = {}) {
60
+ const taskGroupId = normalizeTaskGroupId(input.taskGroupId);
61
+ const status = normalizeStatus(input.status);
62
+ const summary = String(input.summary || "").trim();
63
+ if (!summary) {
64
+ throw new Error("`task-execution` requires `--summary <text>`.");
65
+ }
66
+ const changedFiles = normalizeList(input.changedFiles);
67
+ const testEvidence = normalizeList(input.testEvidence);
68
+ const concerns = normalizeList(input.concerns);
69
+ const blockers = normalizeList(input.blockers);
70
+
71
+ return {
72
+ taskGroupId,
73
+ status,
74
+ summary,
75
+ changedFiles,
76
+ testEvidence,
77
+ concerns,
78
+ blockers,
79
+ recordedAt: new Date().toISOString()
80
+ };
81
+ }
82
+
83
+ function writeTaskExecutionEnvelope(projectPathInput, options = {}) {
84
+ const projectRoot = path.resolve(projectPathInput || process.cwd());
85
+ const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
86
+ const resolved = resolveChangeDir(projectRoot, requestedChangeId);
87
+ if (!resolved.changeDir || !resolved.changeId) {
88
+ const resolveError =
89
+ Array.isArray(resolved.failures) && resolved.failures.length > 0
90
+ ? String(resolved.failures[0] || "").trim()
91
+ : "";
92
+ throw new Error(resolveError || "Unable to resolve active change for task-execution.");
93
+ }
94
+
95
+ const envelope = normalizeTaskExecutionEnvelope(options);
96
+ const signalPath = writeExecutionSignal(projectRoot, {
97
+ changeId: resolved.changeId,
98
+ surface: buildSurface(envelope.taskGroupId),
99
+ status: mapImplementerStatusToSignal(envelope.status),
100
+ advisory: false,
101
+ strict: true,
102
+ failures: envelope.status === "BLOCKED" ? envelope.blockers : [],
103
+ warnings:
104
+ envelope.status === "DONE_WITH_CONCERNS" || envelope.status === "NEEDS_CONTEXT"
105
+ ? envelope.concerns
106
+ : [],
107
+ notes: [envelope.summary, ...envelope.testEvidence.map((item) => `test: ${item}`)],
108
+ details: {
109
+ type: "task_execution",
110
+ envelope
111
+ }
112
+ });
113
+
114
+ return {
115
+ status: mapImplementerStatusToSignal(envelope.status),
116
+ projectRoot,
117
+ changeId: resolved.changeId,
118
+ taskGroupId: envelope.taskGroupId,
119
+ implementerStatus: envelope.status,
120
+ summary: envelope.summary,
121
+ changedFiles: envelope.changedFiles,
122
+ testEvidence: envelope.testEvidence,
123
+ concerns: envelope.concerns,
124
+ blockers: envelope.blockers,
125
+ signalPath
126
+ };
127
+ }
128
+
129
+ function formatTaskExecutionReport(result) {
130
+ const lines = [
131
+ "Task execution envelope",
132
+ `Project: ${result.projectRoot}`,
133
+ `Change: ${result.changeId}`,
134
+ `Task group: ${result.taskGroupId}`,
135
+ `Implementer status: ${result.implementerStatus}`,
136
+ `Signal status: ${result.status}`,
137
+ `Summary: ${result.summary}`,
138
+ `Signal path: ${result.signalPath}`
139
+ ];
140
+ if (result.changedFiles.length > 0) {
141
+ lines.push(`Changed files: ${result.changedFiles.join(", ")}`);
142
+ }
143
+ if (result.testEvidence.length > 0) {
144
+ lines.push(`Test evidence: ${result.testEvidence.join(", ")}`);
145
+ }
146
+ if (result.concerns.length > 0) {
147
+ lines.push(`Concerns: ${result.concerns.join(", ")}`);
148
+ }
149
+ if (result.blockers.length > 0) {
150
+ lines.push(`Blockers: ${result.blockers.join(", ")}`);
151
+ }
152
+ return lines.join("\n");
153
+ }
154
+
155
+ module.exports = {
156
+ VALID_IMPLEMENTER_STATUSES,
157
+ normalizeTaskExecutionEnvelope,
158
+ writeTaskExecutionEnvelope,
159
+ formatTaskExecutionReport
160
+ };
@@ -0,0 +1,197 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { resolveChangeDir, unique } = require("./planning-parsers");
4
+ const { readExecutionSignals, writeExecutionSignal } = require("./execution-signals");
5
+ const { writeFileAtomic, readTextIfExists } = require("./utils");
6
+
7
+ const VALID_REVIEW_STAGES = new Set(["spec", "quality"]);
8
+ const VALID_REVIEW_STATUSES = new Set(["PASS", "WARN", "BLOCK"]);
9
+
10
+ function normalizeReviewStage(value) {
11
+ const normalized = String(value || "").trim().toLowerCase();
12
+ if (!VALID_REVIEW_STAGES.has(normalized)) {
13
+ throw new Error("`task-review --stage` must be `spec` or `quality`.");
14
+ }
15
+ return normalized;
16
+ }
17
+
18
+ function normalizeReviewStatus(value) {
19
+ const normalized = String(value || "").trim().toUpperCase();
20
+ if (!VALID_REVIEW_STATUSES.has(normalized)) {
21
+ throw new Error("`task-review --status` must be PASS, WARN, or BLOCK.");
22
+ }
23
+ return normalized;
24
+ }
25
+
26
+ function normalizeTaskGroupId(value) {
27
+ const normalized = String(value || "").trim();
28
+ if (!normalized) {
29
+ throw new Error("`task-review` requires `--task-group <id>`.");
30
+ }
31
+ return normalized;
32
+ }
33
+
34
+ function normalizeList(value) {
35
+ if (Array.isArray(value)) {
36
+ return unique(value.map((item) => String(item || "").trim()).filter(Boolean));
37
+ }
38
+ return unique(
39
+ String(value || "")
40
+ .split(/[,\n;]/)
41
+ .map((item) => item.trim())
42
+ .filter(Boolean)
43
+ );
44
+ }
45
+
46
+ function buildSafeTaskGroupId(taskGroupId) {
47
+ return String(taskGroupId || "")
48
+ .trim()
49
+ .replace(/[^A-Za-z0-9._-]+/g, "_");
50
+ }
51
+
52
+ function buildTaskReviewSurface(taskGroupId, stage) {
53
+ return `task-review.${buildSafeTaskGroupId(taskGroupId)}.${stage}`;
54
+ }
55
+
56
+ function mapReviewStatusToSignalStatus(status) {
57
+ return status;
58
+ }
59
+
60
+ function normalizeTaskReviewEnvelope(input = {}) {
61
+ const taskGroupId = normalizeTaskGroupId(input.taskGroupId);
62
+ const stage = normalizeReviewStage(input.stage);
63
+ const status = normalizeReviewStatus(input.status);
64
+ const summary = String(input.summary || "").trim();
65
+ if (!summary) {
66
+ throw new Error("`task-review` requires `--summary <text>`.");
67
+ }
68
+ const issues = normalizeList(input.issues);
69
+ const reviewer = String(input.reviewer || "").trim() || "unspecified";
70
+
71
+ return {
72
+ taskGroupId,
73
+ stage,
74
+ status,
75
+ summary,
76
+ issues,
77
+ reviewer,
78
+ recordedAt: new Date().toISOString()
79
+ };
80
+ }
81
+
82
+ function findLatestTaskReviewSignal(signals, taskGroupId, stage) {
83
+ const surface = buildTaskReviewSurface(taskGroupId, stage);
84
+ return (signals || [])
85
+ .filter((signal) => String(signal.surface || "") === surface)
86
+ .sort((left, right) => String(right.timestamp || "").localeCompare(String(left.timestamp || "")))[0] || null;
87
+ }
88
+
89
+ function appendTaskReviewEvidence(verificationText, envelope) {
90
+ const existing = String(verificationText || "").trim();
91
+ const section = [
92
+ `### Task Review ${envelope.taskGroupId} (${envelope.stage})`,
93
+ `- Status: ${envelope.status}`,
94
+ `- Reviewer: ${envelope.reviewer}`,
95
+ `- Summary: ${envelope.summary}`,
96
+ `- Issues: ${envelope.issues.length > 0 ? envelope.issues.join("; ") : "none"}`,
97
+ `- Recorded at: ${envelope.recordedAt}`
98
+ ].join("\n");
99
+
100
+ if (!existing) {
101
+ return `# Verification\n\n## Task Review Evidence\n\n${section}\n`;
102
+ }
103
+ if (!/^\s*##\s+Task Review Evidence\s*$/im.test(existing)) {
104
+ return `${existing}\n\n## Task Review Evidence\n\n${section}\n`;
105
+ }
106
+ return `${existing}\n\n${section}\n`;
107
+ }
108
+
109
+ function writeTaskReviewEnvelope(projectPathInput, options = {}) {
110
+ const projectRoot = path.resolve(projectPathInput || process.cwd());
111
+ const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
112
+ const resolved = resolveChangeDir(projectRoot, requestedChangeId);
113
+ if (!resolved.changeDir || !resolved.changeId) {
114
+ const resolveError =
115
+ Array.isArray(resolved.failures) && resolved.failures.length > 0
116
+ ? String(resolved.failures[0] || "").trim()
117
+ : "";
118
+ throw new Error(resolveError || "Unable to resolve active change for task-review.");
119
+ }
120
+
121
+ const envelope = normalizeTaskReviewEnvelope(options);
122
+ const existingSignals = readExecutionSignals(projectRoot, { changeId: resolved.changeId });
123
+
124
+ if (envelope.stage === "quality") {
125
+ const specSignal = findLatestTaskReviewSignal(existingSignals, envelope.taskGroupId, "spec");
126
+ if (!specSignal || String(specSignal.status || "").toUpperCase() !== "PASS") {
127
+ throw new Error(
128
+ `Quality review for task group ${envelope.taskGroupId} requires a prior spec review PASS.`
129
+ );
130
+ }
131
+ }
132
+
133
+ const signalPath = writeExecutionSignal(projectRoot, {
134
+ changeId: resolved.changeId,
135
+ surface: buildTaskReviewSurface(envelope.taskGroupId, envelope.stage),
136
+ status: mapReviewStatusToSignalStatus(envelope.status),
137
+ advisory: false,
138
+ strict: true,
139
+ failures: envelope.status === "BLOCK" ? envelope.issues : [],
140
+ warnings: envelope.status === "WARN" ? envelope.issues : [],
141
+ notes: [envelope.summary, `reviewer: ${envelope.reviewer}`],
142
+ details: {
143
+ type: "task_review",
144
+ envelope
145
+ }
146
+ });
147
+
148
+ let verificationPath = null;
149
+ if (options.writeVerification === true) {
150
+ verificationPath = path.join(resolved.changeDir, "verification.md");
151
+ fs.mkdirSync(path.dirname(verificationPath), { recursive: true });
152
+ const nextVerification = appendTaskReviewEvidence(readTextIfExists(verificationPath), envelope);
153
+ writeFileAtomic(verificationPath, nextVerification);
154
+ }
155
+
156
+ return {
157
+ status: envelope.status,
158
+ projectRoot,
159
+ changeId: resolved.changeId,
160
+ taskGroupId: envelope.taskGroupId,
161
+ stage: envelope.stage,
162
+ summary: envelope.summary,
163
+ issues: envelope.issues,
164
+ reviewer: envelope.reviewer,
165
+ signalPath,
166
+ verificationPath
167
+ };
168
+ }
169
+
170
+ function formatTaskReviewReport(result) {
171
+ const lines = [
172
+ "Task review envelope",
173
+ `Project: ${result.projectRoot}`,
174
+ `Change: ${result.changeId}`,
175
+ `Task group: ${result.taskGroupId}`,
176
+ `Stage: ${result.stage}`,
177
+ `Status: ${result.status}`,
178
+ `Reviewer: ${result.reviewer}`,
179
+ `Summary: ${result.summary}`,
180
+ `Signal path: ${result.signalPath}`
181
+ ];
182
+ if (result.issues.length > 0) {
183
+ lines.push(`Issues: ${result.issues.join("; ")}`);
184
+ }
185
+ if (result.verificationPath) {
186
+ lines.push(`Verification write-back: ${result.verificationPath}`);
187
+ }
188
+ return lines.join("\n");
189
+ }
190
+
191
+ module.exports = {
192
+ VALID_REVIEW_STAGES,
193
+ VALID_REVIEW_STATUSES,
194
+ normalizeTaskReviewEnvelope,
195
+ writeTaskReviewEnvelope,
196
+ formatTaskReviewReport
197
+ };
package/lib/verify.js CHANGED
@@ -13,6 +13,7 @@ const {
13
13
  readChangeArtifacts,
14
14
  readArtifactTexts
15
15
  } = require("./planning-parsers");
16
+ const { readExecutionSignals, summarizeSignalsBySurface } = require("./execution-signals");
16
17
 
17
18
  const CODE_FILE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".html", ".css", ".scss"]);
18
19
  const NON_IMPLEMENTATION_DIR_NAMES = new Set([
@@ -47,6 +48,7 @@ const MAX_SCANNED_FILES = 2000;
47
48
  const MAX_SCANNED_BYTES_PER_FILE = 512 * 1024;
48
49
  const MAX_SCANNED_DIRECTORIES = 10000;
49
50
  const MAX_SCAN_DEPTH = 32;
51
+ const DEFAULT_EVIDENCE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
50
52
 
51
53
  function buildEnvelope(name, projectRoot, strict) {
52
54
  return {
@@ -226,6 +228,138 @@ function allCovered(checks) {
226
228
  return true;
227
229
  }
228
230
 
231
+ function safeMtimeMs(filePath) {
232
+ try {
233
+ const stat = fs.statSync(filePath);
234
+ return Number(stat.mtimeMs) || 0;
235
+ } catch (_error) {
236
+ return 0;
237
+ }
238
+ }
239
+
240
+ function collectFreshnessBaseline(projectRoot, resolved, artifactPaths) {
241
+ const baselineCandidates = [];
242
+ if (artifactPaths) {
243
+ baselineCandidates.push(
244
+ artifactPaths.proposalPath,
245
+ artifactPaths.tasksPath,
246
+ artifactPaths.bindingsPath,
247
+ artifactPaths.pencilDesignPath,
248
+ artifactPaths.verificationPath
249
+ );
250
+ }
251
+ if (resolved && resolved.changeDir) {
252
+ const specsRoot = path.join(resolved.changeDir, "specs");
253
+ if (fs.existsSync(specsRoot)) {
254
+ const stack = [specsRoot];
255
+ while (stack.length > 0) {
256
+ const current = stack.pop();
257
+ let entries = [];
258
+ try {
259
+ entries = fs.readdirSync(current, { withFileTypes: true });
260
+ } catch (_error) {
261
+ continue;
262
+ }
263
+ for (const entry of entries) {
264
+ const absolutePath = path.join(current, entry.name);
265
+ if (entry.isDirectory() && !entry.isSymbolicLink()) {
266
+ stack.push(absolutePath);
267
+ continue;
268
+ }
269
+ if (entry.isFile() && entry.name === "spec.md") {
270
+ baselineCandidates.push(absolutePath);
271
+ }
272
+ }
273
+ }
274
+ }
275
+ }
276
+ const baselineMs = baselineCandidates.reduce((latest, candidate) => Math.max(latest, safeMtimeMs(candidate)), 0);
277
+ return {
278
+ baselineMs,
279
+ baselineIso: baselineMs > 0 ? new Date(baselineMs).toISOString() : ""
280
+ };
281
+ }
282
+
283
+ function collectVerificationFreshness(projectPathInput, options = {}) {
284
+ const projectRoot = path.resolve(projectPathInput || process.cwd());
285
+ const changeId = options.changeId ? String(options.changeId).trim() : "";
286
+ if (!changeId) {
287
+ return {
288
+ fresh: false,
289
+ changeId: null,
290
+ requiredSurfaces: [],
291
+ surfaces: {},
292
+ staleReasons: ["Missing change id for verification freshness checks."],
293
+ baselineIso: ""
294
+ };
295
+ }
296
+
297
+ const requiredSurfaces =
298
+ Array.isArray(options.requiredSurfaces) && options.requiredSurfaces.length > 0
299
+ ? options.requiredSurfaces
300
+ : ["verify-bindings", "verify-implementation", "verify-structure", "verify-coverage"];
301
+ const maxAgeMs =
302
+ Number.isFinite(Number(options.maxAgeMs)) && Number(options.maxAgeMs) > 0
303
+ ? Number(options.maxAgeMs)
304
+ : DEFAULT_EVIDENCE_MAX_AGE_MS;
305
+ const nowMs = Date.now();
306
+ const signals = readExecutionSignals(projectRoot, { changeId });
307
+ const summary = summarizeSignalsBySurface(signals);
308
+ const baseline = collectFreshnessBaseline(projectRoot, options.resolved, options.artifactPaths);
309
+ const staleReasons = [];
310
+ const surfaces = {};
311
+
312
+ for (const surface of requiredSurfaces) {
313
+ const key = String(surface || "").toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
314
+ const signal = summary[key];
315
+ if (!signal) {
316
+ staleReasons.push(`Missing evidence signal: ${surface}.`);
317
+ surfaces[surface] = {
318
+ present: false,
319
+ stale: true
320
+ };
321
+ continue;
322
+ }
323
+ const timestampMs = Date.parse(String(signal.timestamp || ""));
324
+ if (!Number.isFinite(timestampMs)) {
325
+ staleReasons.push(`Invalid evidence timestamp for ${surface}.`);
326
+ surfaces[surface] = {
327
+ present: true,
328
+ stale: true,
329
+ timestamp: signal.timestamp || ""
330
+ };
331
+ continue;
332
+ }
333
+ const olderThanBaseline = baseline.baselineMs > 0 && timestampMs < baseline.baselineMs;
334
+ const olderThanMaxAge = nowMs - timestampMs > maxAgeMs;
335
+ if (olderThanBaseline) {
336
+ staleReasons.push(
337
+ `${surface} evidence is older than current artifact baseline (${new Date(timestampMs).toISOString()} < ${baseline.baselineIso}).`
338
+ );
339
+ }
340
+ if (olderThanMaxAge) {
341
+ staleReasons.push(
342
+ `${surface} evidence is older than freshness window (${Math.round((nowMs - timestampMs) / 1000)}s).`
343
+ );
344
+ }
345
+ surfaces[surface] = {
346
+ present: true,
347
+ stale: olderThanBaseline || olderThanMaxAge,
348
+ status: signal.status,
349
+ timestamp: signal.timestamp
350
+ };
351
+ }
352
+
353
+ return {
354
+ fresh: staleReasons.length === 0,
355
+ changeId,
356
+ requiredSurfaces,
357
+ surfaces,
358
+ staleReasons: unique(staleReasons),
359
+ baselineIso: baseline.baselineIso
360
+ };
361
+ }
362
+
229
363
  function createSharedSetup(projectPathInput, options = {}) {
230
364
  const projectRoot = path.resolve(projectPathInput || process.cwd());
231
365
  const strict = options && options.strict === true;
@@ -600,6 +734,22 @@ function verifyCoverage(projectPathInput, options = {}) {
600
734
  }
601
735
  };
602
736
 
737
+ const freshness = collectVerificationFreshness(projectRoot, {
738
+ changeId: result.changeId,
739
+ resolved: sharedSetup.resolved,
740
+ artifactPaths: sharedSetup.artifactPaths,
741
+ requiredSurfaces: ["verify-bindings", "verify-implementation", "verify-structure"]
742
+ });
743
+ result.freshness = freshness;
744
+ if (!freshness.fresh) {
745
+ result.warnings.push(
746
+ "Verification freshness is stale for completion-facing claims; re-run verify surfaces before completion wording."
747
+ );
748
+ for (const reason of freshness.staleReasons) {
749
+ result.warnings.push(reason);
750
+ }
751
+ }
752
+
603
753
  return finalize(result);
604
754
  }
605
755
 
@@ -648,5 +798,6 @@ module.exports = {
648
798
  verifyImplementation,
649
799
  verifyStructure,
650
800
  verifyCoverage,
651
- formatVerifyReport
801
+ formatVerifyReport,
802
+ collectVerificationFreshness
652
803
  };
@@ -1,8 +1,8 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
 
4
- const { isPathInside } = require("./fs-safety");
5
4
  const { ensurePenFile } = require("./pen-persistence");
5
+ const { resolvePreferredRegisteredPenPath } = require("./design-source-registry");
6
6
  const { pathExists, readTextIfExists, writeFileAtomic } = require("./utils");
7
7
 
8
8
  const DEFAULT_PROJECT_PEN_RELATIVE_PATH = ".da-vinci/designs/project-baseline.pen";
@@ -20,17 +20,6 @@ function validateChangeId(changeId) {
20
20
  return normalized;
21
21
  }
22
22
 
23
- function extractRegisteredPenPath(projectRoot, registryText) {
24
- const matches = String(registryText || "").match(/\.da-vinci\/designs\/[^\s`]+\.pen/g) || [];
25
- for (const relativePath of matches) {
26
- const resolvedPath = path.resolve(projectRoot, relativePath);
27
- if (isPathInside(projectRoot, resolvedPath)) {
28
- return resolvedPath;
29
- }
30
- }
31
- return "";
32
- }
33
-
34
23
  function buildProjectTemplates(preferredPenRelativePath) {
35
24
  return {
36
25
  "DA-VINCI.md": [
@@ -151,7 +140,7 @@ function bootstrapProjectArtifacts(projectPathInput, options = {}) {
151
140
  fs.mkdirSync(path.join(projectRoot, ".da-vinci", "changes"), { recursive: true });
152
141
 
153
142
  const existingRegistryText = readTextIfExists(path.join(projectRoot, ".da-vinci", "design-registry.md"));
154
- const registeredPenPath = extractRegisteredPenPath(projectRoot, existingRegistryText);
143
+ const registeredPenPath = resolvePreferredRegisteredPenPath(projectRoot, existingRegistryText);
155
144
  const preferredPenPath = registeredPenPath || path.join(projectRoot, DEFAULT_PROJECT_PEN_RELATIVE_PATH);
156
145
  const preferredPenRelativePath = path.relative(projectRoot, preferredPenPath) || DEFAULT_PROJECT_PEN_RELATIVE_PATH;
157
146
 
@@ -2,7 +2,7 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { writeFileAtomic, pathExists, readTextIfExists } = require("./utils");
4
4
 
5
- const WORKFLOW_STATE_VERSION = 1;
5
+ const WORKFLOW_STATE_VERSION = 2;
6
6
  const DEFAULT_STALE_WINDOW_MS = 24 * 60 * 60 * 1000;
7
7
  const PERSISTED_NOTE_EXCLUDE_PATTERNS = [
8
8
  /^No persisted workflow state found for this change; deriving from artifacts\.$/i,
@@ -38,7 +38,9 @@ function fingerprintForPath(targetPath) {
38
38
  function buildWorkflowFingerprint(projectRoot, changeId) {
39
39
  const changeRoot = changeId ? path.join(projectRoot, ".da-vinci", "changes", changeId) : "";
40
40
  const candidates = [
41
+ path.join(projectRoot, "DA-VINCI.md"),
41
42
  path.join(projectRoot, ".da-vinci", "page-map.md"),
43
+ path.join(projectRoot, ".da-vinci", "design-registry.md"),
42
44
  path.join(changeRoot, "proposal.md"),
43
45
  path.join(changeRoot, "design.md"),
44
46
  path.join(changeRoot, "pencil-design.md"),