@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
@@ -10,6 +10,11 @@ Use this priority order:
10
10
  2. Pencil data for presentation
11
11
  3. screenshots only as a visual check
12
12
 
13
+ Exported screenshots are review artifacts only.
14
+
15
+ - store them under `.da-vinci/changes/<change-id>/exports/`
16
+ - do not treat them as the persisted design source
17
+
13
18
  Do not infer behavior from appearance alone.
14
19
 
15
20
  ## Read From Pencil
@@ -33,6 +38,7 @@ If the current active Pencil editor does not match the preferred path in `design
33
38
  - do not silently continue from the unrelated editor
34
39
  - switch to the registered file when possible
35
40
  - otherwise reconstruct the registered project-local `.pen` file from MCP-readable document data before implementation depends on it
41
+ - if the editor is still `new` or another unnamed live document, do not describe the project-local `.pen` source as saved yet
36
42
 
37
43
  ## Visual Adapter Use
38
44
 
@@ -57,9 +63,19 @@ When generating or editing Pencil data:
57
63
 
58
64
  - use only Pencil-supported properties and layout concepts
59
65
  - do not emit web- or CSS-only properties such as `flex` or `margin`
66
+ - preflight non-trivial `batch_design` operation strings before sending them to Pencil when shell access is available
60
67
  - prefer smaller, schema-safe batches on anchor screens so errors do not roll back large composition chunks
68
+ - prefer 12 or fewer operations on anchor surfaces; after two failed batches on the same anchor, drop to micro-batches of 6 or fewer operations until a clean pass lands
69
+ - if unsupported-property rollbacks repeat on the same anchor surface, stop treating that pass as stable forward progress until the schema usage is corrected
70
+ - after any rolled-back batch or structure-changing edit, refresh the live node structure before descendant-targeted follow-up operations
61
71
  - verify the registered project-local `.pen` path becomes shell-visible immediately after the first successful Pencil write
72
+ - do not treat headless interactive `save()` as authoritative persistence; write the project-local `.pen` from MCP-readable snapshot data instead
73
+ - if no registered project-local `.pen` existed at the start of the session, let the first approved anchor surface happen in the live editor, then persist that first approved MCP snapshot under `.da-vinci/designs/`
74
+ - if a registered project-local `.pen` already existed, reopen it for continuity, but after material live edits persist a fresh MCP snapshot back to that same path instead of assuming live edits were flushed automatically
75
+ - use `da-vinci write-pen --output <path> --nodes-file <batch-get-json> --variables-file <get-variables-json> --version <version> --verify-open` when you already have MCP-readable snapshot payloads and need an atomic project-local `.pen` write
76
+ - use `da-vinci snapshot-pen --input <path> --output <path> --verify-open` when a project-local `.pen` already exists and you need to re-canonicalize it from a fresh MCP-readable snapshot
62
77
  - keep workflow markdown out of `.da-vinci/designs/`; reserve that directory for `.pen` files only
78
+ - keep screenshot exports out of `.da-vinci/designs/`; write them under `.da-vinci/changes/<change-id>/exports/`
63
79
  - after the first approved anchor surfaces, extract a shared primitive family before broad page expansion
64
80
 
65
81
  ## Map Pencil To Frontend
@@ -0,0 +1,295 @@
1
+ {
2
+ "version": "2.9",
3
+ "variables": {
4
+ "surface-bg": {
5
+ "type": "color",
6
+ "value": "#F4EFE7"
7
+ },
8
+ "surface-panel": {
9
+ "type": "color",
10
+ "value": "#FFFDF8"
11
+ },
12
+ "surface-border": {
13
+ "type": "color",
14
+ "value": "#D7CCBC"
15
+ },
16
+ "deck-fill": {
17
+ "type": "color",
18
+ "value": "#17304A"
19
+ },
20
+ "deck-fill-deep": {
21
+ "type": "color",
22
+ "value": "#10263A"
23
+ },
24
+ "surface-ink": {
25
+ "type": "color",
26
+ "value": "#132033"
27
+ },
28
+ "surface-muted": {
29
+ "type": "color",
30
+ "value": "#5D6673"
31
+ },
32
+ "accent-amber": {
33
+ "type": "color",
34
+ "value": "#C98A2B"
35
+ }
36
+ },
37
+ "children": [
38
+ {
39
+ "type": "frame",
40
+ "id": "healthy",
41
+ "x": 0,
42
+ "y": 0,
43
+ "name": "Diagnostics Console / Healthy",
44
+ "width": 420,
45
+ "fill": "$surface-bg",
46
+ "layout": "vertical",
47
+ "gap": 18,
48
+ "padding": [
49
+ 18,
50
+ 18,
51
+ 24,
52
+ 18
53
+ ],
54
+ "children": [
55
+ {
56
+ "type": "frame",
57
+ "id": "deckHealthy",
58
+ "name": "Command Deck",
59
+ "width": "fill_container",
60
+ "fill": [
61
+ {
62
+ "type": "gradient",
63
+ "gradientType": "linear",
64
+ "enabled": true,
65
+ "rotation": 180,
66
+ "size": {
67
+ "height": 1
68
+ },
69
+ "colors": [
70
+ {
71
+ "color": "$deck-fill",
72
+ "position": 0
73
+ },
74
+ {
75
+ "color": "$deck-fill-deep",
76
+ "position": 1
77
+ }
78
+ ]
79
+ },
80
+ "#FFFFFF12"
81
+ ],
82
+ "cornerRadius": 26,
83
+ "effect": {
84
+ "type": "shadow",
85
+ "shadowType": "outer",
86
+ "color": "#0B16231F",
87
+ "offset": {
88
+ "x": 0,
89
+ "y": 14
90
+ },
91
+ "blur": 28
92
+ },
93
+ "layout": "vertical",
94
+ "gap": 14,
95
+ "padding": 22,
96
+ "children": [
97
+ {
98
+ "type": "text",
99
+ "id": "healthyEyebrow",
100
+ "name": "heroEyebrow",
101
+ "content": "Diagnostics",
102
+ "fill": "$accent-amber",
103
+ "fontFamily": "Inter",
104
+ "fontSize": 12,
105
+ "fontWeight": "700",
106
+ "letterSpacing": 1.4
107
+ },
108
+ {
109
+ "type": "text",
110
+ "id": "healthyTitle",
111
+ "name": "heroTitle",
112
+ "content": "Healthy runway",
113
+ "fill": "#FFFDF8",
114
+ "fontFamily": "Inter",
115
+ "fontSize": 28,
116
+ "fontWeight": "700",
117
+ "textGrowth": "fixed-width",
118
+ "width": "fill_container"
119
+ }
120
+ ]
121
+ },
122
+ {
123
+ "type": "frame",
124
+ "id": "panelHealthy",
125
+ "name": "Runway Panel",
126
+ "width": "fill_container",
127
+ "fill": "$surface-panel",
128
+ "cornerRadius": 24,
129
+ "stroke": {
130
+ "fill": "$surface-border",
131
+ "thickness": 1
132
+ },
133
+ "layout": "vertical",
134
+ "gap": 12,
135
+ "padding": 18,
136
+ "children": [
137
+ {
138
+ "type": "text",
139
+ "id": "panelHealthyTitle",
140
+ "name": "panelTitle",
141
+ "content": "Launch blocking scenarios",
142
+ "fill": "$surface-ink",
143
+ "fontFamily": "Inter",
144
+ "fontSize": 24,
145
+ "fontWeight": "700",
146
+ "textGrowth": "fixed-width",
147
+ "width": "fill_container"
148
+ },
149
+ {
150
+ "type": "text",
151
+ "id": "panelHealthyBody",
152
+ "name": "panelBody",
153
+ "content": "Routine and high-risk controls are separated so the dangerous path always reads clearly.",
154
+ "fill": "$surface-muted",
155
+ "fontFamily": "Inter",
156
+ "fontSize": 14,
157
+ "fontWeight": "normal",
158
+ "lineHeight": 1.45,
159
+ "textGrowth": "fixed-width",
160
+ "width": "fill_container"
161
+ }
162
+ ]
163
+ }
164
+ ]
165
+ },
166
+ {
167
+ "type": "frame",
168
+ "id": "degraded",
169
+ "x": 500,
170
+ "y": 0,
171
+ "name": "Diagnostics Console / Degraded",
172
+ "width": 420,
173
+ "fill": "$surface-bg",
174
+ "layout": "vertical",
175
+ "gap": 18,
176
+ "padding": [
177
+ 18,
178
+ 18,
179
+ 24,
180
+ 18
181
+ ],
182
+ "children": [
183
+ {
184
+ "type": "frame",
185
+ "id": "deckDegraded",
186
+ "name": "Command Deck",
187
+ "width": "fill_container",
188
+ "fill": [
189
+ {
190
+ "type": "gradient",
191
+ "gradientType": "linear",
192
+ "enabled": true,
193
+ "rotation": 180,
194
+ "size": {
195
+ "height": 1
196
+ },
197
+ "colors": [
198
+ {
199
+ "color": "$deck-fill",
200
+ "position": 0
201
+ },
202
+ {
203
+ "color": "$deck-fill-deep",
204
+ "position": 1
205
+ }
206
+ ]
207
+ },
208
+ "#FFFFFF12"
209
+ ],
210
+ "cornerRadius": 26,
211
+ "effect": {
212
+ "type": "shadow",
213
+ "shadowType": "outer",
214
+ "color": "#0B16231F",
215
+ "offset": {
216
+ "x": 0,
217
+ "y": 14
218
+ },
219
+ "blur": 28
220
+ },
221
+ "layout": "vertical",
222
+ "gap": 14,
223
+ "padding": 22,
224
+ "children": [
225
+ {
226
+ "type": "text",
227
+ "id": "degradedEyebrow",
228
+ "name": "heroEyebrow",
229
+ "content": "Diagnostics",
230
+ "fill": "$accent-amber",
231
+ "fontFamily": "Inter",
232
+ "fontSize": 12,
233
+ "fontWeight": "700",
234
+ "letterSpacing": 1.4
235
+ },
236
+ {
237
+ "type": "text",
238
+ "id": "degradedTitle",
239
+ "name": "heroTitle",
240
+ "content": "Degraded runway",
241
+ "fill": "#FFFDF8",
242
+ "fontFamily": "Inter",
243
+ "fontSize": 28,
244
+ "fontWeight": "700",
245
+ "textGrowth": "fixed-width",
246
+ "width": "fill_container"
247
+ }
248
+ ]
249
+ },
250
+ {
251
+ "type": "frame",
252
+ "id": "panelDegraded",
253
+ "name": "Runway Panel",
254
+ "width": "fill_container",
255
+ "fill": "$surface-panel",
256
+ "cornerRadius": 24,
257
+ "stroke": {
258
+ "fill": "$surface-border",
259
+ "thickness": 1
260
+ },
261
+ "layout": "vertical",
262
+ "gap": 12,
263
+ "padding": 18,
264
+ "children": [
265
+ {
266
+ "type": "text",
267
+ "id": "panelDegradedTitle",
268
+ "name": "panelTitle",
269
+ "content": "Signal drift detected",
270
+ "fill": "$surface-ink",
271
+ "fontFamily": "Inter",
272
+ "fontSize": 24,
273
+ "fontWeight": "700",
274
+ "textGrowth": "fixed-width",
275
+ "width": "fill_container"
276
+ },
277
+ {
278
+ "type": "text",
279
+ "id": "panelDegradedBody",
280
+ "name": "panelBody",
281
+ "content": "Telemetry stays legible while the top surface shifts the operator toward the corrective lane.",
282
+ "fill": "$surface-muted",
283
+ "fontFamily": "Inter",
284
+ "fontSize": 14,
285
+ "fontWeight": "normal",
286
+ "lineHeight": 1.45,
287
+ "textGrowth": "fixed-width",
288
+ "width": "fill_container"
289
+ }
290
+ ]
291
+ }
292
+ ]
293
+ }
294
+ ]
295
+ }
@@ -0,0 +1,199 @@
1
+ const assert = require("assert/strict");
2
+ const {
3
+ PASS,
4
+ WARN,
5
+ BLOCK,
6
+ SKIP,
7
+ evaluateMcpRuntimeGate,
8
+ formatMcpRuntimeGateSection
9
+ } = require("../lib/mcp-runtime-gate");
10
+
11
+ function runTest(name, fn) {
12
+ try {
13
+ fn();
14
+ console.log(`PASS ${name}`);
15
+ } catch (error) {
16
+ console.error(`FAIL ${name}`);
17
+ throw error;
18
+ }
19
+ }
20
+
21
+ runTest("healthy completion passes", () => {
22
+ const result = evaluateMcpRuntimeGate({
23
+ phase: "completion",
24
+ mcpAvailable: true,
25
+ projectRoot: "/repo",
26
+ activeEditor: "/repo/.da-vinci/designs/cipher-redesign.pen",
27
+ registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
28
+ shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
29
+ shellVisiblePenExists: true,
30
+ claimedAnchorIds: ["GfwiK", "mCZ1G", "V8zfE"],
31
+ claimedReviewedScreenIds: ["GfwiK", "mCZ1G", "V8zfE"],
32
+ reviewTargets: ["GfwiK", "mCZ1G", "V8zfE"],
33
+ liveScreens: [
34
+ { id: "GfwiK", name: "Splash" },
35
+ { id: "mCZ1G", name: "Home" },
36
+ { id: "V8zfE", name: "SafeBox" }
37
+ ]
38
+ });
39
+
40
+ assert.equal(result.sourceConvergence.status, PASS);
41
+ assert.equal(result.screenPresence.status, PASS);
42
+ assert.equal(result.reviewExecution.status, PASS);
43
+ assert.equal(result.finalStatus, PASS);
44
+
45
+ const section = formatMcpRuntimeGateSection({}, result, { timestamp: "2026-03-27T00:00:00.000Z" });
46
+ assert.match(section, /Final runtime gate status: PASS/);
47
+ assert.match(section, /Source convergence: PASS/);
48
+ });
49
+
50
+ runTest("unnamed editor blocks runtime gate", () => {
51
+ const result = evaluateMcpRuntimeGate({
52
+ phase: "completion",
53
+ mcpAvailable: true,
54
+ activeEditor: "new",
55
+ registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
56
+ shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
57
+ shellVisiblePenExists: true,
58
+ claimedAnchorIds: ["GfwiK"],
59
+ claimedReviewedScreenIds: ["GfwiK"],
60
+ reviewTargets: ["GfwiK"],
61
+ liveScreens: [{ id: "GfwiK", name: "Splash" }]
62
+ });
63
+
64
+ assert.equal(result.sourceConvergence.status, BLOCK);
65
+ assert.equal(result.finalStatus, BLOCK);
66
+ });
67
+
68
+ runTest("live screens without shell-visible pen block source convergence", () => {
69
+ const result = evaluateMcpRuntimeGate({
70
+ phase: "completion",
71
+ mcpAvailable: true,
72
+ projectRoot: "/repo",
73
+ activeEditor: "/repo/.da-vinci/designs/cipher-redesign.pen",
74
+ registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
75
+ shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
76
+ shellVisiblePenExists: false,
77
+ claimedAnchorIds: ["GfwiK"],
78
+ claimedReviewedScreenIds: ["GfwiK"],
79
+ reviewTargets: ["GfwiK"],
80
+ liveScreens: [{ id: "GfwiK", name: "Splash" }]
81
+ });
82
+
83
+ assert.equal(result.sourceConvergence.status, BLOCK);
84
+ assert.equal(result.finalStatus, BLOCK);
85
+ });
86
+
87
+ runTest("missing claimed anchor blocks screen presence", () => {
88
+ const result = evaluateMcpRuntimeGate({
89
+ phase: "completion",
90
+ mcpAvailable: true,
91
+ projectRoot: "/repo",
92
+ activeEditor: "/repo/.da-vinci/designs/cipher-redesign.pen",
93
+ registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
94
+ shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
95
+ shellVisiblePenExists: true,
96
+ claimedAnchorIds: ["GfwiK", "mCZ1G"],
97
+ claimedReviewedScreenIds: ["GfwiK"],
98
+ reviewTargets: ["GfwiK"],
99
+ liveScreens: [{ id: "GfwiK", name: "Splash" }]
100
+ });
101
+
102
+ assert.equal(result.screenPresence.status, BLOCK);
103
+ assert.equal(result.finalStatus, BLOCK);
104
+ });
105
+
106
+ runTest("ignored review blockers block review execution", () => {
107
+ const result = evaluateMcpRuntimeGate({
108
+ phase: "completion",
109
+ mcpAvailable: true,
110
+ projectRoot: "/repo",
111
+ activeEditor: "/repo/.da-vinci/designs/cipher-redesign.pen",
112
+ registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
113
+ shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
114
+ shellVisiblePenExists: true,
115
+ claimedAnchorIds: ["GfwiK"],
116
+ claimedReviewedScreenIds: ["GfwiK"],
117
+ reviewTargets: ["GfwiK"],
118
+ liveScreens: [{ id: "GfwiK", name: "Splash" }],
119
+ reviewBlockersIgnored: true
120
+ });
121
+
122
+ assert.equal(result.reviewExecution.status, BLOCK);
123
+ assert.equal(result.finalStatus, BLOCK);
124
+ });
125
+
126
+ runTest("mcp unavailable warns instead of passing", () => {
127
+ const result = evaluateMcpRuntimeGate({
128
+ phase: "completion",
129
+ mcpAvailable: false,
130
+ activeEditor: "",
131
+ registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
132
+ shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
133
+ shellVisiblePenExists: true
134
+ });
135
+
136
+ assert.equal(result.sourceConvergence.status, WARN);
137
+ assert.equal(result.screenPresence.status, WARN);
138
+ assert.equal(result.reviewExecution.status, WARN);
139
+ assert.equal(result.finalStatus, WARN);
140
+ });
141
+
142
+ runTest("first-write phase can skip screen and review checks", () => {
143
+ const result = evaluateMcpRuntimeGate({
144
+ phase: "first_write",
145
+ mcpAvailable: true,
146
+ projectRoot: "/repo",
147
+ activeEditor: "/repo/.da-vinci/designs/cipher-redesign.pen",
148
+ registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
149
+ shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
150
+ shellVisiblePenExists: true,
151
+ liveScreens: [{ id: "GfwiK", name: "Splash" }]
152
+ });
153
+
154
+ assert.equal(result.sourceConvergence.status, PASS);
155
+ assert.equal(result.screenPresence.status, SKIP);
156
+ assert.equal(result.reviewExecution.status, SKIP);
157
+ assert.equal(result.finalStatus, PASS);
158
+ });
159
+
160
+ runTest("documented reconciliation downgrades mismatch to warn", () => {
161
+ const result = evaluateMcpRuntimeGate({
162
+ phase: "completion",
163
+ mcpAvailable: true,
164
+ projectRoot: "/repo",
165
+ activeEditor: "/tmp/other-live.pen",
166
+ registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
167
+ shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
168
+ shellVisiblePenExists: true,
169
+ documentedReconciliation: true,
170
+ claimedAnchorIds: ["GfwiK"],
171
+ claimedReviewedScreenIds: ["GfwiK"],
172
+ reviewTargets: ["GfwiK"],
173
+ liveScreens: [{ id: "GfwiK", name: "Splash" }]
174
+ });
175
+
176
+ assert.equal(result.sourceConvergence.status, WARN);
177
+ assert.equal(result.finalStatus, WARN);
178
+ });
179
+
180
+ runTest("same basename but different absolute path blocks without reconciliation", () => {
181
+ const result = evaluateMcpRuntimeGate({
182
+ phase: "completion",
183
+ mcpAvailable: true,
184
+ projectRoot: "/repo",
185
+ activeEditor: "/tmp/cipher-redesign.pen",
186
+ registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
187
+ shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
188
+ shellVisiblePenExists: true,
189
+ claimedAnchorIds: ["GfwiK"],
190
+ claimedReviewedScreenIds: ["GfwiK"],
191
+ reviewTargets: ["GfwiK"],
192
+ liveScreens: [{ id: "GfwiK", name: "Splash" }]
193
+ });
194
+
195
+ assert.equal(result.sourceConvergence.status, BLOCK);
196
+ assert.equal(result.finalStatus, BLOCK);
197
+ });
198
+
199
+ console.log("All MCP runtime gate tests passed.");
@@ -0,0 +1,110 @@
1
+ const assert = require("assert/strict");
2
+ const fs = require("fs");
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const {
6
+ buildPenDocument,
7
+ writePenFromPayloadFiles,
8
+ snapshotPenFile
9
+ } = require("../lib/pen-persistence");
10
+
11
+ const fixturePath = path.join(__dirname, "fixtures", "complex-sample.pen");
12
+
13
+ function runTest(name, fn) {
14
+ try {
15
+ fn();
16
+ console.log(`PASS ${name}`);
17
+ } catch (error) {
18
+ console.error(`FAIL ${name}`);
19
+ throw error;
20
+ }
21
+ }
22
+
23
+ function createTempDir() {
24
+ return fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-pen-persistence-"));
25
+ }
26
+
27
+ runTest("buildPenDocument preserves fixture structure", () => {
28
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
29
+ const document = buildPenDocument({
30
+ version: fixture.version,
31
+ nodes: { nodes: fixture.children },
32
+ variables: { variables: fixture.variables }
33
+ });
34
+
35
+ assert.deepEqual(document, fixture);
36
+ });
37
+
38
+ runTest("writePenFromPayloadFiles writes and reopens a complex sample", () => {
39
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
40
+ const tempDir = createTempDir();
41
+ const nodesFile = path.join(tempDir, "nodes.json");
42
+ const variablesFile = path.join(tempDir, "variables.json");
43
+ const outputPath = path.join(tempDir, "written.pen");
44
+
45
+ fs.writeFileSync(nodesFile, JSON.stringify({ nodes: fixture.children }, null, 2));
46
+ fs.writeFileSync(variablesFile, JSON.stringify({ variables: fixture.variables }, null, 2));
47
+
48
+ const result = writePenFromPayloadFiles({
49
+ outputPath,
50
+ nodesFile,
51
+ variablesFile,
52
+ version: fixture.version,
53
+ verifyWithPencil: true
54
+ });
55
+
56
+ const written = JSON.parse(fs.readFileSync(outputPath, "utf8"));
57
+ assert.deepEqual(written, fixture);
58
+ assert.deepEqual(result.verification.topLevelIds, fixture.children.map((node) => node.id));
59
+ });
60
+
61
+ runTest("snapshotPenFile round-trips a complex sample", () => {
62
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
63
+ const tempDir = createTempDir();
64
+ const outputPath = path.join(tempDir, "snapshotted.pen");
65
+
66
+ const result = snapshotPenFile({
67
+ inputPath: fixturePath,
68
+ outputPath,
69
+ verifyWithPencil: true
70
+ });
71
+
72
+ const written = JSON.parse(fs.readFileSync(outputPath, "utf8"));
73
+ assert.deepEqual(written, fixture);
74
+ assert.deepEqual(result.verification.topLevelIds, fixture.children.map((node) => node.id));
75
+ });
76
+
77
+ runTest("truncated nodes payload is rejected", () => {
78
+ const tempDir = createTempDir();
79
+ const nodesFile = path.join(tempDir, "nodes.json");
80
+ const outputPath = path.join(tempDir, "bad.pen");
81
+
82
+ fs.writeFileSync(
83
+ nodesFile,
84
+ JSON.stringify(
85
+ {
86
+ nodes: [
87
+ {
88
+ type: "frame",
89
+ id: "bad",
90
+ children: "..."
91
+ }
92
+ ]
93
+ },
94
+ null,
95
+ 2
96
+ )
97
+ );
98
+
99
+ assert.throws(
100
+ () =>
101
+ writePenFromPayloadFiles({
102
+ outputPath,
103
+ nodesFile,
104
+ version: "2.9"
105
+ }),
106
+ /truncated/i
107
+ );
108
+ });
109
+
110
+ console.log("All .pen persistence tests passed.");