@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/README.md +41 -10
  3. package/README.zh-CN.md +30 -10
  4. package/SKILL.md +4 -0
  5. package/commands/claude/dv/design.md +2 -1
  6. package/commands/codex/prompts/dv-design.md +2 -1
  7. package/commands/gemini/dv/design.toml +2 -1
  8. package/docs/constraint-files.md +1 -0
  9. package/docs/dv-command-reference.md +14 -2
  10. package/docs/pencil-rendering-workflow.md +9 -7
  11. package/docs/prompt-presets/README.md +4 -0
  12. package/docs/visual-assist-presets/README.md +4 -0
  13. package/docs/workflow-examples.md +13 -11
  14. package/docs/workflow-overview.md +2 -0
  15. package/docs/zh-CN/constraint-files.md +1 -0
  16. package/docs/zh-CN/dv-command-reference.md +14 -2
  17. package/docs/zh-CN/pencil-rendering-workflow.md +9 -7
  18. package/docs/zh-CN/prompt-presets/README.md +5 -1
  19. package/docs/zh-CN/visual-assist-presets/README.md +5 -1
  20. package/docs/zh-CN/workflow-examples.md +13 -11
  21. package/docs/zh-CN/workflow-overview.md +2 -0
  22. package/examples/greenfield-spec-markupflow/README.md +6 -1
  23. package/lib/async-offload-worker.js +26 -0
  24. package/lib/async-offload.js +82 -0
  25. package/lib/audit-parsers.js +223 -51
  26. package/lib/audit.js +91 -23
  27. package/lib/cli.js +749 -433
  28. package/lib/fs-safety.js +1 -4
  29. package/lib/icon-aliases.js +7 -7
  30. package/lib/icon-search.js +21 -14
  31. package/lib/icon-sync.js +220 -41
  32. package/lib/install.js +128 -60
  33. package/lib/mcp-runtime-gate.js +4 -7
  34. package/lib/pen-persistence.js +365 -46
  35. package/lib/pencil-lock.js +237 -25
  36. package/lib/pencil-preflight.js +233 -12
  37. package/lib/pencil-session.js +216 -36
  38. package/lib/supervisor-review.js +56 -34
  39. package/lib/utils.js +121 -0
  40. package/lib/workflow-bootstrap.js +255 -0
  41. package/package.json +13 -3
  42. package/references/artifact-templates.md +1 -0
  43. package/references/checkpoints.md +2 -0
  44. package/references/design-inputs.md +2 -0
  45. package/references/pencil-design-to-code.md +2 -0
  46. package/scripts/fixtures/complex-sample.pen +0 -295
  47. package/scripts/fixtures/mock-pencil.js +0 -49
  48. package/scripts/test-audit-context-delta.js +0 -446
  49. package/scripts/test-audit-design-supervisor.js +0 -537
  50. package/scripts/test-audit-safety.js +0 -92
  51. package/scripts/test-icon-aliases.js +0 -96
  52. package/scripts/test-icon-search.js +0 -77
  53. package/scripts/test-icon-sync.js +0 -178
  54. package/scripts/test-mcp-runtime-gate.js +0 -287
  55. package/scripts/test-mode-consistency.js +0 -339
  56. package/scripts/test-pen-persistence.js +0 -254
  57. package/scripts/test-pencil-lock.js +0 -130
  58. package/scripts/test-pencil-preflight.js +0 -169
  59. package/scripts/test-pencil-session.js +0 -192
  60. package/scripts/test-persistence-flows.js +0 -345
  61. package/scripts/test-supervisor-review-cli.js +0 -619
  62. package/scripts/test-supervisor-review-integration.js +0 -115
@@ -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
- function isPlainObject(value) {
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
- const targetDir = path.dirname(targetPath);
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
- const targetDir = path.dirname(targetPath);
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
- const result = spawnSync(
271
- pencilBin,
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: 8 * 1024 * 1024
277
- }
278
- );
279
-
280
- fs.rmSync(unusedOutput, { force: true });
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}.\n${stdout}${stderr}`.trim()
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
- const payload = extractFirstJson(output);
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("Pencil reopen verification did not return a `nodes` array.");
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.parse(fs.readFileSync(inputPath, "utf8"));
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: { 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 persistedHash = persistedState && persistedState.snapshotHash
476
- ? String(persistedState.snapshotHash)
477
- : hashPenDocument(persistedDocument);
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(persistedState && persistedState.snapshotHash),
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
  };