@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.
- package/CHANGELOG.md +15 -0
- package/README.md +24 -14
- package/README.zh-CN.md +25 -14
- package/commands/claude/dv/breakdown.md +8 -0
- package/commands/claude/dv/build.md +11 -0
- package/commands/claude/dv/design.md +5 -2
- package/commands/claude/dv/tasks.md +8 -0
- package/commands/claude/dv/verify.md +9 -0
- package/commands/codex/prompts/dv-breakdown.md +8 -0
- package/commands/codex/prompts/dv-build.md +11 -0
- package/commands/codex/prompts/dv-design.md +5 -2
- package/commands/codex/prompts/dv-tasks.md +8 -0
- package/commands/codex/prompts/dv-verify.md +8 -0
- package/commands/gemini/dv/breakdown.toml +8 -0
- package/commands/gemini/dv/build.toml +11 -0
- package/commands/gemini/dv/design.toml +5 -2
- package/commands/gemini/dv/tasks.toml +8 -0
- package/commands/gemini/dv/verify.toml +8 -0
- package/docs/dv-command-reference.md +43 -0
- package/docs/execution-chain-plan.md +10 -3
- package/docs/mode-use-cases.md +2 -1
- package/docs/pencil-rendering-workflow.md +15 -12
- package/docs/prompt-presets/README.md +1 -1
- package/docs/prompt-presets/desktop-app.md +3 -3
- package/docs/prompt-presets/mobile-app.md +3 -3
- package/docs/prompt-presets/tablet-app.md +3 -3
- package/docs/prompt-presets/web-app.md +3 -3
- package/docs/skill-usage.md +45 -38
- package/docs/workflow-examples.md +16 -13
- package/docs/workflow-overview.md +2 -0
- package/docs/zh-CN/dv-command-reference.md +43 -0
- package/docs/zh-CN/mode-use-cases.md +2 -1
- package/docs/zh-CN/pencil-rendering-workflow.md +15 -12
- package/docs/zh-CN/prompt-presets/README.md +1 -1
- package/docs/zh-CN/prompt-presets/desktop-app.md +3 -3
- package/docs/zh-CN/prompt-presets/mobile-app.md +3 -3
- package/docs/zh-CN/prompt-presets/tablet-app.md +3 -3
- package/docs/zh-CN/prompt-presets/web-app.md +3 -3
- package/docs/zh-CN/skill-usage.md +45 -38
- package/docs/zh-CN/workflow-examples.md +15 -13
- package/docs/zh-CN/workflow-overview.md +2 -0
- package/examples/greenfield-spec-markupflow/.da-vinci/state/execution-signals/demo__lint-tasks.json +16 -0
- package/lib/audit-parsers.js +18 -9
- package/lib/audit.js +3 -26
- package/lib/cli.js +50 -1
- package/lib/design-source-registry.js +146 -0
- package/lib/save-current-design.js +790 -0
- package/lib/supervisor-review.js +1 -1
- package/lib/workflow-bootstrap.js +2 -13
- package/lib/workflow-persisted-state.js +3 -1
- package/lib/workflow-state.js +51 -3
- package/package.json +1 -1
- package/tui/catalog.js +103 -0
- 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
|
+
};
|