@xenonbyte/da-vinci-workflow 0.1.14 → 0.1.16

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 (46) hide show
  1. package/CHANGELOG.md +20 -2
  2. package/README.md +41 -1
  3. package/README.zh-CN.md +42 -1
  4. package/SKILL.md +22 -0
  5. package/commands/claude/dv/design.md +8 -0
  6. package/commands/claude/dv/verify.md +2 -0
  7. package/commands/codex/prompts/dv-design.md +8 -0
  8. package/commands/codex/prompts/dv-verify.md +1 -0
  9. package/commands/gemini/dv/design.toml +8 -0
  10. package/commands/gemini/dv/verify.toml +1 -0
  11. package/docs/mcp-aware-gate-implementation.md +291 -0
  12. package/docs/mcp-aware-gate-tests.md +244 -0
  13. package/docs/mcp-aware-gate.md +246 -0
  14. package/docs/mode-use-cases.md +7 -1
  15. package/docs/prompt-presets/README.md +3 -0
  16. package/docs/prompt-presets/desktop-app.md +19 -1
  17. package/docs/prompt-presets/mobile-app.md +19 -1
  18. package/docs/prompt-presets/tablet-app.md +19 -1
  19. package/docs/prompt-presets/web-app.md +19 -1
  20. package/docs/visual-assist-presets/README.md +5 -0
  21. package/docs/workflow-examples.md +24 -5
  22. package/docs/zh-CN/mcp-aware-gate-implementation.md +290 -0
  23. package/docs/zh-CN/mcp-aware-gate-tests.md +244 -0
  24. package/docs/zh-CN/mcp-aware-gate.md +249 -0
  25. package/docs/zh-CN/mode-use-cases.md +15 -4
  26. package/docs/zh-CN/prompt-presets/README.md +3 -0
  27. package/docs/zh-CN/prompt-presets/desktop-app.md +19 -1
  28. package/docs/zh-CN/prompt-presets/mobile-app.md +19 -1
  29. package/docs/zh-CN/prompt-presets/tablet-app.md +19 -1
  30. package/docs/zh-CN/prompt-presets/web-app.md +19 -1
  31. package/docs/zh-CN/visual-assist-presets/README.md +5 -0
  32. package/docs/zh-CN/workflow-examples.md +24 -5
  33. package/lib/audit.js +348 -0
  34. package/lib/cli.js +142 -1
  35. package/lib/mcp-runtime-gate.js +342 -0
  36. package/lib/pen-persistence.js +326 -0
  37. package/lib/pencil-preflight.js +438 -0
  38. package/package.json +5 -2
  39. package/references/artifact-templates.md +28 -1
  40. package/references/checkpoints.md +75 -1
  41. package/references/design-inputs.md +2 -1
  42. package/references/pencil-design-to-code.md +16 -0
  43. package/scripts/fixtures/complex-sample.pen +295 -0
  44. package/scripts/test-mcp-runtime-gate.js +199 -0
  45. package/scripts/test-pen-persistence.js +110 -0
  46. package/scripts/test-pencil-preflight.js +153 -0
@@ -0,0 +1,326 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+ const { spawnSync } = require("child_process");
5
+
6
+ const DEFAULT_PEN_VERSION = "2.9";
7
+ const DEFAULT_READ_DEPTH = 50;
8
+
9
+ function isPlainObject(value) {
10
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+
13
+ function extractFirstJson(text) {
14
+ const source = String(text || "");
15
+ const decoder = new JSONDecoder();
16
+ return decoder.decodeFirst(source);
17
+ }
18
+
19
+ class JSONDecoder {
20
+ constructor() {
21
+ this.decoder = JSON;
22
+ }
23
+
24
+ decodeFirst(text) {
25
+ for (let index = 0; index < text.length; index += 1) {
26
+ const ch = text[index];
27
+ if (ch !== "{" && ch !== "[") {
28
+ continue;
29
+ }
30
+
31
+ try {
32
+ const { value } = this.rawDecode(text.slice(index));
33
+ return value;
34
+ } catch (error) {
35
+ continue;
36
+ }
37
+ }
38
+
39
+ throw new Error("No JSON payload found.");
40
+ }
41
+
42
+ rawDecode(text) {
43
+ let end = 0;
44
+ let inString = false;
45
+ let escape = false;
46
+ let depth = 0;
47
+ const opening = text[0];
48
+ const closing = opening === "{" ? "}" : "]";
49
+
50
+ for (let index = 0; index < text.length; index += 1) {
51
+ const ch = text[index];
52
+
53
+ if (inString) {
54
+ if (escape) {
55
+ escape = false;
56
+ } else if (ch === "\\") {
57
+ escape = true;
58
+ } else if (ch === "\"") {
59
+ inString = false;
60
+ }
61
+ continue;
62
+ }
63
+
64
+ if (ch === "\"") {
65
+ inString = true;
66
+ continue;
67
+ }
68
+
69
+ if (ch === opening) {
70
+ depth += 1;
71
+ } else if (ch === closing) {
72
+ depth -= 1;
73
+ if (depth === 0) {
74
+ end = index + 1;
75
+ break;
76
+ }
77
+ }
78
+ }
79
+
80
+ if (end === 0) {
81
+ throw new Error("Incomplete JSON payload.");
82
+ }
83
+
84
+ return {
85
+ value: JSON.parse(text.slice(0, end)),
86
+ end
87
+ };
88
+ }
89
+ }
90
+
91
+ function readJsonPayload(filePath) {
92
+ const raw = fs.readFileSync(filePath, "utf8");
93
+ try {
94
+ return JSON.parse(raw);
95
+ } catch (error) {
96
+ return extractFirstJson(raw);
97
+ }
98
+ }
99
+
100
+ function hasTruncatedChildren(value) {
101
+ if (Array.isArray(value)) {
102
+ return value.some((item) => hasTruncatedChildren(item));
103
+ }
104
+
105
+ if (!isPlainObject(value)) {
106
+ return value === "...";
107
+ }
108
+
109
+ return Object.values(value).some((item) => hasTruncatedChildren(item));
110
+ }
111
+
112
+ function normalizeNodesPayload(payload) {
113
+ const nodes = getNodesArray(payload);
114
+
115
+ if (!nodes) {
116
+ throw new Error("Nodes payload must be an array or an object with a `nodes` array.");
117
+ }
118
+
119
+ if (hasTruncatedChildren(nodes)) {
120
+ throw new Error("Nodes payload is truncated (`...`). Re-read the live document with a deeper batch_get before persisting.");
121
+ }
122
+
123
+ return nodes;
124
+ }
125
+
126
+ function getNodesArray(payload) {
127
+ if (Array.isArray(payload)) {
128
+ return payload;
129
+ }
130
+
131
+ if (payload && Array.isArray(payload.nodes)) {
132
+ return payload.nodes;
133
+ }
134
+
135
+ return null;
136
+ }
137
+
138
+ function normalizeVariablesPayload(payload) {
139
+ if (!payload) {
140
+ return undefined;
141
+ }
142
+
143
+ if (isPlainObject(payload.variables)) {
144
+ return payload.variables;
145
+ }
146
+
147
+ if (isPlainObject(payload)) {
148
+ return payload;
149
+ }
150
+
151
+ throw new Error("Variables payload must be an object or an object with a `variables` key.");
152
+ }
153
+
154
+ function buildPenDocument({ version = DEFAULT_PEN_VERSION, nodes, variables }) {
155
+ const document = {
156
+ version,
157
+ children: normalizeNodesPayload(nodes)
158
+ };
159
+
160
+ const normalizedVariables = normalizeVariablesPayload(variables);
161
+ if (normalizedVariables && Object.keys(normalizedVariables).length > 0) {
162
+ document.variables = normalizedVariables;
163
+ }
164
+
165
+ return document;
166
+ }
167
+
168
+ function writePenDocumentAtomic(outputPath, document) {
169
+ const targetPath = path.resolve(outputPath);
170
+ const targetDir = path.dirname(targetPath);
171
+ const tempPath = path.join(
172
+ targetDir,
173
+ `.${path.basename(targetPath)}.tmp-${process.pid}-${Date.now()}`
174
+ );
175
+
176
+ fs.mkdirSync(targetDir, { recursive: true });
177
+ fs.writeFileSync(tempPath, JSON.stringify(document, null, 2) + "\n", "utf8");
178
+ fs.renameSync(tempPath, targetPath);
179
+ return targetPath;
180
+ }
181
+
182
+ function runPencilInteractive(inputPath, commands, options = {}) {
183
+ const pencilBin = options.pencilBin || "pencil";
184
+ const unusedOutput = path.join(
185
+ os.tmpdir(),
186
+ `da-vinci-verify-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.pen`
187
+ );
188
+ const payload = Array.isArray(commands) ? commands.join("\n") : String(commands || "");
189
+ const result = spawnSync(
190
+ pencilBin,
191
+ ["interactive", "-i", path.resolve(inputPath), "-o", unusedOutput],
192
+ {
193
+ input: `${payload}\nexit()\n`,
194
+ encoding: "utf8",
195
+ maxBuffer: 8 * 1024 * 1024
196
+ }
197
+ );
198
+
199
+ fs.rmSync(unusedOutput, { force: true });
200
+
201
+ const stdout = result.stdout || "";
202
+ const stderr = result.stderr || "";
203
+ if (result.status !== 0) {
204
+ throw new Error(
205
+ `Pencil interactive failed for ${inputPath}.\n${stdout}${stderr}`.trim()
206
+ );
207
+ }
208
+
209
+ return `${stdout}${stderr}`;
210
+ }
211
+
212
+ function verifyPenFileWithPencil(filePath, options = {}) {
213
+ const output = runPencilInteractive(filePath, ['batch_get({ readDepth: 1 })'], options);
214
+ const payload = extractFirstJson(output);
215
+ const nodes = getNodesArray(payload);
216
+ const expectedTopLevelIds = options.expectedTopLevelIds || [];
217
+
218
+ if (!nodes) {
219
+ throw new Error("Pencil reopen verification did not return a `nodes` array.");
220
+ }
221
+
222
+ if (expectedTopLevelIds.length > 0) {
223
+ const actualIds = nodes.map((node) => node.id);
224
+ if (JSON.stringify(actualIds) !== JSON.stringify(expectedTopLevelIds)) {
225
+ throw new Error(
226
+ `Reopened .pen file top-level ids do not match. Expected ${expectedTopLevelIds.join(", ")}, got ${actualIds.join(", ")}.`
227
+ );
228
+ }
229
+ }
230
+
231
+ return {
232
+ topLevelCount: nodes.length,
233
+ topLevelIds: nodes.map((node) => node.id)
234
+ };
235
+ }
236
+
237
+ function writePenFromPayloadFiles(options) {
238
+ const outputPath = path.resolve(options.outputPath);
239
+ const nodes = readJsonPayload(options.nodesFile);
240
+ const variables = options.variablesFile ? readJsonPayload(options.variablesFile) : undefined;
241
+ const document = buildPenDocument({
242
+ version: options.version || DEFAULT_PEN_VERSION,
243
+ nodes,
244
+ variables
245
+ });
246
+
247
+ writePenDocumentAtomic(outputPath, document);
248
+
249
+ const verification = options.verifyWithPencil
250
+ ? verifyPenFileWithPencil(outputPath, {
251
+ pencilBin: options.pencilBin,
252
+ expectedTopLevelIds: document.children.map((node) => node.id)
253
+ })
254
+ : null;
255
+
256
+ return {
257
+ outputPath,
258
+ document,
259
+ verification
260
+ };
261
+ }
262
+
263
+ function readPenVersion(inputPath) {
264
+ const payload = JSON.parse(fs.readFileSync(inputPath, "utf8"));
265
+ return payload.version || DEFAULT_PEN_VERSION;
266
+ }
267
+
268
+ function capturePenSnapshot(inputPath, options = {}) {
269
+ const readDepth = options.readDepth || DEFAULT_READ_DEPTH;
270
+ const nodesOutput = runPencilInteractive(
271
+ inputPath,
272
+ [
273
+ `batch_get({ readDepth: ${readDepth}, includePathGeometry: true, resolveInstances: false, resolveVariables: false })`
274
+ ],
275
+ options
276
+ );
277
+ const variablesOutput = runPencilInteractive(inputPath, ["get_variables()"], options);
278
+
279
+ return {
280
+ nodes: normalizeNodesPayload(extractFirstJson(nodesOutput)),
281
+ variables: normalizeVariablesPayload(extractFirstJson(variablesOutput))
282
+ };
283
+ }
284
+
285
+ function snapshotPenFile(options) {
286
+ const inputPath = path.resolve(options.inputPath);
287
+ const outputPath = path.resolve(options.outputPath);
288
+ const snapshot = capturePenSnapshot(inputPath, options);
289
+ const document = buildPenDocument({
290
+ version: options.version || readPenVersion(inputPath),
291
+ nodes: snapshot.nodes,
292
+ variables: snapshot.variables
293
+ });
294
+
295
+ writePenDocumentAtomic(outputPath, document);
296
+
297
+ const verification = options.verifyWithPencil
298
+ ? verifyPenFileWithPencil(outputPath, {
299
+ pencilBin: options.pencilBin,
300
+ expectedTopLevelIds: document.children.map((node) => node.id)
301
+ })
302
+ : null;
303
+
304
+ return {
305
+ inputPath,
306
+ outputPath,
307
+ document,
308
+ verification
309
+ };
310
+ }
311
+
312
+ module.exports = {
313
+ DEFAULT_PEN_VERSION,
314
+ DEFAULT_READ_DEPTH,
315
+ extractFirstJson,
316
+ readJsonPayload,
317
+ normalizeNodesPayload,
318
+ normalizeVariablesPayload,
319
+ buildPenDocument,
320
+ writePenDocumentAtomic,
321
+ runPencilInteractive,
322
+ verifyPenFileWithPencil,
323
+ writePenFromPayloadFiles,
324
+ capturePenSnapshot,
325
+ snapshotPenFile
326
+ };