@xenonbyte/da-vinci-workflow 0.1.16 → 0.1.17

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.
@@ -27,6 +27,9 @@ runTest("healthy completion passes", () => {
27
27
  registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
28
28
  shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
29
29
  shellVisiblePenExists: true,
30
+ penSyncVerified: true,
31
+ liveSnapshotHash: "abc123",
32
+ persistedSnapshotHash: "abc123",
30
33
  claimedAnchorIds: ["GfwiK", "mCZ1G", "V8zfE"],
31
34
  claimedReviewedScreenIds: ["GfwiK", "mCZ1G", "V8zfE"],
32
35
  reviewTargets: ["GfwiK", "mCZ1G", "V8zfE"],
@@ -55,6 +58,9 @@ runTest("unnamed editor blocks runtime gate", () => {
55
58
  registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
56
59
  shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
57
60
  shellVisiblePenExists: true,
61
+ penSyncVerified: true,
62
+ liveSnapshotHash: "abc123",
63
+ persistedSnapshotHash: "abc123",
58
64
  claimedAnchorIds: ["GfwiK"],
59
65
  claimedReviewedScreenIds: ["GfwiK"],
60
66
  reviewTargets: ["GfwiK"],
@@ -74,6 +80,9 @@ runTest("live screens without shell-visible pen block source convergence", () =>
74
80
  registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
75
81
  shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
76
82
  shellVisiblePenExists: false,
83
+ penSyncVerified: true,
84
+ liveSnapshotHash: "abc123",
85
+ persistedSnapshotHash: "abc123",
77
86
  claimedAnchorIds: ["GfwiK"],
78
87
  claimedReviewedScreenIds: ["GfwiK"],
79
88
  reviewTargets: ["GfwiK"],
@@ -93,6 +102,9 @@ runTest("missing claimed anchor blocks screen presence", () => {
93
102
  registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
94
103
  shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
95
104
  shellVisiblePenExists: true,
105
+ penSyncVerified: true,
106
+ liveSnapshotHash: "abc123",
107
+ persistedSnapshotHash: "abc123",
96
108
  claimedAnchorIds: ["GfwiK", "mCZ1G"],
97
109
  claimedReviewedScreenIds: ["GfwiK"],
98
110
  reviewTargets: ["GfwiK"],
@@ -112,6 +124,9 @@ runTest("ignored review blockers block review execution", () => {
112
124
  registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
113
125
  shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
114
126
  shellVisiblePenExists: true,
127
+ penSyncVerified: true,
128
+ liveSnapshotHash: "abc123",
129
+ persistedSnapshotHash: "abc123",
115
130
  claimedAnchorIds: ["GfwiK"],
116
131
  claimedReviewedScreenIds: ["GfwiK"],
117
132
  reviewTargets: ["GfwiK"],
@@ -148,6 +163,9 @@ runTest("first-write phase can skip screen and review checks", () => {
148
163
  registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
149
164
  shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
150
165
  shellVisiblePenExists: true,
166
+ penSyncVerified: true,
167
+ liveSnapshotHash: "abc123",
168
+ persistedSnapshotHash: "abc123",
151
169
  liveScreens: [{ id: "GfwiK", name: "Splash" }]
152
170
  });
153
171
 
@@ -166,6 +184,9 @@ runTest("documented reconciliation downgrades mismatch to warn", () => {
166
184
  registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
167
185
  shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
168
186
  shellVisiblePenExists: true,
187
+ penSyncVerified: true,
188
+ liveSnapshotHash: "abc123",
189
+ persistedSnapshotHash: "abc123",
169
190
  documentedReconciliation: true,
170
191
  claimedAnchorIds: ["GfwiK"],
171
192
  claimedReviewedScreenIds: ["GfwiK"],
@@ -186,6 +207,73 @@ runTest("same basename but different absolute path blocks without reconciliation
186
207
  registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
187
208
  shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
188
209
  shellVisiblePenExists: true,
210
+ penSyncVerified: true,
211
+ liveSnapshotHash: "abc123",
212
+ persistedSnapshotHash: "abc123",
213
+ claimedAnchorIds: ["GfwiK"],
214
+ claimedReviewedScreenIds: ["GfwiK"],
215
+ reviewTargets: ["GfwiK"],
216
+ liveScreens: [{ id: "GfwiK", name: "Splash" }]
217
+ });
218
+
219
+ assert.equal(result.sourceConvergence.status, BLOCK);
220
+ assert.equal(result.finalStatus, BLOCK);
221
+ });
222
+
223
+ runTest("completion without explicit pen sync verification blocks", () => {
224
+ const result = evaluateMcpRuntimeGate({
225
+ phase: "completion",
226
+ mcpAvailable: true,
227
+ projectRoot: "/repo",
228
+ activeEditor: "/repo/.da-vinci/designs/cipher-redesign.pen",
229
+ registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
230
+ shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
231
+ shellVisiblePenExists: true,
232
+ claimedAnchorIds: ["GfwiK"],
233
+ claimedReviewedScreenIds: ["GfwiK"],
234
+ reviewTargets: ["GfwiK"],
235
+ liveScreens: [{ id: "GfwiK", name: "Splash" }]
236
+ });
237
+
238
+ assert.equal(result.sourceConvergence.status, BLOCK);
239
+ assert.equal(result.finalStatus, BLOCK);
240
+ });
241
+
242
+ runTest("completion with mismatched live and persisted hashes blocks", () => {
243
+ const result = evaluateMcpRuntimeGate({
244
+ phase: "completion",
245
+ mcpAvailable: true,
246
+ projectRoot: "/repo",
247
+ activeEditor: "/repo/.da-vinci/designs/cipher-redesign.pen",
248
+ registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
249
+ shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
250
+ shellVisiblePenExists: true,
251
+ penSyncVerified: true,
252
+ liveSnapshotHash: "newer",
253
+ persistedSnapshotHash: "older",
254
+ claimedAnchorIds: ["GfwiK"],
255
+ claimedReviewedScreenIds: ["GfwiK"],
256
+ reviewTargets: ["GfwiK"],
257
+ liveScreens: [{ id: "GfwiK", name: "Splash" }]
258
+ });
259
+
260
+ assert.equal(result.sourceConvergence.status, BLOCK);
261
+ assert.equal(result.finalStatus, BLOCK);
262
+ });
263
+
264
+ runTest("registered pen plus empty filePath usage blocks", () => {
265
+ const result = evaluateMcpRuntimeGate({
266
+ phase: "completion",
267
+ mcpAvailable: true,
268
+ projectRoot: "/repo",
269
+ activeEditor: "/repo/.da-vinci/designs/cipher-redesign.pen",
270
+ registeredPenPath: ".da-vinci/designs/cipher-redesign.pen",
271
+ shellVisiblePenPath: ".da-vinci/designs/cipher-redesign.pen",
272
+ shellVisiblePenExists: true,
273
+ penSyncVerified: true,
274
+ liveSnapshotHash: "abc123",
275
+ persistedSnapshotHash: "abc123",
276
+ usedEmptyFilePath: true,
189
277
  claimedAnchorIds: ["GfwiK"],
190
278
  claimedReviewedScreenIds: ["GfwiK"],
191
279
  reviewTargets: ["GfwiK"],
@@ -5,8 +5,16 @@ const path = require("path");
5
5
  const {
6
6
  buildPenDocument,
7
7
  writePenFromPayloadFiles,
8
- snapshotPenFile
8
+ snapshotPenFile,
9
+ ensurePenFile,
10
+ comparePenSync,
11
+ getStandardPenStatePath
9
12
  } = require("../lib/pen-persistence");
13
+ const {
14
+ acquirePencilLock,
15
+ releasePencilLock,
16
+ getPencilLockStatus
17
+ } = require("../lib/pencil-lock");
10
18
 
11
19
  const fixturePath = path.join(__dirname, "fixtures", "complex-sample.pen");
12
20
 
@@ -24,6 +32,17 @@ function createTempDir() {
24
32
  return fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-pen-persistence-"));
25
33
  }
26
34
 
35
+ function writePayloadFiles(tempDir, fixture) {
36
+ const nodesFile = path.join(tempDir, "nodes.json");
37
+ const variablesFile = path.join(tempDir, "variables.json");
38
+ fs.writeFileSync(nodesFile, JSON.stringify({ nodes: fixture.children }, null, 2));
39
+ fs.writeFileSync(variablesFile, JSON.stringify({ variables: fixture.variables }, null, 2));
40
+ return {
41
+ nodesFile,
42
+ variablesFile
43
+ };
44
+ }
45
+
27
46
  runTest("buildPenDocument preserves fixture structure", () => {
28
47
  const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
29
48
  const document = buildPenDocument({
@@ -38,13 +57,9 @@ runTest("buildPenDocument preserves fixture structure", () => {
38
57
  runTest("writePenFromPayloadFiles writes and reopens a complex sample", () => {
39
58
  const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
40
59
  const tempDir = createTempDir();
41
- const nodesFile = path.join(tempDir, "nodes.json");
42
- const variablesFile = path.join(tempDir, "variables.json");
60
+ const { nodesFile, variablesFile } = writePayloadFiles(tempDir, fixture);
43
61
  const outputPath = path.join(tempDir, "written.pen");
44
62
 
45
- fs.writeFileSync(nodesFile, JSON.stringify({ nodes: fixture.children }, null, 2));
46
- fs.writeFileSync(variablesFile, JSON.stringify({ variables: fixture.variables }, null, 2));
47
-
48
63
  const result = writePenFromPayloadFiles({
49
64
  outputPath,
50
65
  nodesFile,
@@ -56,6 +71,7 @@ runTest("writePenFromPayloadFiles writes and reopens a complex sample", () => {
56
71
  const written = JSON.parse(fs.readFileSync(outputPath, "utf8"));
57
72
  assert.deepEqual(written, fixture);
58
73
  assert.deepEqual(result.verification.topLevelIds, fixture.children.map((node) => node.id));
74
+ assert.equal(result.state.snapshotHash, JSON.parse(fs.readFileSync(result.statePath, "utf8")).snapshotHash);
59
75
  });
60
76
 
61
77
  runTest("snapshotPenFile round-trips a complex sample", () => {
@@ -74,6 +90,130 @@ runTest("snapshotPenFile round-trips a complex sample", () => {
74
90
  assert.deepEqual(result.verification.topLevelIds, fixture.children.map((node) => node.id));
75
91
  });
76
92
 
93
+ runTest("ensurePenFile seeds a missing project-local .pen and state", () => {
94
+ const tempDir = createTempDir();
95
+ const projectRoot = path.join(tempDir, "project");
96
+ const outputPath = path.join(projectRoot, ".da-vinci", "designs", "seed.pen");
97
+
98
+ const result = ensurePenFile({
99
+ outputPath,
100
+ verifyWithPencil: true
101
+ });
102
+
103
+ const written = JSON.parse(fs.readFileSync(outputPath, "utf8"));
104
+ assert.equal(result.created, true);
105
+ assert.deepEqual(written, { version: "2.9", children: [] });
106
+ assert.equal(fs.existsSync(getStandardPenStatePath(outputPath)), true);
107
+ assert.deepEqual(result.verification.topLevelIds, []);
108
+ });
109
+
110
+ runTest("ensurePenFile preserves an existing .pen and refreshes state", () => {
111
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
112
+ const tempDir = createTempDir();
113
+ const outputPath = path.join(tempDir, "project", ".da-vinci", "designs", "existing.pen");
114
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
115
+ fs.writeFileSync(outputPath, JSON.stringify(fixture, null, 2));
116
+
117
+ const result = ensurePenFile({ outputPath });
118
+
119
+ assert.equal(result.created, false);
120
+ assert.deepEqual(JSON.parse(fs.readFileSync(outputPath, "utf8")), fixture);
121
+ assert.equal(fs.existsSync(result.statePath), true);
122
+ });
123
+
124
+ runTest("comparePenSync passes when payload matches persisted .pen", () => {
125
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
126
+ const tempDir = createTempDir();
127
+ const { nodesFile, variablesFile } = writePayloadFiles(tempDir, fixture);
128
+ const outputPath = path.join(tempDir, "written.pen");
129
+
130
+ writePenFromPayloadFiles({
131
+ outputPath,
132
+ nodesFile,
133
+ variablesFile,
134
+ version: fixture.version
135
+ });
136
+
137
+ const result = comparePenSync({
138
+ penPath: outputPath,
139
+ nodesFile,
140
+ variablesFile,
141
+ version: fixture.version
142
+ });
143
+
144
+ assert.equal(result.inSync, true);
145
+ assert.equal(result.usedStateFile, true);
146
+ });
147
+
148
+ runTest("comparePenSync fails when live payload is newer than disk", () => {
149
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
150
+ const tempDir = createTempDir();
151
+ const { nodesFile, variablesFile } = writePayloadFiles(tempDir, fixture);
152
+ const outputPath = path.join(tempDir, "written.pen");
153
+
154
+ writePenFromPayloadFiles({
155
+ outputPath,
156
+ nodesFile,
157
+ variablesFile,
158
+ version: fixture.version
159
+ });
160
+
161
+ const drifted = JSON.parse(fs.readFileSync(nodesFile, "utf8"));
162
+ drifted.nodes = drifted.nodes.concat([
163
+ {
164
+ id: "new-screen",
165
+ type: "frame",
166
+ children: []
167
+ }
168
+ ]);
169
+ fs.writeFileSync(nodesFile, JSON.stringify(drifted, null, 2));
170
+
171
+ const result = comparePenSync({
172
+ penPath: outputPath,
173
+ nodesFile,
174
+ variablesFile,
175
+ version: fixture.version
176
+ });
177
+
178
+ assert.equal(result.inSync, false);
179
+ assert.notEqual(result.liveHash, result.persistedHash);
180
+ });
181
+
182
+ runTest("global Pencil lock serializes projects", () => {
183
+ const tempDir = createTempDir();
184
+ const homeDir = path.join(tempDir, "home");
185
+ const projectA = path.join(tempDir, "project-a");
186
+ const projectB = path.join(tempDir, "project-b");
187
+
188
+ const first = acquirePencilLock({
189
+ projectPath: projectA,
190
+ owner: "project-a",
191
+ homeDir
192
+ });
193
+ assert.equal(first.acquired, true);
194
+
195
+ assert.throws(
196
+ () =>
197
+ acquirePencilLock({
198
+ projectPath: projectB,
199
+ owner: "project-b",
200
+ homeDir,
201
+ waitMs: 0
202
+ }),
203
+ /already held/i
204
+ );
205
+
206
+ const status = getPencilLockStatus({ homeDir });
207
+ assert.equal(status.lock.projectPath, path.resolve(projectA));
208
+
209
+ const released = releasePencilLock({
210
+ projectPath: projectA,
211
+ homeDir
212
+ });
213
+ assert.equal(released.released, true);
214
+ assert.equal(getPencilLockStatus({ homeDir }).lock, null);
215
+ });
216
+
77
217
  runTest("truncated nodes payload is rejected", () => {
78
218
  const tempDir = createTempDir();
79
219
  const nodesFile = path.join(tempDir, "nodes.json");
@@ -0,0 +1,152 @@
1
+ const assert = require("assert/strict");
2
+ const fs = require("fs");
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const {
6
+ beginPencilSession,
7
+ persistPencilSession,
8
+ endPencilSession,
9
+ getPencilSessionStatus,
10
+ getSessionStatePath
11
+ } = require("../lib/pencil-session");
12
+
13
+ const fixturePath = path.join(__dirname, "fixtures", "complex-sample.pen");
14
+
15
+ function runTest(name, fn) {
16
+ try {
17
+ fn();
18
+ console.log(`PASS ${name}`);
19
+ } catch (error) {
20
+ console.error(`FAIL ${name}`);
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ function createTempDir() {
26
+ return fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-pencil-session-"));
27
+ }
28
+
29
+ function writePayloadFiles(tempDir, fixture) {
30
+ const nodesFile = path.join(tempDir, "nodes.json");
31
+ const variablesFile = path.join(tempDir, "variables.json");
32
+ fs.writeFileSync(nodesFile, JSON.stringify({ nodes: fixture.children }, null, 2));
33
+ fs.writeFileSync(variablesFile, JSON.stringify({ variables: fixture.variables }, null, 2));
34
+ return { nodesFile, variablesFile };
35
+ }
36
+
37
+ runTest("pencil-session begin seeds pen and acquires lock", () => {
38
+ const tempDir = createTempDir();
39
+ const projectRoot = path.join(tempDir, "project");
40
+ const homeDir = path.join(tempDir, "home");
41
+ const penPath = path.join(projectRoot, ".da-vinci", "designs", "cipher.pen");
42
+
43
+ const result = beginPencilSession({
44
+ projectPath: projectRoot,
45
+ penPath,
46
+ homeDir
47
+ });
48
+
49
+ assert.equal(fs.existsSync(penPath), true);
50
+ assert.equal(result.session.status, "active");
51
+ assert.equal(result.session.penPath, path.resolve(penPath));
52
+ assert.equal(fs.existsSync(getSessionStatePath(projectRoot)), true);
53
+
54
+ endPencilSession({
55
+ projectPath: projectRoot,
56
+ penPath,
57
+ homeDir,
58
+ force: true
59
+ });
60
+ });
61
+
62
+ runTest("pencil-session persist writes and syncs latest payload", () => {
63
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
64
+ const tempDir = createTempDir();
65
+ const projectRoot = path.join(tempDir, "project");
66
+ const homeDir = path.join(tempDir, "home");
67
+ const penPath = path.join(projectRoot, ".da-vinci", "designs", "cipher.pen");
68
+ const { nodesFile, variablesFile } = writePayloadFiles(tempDir, fixture);
69
+
70
+ beginPencilSession({
71
+ projectPath: projectRoot,
72
+ penPath,
73
+ homeDir
74
+ });
75
+
76
+ const persistResult = persistPencilSession({
77
+ projectPath: projectRoot,
78
+ penPath,
79
+ nodesFile,
80
+ variablesFile,
81
+ version: fixture.version,
82
+ homeDir
83
+ });
84
+
85
+ assert.equal(persistResult.syncResult.inSync, true);
86
+ assert.equal(persistResult.session.status, "active");
87
+ assert.ok(persistResult.session.lastPersistedHash);
88
+
89
+ const status = getPencilSessionStatus({
90
+ projectPath: projectRoot,
91
+ homeDir
92
+ });
93
+ assert.equal(status.session.lastPersistedHash, persistResult.session.lastPersistedHash);
94
+
95
+ endPencilSession({
96
+ projectPath: projectRoot,
97
+ penPath,
98
+ nodesFile,
99
+ variablesFile,
100
+ version: fixture.version,
101
+ homeDir
102
+ });
103
+ });
104
+
105
+ runTest("pencil-session end fails when live payload is stale", () => {
106
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
107
+ const tempDir = createTempDir();
108
+ const projectRoot = path.join(tempDir, "project");
109
+ const homeDir = path.join(tempDir, "home");
110
+ const penPath = path.join(projectRoot, ".da-vinci", "designs", "cipher.pen");
111
+ const { nodesFile, variablesFile } = writePayloadFiles(tempDir, fixture);
112
+
113
+ beginPencilSession({
114
+ projectPath: projectRoot,
115
+ penPath,
116
+ homeDir
117
+ });
118
+ persistPencilSession({
119
+ projectPath: projectRoot,
120
+ penPath,
121
+ nodesFile,
122
+ variablesFile,
123
+ version: fixture.version,
124
+ homeDir
125
+ });
126
+
127
+ const drifted = JSON.parse(fs.readFileSync(nodesFile, "utf8"));
128
+ drifted.nodes.push({ id: "drift", type: "frame", children: [] });
129
+ fs.writeFileSync(nodesFile, JSON.stringify(drifted, null, 2));
130
+
131
+ assert.throws(
132
+ () =>
133
+ endPencilSession({
134
+ projectPath: projectRoot,
135
+ penPath,
136
+ nodesFile,
137
+ variablesFile,
138
+ version: fixture.version,
139
+ homeDir
140
+ }),
141
+ /out of sync/i
142
+ );
143
+
144
+ endPencilSession({
145
+ projectPath: projectRoot,
146
+ penPath,
147
+ homeDir,
148
+ force: true
149
+ });
150
+ });
151
+
152
+ console.log("All Pencil session tests passed.");