@xenonbyte/da-vinci-workflow 0.1.24 → 0.1.26
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 +35 -1
- package/README.md +41 -10
- package/README.zh-CN.md +30 -10
- package/SKILL.md +4 -0
- package/commands/claude/dv/design.md +2 -1
- package/commands/codex/prompts/dv-design.md +2 -1
- package/commands/gemini/dv/design.toml +2 -1
- package/docs/constraint-files.md +1 -0
- package/docs/dv-command-reference.md +14 -2
- package/docs/pencil-rendering-workflow.md +9 -7
- package/docs/prompt-presets/README.md +4 -0
- package/docs/visual-assist-presets/README.md +4 -0
- package/docs/workflow-examples.md +13 -11
- package/docs/workflow-overview.md +2 -0
- package/docs/zh-CN/constraint-files.md +1 -0
- package/docs/zh-CN/dv-command-reference.md +14 -2
- package/docs/zh-CN/pencil-rendering-workflow.md +9 -7
- package/docs/zh-CN/prompt-presets/README.md +5 -1
- package/docs/zh-CN/visual-assist-presets/README.md +5 -1
- package/docs/zh-CN/workflow-examples.md +13 -11
- package/docs/zh-CN/workflow-overview.md +2 -0
- package/examples/greenfield-spec-markupflow/README.md +6 -1
- package/lib/async-offload-worker.js +26 -0
- package/lib/async-offload.js +82 -0
- package/lib/audit-parsers.js +223 -51
- package/lib/audit.js +91 -23
- package/lib/cli.js +749 -433
- package/lib/fs-safety.js +1 -4
- package/lib/icon-aliases.js +7 -7
- package/lib/icon-search.js +21 -14
- package/lib/icon-sync.js +220 -41
- package/lib/install.js +128 -60
- package/lib/mcp-runtime-gate.js +4 -7
- package/lib/pen-persistence.js +365 -46
- package/lib/pencil-lock.js +237 -25
- package/lib/pencil-preflight.js +233 -12
- package/lib/pencil-session.js +216 -36
- package/lib/supervisor-review.js +56 -34
- package/lib/utils.js +121 -0
- package/lib/workflow-bootstrap.js +255 -0
- package/package.json +13 -3
- package/references/artifact-templates.md +1 -0
- package/references/checkpoints.md +2 -0
- package/references/design-inputs.md +2 -0
- package/references/pencil-design-to-code.md +2 -0
- package/scripts/fixtures/complex-sample.pen +0 -295
- package/scripts/fixtures/mock-pencil.js +0 -49
- package/scripts/test-audit-context-delta.js +0 -446
- package/scripts/test-audit-design-supervisor.js +0 -537
- package/scripts/test-audit-safety.js +0 -92
- package/scripts/test-icon-aliases.js +0 -96
- package/scripts/test-icon-search.js +0 -77
- package/scripts/test-icon-sync.js +0 -178
- package/scripts/test-mcp-runtime-gate.js +0 -287
- package/scripts/test-mode-consistency.js +0 -339
- package/scripts/test-pen-persistence.js +0 -254
- package/scripts/test-pencil-lock.js +0 -130
- package/scripts/test-pencil-preflight.js +0 -169
- package/scripts/test-pencil-session.js +0 -192
- package/scripts/test-persistence-flows.js +0 -345
- package/scripts/test-supervisor-review-cli.js +0 -619
- package/scripts/test-supervisor-review-integration.js +0 -115
package/lib/pen-persistence.js
CHANGED
|
@@ -3,13 +3,13 @@ const os = require("os");
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const crypto = require("crypto");
|
|
5
5
|
const { spawnSync } = require("child_process");
|
|
6
|
+
const { isPlainObject, readJsonFile, writeFileAtomic } = require("./utils");
|
|
7
|
+
const { runModuleExportInWorker } = require("./async-offload");
|
|
6
8
|
|
|
7
9
|
const DEFAULT_PEN_VERSION = "2.9";
|
|
8
10
|
const DEFAULT_READ_DEPTH = 50;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
12
|
-
}
|
|
11
|
+
const DEFAULT_PENCIL_INTERACTIVE_MAX_BUFFER = 50 * 1024 * 1024;
|
|
12
|
+
const DEFAULT_PENCIL_INTERACTIVE_TIMEOUT_MS = 2 * 60 * 1000;
|
|
13
13
|
|
|
14
14
|
function extractFirstJson(text) {
|
|
15
15
|
const source = String(text || "");
|
|
@@ -18,10 +18,6 @@ function extractFirstJson(text) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
class JSONDecoder {
|
|
21
|
-
constructor() {
|
|
22
|
-
this.decoder = JSON;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
21
|
decodeFirst(text) {
|
|
26
22
|
for (let index = 0; index < text.length; index += 1) {
|
|
27
23
|
const ch = text[index];
|
|
@@ -44,6 +40,7 @@ class JSONDecoder {
|
|
|
44
40
|
let end = 0;
|
|
45
41
|
let inString = false;
|
|
46
42
|
let escape = false;
|
|
43
|
+
let unicodeDigitsRemaining = 0;
|
|
47
44
|
let depth = 0;
|
|
48
45
|
const opening = text[0];
|
|
49
46
|
const closing = opening === "{" ? "}" : "]";
|
|
@@ -52,7 +49,15 @@ class JSONDecoder {
|
|
|
52
49
|
const ch = text[index];
|
|
53
50
|
|
|
54
51
|
if (inString) {
|
|
52
|
+
if (unicodeDigitsRemaining > 0) {
|
|
53
|
+
unicodeDigitsRemaining -= 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
55
57
|
if (escape) {
|
|
58
|
+
if (ch === "u") {
|
|
59
|
+
unicodeDigitsRemaining = 4;
|
|
60
|
+
}
|
|
56
61
|
escape = false;
|
|
57
62
|
} else if (ch === "\\") {
|
|
58
63
|
escape = true;
|
|
@@ -110,7 +115,52 @@ function hasTruncatedChildren(value) {
|
|
|
110
115
|
return Object.values(value).some((item) => hasTruncatedChildren(item));
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
function looksLikePenStatePayload(payload) {
|
|
119
|
+
if (!isPlainObject(payload)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const hasLiveNodes = Array.isArray(payload.nodes);
|
|
124
|
+
if (hasLiveNodes) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const hasSnapshotHash = typeof payload.snapshotHash === "string" && payload.snapshotHash.length > 0;
|
|
129
|
+
const hasPenPath = typeof payload.penPath === "string" && payload.penPath.length > 0;
|
|
130
|
+
const hasStateCounters =
|
|
131
|
+
Object.prototype.hasOwnProperty.call(payload, "topLevelCount") ||
|
|
132
|
+
Object.prototype.hasOwnProperty.call(payload, "topLevelIds");
|
|
133
|
+
return hasSnapshotHash && hasPenPath && hasStateCounters;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertNodesPayloadIsLiveSnapshot(payload, sourcePath) {
|
|
137
|
+
if (!looksLikePenStatePayload(payload)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const location = sourcePath ? ` (${formatPathForError(sourcePath)})` : "";
|
|
142
|
+
throw new Error(
|
|
143
|
+
`\`--nodes-file\` payload${location} appears to be pen state metadata, not a live MCP nodes snapshot. ` +
|
|
144
|
+
"Use JSON from `batch_get` (an array or an object with `nodes`)."
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function formatPathForError(sourcePath) {
|
|
149
|
+
const resolved = path.resolve(sourcePath);
|
|
150
|
+
const relative = path.relative(process.cwd(), resolved);
|
|
151
|
+
if (!relative || relative === "") {
|
|
152
|
+
return ".";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
156
|
+
return relative;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return resolved;
|
|
160
|
+
}
|
|
161
|
+
|
|
113
162
|
function normalizeNodesPayload(payload) {
|
|
163
|
+
assertNodesPayloadIsLiveSnapshot(payload);
|
|
114
164
|
const nodes = getNodesArray(payload);
|
|
115
165
|
|
|
116
166
|
if (!nodes) {
|
|
@@ -207,29 +257,13 @@ function getStandardPenStatePath(outputPath) {
|
|
|
207
257
|
|
|
208
258
|
function writePenDocumentAtomic(outputPath, document) {
|
|
209
259
|
const targetPath = path.resolve(outputPath);
|
|
210
|
-
|
|
211
|
-
const tempPath = path.join(
|
|
212
|
-
targetDir,
|
|
213
|
-
`.${path.basename(targetPath)}.tmp-${process.pid}-${Date.now()}`
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
217
|
-
fs.writeFileSync(tempPath, JSON.stringify(document, null, 2) + "\n", "utf8");
|
|
218
|
-
fs.renameSync(tempPath, targetPath);
|
|
260
|
+
writeFileAtomic(targetPath, JSON.stringify(document, null, 2) + "\n");
|
|
219
261
|
return targetPath;
|
|
220
262
|
}
|
|
221
263
|
|
|
222
264
|
function writeJsonFileAtomic(outputPath, payload) {
|
|
223
265
|
const targetPath = path.resolve(outputPath);
|
|
224
|
-
|
|
225
|
-
const tempPath = path.join(
|
|
226
|
-
targetDir,
|
|
227
|
-
`.${path.basename(targetPath)}.tmp-${process.pid}-${Date.now()}`
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
231
|
-
fs.writeFileSync(tempPath, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
232
|
-
fs.renameSync(tempPath, targetPath);
|
|
266
|
+
writeFileAtomic(targetPath, JSON.stringify(payload, null, 2) + "\n");
|
|
233
267
|
return targetPath;
|
|
234
268
|
}
|
|
235
269
|
|
|
@@ -262,42 +296,88 @@ function readPenState(outputPath) {
|
|
|
262
296
|
|
|
263
297
|
function runPencilInteractive(inputPath, commands, options = {}) {
|
|
264
298
|
const pencilBin = options.pencilBin || "pencil";
|
|
299
|
+
const configuredMaxBuffer = Number(options.maxBuffer);
|
|
300
|
+
const configuredTimeoutMs = Number(options.pencilTimeoutMs || options.timeoutMs);
|
|
301
|
+
const maxBuffer =
|
|
302
|
+
Number.isFinite(configuredMaxBuffer) && configuredMaxBuffer > 0
|
|
303
|
+
? configuredMaxBuffer
|
|
304
|
+
: DEFAULT_PENCIL_INTERACTIVE_MAX_BUFFER;
|
|
305
|
+
const timeoutMs =
|
|
306
|
+
Number.isFinite(configuredTimeoutMs) && configuredTimeoutMs > 0
|
|
307
|
+
? configuredTimeoutMs
|
|
308
|
+
: DEFAULT_PENCIL_INTERACTIVE_TIMEOUT_MS;
|
|
265
309
|
const unusedOutput = path.join(
|
|
266
310
|
os.tmpdir(),
|
|
267
311
|
`da-vinci-verify-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.pen`
|
|
268
312
|
);
|
|
269
313
|
const payload = Array.isArray(commands) ? commands.join("\n") : String(commands || "");
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
["interactive", "-i", path.resolve(inputPath), "-o", unusedOutput],
|
|
273
|
-
{
|
|
314
|
+
let result;
|
|
315
|
+
try {
|
|
316
|
+
result = spawnSync(pencilBin, ["interactive", "-i", path.resolve(inputPath), "-o", unusedOutput], {
|
|
274
317
|
input: `${payload}\nexit()\n`,
|
|
275
318
|
encoding: "utf8",
|
|
276
|
-
maxBuffer
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
319
|
+
maxBuffer,
|
|
320
|
+
timeout: timeoutMs
|
|
321
|
+
});
|
|
322
|
+
} finally {
|
|
323
|
+
fs.rmSync(unusedOutput, { force: true });
|
|
324
|
+
}
|
|
281
325
|
|
|
282
326
|
const stdout = result.stdout || "";
|
|
283
327
|
const stderr = result.stderr || "";
|
|
284
328
|
if (result.status !== 0) {
|
|
329
|
+
const errorMessage =
|
|
330
|
+
result.error && result.error.message
|
|
331
|
+
? `\n${result.error.message}`
|
|
332
|
+
: "";
|
|
285
333
|
throw new Error(
|
|
286
|
-
`Pencil interactive failed for ${inputPath}
|
|
334
|
+
`Pencil interactive failed for ${inputPath}.${errorMessage}\n${stdout}${stderr}`.trim()
|
|
287
335
|
);
|
|
288
336
|
}
|
|
289
337
|
|
|
290
338
|
return `${stdout}${stderr}`;
|
|
291
339
|
}
|
|
292
340
|
|
|
341
|
+
function formatPencilOutputPreview(output, maxChars = 2400) {
|
|
342
|
+
const text = String(output || "").trim();
|
|
343
|
+
if (!text) {
|
|
344
|
+
return "(no stdout/stderr output)";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (text.length <= maxChars) {
|
|
348
|
+
return text;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const headLength = Math.floor(maxChars / 2);
|
|
352
|
+
const tailLength = maxChars - headLength;
|
|
353
|
+
return `${text.slice(0, headLength)}\n...\n${text.slice(text.length - tailLength)}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
293
356
|
function verifyPenFileWithPencil(filePath, options = {}) {
|
|
294
357
|
const output = runPencilInteractive(filePath, ['batch_get({ readDepth: 1 })'], options);
|
|
295
|
-
|
|
358
|
+
let payload;
|
|
359
|
+
try {
|
|
360
|
+
payload = extractFirstJson(output);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
[
|
|
364
|
+
`Pencil reopen verification could not parse JSON for ${filePath}: ${error.message || String(error)}`,
|
|
365
|
+
"Pencil output preview:",
|
|
366
|
+
formatPencilOutputPreview(output)
|
|
367
|
+
].join("\n")
|
|
368
|
+
);
|
|
369
|
+
}
|
|
296
370
|
const nodes = getNodesArray(payload);
|
|
297
371
|
const expectedTopLevelIds = options.expectedTopLevelIds || [];
|
|
298
372
|
|
|
299
373
|
if (!nodes) {
|
|
300
|
-
throw new Error(
|
|
374
|
+
throw new Error(
|
|
375
|
+
[
|
|
376
|
+
`Pencil reopen verification did not return a \`nodes\` array for ${filePath}.`,
|
|
377
|
+
"Pencil output preview:",
|
|
378
|
+
formatPencilOutputPreview(output)
|
|
379
|
+
].join("\n")
|
|
380
|
+
);
|
|
301
381
|
}
|
|
302
382
|
|
|
303
383
|
if (expectedTopLevelIds.length > 0) {
|
|
@@ -318,6 +398,7 @@ function verifyPenFileWithPencil(filePath, options = {}) {
|
|
|
318
398
|
function writePenFromPayloadFiles(options) {
|
|
319
399
|
const outputPath = path.resolve(options.outputPath);
|
|
320
400
|
const nodes = readJsonPayload(options.nodesFile);
|
|
401
|
+
assertNodesPayloadIsLiveSnapshot(nodes, options.nodesFile);
|
|
321
402
|
const variables = options.variablesFile ? readJsonPayload(options.variablesFile) : undefined;
|
|
322
403
|
const document = buildPenDocument({
|
|
323
404
|
version: options.version || DEFAULT_PEN_VERSION,
|
|
@@ -346,7 +427,7 @@ function writePenFromPayloadFiles(options) {
|
|
|
346
427
|
}
|
|
347
428
|
|
|
348
429
|
function readPenVersion(inputPath) {
|
|
349
|
-
const payload = JSON.
|
|
430
|
+
const payload = readJsonFile(inputPath, `.pen document JSON at ${path.resolve(inputPath)}`);
|
|
350
431
|
return payload.version || DEFAULT_PEN_VERSION;
|
|
351
432
|
}
|
|
352
433
|
|
|
@@ -402,6 +483,201 @@ function readPenDocument(inputPath) {
|
|
|
402
483
|
return readJsonPayload(inputPath);
|
|
403
484
|
}
|
|
404
485
|
|
|
486
|
+
function assertPenDocumentShape(document, inputPath) {
|
|
487
|
+
if (!isPlainObject(document) || !Array.isArray(document.children)) {
|
|
488
|
+
const location = inputPath ? `: ${path.resolve(inputPath)}` : "";
|
|
489
|
+
throw new Error(`Invalid .pen document (missing \`children\` array)${location}`);
|
|
490
|
+
}
|
|
491
|
+
return document;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function assertPenPathReadable(penPath) {
|
|
495
|
+
const resolvedPath = path.resolve(penPath);
|
|
496
|
+
|
|
497
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
498
|
+
throw new Error(`.pen file does not exist: ${resolvedPath}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const stat = fs.statSync(resolvedPath);
|
|
502
|
+
if (!stat.isFile()) {
|
|
503
|
+
throw new Error(`.pen path is not a file: ${resolvedPath}`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
resolvedPath,
|
|
508
|
+
sizeBytes: stat.size
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function hashFileSha256(filePath) {
|
|
513
|
+
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function inspectPenSource(penPath) {
|
|
517
|
+
const { resolvedPath, sizeBytes } = assertPenPathReadable(penPath);
|
|
518
|
+
const document = assertPenDocumentShape(readPenDocument(resolvedPath), resolvedPath);
|
|
519
|
+
const snapshotHash = hashPenDocument(document);
|
|
520
|
+
const state = readPenState(resolvedPath);
|
|
521
|
+
const stateHash =
|
|
522
|
+
state && typeof state.snapshotHash === "string" && state.snapshotHash.trim()
|
|
523
|
+
? String(state.snapshotHash).trim()
|
|
524
|
+
: "";
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
path: resolvedPath,
|
|
528
|
+
sizeBytes,
|
|
529
|
+
fileSha256: hashFileSha256(resolvedPath),
|
|
530
|
+
snapshotHash,
|
|
531
|
+
statePath: getStandardPenStatePath(resolvedPath),
|
|
532
|
+
stateHash: stateHash || null,
|
|
533
|
+
stateMatchesSnapshot: stateHash ? stateHash === snapshotHash : null
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function normalizePenPathList(values) {
|
|
538
|
+
const list = Array.isArray(values) ? values : [];
|
|
539
|
+
const normalized = [];
|
|
540
|
+
const seen = new Set();
|
|
541
|
+
|
|
542
|
+
for (const value of list) {
|
|
543
|
+
if (!value) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const resolvedPath = path.resolve(String(value));
|
|
547
|
+
if (seen.has(resolvedPath)) {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
seen.add(resolvedPath);
|
|
551
|
+
normalized.push(resolvedPath);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return normalized;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function comparePenBaselineAlignment(options) {
|
|
558
|
+
if (!options.penPath) {
|
|
559
|
+
throw new Error("A primary `.pen` path is required for baseline alignment checks.");
|
|
560
|
+
}
|
|
561
|
+
const penPath = path.resolve(options.penPath);
|
|
562
|
+
|
|
563
|
+
const baselinePaths = normalizePenPathList(options.baselinePaths).filter(
|
|
564
|
+
(candidatePath) => candidatePath !== penPath
|
|
565
|
+
);
|
|
566
|
+
if (baselinePaths.length === 0) {
|
|
567
|
+
throw new Error("Baseline alignment requires at least one comparison `.pen` path.");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const preferredSourcePath = options.preferredSource
|
|
571
|
+
? path.resolve(options.preferredSource)
|
|
572
|
+
: null;
|
|
573
|
+
const knownPaths = new Set([penPath, ...baselinePaths]);
|
|
574
|
+
if (preferredSourcePath && !knownPaths.has(preferredSourcePath)) {
|
|
575
|
+
throw new Error(
|
|
576
|
+
`Preferred source is not part of the compared paths. Add it as \`--pen\` or \`--baseline\`: ${preferredSourcePath}`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const primary = inspectPenSource(penPath);
|
|
581
|
+
const baselines = baselinePaths.map((candidatePath) => {
|
|
582
|
+
const source = inspectPenSource(candidatePath);
|
|
583
|
+
return {
|
|
584
|
+
...source,
|
|
585
|
+
matchesPrimary: source.snapshotHash === primary.snapshotHash
|
|
586
|
+
};
|
|
587
|
+
});
|
|
588
|
+
const inSync = baselines.every((source) => source.matchesPrimary);
|
|
589
|
+
|
|
590
|
+
let status = "PASS";
|
|
591
|
+
let decision = "aligned";
|
|
592
|
+
let message = "All compared `.pen` sources match the project-local baseline hash.";
|
|
593
|
+
|
|
594
|
+
if (!inSync) {
|
|
595
|
+
if (!preferredSourcePath) {
|
|
596
|
+
status = "BLOCK";
|
|
597
|
+
decision = "diverged";
|
|
598
|
+
message =
|
|
599
|
+
"Compared `.pen` sources diverged. Confirm source priority before continuing (`--prefer-source`).";
|
|
600
|
+
} else if (preferredSourcePath === penPath) {
|
|
601
|
+
status = "WARN";
|
|
602
|
+
decision = "diverged_prefer_pen";
|
|
603
|
+
message =
|
|
604
|
+
"Compared `.pen` sources diverged, but source priority explicitly keeps the project-local `.pen` as baseline.";
|
|
605
|
+
} else {
|
|
606
|
+
status = "BLOCK";
|
|
607
|
+
decision = "diverged_prefer_external";
|
|
608
|
+
message =
|
|
609
|
+
"Compared `.pen` sources diverged and preferred source points to a non-project file. Sync preferred source into the project path before continuing.";
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
penPath,
|
|
615
|
+
baselinePaths,
|
|
616
|
+
preferredSourcePath,
|
|
617
|
+
primary,
|
|
618
|
+
baselines,
|
|
619
|
+
inSync,
|
|
620
|
+
status,
|
|
621
|
+
decision,
|
|
622
|
+
message
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function formatPenBaselineAlignmentReport(result) {
|
|
627
|
+
const lines = [
|
|
628
|
+
`Primary .pen: ${result.primary.path}`,
|
|
629
|
+
`Primary snapshot hash: ${result.primary.snapshotHash}`,
|
|
630
|
+
`Primary file sha256: ${result.primary.fileSha256}`,
|
|
631
|
+
`Primary size: ${result.primary.sizeBytes} bytes`
|
|
632
|
+
];
|
|
633
|
+
|
|
634
|
+
for (const source of result.baselines) {
|
|
635
|
+
lines.push("");
|
|
636
|
+
lines.push(`Baseline: ${source.path}`);
|
|
637
|
+
lines.push(`- Snapshot hash: ${source.snapshotHash}`);
|
|
638
|
+
lines.push(`- File sha256: ${source.fileSha256}`);
|
|
639
|
+
lines.push(`- Size: ${source.sizeBytes} bytes`);
|
|
640
|
+
lines.push(`- Matches primary: ${source.matchesPrimary ? "yes" : "no"}`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
lines.push("");
|
|
644
|
+
lines.push(`Status: ${result.status}`);
|
|
645
|
+
lines.push(`Decision: ${result.decision}`);
|
|
646
|
+
lines.push(`Summary: ${result.message}`);
|
|
647
|
+
if (result.preferredSourcePath) {
|
|
648
|
+
lines.push(`Preferred source: ${result.preferredSourcePath}`);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return lines.join("\n");
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function syncPenSource(options) {
|
|
655
|
+
if (!options.sourcePath || !options.targetPath) {
|
|
656
|
+
throw new Error("Both source and target `.pen` paths are required for source sync.");
|
|
657
|
+
}
|
|
658
|
+
const sourcePath = path.resolve(options.sourcePath);
|
|
659
|
+
const targetPath = path.resolve(options.targetPath);
|
|
660
|
+
|
|
661
|
+
const sourceInfo = inspectPenSource(sourcePath);
|
|
662
|
+
const sourceDocument = assertPenDocumentShape(readPenDocument(sourcePath), sourcePath);
|
|
663
|
+
writePenDocumentAtomic(targetPath, sourceDocument);
|
|
664
|
+
const state = buildPenState(targetPath, sourceDocument, {
|
|
665
|
+
source: options.stateSource || "sync-pen-source"
|
|
666
|
+
});
|
|
667
|
+
const statePath = writePenStateAtomic(targetPath, state);
|
|
668
|
+
|
|
669
|
+
const targetInfo = inspectPenSource(targetPath);
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
sourcePath,
|
|
673
|
+
targetPath,
|
|
674
|
+
sourceInfo,
|
|
675
|
+
targetInfo,
|
|
676
|
+
state,
|
|
677
|
+
statePath
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
405
681
|
function ensurePenFile(options) {
|
|
406
682
|
const outputPath = path.resolve(options.outputPath);
|
|
407
683
|
const exists = fs.existsSync(outputPath);
|
|
@@ -416,7 +692,7 @@ function ensurePenFile(options) {
|
|
|
416
692
|
} else {
|
|
417
693
|
document = buildPenDocument({
|
|
418
694
|
version: options.version || DEFAULT_PEN_VERSION,
|
|
419
|
-
nodes:
|
|
695
|
+
nodes: [],
|
|
420
696
|
variables: {}
|
|
421
697
|
});
|
|
422
698
|
writePenDocumentAtomic(outputPath, document);
|
|
@@ -447,6 +723,7 @@ function ensurePenFile(options) {
|
|
|
447
723
|
|
|
448
724
|
function hashPayloadFiles(options) {
|
|
449
725
|
const nodes = readJsonPayload(options.nodesFile);
|
|
726
|
+
assertNodesPayloadIsLiveSnapshot(nodes, options.nodesFile);
|
|
450
727
|
const variables = options.variablesFile ? readJsonPayload(options.variablesFile) : undefined;
|
|
451
728
|
const document = buildPenDocument({
|
|
452
729
|
version: options.version || DEFAULT_PEN_VERSION,
|
|
@@ -466,30 +743,61 @@ function comparePenSync(options) {
|
|
|
466
743
|
throw new Error(`Registered .pen file does not exist: ${penPath}`);
|
|
467
744
|
}
|
|
468
745
|
|
|
469
|
-
const persistedDocument = readPenDocument(penPath);
|
|
746
|
+
const persistedDocument = assertPenDocumentShape(readPenDocument(penPath), penPath);
|
|
747
|
+
const diskHash = hashPenDocument(persistedDocument);
|
|
470
748
|
const live = hashPayloadFiles({
|
|
471
749
|
...options,
|
|
472
750
|
version: options.version || persistedDocument.version || DEFAULT_PEN_VERSION
|
|
473
751
|
});
|
|
474
752
|
const persistedState = readPenState(penPath);
|
|
475
|
-
const
|
|
476
|
-
? String(persistedState.snapshotHash)
|
|
477
|
-
|
|
753
|
+
const stateHash =
|
|
754
|
+
persistedState && persistedState.snapshotHash ? String(persistedState.snapshotHash).trim() : "";
|
|
755
|
+
const stateMatchesDisk = stateHash ? stateHash === diskHash : null;
|
|
756
|
+
const persistedHash = stateHash && stateMatchesDisk ? stateHash : diskHash;
|
|
478
757
|
|
|
479
758
|
return {
|
|
480
759
|
penPath,
|
|
481
760
|
statePath: getStandardPenStatePath(penPath),
|
|
761
|
+
diskHash,
|
|
762
|
+
stateHash: stateHash || null,
|
|
763
|
+
stateMatchesDisk,
|
|
482
764
|
persistedHash,
|
|
483
765
|
liveHash: live.snapshotHash,
|
|
484
766
|
inSync: persistedHash === live.snapshotHash,
|
|
485
|
-
usedStateFile: Boolean(
|
|
767
|
+
usedStateFile: Boolean(stateHash && stateMatchesDisk),
|
|
486
768
|
state: persistedState
|
|
487
769
|
};
|
|
488
770
|
}
|
|
489
771
|
|
|
772
|
+
function writePenFromPayloadFilesAsync(options) {
|
|
773
|
+
return runModuleExportInWorker(__filename, "writePenFromPayloadFiles", [options]);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function snapshotPenFileAsync(options) {
|
|
777
|
+
return runModuleExportInWorker(__filename, "snapshotPenFile", [options]);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function ensurePenFileAsync(options) {
|
|
781
|
+
return runModuleExportInWorker(__filename, "ensurePenFile", [options]);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function comparePenSyncAsync(options) {
|
|
785
|
+
return runModuleExportInWorker(__filename, "comparePenSync", [options]);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function comparePenBaselineAlignmentAsync(options) {
|
|
789
|
+
return runModuleExportInWorker(__filename, "comparePenBaselineAlignment", [options]);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function syncPenSourceAsync(options) {
|
|
793
|
+
return runModuleExportInWorker(__filename, "syncPenSource", [options]);
|
|
794
|
+
}
|
|
795
|
+
|
|
490
796
|
module.exports = {
|
|
491
797
|
DEFAULT_PEN_VERSION,
|
|
492
798
|
DEFAULT_READ_DEPTH,
|
|
799
|
+
DEFAULT_PENCIL_INTERACTIVE_MAX_BUFFER,
|
|
800
|
+
DEFAULT_PENCIL_INTERACTIVE_TIMEOUT_MS,
|
|
493
801
|
extractFirstJson,
|
|
494
802
|
readJsonPayload,
|
|
495
803
|
normalizeNodesPayload,
|
|
@@ -506,10 +814,21 @@ module.exports = {
|
|
|
506
814
|
runPencilInteractive,
|
|
507
815
|
verifyPenFileWithPencil,
|
|
508
816
|
writePenFromPayloadFiles,
|
|
817
|
+
writePenFromPayloadFilesAsync,
|
|
509
818
|
capturePenSnapshot,
|
|
510
819
|
snapshotPenFile,
|
|
820
|
+
snapshotPenFileAsync,
|
|
511
821
|
readPenDocument,
|
|
822
|
+
assertPenDocumentShape,
|
|
512
823
|
ensurePenFile,
|
|
824
|
+
ensurePenFileAsync,
|
|
513
825
|
hashPayloadFiles,
|
|
514
|
-
comparePenSync
|
|
826
|
+
comparePenSync,
|
|
827
|
+
comparePenSyncAsync,
|
|
828
|
+
inspectPenSource,
|
|
829
|
+
comparePenBaselineAlignment,
|
|
830
|
+
comparePenBaselineAlignmentAsync,
|
|
831
|
+
formatPenBaselineAlignmentReport,
|
|
832
|
+
syncPenSource,
|
|
833
|
+
syncPenSourceAsync
|
|
515
834
|
};
|