@xenonbyte/da-vinci-workflow 0.2.2 → 0.2.3

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 (54) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +24 -14
  3. package/README.zh-CN.md +25 -14
  4. package/commands/claude/dv/breakdown.md +8 -0
  5. package/commands/claude/dv/build.md +11 -0
  6. package/commands/claude/dv/design.md +5 -2
  7. package/commands/claude/dv/tasks.md +8 -0
  8. package/commands/claude/dv/verify.md +9 -0
  9. package/commands/codex/prompts/dv-breakdown.md +8 -0
  10. package/commands/codex/prompts/dv-build.md +11 -0
  11. package/commands/codex/prompts/dv-design.md +5 -2
  12. package/commands/codex/prompts/dv-tasks.md +8 -0
  13. package/commands/codex/prompts/dv-verify.md +8 -0
  14. package/commands/gemini/dv/breakdown.toml +8 -0
  15. package/commands/gemini/dv/build.toml +11 -0
  16. package/commands/gemini/dv/design.toml +5 -2
  17. package/commands/gemini/dv/tasks.toml +8 -0
  18. package/commands/gemini/dv/verify.toml +8 -0
  19. package/docs/dv-command-reference.md +43 -0
  20. package/docs/execution-chain-plan.md +10 -3
  21. package/docs/mode-use-cases.md +2 -1
  22. package/docs/pencil-rendering-workflow.md +15 -12
  23. package/docs/prompt-presets/README.md +1 -1
  24. package/docs/prompt-presets/desktop-app.md +3 -3
  25. package/docs/prompt-presets/mobile-app.md +3 -3
  26. package/docs/prompt-presets/tablet-app.md +3 -3
  27. package/docs/prompt-presets/web-app.md +3 -3
  28. package/docs/skill-usage.md +45 -38
  29. package/docs/workflow-examples.md +16 -13
  30. package/docs/workflow-overview.md +2 -0
  31. package/docs/zh-CN/dv-command-reference.md +43 -0
  32. package/docs/zh-CN/mode-use-cases.md +2 -1
  33. package/docs/zh-CN/pencil-rendering-workflow.md +15 -12
  34. package/docs/zh-CN/prompt-presets/README.md +1 -1
  35. package/docs/zh-CN/prompt-presets/desktop-app.md +3 -3
  36. package/docs/zh-CN/prompt-presets/mobile-app.md +3 -3
  37. package/docs/zh-CN/prompt-presets/tablet-app.md +3 -3
  38. package/docs/zh-CN/prompt-presets/web-app.md +3 -3
  39. package/docs/zh-CN/skill-usage.md +45 -38
  40. package/docs/zh-CN/workflow-examples.md +15 -13
  41. package/docs/zh-CN/workflow-overview.md +2 -0
  42. package/examples/greenfield-spec-markupflow/.da-vinci/state/execution-signals/demo__lint-tasks.json +16 -0
  43. package/lib/audit-parsers.js +18 -9
  44. package/lib/audit.js +3 -26
  45. package/lib/cli.js +50 -1
  46. package/lib/design-source-registry.js +146 -0
  47. package/lib/save-current-design.js +790 -0
  48. package/lib/supervisor-review.js +1 -1
  49. package/lib/workflow-bootstrap.js +2 -13
  50. package/lib/workflow-persisted-state.js +3 -1
  51. package/lib/workflow-state.js +51 -3
  52. package/package.json +1 -1
  53. package/tui/catalog.js +103 -0
  54. package/tui/index.js +2274 -418
@@ -0,0 +1,790 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+ const {
5
+ DEFAULT_READ_DEPTH,
6
+ normalizeNodesPayload,
7
+ normalizeVariablesPayload,
8
+ readPenDocument,
9
+ runPencilInteractive,
10
+ extractFirstJson
11
+ } = require("./pen-persistence");
12
+ const { readSessionState, persistPencilSession } = require("./pencil-session");
13
+ const { getPencilLockStatus, isStaleLock } = require("./pencil-lock");
14
+ const {
15
+ resolvePreferredRegisteredPenPath,
16
+ normalizeRegisteredPenPath,
17
+ normalizeSessionPenPath,
18
+ normalizeActiveEditorPath
19
+ } = require("./design-source-registry");
20
+ const { readTextIfExists } = require("./utils");
21
+
22
+ const SAVE_STATUS = Object.freeze({
23
+ SAVED: "saved",
24
+ BLOCKED: "blocked",
25
+ UNAVAILABLE: "unavailable"
26
+ });
27
+
28
+ const SAVE_CODES = Object.freeze({
29
+ SAVE_COMPLETED: "SAVE_COMPLETED",
30
+ REGISTERED_PEN_MISSING: "REGISTERED_PEN_MISSING",
31
+ REGISTERED_PEN_INVALID: "REGISTERED_PEN_INVALID",
32
+ SESSION_STATE_MISSING: "SESSION_STATE_MISSING",
33
+ SESSION_STATE_INVALID: "SESSION_STATE_INVALID",
34
+ SESSION_NOT_ACTIVE: "SESSION_NOT_ACTIVE",
35
+ SESSION_PEN_MISSING: "SESSION_PEN_MISSING",
36
+ SESSION_PEN_INVALID: "SESSION_PEN_INVALID",
37
+ ACTIVE_EDITOR_MISSING: "ACTIVE_EDITOR_MISSING",
38
+ ACTIVE_EDITOR_NEW: "ACTIVE_EDITOR_NEW",
39
+ ACTIVE_EDITOR_INVALID: "ACTIVE_EDITOR_INVALID",
40
+ BOUND_SOURCE_MISMATCH: "BOUND_SOURCE_MISMATCH",
41
+ LOCK_MISSING: "LOCK_MISSING",
42
+ LOCK_INVALID: "LOCK_INVALID",
43
+ LOCK_STALE: "LOCK_STALE",
44
+ LOCK_OWNED_BY_OTHER_PROJECT: "LOCK_OWNED_BY_OTHER_PROJECT",
45
+ BOUND_PEN_MISSING: "BOUND_PEN_MISSING",
46
+ BOUND_PEN_VERSION_UNREADABLE: "BOUND_PEN_VERSION_UNREADABLE",
47
+ SNAPSHOT_NODES_TRUNCATED: "SNAPSHOT_NODES_TRUNCATED",
48
+ SNAPSHOT_NODES_INVALID: "SNAPSHOT_NODES_INVALID",
49
+ SNAPSHOT_VARIABLES_INVALID: "SNAPSHOT_VARIABLES_INVALID",
50
+ ACTIVE_EDITOR_DRIFT: "ACTIVE_EDITOR_DRIFT",
51
+ PERSIST_FAILED: "PERSIST_FAILED",
52
+ PERSIST_NOT_IN_SYNC: "PERSIST_NOT_IN_SYNC",
53
+ MCP_BRIDGE_UNAVAILABLE: "MCP_BRIDGE_UNAVAILABLE",
54
+ MCP_EDITOR_STATE_UNAVAILABLE: "MCP_EDITOR_STATE_UNAVAILABLE",
55
+ MCP_SNAPSHOT_UNAVAILABLE: "MCP_SNAPSHOT_UNAVAILABLE"
56
+ });
57
+
58
+ function toObject(value) {
59
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
60
+ return {};
61
+ }
62
+ return value;
63
+ }
64
+
65
+ function cleanDetails(details) {
66
+ const payload = toObject(details);
67
+ return Object.keys(payload).reduce((accumulator, key) => {
68
+ if (payload[key] !== undefined) {
69
+ accumulator[key] = payload[key];
70
+ }
71
+ return accumulator;
72
+ }, {});
73
+ }
74
+
75
+ function createResult(status, code, message, details = {}) {
76
+ return {
77
+ status,
78
+ code,
79
+ message: String(message || "").trim() || "(no message provided)",
80
+ details: cleanDetails(details)
81
+ };
82
+ }
83
+
84
+ function blocked(code, message, details = {}) {
85
+ return createResult(SAVE_STATUS.BLOCKED, code, message, details);
86
+ }
87
+
88
+ function unavailable(code, message, details = {}) {
89
+ return createResult(SAVE_STATUS.UNAVAILABLE, code, message, details);
90
+ }
91
+
92
+ function saved(message, details = {}) {
93
+ return createResult(SAVE_STATUS.SAVED, SAVE_CODES.SAVE_COMPLETED, message, details);
94
+ }
95
+
96
+ function mapValidationCodeToSaveCode(reasonCode) {
97
+ switch (reasonCode) {
98
+ case "registered_pen_missing":
99
+ return SAVE_CODES.REGISTERED_PEN_MISSING;
100
+ case "registered_pen_outside_project_root":
101
+ case "registered_pen_not_pen_path":
102
+ return SAVE_CODES.REGISTERED_PEN_INVALID;
103
+ case "session_pen_missing":
104
+ return SAVE_CODES.SESSION_PEN_MISSING;
105
+ case "session_pen_outside_project_root":
106
+ case "session_pen_not_pen_path":
107
+ return SAVE_CODES.SESSION_PEN_INVALID;
108
+ case "active_editor_missing":
109
+ return SAVE_CODES.ACTIVE_EDITOR_MISSING;
110
+ case "active_editor_new":
111
+ return SAVE_CODES.ACTIVE_EDITOR_NEW;
112
+ case "active_editor_outside_project_root":
113
+ case "active_editor_not_pen_path":
114
+ return SAVE_CODES.ACTIVE_EDITOR_INVALID;
115
+ default:
116
+ return SAVE_CODES.BOUND_SOURCE_MISMATCH;
117
+ }
118
+ }
119
+
120
+ function buildValidationMessage(reasonCode) {
121
+ switch (reasonCode) {
122
+ case "registered_pen_missing":
123
+ return "Registered project-local `.pen` path is missing.";
124
+ case "registered_pen_outside_project_root":
125
+ return "Registered `.pen` path resolves outside the project root.";
126
+ case "registered_pen_not_pen_path":
127
+ return "Registered `.pen` path does not end with `.pen`.";
128
+ case "session_pen_missing":
129
+ return "Session-bound `.pen` path is missing.";
130
+ case "session_pen_outside_project_root":
131
+ return "Session-bound `.pen` path resolves outside the project root.";
132
+ case "session_pen_not_pen_path":
133
+ return "Session-bound `.pen` path does not end with `.pen`.";
134
+ case "active_editor_missing":
135
+ return "Active editor path is missing from runtime state.";
136
+ case "active_editor_new":
137
+ return "Active editor is still `new`; save cannot continue.";
138
+ case "active_editor_outside_project_root":
139
+ return "Active editor path resolves outside the project root.";
140
+ case "active_editor_not_pen_path":
141
+ return "Active editor path does not end with `.pen`.";
142
+ default:
143
+ return "Registered, session, and active editor sources did not converge.";
144
+ }
145
+ }
146
+
147
+ function normalizeProjectRoot(projectPath) {
148
+ return path.resolve(projectPath || process.cwd());
149
+ }
150
+
151
+ function extractActiveEditorPath(editorState) {
152
+ if (typeof editorState === "string") {
153
+ return editorState.trim();
154
+ }
155
+
156
+ const payload = toObject(editorState);
157
+ if (typeof payload.activeEditorPath === "string") {
158
+ return payload.activeEditorPath.trim();
159
+ }
160
+ if (typeof payload.activeEditor === "string") {
161
+ return payload.activeEditor.trim();
162
+ }
163
+ if (typeof payload.filePath === "string") {
164
+ return payload.filePath.trim();
165
+ }
166
+ if (payload.editor && typeof payload.editor.filePath === "string") {
167
+ return payload.editor.filePath.trim();
168
+ }
169
+ return "";
170
+ }
171
+
172
+ function resolveBridgeMethod(bridge, methodNames) {
173
+ for (const name of methodNames) {
174
+ if (bridge && typeof bridge[name] === "function") {
175
+ return bridge[name].bind(bridge);
176
+ }
177
+ }
178
+ return null;
179
+ }
180
+
181
+ async function invokeBridgeMethod(bridge, methodNames, args, failureCode, failureMessage) {
182
+ const method = resolveBridgeMethod(bridge, methodNames);
183
+ if (!method) {
184
+ return unavailable(
185
+ SAVE_CODES.MCP_BRIDGE_UNAVAILABLE,
186
+ "Runtime cannot access the MCP bridge required for Save Current Design.",
187
+ {
188
+ missingMethod: methodNames[0]
189
+ }
190
+ );
191
+ }
192
+
193
+ try {
194
+ const value = await Promise.resolve(method(args));
195
+ return {
196
+ status: "ok",
197
+ value
198
+ };
199
+ } catch (error) {
200
+ return unavailable(failureCode, failureMessage, {
201
+ error: error && error.message ? error.message : String(error || "unknown bridge error")
202
+ });
203
+ }
204
+ }
205
+
206
+ function readBoundPenVersion(boundPenPath) {
207
+ const resolvedPath = path.resolve(boundPenPath);
208
+ if (!fs.existsSync(resolvedPath)) {
209
+ return blocked(
210
+ SAVE_CODES.BOUND_PEN_MISSING,
211
+ "Bound `.pen` file is missing on disk.",
212
+ {
213
+ boundPenPath: resolvedPath
214
+ }
215
+ );
216
+ }
217
+
218
+ try {
219
+ const payload = readPenDocument(resolvedPath);
220
+ const version =
221
+ payload && typeof payload.version === "string" && payload.version.trim()
222
+ ? payload.version.trim()
223
+ : "";
224
+ if (!version) {
225
+ return blocked(
226
+ SAVE_CODES.BOUND_PEN_VERSION_UNREADABLE,
227
+ "Bound `.pen` version is missing or empty; Save Current Design requires explicit versioning.",
228
+ {
229
+ boundPenPath: resolvedPath
230
+ }
231
+ );
232
+ }
233
+ return {
234
+ status: "PASS",
235
+ boundPenPath: resolvedPath,
236
+ version
237
+ };
238
+ } catch (error) {
239
+ return blocked(
240
+ SAVE_CODES.BOUND_PEN_VERSION_UNREADABLE,
241
+ "Unable to resolve bound `.pen` version from disk.",
242
+ {
243
+ boundPenPath: resolvedPath,
244
+ error: error && error.message ? error.message : String(error)
245
+ }
246
+ );
247
+ }
248
+ }
249
+
250
+ function buildBridgeCommand(method, args = {}) {
251
+ if (method === "get_variables") {
252
+ return "get_variables()";
253
+ }
254
+ return `${method}(${JSON.stringify(toObject(args))})`;
255
+ }
256
+
257
+ function createLocalPencilBridge(options = {}) {
258
+ const penPath = String(options.penPath || "").trim();
259
+ if (!penPath) {
260
+ return null;
261
+ }
262
+ const resolvedPenPath = path.resolve(penPath);
263
+ const pencilOptions = {
264
+ pencilBin: options.pencilBin,
265
+ pencilTimeoutMs: options.pencilTimeoutMs,
266
+ maxBuffer: options.maxBuffer
267
+ };
268
+
269
+ function runAndParse(method, args) {
270
+ const command = buildBridgeCommand(method, args);
271
+ const output = runPencilInteractive(resolvedPenPath, [command], pencilOptions);
272
+ return extractFirstJson(output);
273
+ }
274
+
275
+ return {
276
+ get_editor_state(args = {}) {
277
+ try {
278
+ return runAndParse("get_editor_state", args);
279
+ } catch (_error) {
280
+ return { activeEditor: resolvedPenPath };
281
+ }
282
+ },
283
+ batch_get(args = {}) {
284
+ return runAndParse("batch_get", args);
285
+ },
286
+ get_variables() {
287
+ return runAndParse("get_variables");
288
+ }
289
+ };
290
+ }
291
+
292
+ function validateBoundDesignSource(input = {}) {
293
+ const projectRoot = normalizeProjectRoot(input.projectRoot);
294
+ const registered = normalizeRegisteredPenPath(projectRoot, input.registeredPenPath);
295
+ if (!registered.ok) {
296
+ return {
297
+ status: "BLOCK",
298
+ blockerCode: mapValidationCodeToSaveCode(registered.code),
299
+ reasonCode: registered.code,
300
+ message: buildValidationMessage(registered.code),
301
+ details: {
302
+ projectRoot,
303
+ registeredPenPath: input.registeredPenPath || "",
304
+ sessionPenPath: input.sessionPenPath || "",
305
+ activeEditorPath: input.activeEditorPath || "",
306
+ registeredResolvedPath: registered.resolvedPath || ""
307
+ }
308
+ };
309
+ }
310
+
311
+ const session = normalizeSessionPenPath(projectRoot, input.sessionPenPath);
312
+ if (!session.ok) {
313
+ return {
314
+ status: "BLOCK",
315
+ blockerCode: mapValidationCodeToSaveCode(session.code),
316
+ reasonCode: session.code,
317
+ message: buildValidationMessage(session.code),
318
+ details: {
319
+ projectRoot,
320
+ registeredPenPath: input.registeredPenPath || "",
321
+ sessionPenPath: input.sessionPenPath || "",
322
+ activeEditorPath: input.activeEditorPath || "",
323
+ sessionResolvedPath: session.resolvedPath || ""
324
+ }
325
+ };
326
+ }
327
+
328
+ const editor = normalizeActiveEditorPath(projectRoot, input.activeEditorPath);
329
+ if (!editor.ok) {
330
+ return {
331
+ status: "BLOCK",
332
+ blockerCode: mapValidationCodeToSaveCode(editor.code),
333
+ reasonCode: editor.code,
334
+ message: buildValidationMessage(editor.code),
335
+ details: {
336
+ projectRoot,
337
+ registeredPenPath: input.registeredPenPath || "",
338
+ sessionPenPath: input.sessionPenPath || "",
339
+ activeEditorPath: input.activeEditorPath || "",
340
+ activeEditorResolvedPath: editor.resolvedPath || ""
341
+ }
342
+ };
343
+ }
344
+
345
+ const resolvedPaths = [registered.resolvedPath, session.resolvedPath, editor.resolvedPath];
346
+ const uniqueResolved = Array.from(new Set(resolvedPaths));
347
+ if (uniqueResolved.length !== 1) {
348
+ return {
349
+ status: "BLOCK",
350
+ blockerCode: SAVE_CODES.BOUND_SOURCE_MISMATCH,
351
+ reasonCode: "bound_source_mismatch",
352
+ message: "Registered, session, and active editor sources do not converge on one `.pen` file.",
353
+ details: {
354
+ projectRoot,
355
+ registeredResolvedPath: registered.resolvedPath,
356
+ sessionResolvedPath: session.resolvedPath,
357
+ activeEditorResolvedPath: editor.resolvedPath
358
+ }
359
+ };
360
+ }
361
+
362
+ return {
363
+ status: "PASS",
364
+ boundPenPath: uniqueResolved[0]
365
+ };
366
+ }
367
+
368
+ function normalizeSnapshotPayloads(nodesPayload, variablesPayload) {
369
+ let normalizedNodes;
370
+ try {
371
+ normalizedNodes = normalizeNodesPayload(nodesPayload);
372
+ } catch (error) {
373
+ const message = error && error.message ? error.message : String(error);
374
+ const truncated = /truncated/i.test(message);
375
+ return blocked(
376
+ truncated ? SAVE_CODES.SNAPSHOT_NODES_TRUNCATED : SAVE_CODES.SNAPSHOT_NODES_INVALID,
377
+ truncated
378
+ ? "Live nodes payload is truncated (`...`); save is blocked until a complete snapshot is captured."
379
+ : "Live nodes payload is invalid for persistence.",
380
+ { error: message }
381
+ );
382
+ }
383
+
384
+ let normalizedVariables;
385
+ try {
386
+ normalizedVariables = normalizeVariablesPayload(variablesPayload) || {};
387
+ } catch (error) {
388
+ return blocked(
389
+ SAVE_CODES.SNAPSHOT_VARIABLES_INVALID,
390
+ "Live variables payload is invalid for persistence.",
391
+ {
392
+ error: error && error.message ? error.message : String(error)
393
+ }
394
+ );
395
+ }
396
+
397
+ return {
398
+ status: "PASS",
399
+ nodes: normalizedNodes,
400
+ variables: normalizedVariables
401
+ };
402
+ }
403
+
404
+ async function captureLivePencilSnapshot(options = {}) {
405
+ const projectRoot = normalizeProjectRoot(options.projectRoot);
406
+ const bridge = options.bridge;
407
+ const readDepth =
408
+ Number.isFinite(Number(options.readDepth)) && Number(options.readDepth) > 0
409
+ ? Number(options.readDepth)
410
+ : DEFAULT_READ_DEPTH;
411
+
412
+ if (!bridge || typeof bridge !== "object") {
413
+ return unavailable(
414
+ SAVE_CODES.MCP_BRIDGE_UNAVAILABLE,
415
+ "Runtime cannot access the MCP bridge required for live snapshot capture."
416
+ );
417
+ }
418
+
419
+ const firstEditorRead = await invokeBridgeMethod(
420
+ bridge,
421
+ ["get_editor_state", "getEditorState"],
422
+ { include_schema: false },
423
+ SAVE_CODES.MCP_EDITOR_STATE_UNAVAILABLE,
424
+ "Runtime could not read the active editor before snapshot capture."
425
+ );
426
+ if (firstEditorRead.status !== "ok") {
427
+ return firstEditorRead;
428
+ }
429
+ const firstActiveEditorPath = extractActiveEditorPath(firstEditorRead.value);
430
+
431
+ const firstValidation = validateBoundDesignSource({
432
+ projectRoot,
433
+ registeredPenPath: options.registeredPenPath,
434
+ sessionPenPath: options.sessionPenPath,
435
+ activeEditorPath: firstActiveEditorPath
436
+ });
437
+ if (firstValidation.status === "BLOCK") {
438
+ return blocked(firstValidation.blockerCode, firstValidation.message, {
439
+ ...firstValidation.details,
440
+ phase: "before_capture"
441
+ });
442
+ }
443
+
444
+ const nodesRead = await invokeBridgeMethod(
445
+ bridge,
446
+ ["batch_get", "batchGet"],
447
+ {
448
+ readDepth,
449
+ includePathGeometry: true,
450
+ resolveInstances: false,
451
+ resolveVariables: false
452
+ },
453
+ SAVE_CODES.MCP_SNAPSHOT_UNAVAILABLE,
454
+ "Runtime could not capture live nodes from the MCP bridge."
455
+ );
456
+ if (nodesRead.status !== "ok") {
457
+ return nodesRead;
458
+ }
459
+
460
+ const variablesRead = await invokeBridgeMethod(
461
+ bridge,
462
+ ["get_variables", "getVariables"],
463
+ {},
464
+ SAVE_CODES.MCP_SNAPSHOT_UNAVAILABLE,
465
+ "Runtime could not capture live variables from the MCP bridge."
466
+ );
467
+ if (variablesRead.status !== "ok") {
468
+ return variablesRead;
469
+ }
470
+
471
+ const snapshotPayload = normalizeSnapshotPayloads(nodesRead.value, variablesRead.value);
472
+ if (snapshotPayload.status === SAVE_STATUS.BLOCKED) {
473
+ return snapshotPayload;
474
+ }
475
+
476
+ const secondEditorRead = await invokeBridgeMethod(
477
+ bridge,
478
+ ["get_editor_state", "getEditorState"],
479
+ { include_schema: false },
480
+ SAVE_CODES.MCP_EDITOR_STATE_UNAVAILABLE,
481
+ "Runtime could not read the active editor after snapshot capture."
482
+ );
483
+ if (secondEditorRead.status !== "ok") {
484
+ return secondEditorRead;
485
+ }
486
+ const secondActiveEditorPath = extractActiveEditorPath(secondEditorRead.value);
487
+
488
+ const secondValidation = validateBoundDesignSource({
489
+ projectRoot,
490
+ registeredPenPath: options.registeredPenPath,
491
+ sessionPenPath: options.sessionPenPath,
492
+ activeEditorPath: secondActiveEditorPath
493
+ });
494
+ if (secondValidation.status === "BLOCK") {
495
+ return blocked(
496
+ SAVE_CODES.ACTIVE_EDITOR_DRIFT,
497
+ "Active editor drifted during snapshot capture; save is blocked.",
498
+ {
499
+ phase: "after_capture",
500
+ firstActiveEditorPath,
501
+ secondActiveEditorPath,
502
+ firstBoundPenPath: firstValidation.boundPenPath || "",
503
+ secondBoundPenPath: "",
504
+ driftReasonCode: secondValidation.reasonCode
505
+ }
506
+ );
507
+ }
508
+
509
+ if (secondValidation.boundPenPath !== firstValidation.boundPenPath) {
510
+ return blocked(
511
+ SAVE_CODES.ACTIVE_EDITOR_DRIFT,
512
+ "Active editor drifted during snapshot capture; save is blocked.",
513
+ {
514
+ phase: "after_capture",
515
+ firstActiveEditorPath,
516
+ secondActiveEditorPath,
517
+ firstBoundPenPath: firstValidation.boundPenPath || "",
518
+ secondBoundPenPath: secondValidation.boundPenPath || "",
519
+ driftReasonCode: "bound_source_mismatch"
520
+ }
521
+ );
522
+ }
523
+
524
+ return {
525
+ status: "PASS",
526
+ boundPenPath: firstValidation.boundPenPath,
527
+ firstActiveEditorPath,
528
+ secondActiveEditorPath,
529
+ nodes: snapshotPayload.nodes,
530
+ variables: snapshotPayload.variables
531
+ };
532
+ }
533
+
534
+ function materializeSnapshotFiles(snapshot) {
535
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-save-current-design-"));
536
+ const nodesFile = path.join(tempDir, "nodes.json");
537
+ const variablesFile = path.join(tempDir, "variables.json");
538
+
539
+ fs.writeFileSync(nodesFile, JSON.stringify({ nodes: snapshot.nodes }, null, 2));
540
+ fs.writeFileSync(variablesFile, JSON.stringify({ variables: snapshot.variables || {} }, null, 2));
541
+
542
+ return {
543
+ tempDir,
544
+ nodesFile,
545
+ variablesFile
546
+ };
547
+ }
548
+
549
+ function preflightSessionAndLock(projectRoot, registeredPenPath, options = {}) {
550
+ let session;
551
+ try {
552
+ session = readSessionState(projectRoot);
553
+ } catch (error) {
554
+ return blocked(
555
+ SAVE_CODES.SESSION_STATE_INVALID,
556
+ "Pencil session state is unreadable. Repair or restart the session before saving.",
557
+ {
558
+ projectRoot,
559
+ error: error && error.message ? error.message : String(error)
560
+ }
561
+ );
562
+ }
563
+ if (!session) {
564
+ return blocked(
565
+ SAVE_CODES.SESSION_STATE_MISSING,
566
+ "Pencil session state is missing. Start or resume a session before saving.",
567
+ {
568
+ projectRoot
569
+ }
570
+ );
571
+ }
572
+
573
+ if (session.status !== "active") {
574
+ return blocked(
575
+ SAVE_CODES.SESSION_NOT_ACTIVE,
576
+ "Pencil session is not active. Resume an active session before saving.",
577
+ {
578
+ projectRoot,
579
+ sessionStatus: session.status
580
+ }
581
+ );
582
+ }
583
+
584
+ const lockStatus = getPencilLockStatus({ homeDir: options.homeDir });
585
+ if (!lockStatus.lock) {
586
+ return blocked(
587
+ SAVE_CODES.LOCK_MISSING,
588
+ "Pencil lock is missing. Save Current Design cannot proceed without lock ownership.",
589
+ {
590
+ lockPath: lockStatus.lockPath
591
+ }
592
+ );
593
+ }
594
+
595
+ const lock = lockStatus.lock;
596
+ if (typeof lock !== "object" || lock.__invalid) {
597
+ return blocked(
598
+ SAVE_CODES.LOCK_INVALID,
599
+ "Pencil lock payload is invalid; Save Current Design cannot trust lock ownership.",
600
+ {
601
+ lockPath: lockStatus.lockPath
602
+ }
603
+ );
604
+ }
605
+
606
+ if (typeof lock.projectPath !== "string" || !lock.projectPath.trim()) {
607
+ return blocked(
608
+ SAVE_CODES.LOCK_INVALID,
609
+ "Pencil lock payload is missing `projectPath`; Save Current Design cannot verify lock ownership.",
610
+ {
611
+ lockPath: lockStatus.lockPath
612
+ }
613
+ );
614
+ }
615
+
616
+ if (isStaleLock(lock)) {
617
+ return blocked(
618
+ SAVE_CODES.LOCK_STALE,
619
+ "Pencil lock appears stale; restart the session before saving.",
620
+ {
621
+ lockPath: lockStatus.lockPath,
622
+ lockProjectPath: typeof lock.projectPath === "string" ? lock.projectPath : ""
623
+ }
624
+ );
625
+ }
626
+
627
+ const lockProjectPath = path.resolve(lock.projectPath);
628
+ if (lockProjectPath !== path.resolve(projectRoot)) {
629
+ return blocked(
630
+ SAVE_CODES.LOCK_OWNED_BY_OTHER_PROJECT,
631
+ "Pencil lock is currently owned by a different project.",
632
+ {
633
+ lockPath: lockStatus.lockPath,
634
+ lockProjectPath
635
+ }
636
+ );
637
+ }
638
+
639
+ const versionCheck = readBoundPenVersion(registeredPenPath);
640
+ if (versionCheck.status === SAVE_STATUS.BLOCKED) {
641
+ return versionCheck;
642
+ }
643
+
644
+ return {
645
+ status: "PASS",
646
+ session,
647
+ lockStatus,
648
+ penVersion: versionCheck.version,
649
+ registeredPenPath: versionCheck.boundPenPath
650
+ };
651
+ }
652
+
653
+ async function saveCurrentDesign(options = {}) {
654
+ const projectRoot = normalizeProjectRoot(options.projectPath);
655
+ const registryPath = path.join(projectRoot, ".da-vinci", "design-registry.md");
656
+ const registryText = readTextIfExists(registryPath);
657
+ const registeredPenPath = resolvePreferredRegisteredPenPath(projectRoot, registryText);
658
+
659
+ if (!registeredPenPath) {
660
+ return blocked(
661
+ SAVE_CODES.REGISTERED_PEN_MISSING,
662
+ "No registered project-local `.pen` source was found in `.da-vinci/design-registry.md`.",
663
+ {
664
+ projectRoot,
665
+ registryPath
666
+ }
667
+ );
668
+ }
669
+
670
+ const preflight = preflightSessionAndLock(projectRoot, registeredPenPath, {
671
+ homeDir: options.homeDir
672
+ });
673
+ if (preflight.status === SAVE_STATUS.BLOCKED) {
674
+ return preflight;
675
+ }
676
+
677
+ const capture = await captureLivePencilSnapshot({
678
+ projectRoot,
679
+ registeredPenPath,
680
+ sessionPenPath: preflight.session.penPath,
681
+ bridge:
682
+ options.bridge ||
683
+ (options.allowLocalBridge === true
684
+ ? createLocalPencilBridge({
685
+ penPath: preflight.session.penPath,
686
+ pencilBin: options.pencilBin,
687
+ pencilTimeoutMs: options.pencilTimeoutMs,
688
+ maxBuffer: options.maxBuffer
689
+ })
690
+ : null),
691
+ readDepth: options.readDepth
692
+ });
693
+
694
+ if (capture.status === SAVE_STATUS.BLOCKED || capture.status === SAVE_STATUS.UNAVAILABLE) {
695
+ return capture;
696
+ }
697
+
698
+ const persistSession =
699
+ typeof options.persistSession === "function" ? options.persistSession : persistPencilSession;
700
+
701
+ let materialized = null;
702
+ try {
703
+ materialized = materializeSnapshotFiles(capture);
704
+ let persistResult;
705
+ try {
706
+ persistResult = persistSession({
707
+ projectPath: projectRoot,
708
+ penPath: capture.boundPenPath,
709
+ nodesFile: materialized.nodesFile,
710
+ variablesFile: materialized.variablesFile,
711
+ version: preflight.penVersion,
712
+ homeDir: options.homeDir
713
+ });
714
+ } catch (error) {
715
+ return blocked(
716
+ SAVE_CODES.PERSIST_FAILED,
717
+ "High-level save failed while persisting the validated live snapshot.",
718
+ {
719
+ projectRoot,
720
+ boundPenPath: capture.boundPenPath,
721
+ error: error && error.message ? error.message : String(error)
722
+ }
723
+ );
724
+ }
725
+
726
+ const syncResult = persistResult.syncResult || null;
727
+ if (!syncResult || syncResult.inSync !== true) {
728
+ return blocked(
729
+ SAVE_CODES.PERSIST_NOT_IN_SYNC,
730
+ "Persistence completed, but the sync signal reports `inSync === false`.",
731
+ {
732
+ projectRoot,
733
+ boundPenPath: capture.boundPenPath,
734
+ inSync: Boolean(syncResult && syncResult.inSync),
735
+ persistedHash: syncResult ? syncResult.persistedHash : null,
736
+ liveHash: syncResult ? syncResult.liveHash : null
737
+ }
738
+ );
739
+ }
740
+
741
+ return saved("Current design was saved through the bound-source persistence flow.", {
742
+ projectRoot,
743
+ boundPenPath: capture.boundPenPath,
744
+ penVersion: preflight.penVersion,
745
+ syncResult: {
746
+ inSync: syncResult.inSync,
747
+ persistedHash: syncResult.persistedHash,
748
+ liveHash: syncResult.liveHash
749
+ }
750
+ });
751
+ } finally {
752
+ if (materialized && materialized.tempDir) {
753
+ fs.rmSync(materialized.tempDir, { recursive: true, force: true });
754
+ }
755
+ }
756
+ }
757
+
758
+ function formatSaveCurrentDesignReport(result) {
759
+ const payload = toObject(result);
760
+ const status = String(payload.status || "unknown").toUpperCase();
761
+ const details = toObject(payload.details);
762
+ const lines = [
763
+ "Da Vinci Save Current Design",
764
+ `Status: ${status}`,
765
+ `Code: ${payload.code || "(missing)"}`,
766
+ `Message: ${payload.message || "(missing)"}`
767
+ ];
768
+
769
+ if (details.boundPenPath) {
770
+ lines.push(`Bound .pen: ${details.boundPenPath}`);
771
+ }
772
+ if (details.projectRoot) {
773
+ lines.push(`Project: ${details.projectRoot}`);
774
+ }
775
+ if (payload.status === SAVE_STATUS.UNAVAILABLE) {
776
+ lines.push("Bridge: unavailable");
777
+ }
778
+
779
+ return lines.join("\n");
780
+ }
781
+
782
+ module.exports = {
783
+ SAVE_STATUS,
784
+ SAVE_CODES,
785
+ createLocalPencilBridge,
786
+ validateBoundDesignSource,
787
+ captureLivePencilSnapshot,
788
+ saveCurrentDesign,
789
+ formatSaveCurrentDesignReport
790
+ };