@xenonbyte/da-vinci-workflow 0.1.23 → 0.1.25
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 +26 -0
- package/README.md +14 -1
- package/README.zh-CN.md +14 -1
- package/SKILL.md +3 -2
- package/bin/design-supervisor.js +39 -0
- package/commands/claude/dv/design.md +1 -1
- package/commands/codex/prompts/dv-design.md +1 -1
- package/commands/gemini/dv/design.toml +1 -1
- package/docs/constraint-files.md +5 -1
- package/docs/dv-command-reference.md +6 -0
- package/docs/zh-CN/constraint-files.md +5 -1
- package/docs/zh-CN/dv-command-reference.md +6 -0
- package/lib/audit-parsers.js +216 -18
- package/lib/audit.js +96 -3
- package/lib/cli.js +97 -0
- package/lib/pen-persistence.js +47 -0
- package/lib/supervisor-review.js +680 -0
- package/package.json +6 -3
- package/references/artifact-templates.md +3 -0
- package/scripts/test-audit-design-supervisor.js +343 -0
- package/scripts/test-mode-consistency.js +5 -0
- package/scripts/test-pen-persistence.js +149 -0
- package/scripts/test-supervisor-review-cli.js +619 -0
- package/scripts/test-supervisor-review-integration.js +115 -0
package/lib/audit.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
buildContextDeltaReferenceIndex,
|
|
12
12
|
resolveSupersedesReferenceIndices,
|
|
13
13
|
inspectContextDelta,
|
|
14
|
+
getConfiguredDesignSupervisorReviewers,
|
|
14
15
|
hasConfiguredDesignSupervisorReview,
|
|
15
16
|
isDesignSupervisorReviewRequired,
|
|
16
17
|
inspectDesignSupervisorReview
|
|
@@ -195,6 +196,10 @@ function appendTraversalWarnings(projectRoot, rootDir, scan, warnings) {
|
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
function normalizeReviewerToken(value) {
|
|
200
|
+
return String(value || "").trim().toLowerCase();
|
|
201
|
+
}
|
|
202
|
+
|
|
198
203
|
function auditProject(projectPathInput, options = {}) {
|
|
199
204
|
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
200
205
|
const mode = options.mode || "integrity";
|
|
@@ -204,6 +209,8 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
204
209
|
const designRegistryPath = path.join(daVinciDir, "design-registry.md");
|
|
205
210
|
const pencilSessionPath = getSessionStatePath(projectRoot);
|
|
206
211
|
const daVinciText = readTextIfExists(path.join(projectRoot, "DA-VINCI.md"));
|
|
212
|
+
const configuredSupervisorReviewers = getConfiguredDesignSupervisorReviewers(daVinciText);
|
|
213
|
+
const configuredReviewerSet = new Set(configuredSupervisorReviewers.map((value) => normalizeReviewerToken(value)));
|
|
207
214
|
const designSupervisorConfigured = hasConfiguredDesignSupervisorReview(daVinciText);
|
|
208
215
|
const designSupervisorRequired = isDesignSupervisorReviewRequired(daVinciText);
|
|
209
216
|
|
|
@@ -467,6 +474,15 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
467
474
|
const enforceAsFailure =
|
|
468
475
|
mode === "completion" && scopedChangeDirs.includes(changeDir) && designSupervisorRequired;
|
|
469
476
|
|
|
477
|
+
if (review.usedStructuredSectionFallback) {
|
|
478
|
+
warnings.push(
|
|
479
|
+
`Latest design-supervisor review entry in ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
480
|
+
`is a legacy/non-structured format; gate evaluation used ${review.selectedSectionHeading}.`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
let reviewRecordValid = true;
|
|
485
|
+
|
|
470
486
|
if (!review.found) {
|
|
471
487
|
const message =
|
|
472
488
|
`DA-VINCI.md configures Design-supervisor reviewers, but ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
@@ -476,6 +492,7 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
476
492
|
} else {
|
|
477
493
|
warnings.push(message);
|
|
478
494
|
}
|
|
495
|
+
reviewRecordValid = false;
|
|
479
496
|
} else if (!review.status) {
|
|
480
497
|
const message =
|
|
481
498
|
`Design-supervisor review is recorded in ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
@@ -485,6 +502,7 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
485
502
|
} else {
|
|
486
503
|
warnings.push(message);
|
|
487
504
|
}
|
|
505
|
+
reviewRecordValid = false;
|
|
488
506
|
} else if (!review.hasIssueList || !review.hasRevisionOutcome) {
|
|
489
507
|
const missingFields = [
|
|
490
508
|
!review.hasIssueList ? "Issue list" : null,
|
|
@@ -500,7 +518,82 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
500
518
|
} else {
|
|
501
519
|
warnings.push(message);
|
|
502
520
|
}
|
|
503
|
-
|
|
521
|
+
reviewRecordValid = false;
|
|
522
|
+
} else if (!review.hasConfiguredReviewers) {
|
|
523
|
+
const message =
|
|
524
|
+
`Design-supervisor review in ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
525
|
+
"is missing required field(s): Configured reviewers.";
|
|
526
|
+
if (enforceAsFailure) {
|
|
527
|
+
failures.push(message);
|
|
528
|
+
} else {
|
|
529
|
+
warnings.push(message);
|
|
530
|
+
}
|
|
531
|
+
reviewRecordValid = false;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (reviewRecordValid && configuredReviewerSet.size > 0) {
|
|
535
|
+
const reviewConfiguredSet = new Set(
|
|
536
|
+
review.configuredReviewers.map((value) => normalizeReviewerToken(value))
|
|
537
|
+
);
|
|
538
|
+
const missingConfiguredReviewers = configuredSupervisorReviewers.filter(
|
|
539
|
+
(reviewer) => !reviewConfiguredSet.has(normalizeReviewerToken(reviewer))
|
|
540
|
+
);
|
|
541
|
+
if (missingConfiguredReviewers.length > 0) {
|
|
542
|
+
const message =
|
|
543
|
+
`Design-supervisor review in ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
544
|
+
`does not list configured reviewer(s): ${missingConfiguredReviewers.join(", ")}.`;
|
|
545
|
+
if (enforceAsFailure) {
|
|
546
|
+
failures.push(message);
|
|
547
|
+
} else {
|
|
548
|
+
warnings.push(message);
|
|
549
|
+
}
|
|
550
|
+
reviewRecordValid = false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (reviewRecordValid) {
|
|
555
|
+
if (designSupervisorRequired && review.reviewSource !== "skill") {
|
|
556
|
+
const message =
|
|
557
|
+
`Design-supervisor review in ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
558
|
+
"must be skill-backed (`Review source: skill`) when `Require Supervisor Review: true`.";
|
|
559
|
+
if (enforceAsFailure) {
|
|
560
|
+
failures.push(message);
|
|
561
|
+
} else {
|
|
562
|
+
warnings.push(message);
|
|
563
|
+
}
|
|
564
|
+
reviewRecordValid = false;
|
|
565
|
+
} else if (designSupervisorRequired && !review.hasExecutedReviewers) {
|
|
566
|
+
const message =
|
|
567
|
+
`Design-supervisor review in ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
568
|
+
"is missing required field(s): Executed reviewers.";
|
|
569
|
+
if (enforceAsFailure) {
|
|
570
|
+
failures.push(message);
|
|
571
|
+
} else {
|
|
572
|
+
warnings.push(message);
|
|
573
|
+
}
|
|
574
|
+
reviewRecordValid = false;
|
|
575
|
+
} else if (designSupervisorRequired && configuredReviewerSet.size > 0) {
|
|
576
|
+
const executedReviewerSet = new Set(
|
|
577
|
+
review.executedReviewers.map((value) => normalizeReviewerToken(value))
|
|
578
|
+
);
|
|
579
|
+
const missingExecuted = configuredSupervisorReviewers.filter(
|
|
580
|
+
(reviewer) => !executedReviewerSet.has(normalizeReviewerToken(reviewer))
|
|
581
|
+
);
|
|
582
|
+
if (missingExecuted.length > 0) {
|
|
583
|
+
const message =
|
|
584
|
+
`Design-supervisor review in ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
585
|
+
`did not execute configured reviewer skill(s): ${missingExecuted.join(", ")}.`;
|
|
586
|
+
if (enforceAsFailure) {
|
|
587
|
+
failures.push(message);
|
|
588
|
+
} else {
|
|
589
|
+
warnings.push(message);
|
|
590
|
+
}
|
|
591
|
+
reviewRecordValid = false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (reviewRecordValid && review.status === "BLOCK") {
|
|
504
597
|
const message =
|
|
505
598
|
`Design-supervisor review is still BLOCK in ${relativeTo(projectRoot, pencilDesignPath)}.`;
|
|
506
599
|
if (enforceAsFailure) {
|
|
@@ -508,7 +601,7 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
508
601
|
} else {
|
|
509
602
|
warnings.push(message);
|
|
510
603
|
}
|
|
511
|
-
} else if (review.status === "WARN" && !review.acceptedWarn) {
|
|
604
|
+
} else if (reviewRecordValid && review.status === "WARN" && !review.acceptedWarn) {
|
|
512
605
|
const message =
|
|
513
606
|
`Design-supervisor review is WARN in ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
514
607
|
"but the warning was not explicitly accepted.";
|
|
@@ -517,7 +610,7 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
517
610
|
} else {
|
|
518
611
|
warnings.push(message);
|
|
519
612
|
}
|
|
520
|
-
} else {
|
|
613
|
+
} else if (reviewRecordValid) {
|
|
521
614
|
notes.push(
|
|
522
615
|
`Detected design-supervisor review status ${review.status} in ${relativeTo(projectRoot, pencilDesignPath)}.`
|
|
523
616
|
);
|
package/lib/cli.js
CHANGED
|
@@ -42,6 +42,10 @@ const {
|
|
|
42
42
|
loadIconAliases,
|
|
43
43
|
expandQueryWithAliases
|
|
44
44
|
} = require("./icon-aliases");
|
|
45
|
+
const {
|
|
46
|
+
runDesignSupervisorReview,
|
|
47
|
+
formatDesignSupervisorReviewReport
|
|
48
|
+
} = require("./supervisor-review");
|
|
45
49
|
|
|
46
50
|
function getOption(args, name) {
|
|
47
51
|
const direct = args.find((arg) => arg.startsWith(`${name}=`));
|
|
@@ -105,6 +109,7 @@ function printHelp() {
|
|
|
105
109
|
" da-vinci audit [project-path]",
|
|
106
110
|
" da-vinci icon-sync [--output <path>] [--timeout-ms <value>] [--strict]",
|
|
107
111
|
" da-vinci icon-search --query <text> [--family <value>] [--top <value>] [--catalog <path>] [--aliases <path>] [--json]",
|
|
112
|
+
" da-vinci supervisor-review --project <path> --change <id> [--run-reviewers] [--review-concurrency <value>] [--review-retries <value>] [--review-retry-delay-ms <value>] [--source <skill|manual|inferred>] [--executed-reviewers <csv>] [--status <PASS|WARN|BLOCK>] [--issue-list <text>] [--revision-outcome <text>] [--write] [--json]",
|
|
108
113
|
" da-vinci preflight-pencil --ops-file <path>",
|
|
109
114
|
" da-vinci ensure-pen --output <path>",
|
|
110
115
|
" da-vinci write-pen --output <path> --nodes-file <path> [--variables-file <path>]",
|
|
@@ -125,8 +130,21 @@ function printHelp() {
|
|
|
125
130
|
" --project <path> override project path for audit",
|
|
126
131
|
" --catalog <path> icon catalog path for icon-search/icon-sync (default: ~/.da-vinci/icon-catalog.json)",
|
|
127
132
|
" --aliases <path> icon alias mapping file for icon-search (default: ~/.da-vinci/icon-aliases.json)",
|
|
133
|
+
" --pencil-design <path> explicit pencil-design.md path for supervisor-review",
|
|
128
134
|
" --query <text> icon-search query text",
|
|
129
135
|
" --family <value> icon-search family filter: all, material, rounded, outlined, sharp, lucide, feather, phosphor",
|
|
136
|
+
" --status <value> PASS, WARN, or BLOCK for supervisor-review",
|
|
137
|
+
" --source <value> review source: skill, manual, inferred",
|
|
138
|
+
" --executed-reviewers <csv> reviewer skills that executed this review",
|
|
139
|
+
" --run-reviewers execute configured reviewer skills through codex exec",
|
|
140
|
+
" --review-concurrency <value> max parallel reviewer executions (default 2)",
|
|
141
|
+
" --review-retries <value> retry count per reviewer before failing (default 1)",
|
|
142
|
+
" --review-retry-delay-ms <value> initial retry delay in milliseconds (default 400, exponential backoff)",
|
|
143
|
+
" --codex-bin <path> codex executable path for --run-reviewers (default: codex)",
|
|
144
|
+
" --max-images <value> max screenshots attached to reviewer runs (default 6)",
|
|
145
|
+
" --review-timeout-ms <value> timeout per reviewer run in milliseconds",
|
|
146
|
+
" --issue-list <text> supervisor-review issue summary",
|
|
147
|
+
" --revision-outcome <text> supervisor-review revision result summary",
|
|
130
148
|
" --top <value> icon-search result count (1-50, default 8)",
|
|
131
149
|
" --timeout-ms <value> network timeout for icon-sync requests",
|
|
132
150
|
" --strict fail icon-sync when any upstream source request fails",
|
|
@@ -162,6 +180,18 @@ async function runCli(argv) {
|
|
|
162
180
|
"--top",
|
|
163
181
|
"--catalog",
|
|
164
182
|
"--aliases",
|
|
183
|
+
"--pencil-design",
|
|
184
|
+
"--status",
|
|
185
|
+
"--source",
|
|
186
|
+
"--executed-reviewers",
|
|
187
|
+
"--codex-bin",
|
|
188
|
+
"--max-images",
|
|
189
|
+
"--review-timeout-ms",
|
|
190
|
+
"--review-concurrency",
|
|
191
|
+
"--review-retries",
|
|
192
|
+
"--review-retry-delay-ms",
|
|
193
|
+
"--issue-list",
|
|
194
|
+
"--revision-outcome",
|
|
165
195
|
"--timeout-ms",
|
|
166
196
|
"--ops-file",
|
|
167
197
|
"--input",
|
|
@@ -394,6 +424,73 @@ async function runCli(argv) {
|
|
|
394
424
|
return;
|
|
395
425
|
}
|
|
396
426
|
|
|
427
|
+
if (command === "supervisor-review") {
|
|
428
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
429
|
+
console.log(
|
|
430
|
+
[
|
|
431
|
+
"da-vinci supervisor-review",
|
|
432
|
+
"",
|
|
433
|
+
"Usage:",
|
|
434
|
+
" da-vinci supervisor-review --project <path> --change <id> [--run-reviewers] [--review-concurrency <value>] [--review-retries <value>] [--review-retry-delay-ms <value>] [--source <skill|manual|inferred>] [--executed-reviewers <csv>] [--status <PASS|WARN|BLOCK>] [--issue-list <text>] [--revision-outcome <text>] [--write] [--json]",
|
|
435
|
+
" da-vinci supervisor-review --project <path> --pencil-design <path> [--run-reviewers] [--review-concurrency <value>] [--review-retries <value>] [--review-retry-delay-ms <value>] [--source <skill|manual|inferred>] [--executed-reviewers <csv>] [--status <PASS|WARN|BLOCK>] [--issue-list <text>] [--revision-outcome <text>] [--write] [--json]",
|
|
436
|
+
"",
|
|
437
|
+
"Notes:",
|
|
438
|
+
" - omit --status to infer a conservative review status from current design artifacts",
|
|
439
|
+
" - use --run-reviewers to execute configured reviewer skills automatically via codex exec",
|
|
440
|
+
" - `design-supervisor review` is a compatibility alias for this command"
|
|
441
|
+
].join("\n")
|
|
442
|
+
);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const projectPath = getOption(argv, "--project") || process.cwd();
|
|
447
|
+
const changeId = getOption(argv, "--change");
|
|
448
|
+
const pencilDesignPath = getOption(argv, "--pencil-design");
|
|
449
|
+
const status = getOption(argv, "--status");
|
|
450
|
+
const source = getOption(argv, "--source");
|
|
451
|
+
const executedReviewers = getOption(argv, "--executed-reviewers");
|
|
452
|
+
const codexBin = getOption(argv, "--codex-bin");
|
|
453
|
+
const maxImages = getOption(argv, "--max-images");
|
|
454
|
+
const reviewerTimeoutMs = getOption(argv, "--review-timeout-ms");
|
|
455
|
+
const reviewConcurrency = getOption(argv, "--review-concurrency");
|
|
456
|
+
const reviewerRetries = getOption(argv, "--review-retries");
|
|
457
|
+
const reviewerRetryDelayMs = getOption(argv, "--review-retry-delay-ms");
|
|
458
|
+
const issueList = getOption(argv, "--issue-list");
|
|
459
|
+
const revisionOutcome = getOption(argv, "--revision-outcome");
|
|
460
|
+
const write = argv.includes("--write");
|
|
461
|
+
const acceptWarn = argv.includes("--accept-warn");
|
|
462
|
+
const runReviewers = argv.includes("--run-reviewers");
|
|
463
|
+
const jsonOutput = argv.includes("--json");
|
|
464
|
+
|
|
465
|
+
const result = await runDesignSupervisorReview({
|
|
466
|
+
projectPath,
|
|
467
|
+
changeId,
|
|
468
|
+
pencilDesignPath,
|
|
469
|
+
status,
|
|
470
|
+
source,
|
|
471
|
+
executedReviewers,
|
|
472
|
+
codexBin,
|
|
473
|
+
maxImages,
|
|
474
|
+
reviewerTimeoutMs,
|
|
475
|
+
reviewConcurrency,
|
|
476
|
+
reviewerRetries,
|
|
477
|
+
reviewerRetryDelayMs,
|
|
478
|
+
issueList,
|
|
479
|
+
revisionOutcome,
|
|
480
|
+
write,
|
|
481
|
+
acceptWarn,
|
|
482
|
+
runReviewers
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
if (jsonOutput) {
|
|
486
|
+
console.log(JSON.stringify(result, null, 2));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
console.log(formatDesignSupervisorReviewReport(result));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
397
494
|
if (command === "ensure-pen") {
|
|
398
495
|
const outputPath = getOption(argv, "--output");
|
|
399
496
|
const version = getOption(argv, "--version");
|
package/lib/pen-persistence.js
CHANGED
|
@@ -110,7 +110,52 @@ function hasTruncatedChildren(value) {
|
|
|
110
110
|
return Object.values(value).some((item) => hasTruncatedChildren(item));
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
function looksLikePenStatePayload(payload) {
|
|
114
|
+
if (!isPlainObject(payload)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const hasLiveNodes = Array.isArray(payload.nodes);
|
|
119
|
+
if (hasLiveNodes) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const hasSnapshotHash = typeof payload.snapshotHash === "string" && payload.snapshotHash.length > 0;
|
|
124
|
+
const hasPenPath = typeof payload.penPath === "string" && payload.penPath.length > 0;
|
|
125
|
+
const hasStateCounters =
|
|
126
|
+
Object.prototype.hasOwnProperty.call(payload, "topLevelCount") ||
|
|
127
|
+
Object.prototype.hasOwnProperty.call(payload, "topLevelIds");
|
|
128
|
+
return hasSnapshotHash && hasPenPath && hasStateCounters;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function assertNodesPayloadIsLiveSnapshot(payload, sourcePath) {
|
|
132
|
+
if (!looksLikePenStatePayload(payload)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const location = sourcePath ? ` (${formatPathForError(sourcePath)})` : "";
|
|
137
|
+
throw new Error(
|
|
138
|
+
`\`--nodes-file\` payload${location} appears to be pen state metadata, not a live MCP nodes snapshot. ` +
|
|
139
|
+
"Use JSON from `batch_get` (an array or an object with `nodes`)."
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatPathForError(sourcePath) {
|
|
144
|
+
const resolved = path.resolve(sourcePath);
|
|
145
|
+
const relative = path.relative(process.cwd(), resolved);
|
|
146
|
+
if (!relative || relative === "") {
|
|
147
|
+
return ".";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
151
|
+
return relative;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return resolved;
|
|
155
|
+
}
|
|
156
|
+
|
|
113
157
|
function normalizeNodesPayload(payload) {
|
|
158
|
+
assertNodesPayloadIsLiveSnapshot(payload);
|
|
114
159
|
const nodes = getNodesArray(payload);
|
|
115
160
|
|
|
116
161
|
if (!nodes) {
|
|
@@ -318,6 +363,7 @@ function verifyPenFileWithPencil(filePath, options = {}) {
|
|
|
318
363
|
function writePenFromPayloadFiles(options) {
|
|
319
364
|
const outputPath = path.resolve(options.outputPath);
|
|
320
365
|
const nodes = readJsonPayload(options.nodesFile);
|
|
366
|
+
assertNodesPayloadIsLiveSnapshot(nodes, options.nodesFile);
|
|
321
367
|
const variables = options.variablesFile ? readJsonPayload(options.variablesFile) : undefined;
|
|
322
368
|
const document = buildPenDocument({
|
|
323
369
|
version: options.version || DEFAULT_PEN_VERSION,
|
|
@@ -447,6 +493,7 @@ function ensurePenFile(options) {
|
|
|
447
493
|
|
|
448
494
|
function hashPayloadFiles(options) {
|
|
449
495
|
const nodes = readJsonPayload(options.nodesFile);
|
|
496
|
+
assertNodesPayloadIsLiveSnapshot(nodes, options.nodesFile);
|
|
450
497
|
const variables = options.variablesFile ? readJsonPayload(options.variablesFile) : undefined;
|
|
451
498
|
const document = buildPenDocument({
|
|
452
499
|
version: options.version || DEFAULT_PEN_VERSION,
|