@davmash/bygone 0.9.8 → 0.10.1
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 -1
- package/README.md +74 -1
- package/cli/commandSpec.js +2 -0
- package/cli/completions.js +3 -3
- package/cli/present.js +41 -14
- package/cli/tour.js +22 -1
- package/cli/tourExport.js +147 -0
- package/cli/tourFile.js +109 -4
- package/cli/tourHistory.js +31 -1
- package/completions/_bygone +1 -1
- package/completions/bygone +1 -1
- package/completions/bygone.fish +1 -1
- package/docs/change-tour-format.md +85 -68
- package/docs/explore.md +14 -3
- package/docs/generating-change-tours.md +84 -46
- package/docs/present.md +103 -14
- package/docs/product-surface.md +19 -8
- package/docs/sharing-tours.md +95 -0
- package/docs/visual-walkthrough/README.md +89 -0
- package/docs/visual-walkthrough/build-tour.mjs +54 -0
- package/docs/visual-walkthrough/capture-desktop.cjs +42 -0
- package/docs/visual-walkthrough/capture-desktop.mjs +21 -0
- package/docs/visual-walkthrough/captures.md +49 -0
- package/docs/visual-walkthrough/create-demo.mjs +57 -0
- package/docs/visual-walkthrough/deconstructed-demo.bygone +100 -0
- package/docs/visual-walkthrough/images/01-two-files.png +0 -0
- package/docs/visual-walkthrough/images/02-many-panels.png +0 -0
- package/docs/visual-walkthrough/images/03-directories.png +0 -0
- package/docs/visual-walkthrough/images/04-search.png +0 -0
- package/docs/visual-walkthrough/images/05-repository-history.png +0 -0
- package/docs/visual-walkthrough/images/06-file-history.png +0 -0
- package/docs/visual-walkthrough/images/07-branch-review.png +0 -0
- package/docs/visual-walkthrough/images/08-tour-reader.png +0 -0
- package/docs/visual-walkthrough/images/09-focused-step.png +0 -0
- package/docs/visual-walkthrough/images/10-deconstructed.png +0 -0
- package/docs/visual-walkthrough/images/11-exploration.png +0 -0
- package/docs/visual-walkthrough/images/12-authoring.png +0 -0
- package/docs/visual-walkthrough/images/13-export.png +0 -0
- package/docs/visual-walkthrough/images/14-vscode.png +0 -0
- package/docs/visual-walkthrough/images/15-cli.png +0 -0
- package/docs/visual-walkthrough/narrative.md +134 -0
- package/docs/visual-walkthrough/outline.md +52 -0
- package/docs/visual-walkthrough/walkthrough.bygone +340 -0
- package/docs/walkthrough.md +6 -101
- package/examples/bygone-history.bygone +11 -44
- package/examples/marker-focus.bygone +116 -0
- package/examples/navigation-lab.bygone.yaml +401 -336
- package/examples/tour-orientation.bygone +349 -0
- package/media/diff.worker.js +13 -3
- package/media/webview.css +1 -1
- package/media/webview.js +394 -319
- package/out/changeTour.js +865 -397
- package/out/deepLink.js +167 -0
- package/out/historyDirectory.js +66 -0
- package/out/standalone-main.js +20532 -16832
- package/out/tourCoverage.js +850 -386
- package/out/tourDocument.js +97 -0
- package/package.json +1 -1
- package/schemas/change-tour-source.schema.json +91 -64
- package/skills/pr-tour-guide/SKILL.md +144 -128
- package/skills/pr-tour-guide/references/expectations-and-feedback.md +57 -0
- package/standalone/index.html +10 -25
- package/web/export-bootstrap.js +62 -0
- package/web/export-licenses.txt +78 -0
- package/web/export-runtime.json +12 -0
- package/web/index.html +54 -47
- package/web/presenter.css +332 -178
- package/web/web-host.js +239 -137
package/out/tourCoverage.js
CHANGED
|
@@ -154,8 +154,8 @@ function listBranchCommits(repoRoot, mergeBaseOid, headOid, runGit2 = createGitR
|
|
|
154
154
|
"--format=%H%x00%h%x00%cI%x00%s%x00%P%x1e",
|
|
155
155
|
`${mergeBaseOid}..${headOid}`
|
|
156
156
|
], repoRoot);
|
|
157
|
-
return output.split("").map((
|
|
158
|
-
const [oid = "", shortOid = "", timestamp = "", summary = "", parents = ""] =
|
|
157
|
+
return output.split("").map((record2) => record2.replace(/^\n+|\n+$/g, "")).filter(Boolean).map((record2) => {
|
|
158
|
+
const [oid = "", shortOid = "", timestamp = "", summary = "", parents = ""] = record2.split("\0");
|
|
159
159
|
return {
|
|
160
160
|
oid,
|
|
161
161
|
shortOid,
|
|
@@ -471,21 +471,150 @@ function runGit(repoRoot, args) {
|
|
|
471
471
|
});
|
|
472
472
|
}
|
|
473
473
|
|
|
474
|
+
// src/tourImage.ts
|
|
475
|
+
var MAX_TOUR_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
476
|
+
function record(value) {
|
|
477
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
478
|
+
}
|
|
479
|
+
function validPath(value) {
|
|
480
|
+
return typeof value === "string" && value.length > 0 && !/[\\\x00-\x1f:]/.test(value) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
481
|
+
}
|
|
482
|
+
function validateTourImageSource(value, label) {
|
|
483
|
+
if (value === void 0) return;
|
|
484
|
+
if (!record(value) || Object.keys(value).some((key) => !["file", "revision", "alt"].includes(key)) || !validPath(value.file) || !["base", "head"].includes(String(value.revision)) || typeof value.alt !== "string" || !value.alt.trim()) {
|
|
485
|
+
throw new Error(`${label} must contain a repository-relative file, base/head revision, and non-empty alt text.`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function validateTourImage(value, label) {
|
|
489
|
+
if (value === void 0) return;
|
|
490
|
+
if (!record(value) || !validPath(value.path) || typeof value.alt !== "string" || !value.alt.trim() || typeof value.revision !== "string" || !/^[a-f0-9]{40,64}$/.test(value.revision) || typeof value.dataUrl !== "string" || !value.dataUrl.startsWith("data:image/png;base64,")) {
|
|
491
|
+
throw new Error(`${label} must contain pinned, embedded PNG evidence and alt text.`);
|
|
492
|
+
}
|
|
493
|
+
const base64 = value.dataUrl.slice(22);
|
|
494
|
+
const decodedBytes = base64.length / 4 * 3 - (base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0);
|
|
495
|
+
if (decodedBytes > MAX_TOUR_IMAGE_BYTES || base64.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(base64)) throw new Error(`${label} must be a PNG of at most 8 MiB.`);
|
|
496
|
+
const header = Uint8Array.from(atob(base64.slice(0, 44)), (character) => character.charCodeAt(0));
|
|
497
|
+
validatePngHeader(header, label);
|
|
498
|
+
}
|
|
499
|
+
function validatePngHeader(bytes, label) {
|
|
500
|
+
const signature = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82];
|
|
501
|
+
if (bytes.length < 33 || signature.some((byte, index) => bytes[index] !== byte)) {
|
|
502
|
+
throw new Error(`${label} must be a PNG with an IHDR header.`);
|
|
503
|
+
}
|
|
504
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
505
|
+
const width = view.getUint32(16), height = view.getUint32(20);
|
|
506
|
+
if (!width || !height || width > 16384 || height > 16384 || width * height > 64 * 1024 * 1024) {
|
|
507
|
+
throw new Error(`${label} exceeds the PNG dimension limit.`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
474
511
|
// src/changeTour.ts
|
|
475
512
|
var import_child_process3 = require("child_process");
|
|
476
513
|
|
|
514
|
+
// src/tourFraming.ts
|
|
515
|
+
function validateTourFraming(value, version, path) {
|
|
516
|
+
for (const key of ["opening", "conclusion"]) {
|
|
517
|
+
const passage = value[key];
|
|
518
|
+
if (passage === void 0) continue;
|
|
519
|
+
if (Number(version) < 5) throw new Error(`${path}.${key} requires version 5.`);
|
|
520
|
+
if (!passage || typeof passage !== "object" || Array.isArray(passage)) {
|
|
521
|
+
throw new Error(`${path}.${key} must be an object.`);
|
|
522
|
+
}
|
|
523
|
+
const fields = passage;
|
|
524
|
+
for (const field of Object.keys(fields)) {
|
|
525
|
+
if (!["title", "summary", "bullets"].includes(field)) throw new Error(`${path}.${key}.${field} is unsupported.`);
|
|
526
|
+
}
|
|
527
|
+
for (const field of ["title", "summary"]) requireProse(fields[field], `${path}.${key}.${field}`);
|
|
528
|
+
if (fields.bullets !== void 0) {
|
|
529
|
+
if (!Array.isArray(fields.bullets)) throw new Error(`${path}.${key}.bullets must be an array.`);
|
|
530
|
+
fields.bullets.forEach((bullet, index) => requireProse(bullet, `${path}.${key}.bullets[${index}]`));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function validateOverviewPurpose(value, version, path) {
|
|
535
|
+
if (value === void 0) return;
|
|
536
|
+
if (version < 5) {
|
|
537
|
+
if (value && typeof value === "object" && "purpose" in value) throw new Error(`${path}.purpose requires version 5.`);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
requireProse(value == null ? void 0 : value.purpose, `${path}.purpose`);
|
|
541
|
+
}
|
|
542
|
+
function requireProse(value, path) {
|
|
543
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${path} must be a non-empty string.`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/tourDetails.ts
|
|
547
|
+
function validateChangeTourDetails(value, path) {
|
|
548
|
+
if (value === void 0) return;
|
|
549
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
550
|
+
requireOnlyKeys(value, ["audience"], path);
|
|
551
|
+
validateAudience(value.audience, `${path}.audience`);
|
|
552
|
+
}
|
|
553
|
+
function validateAudience(value, path) {
|
|
554
|
+
if (value === void 0) return;
|
|
555
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
556
|
+
requireOnlyKeys(value, [
|
|
557
|
+
"codebase",
|
|
558
|
+
"language",
|
|
559
|
+
"domain",
|
|
560
|
+
"readingDemand",
|
|
561
|
+
"purpose",
|
|
562
|
+
"scope",
|
|
563
|
+
"estimatedMinutes"
|
|
564
|
+
], path);
|
|
565
|
+
if (typeof value.codebase !== "string" || !["none", "some", "maintainer"].includes(value.codebase)) {
|
|
566
|
+
throw new Error(`${path}.codebase must be none, some, or maintainer.`);
|
|
567
|
+
}
|
|
568
|
+
if (typeof value.language !== "string" || !["none", "basic", "fluent", "advanced"].includes(value.language)) {
|
|
569
|
+
throw new Error(`${path}.language must be none, basic, fluent, or advanced.`);
|
|
570
|
+
}
|
|
571
|
+
if (value.domain !== void 0) requireStringArray(value.domain, `${path}.domain`);
|
|
572
|
+
if (typeof value.readingDemand !== "string" || !["general", "technical", "specialist"].includes(value.readingDemand)) {
|
|
573
|
+
throw new Error(`${path}.readingDemand must be general, technical, or specialist.`);
|
|
574
|
+
}
|
|
575
|
+
if (typeof value.purpose !== "string" || !["overview", "explanation", "review"].includes(value.purpose)) {
|
|
576
|
+
throw new Error(`${path}.purpose must be overview, explanation, or review.`);
|
|
577
|
+
}
|
|
578
|
+
requireString(value.scope, `${path}.scope`);
|
|
579
|
+
if (value.estimatedMinutes !== void 0 && (typeof value.estimatedMinutes !== "number" || !Number.isFinite(value.estimatedMinutes) || value.estimatedMinutes <= 0)) {
|
|
580
|
+
throw new Error(`${path}.estimatedMinutes must be a finite positive number.`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function isRecord(value) {
|
|
584
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
585
|
+
}
|
|
586
|
+
function requireString(value, path) {
|
|
587
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
588
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function requireStringArray(value, path) {
|
|
592
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) {
|
|
593
|
+
throw new Error(`${path} must be an array of strings.`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function requireOnlyKeys(value, allowed, path) {
|
|
597
|
+
const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
598
|
+
if (unexpected.length > 0) {
|
|
599
|
+
throw new Error(`${path} contains unknown field${unexpected.length === 1 ? "" : "s"}: ${unexpected.join(", ")}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
477
603
|
// src/changeTourSource.ts
|
|
478
|
-
var CHANGE_TOUR_SOURCE_VERSION =
|
|
604
|
+
var CHANGE_TOUR_SOURCE_VERSION = 5;
|
|
479
605
|
function parseChangeTourSource(value) {
|
|
480
|
-
if (
|
|
606
|
+
if (isRecord2(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_SOURCE_VERSION) {
|
|
481
607
|
throw new Error(
|
|
482
608
|
`This tour uses source format version ${value.version}, but this version of Bygone supports up to version ${CHANGE_TOUR_SOURCE_VERSION}. Upgrade Bygone to open it.`
|
|
483
609
|
);
|
|
484
610
|
}
|
|
485
|
-
if (
|
|
611
|
+
if (isRecord2(value) && value.version === 3) {
|
|
612
|
+
throw new Error("Tour source format version 3 is retired. Remove the review block, set source version to 4, and recompile the tour.");
|
|
613
|
+
}
|
|
614
|
+
if (!isRecord2(value) || value.version !== 1 && value.version !== 2 && value.version !== 4 && value.version !== CHANGE_TOUR_SOURCE_VERSION) {
|
|
486
615
|
throw new Error("Unsupported or missing change-tour source version.");
|
|
487
616
|
}
|
|
488
|
-
|
|
617
|
+
requireOnlyKeys2(value, [
|
|
489
618
|
"version",
|
|
490
619
|
"title",
|
|
491
620
|
"windowTitle",
|
|
@@ -496,62 +625,65 @@ function parseChangeTourSource(value) {
|
|
|
496
625
|
"chapters",
|
|
497
626
|
"tours",
|
|
498
627
|
"coverage",
|
|
499
|
-
"
|
|
628
|
+
"opening",
|
|
629
|
+
"conclusion",
|
|
630
|
+
"details"
|
|
500
631
|
], "source");
|
|
632
|
+
validateTourFraming(value, value.version, "source");
|
|
501
633
|
optionalString(value.title, "title");
|
|
502
634
|
optionalString(value.windowTitle, "windowTitle");
|
|
503
635
|
optionalString(value.sourceUrl, "sourceUrl");
|
|
504
636
|
if (value.range !== void 0) {
|
|
505
|
-
if (!
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
637
|
+
if (!isRecord2(value.range)) throw new Error("range must be an object.");
|
|
638
|
+
requireOnlyKeys2(value.range, ["base", "head"], "range");
|
|
639
|
+
requireString2(value.range.base, "range.base");
|
|
640
|
+
requireString2(value.range.head, "range.head");
|
|
509
641
|
}
|
|
510
642
|
if (value.coverage !== void 0) {
|
|
511
|
-
if (!
|
|
643
|
+
if (!isRecord2(value.coverage) || !Array.isArray(value.coverage.exclusions)) {
|
|
512
644
|
throw new Error("coverage must contain an exclusions array.");
|
|
513
645
|
}
|
|
514
|
-
|
|
646
|
+
requireOnlyKeys2(value.coverage, ["exclusions"], "coverage");
|
|
515
647
|
for (const [index, exclusion] of value.coverage.exclusions.entries()) {
|
|
516
648
|
const exclusionPath = `coverage.exclusions[${index}]`;
|
|
517
|
-
if (!
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if (exclusion.hunks !== void 0)
|
|
649
|
+
if (!isRecord2(exclusion)) throw new Error(`${exclusionPath} must be an object.`);
|
|
650
|
+
requireOnlyKeys2(exclusion, ["path", "hunks", "reason"], exclusionPath);
|
|
651
|
+
requireString2(exclusion.path, `${exclusionPath}.path`);
|
|
652
|
+
requireString2(exclusion.reason, `${exclusionPath}.reason`);
|
|
653
|
+
if (exclusion.hunks !== void 0) requireStringArray2(exclusion.hunks, `${exclusionPath}.hunks`);
|
|
522
654
|
}
|
|
523
655
|
}
|
|
524
|
-
if (!
|
|
656
|
+
if (!isRecord2(value.anchors)) {
|
|
525
657
|
throw new Error("anchors must be an object.");
|
|
526
658
|
}
|
|
527
659
|
for (const [id, anchor] of Object.entries(value.anchors)) {
|
|
528
|
-
if (!
|
|
660
|
+
if (!isRecord2(anchor)) {
|
|
529
661
|
throw new Error(`anchors.${id} must be an object.`);
|
|
530
662
|
}
|
|
531
|
-
|
|
532
|
-
|
|
663
|
+
requireOnlyKeys2(anchor, ["file", "revision", "contains", "occurrence"], `anchors.${id}`);
|
|
664
|
+
requireString2(anchor.file, `anchors.${id}.file`);
|
|
533
665
|
if (anchor.revision !== "base" && anchor.revision !== "head") {
|
|
534
666
|
throw new Error(`anchors.${id}.revision must be base or head.`);
|
|
535
667
|
}
|
|
536
|
-
|
|
668
|
+
requireString2(anchor.contains, `anchors.${id}.contains`);
|
|
537
669
|
if (anchor.occurrence !== void 0 && (!Number.isInteger(anchor.occurrence) || Number(anchor.occurrence) < 1)) {
|
|
538
670
|
throw new Error(`anchors.${id}.occurrence must be a positive integer.`);
|
|
539
671
|
}
|
|
540
672
|
}
|
|
541
|
-
const rawConnections = Array.isArray(value.connections) ? value.connections :
|
|
673
|
+
const rawConnections = Array.isArray(value.connections) ? value.connections : isRecord2(value.connections) ? Object.entries(value.connections).map(([id, connection]) => isRecord2(connection) ? { id, ...connection } : connection) : null;
|
|
542
674
|
if (!rawConnections || !Array.isArray(value.chapters) || value.chapters.length === 0) {
|
|
543
675
|
throw new Error("connections must be an array or object, and chapters must be a non-empty array.");
|
|
544
676
|
}
|
|
545
677
|
const connectionIds = /* @__PURE__ */ new Set();
|
|
546
678
|
for (const [index, connection] of rawConnections.entries()) {
|
|
547
|
-
if (!
|
|
679
|
+
if (!isRecord2(connection)) {
|
|
548
680
|
throw new Error(`connections[${index}] must be an object.`);
|
|
549
681
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
682
|
+
requireOnlyKeys2(connection, ["id", "from", "to", "label"], `connections[${index}]`);
|
|
683
|
+
requireString2(connection.id, `connections[${index}].id`);
|
|
684
|
+
requireString2(connection.from, `connections[${index}].from`);
|
|
685
|
+
requireString2(connection.to, `connections[${index}].to`);
|
|
686
|
+
requireString2(connection.label, `connections[${index}].label`);
|
|
555
687
|
const { id, from, to } = connection;
|
|
556
688
|
if (!value.anchors[from] || !value.anchors[to]) {
|
|
557
689
|
throw new Error(`Connection ${id} references an unknown anchor.`);
|
|
@@ -561,7 +693,7 @@ function parseChangeTourSource(value) {
|
|
|
561
693
|
}
|
|
562
694
|
connectionIds.add(id);
|
|
563
695
|
}
|
|
564
|
-
|
|
696
|
+
validateSourceChapters(
|
|
565
697
|
value.chapters,
|
|
566
698
|
"chapters",
|
|
567
699
|
value.version,
|
|
@@ -569,10 +701,14 @@ function parseChangeTourSource(value) {
|
|
|
569
701
|
connectionIds,
|
|
570
702
|
"root"
|
|
571
703
|
);
|
|
704
|
+
if (value.details !== void 0) {
|
|
705
|
+
if (value.version < 5) throw new Error("source.details requires version 5.");
|
|
706
|
+
validateChangeTourDetails(value.details, "details");
|
|
707
|
+
}
|
|
572
708
|
if (value.tours !== void 0) {
|
|
573
|
-
if (value.version
|
|
574
|
-
if (!
|
|
575
|
-
|
|
709
|
+
if (value.version < 4) throw new Error("Independent tours require version 4.");
|
|
710
|
+
if (!isRecord2(value.tours)) throw new Error("tours must be an object.");
|
|
711
|
+
requireOnlyKeys2(value.tours, ["historical", "deconstructed"], "tours");
|
|
576
712
|
if (value.tours.historical === void 0 && value.tours.deconstructed === void 0) {
|
|
577
713
|
throw new Error("tours must contain a historical or deconstructed tour.");
|
|
578
714
|
}
|
|
@@ -580,15 +716,16 @@ function parseChangeTourSource(value) {
|
|
|
580
716
|
const tour = value.tours[mode];
|
|
581
717
|
if (tour === void 0) continue;
|
|
582
718
|
const path = `tours.${mode}`;
|
|
583
|
-
if (!
|
|
584
|
-
|
|
719
|
+
if (!isRecord2(tour)) throw new Error(`${path} must be an object.`);
|
|
720
|
+
requireOnlyKeys2(tour, ["chapters", "opening", "conclusion", "details"], path);
|
|
721
|
+
validateTourFraming(tour, value.version, path);
|
|
585
722
|
validateSourceChapters(tour.chapters, `${path}.chapters`, value.version, value.anchors, connectionIds, mode);
|
|
723
|
+
if (tour.details !== void 0) {
|
|
724
|
+
if (value.version < 5) throw new Error(`${path}.details requires source version 5.`);
|
|
725
|
+
validateChangeTourDetails(tour.details, `${path}.details`);
|
|
726
|
+
}
|
|
586
727
|
}
|
|
587
728
|
}
|
|
588
|
-
if (value.review !== void 0) {
|
|
589
|
-
if (value.version !== 3) throw new Error("Review notes require version 3.");
|
|
590
|
-
validateChangeTourReview(value.review, { authoredWalkthroughSteps });
|
|
591
|
-
}
|
|
592
729
|
return { ...value, connections: rawConnections };
|
|
593
730
|
}
|
|
594
731
|
function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, connectionIds, mode) {
|
|
@@ -597,27 +734,31 @@ function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, c
|
|
|
597
734
|
}
|
|
598
735
|
const chapterIds = /* @__PURE__ */ new Set();
|
|
599
736
|
const sceneIds = /* @__PURE__ */ new Set();
|
|
600
|
-
const authoredWalkthroughSteps = /* @__PURE__ */ new Map();
|
|
601
737
|
for (const [chapterIndex, chapter] of chaptersValue.entries()) {
|
|
602
738
|
const chapterPath = `${chaptersPath}[${chapterIndex}]`;
|
|
603
|
-
if (!
|
|
739
|
+
if (!isRecord2(chapter) || !Array.isArray(chapter.scenes) || chapter.scenes.length === 0) {
|
|
604
740
|
throw new Error(`${chapterPath} must contain a non-empty scenes array.`);
|
|
605
741
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
742
|
+
requireString2(chapter.id, `${chapterPath}.id`);
|
|
743
|
+
requireString2(chapter.title, `${chapterPath}.title`);
|
|
744
|
+
if (chapter.summary !== void 0) {
|
|
745
|
+
if (version < 5) throw new Error(`${chapterPath}.summary requires source version 5.`);
|
|
746
|
+
requireProse2(chapter.summary, `${chapterPath}.summary`);
|
|
747
|
+
}
|
|
748
|
+
requireOnlyKeys2(chapter, ["id", "title", "summary", "scenes"], chapterPath);
|
|
609
749
|
if (chapterIds.has(chapter.id)) throw new Error(`Duplicate chapter id: ${chapter.id}`);
|
|
610
750
|
chapterIds.add(chapter.id);
|
|
611
751
|
for (const [sceneIndex, scene] of chapter.scenes.entries()) {
|
|
612
752
|
const path = `${chapterPath}.scenes[${sceneIndex}]`;
|
|
613
|
-
if (!
|
|
614
|
-
|
|
615
|
-
|
|
753
|
+
if (!isRecord2(scene)) throw new Error(`${path} must be an object.`);
|
|
754
|
+
requireString2(scene.id, `${path}.id`);
|
|
755
|
+
requireString2(scene.title, `${path}.title`);
|
|
616
756
|
if (sceneIds.has(scene.id)) throw new Error(`Duplicate scene id: ${scene.id}`);
|
|
617
757
|
sceneIds.add(scene.id);
|
|
618
|
-
if (scene.overview !== void 0 && version
|
|
619
|
-
throw new Error(`${path}.overview requires version
|
|
758
|
+
if (scene.overview !== void 0 && version < 4) {
|
|
759
|
+
throw new Error(`${path}.overview requires version 4.`);
|
|
620
760
|
}
|
|
761
|
+
validateOverviewPurpose(scene.overview, version, `${path}.overview`);
|
|
621
762
|
validateNarrative(scene, path);
|
|
622
763
|
if (scene.kind === "deconstructed-diff") {
|
|
623
764
|
if (mode === "historical") {
|
|
@@ -633,9 +774,6 @@ function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, c
|
|
|
633
774
|
}
|
|
634
775
|
if (requireRealEvidence || scene.steps !== void 0) {
|
|
635
776
|
validateWalkthroughSteps(scene, path, anchors, connectionIds);
|
|
636
|
-
if (scene.steps !== void 0) {
|
|
637
|
-
authoredWalkthroughSteps.set(scene.id, collectStepIds(scene.steps));
|
|
638
|
-
}
|
|
639
777
|
}
|
|
640
778
|
continue;
|
|
641
779
|
}
|
|
@@ -652,22 +790,20 @@ function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, c
|
|
|
652
790
|
if (scene.kind !== void 0 && scene.kind !== "walkthrough") {
|
|
653
791
|
throw new Error(`${path}.kind must be walkthrough, stacked-diff, or deconstructed-diff.`);
|
|
654
792
|
}
|
|
655
|
-
|
|
793
|
+
requireOnlyKeys2(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "steps"], path);
|
|
656
794
|
validateSceneOverview(scene.overview, `${path}.overview`, { allowComparison: false });
|
|
657
795
|
validateWalkthroughSteps(scene, path, anchors, connectionIds);
|
|
658
|
-
authoredWalkthroughSteps.set(scene.id, collectStepIds(scene.steps));
|
|
659
796
|
}
|
|
660
797
|
}
|
|
661
|
-
if (mode === "deconstructed" && !chaptersValue.some((chapter) =>
|
|
798
|
+
if (mode === "deconstructed" && !chaptersValue.some((chapter) => isRecord2(chapter) && Array.isArray(chapter.scenes) && chapter.scenes.some((scene) => isRecord2(scene) && scene.kind === "deconstructed-diff"))) {
|
|
662
799
|
throw new Error(`${chaptersPath} must contain at least one deconstructed-diff scene.`);
|
|
663
800
|
}
|
|
664
|
-
return authoredWalkthroughSteps;
|
|
665
801
|
}
|
|
666
802
|
function stackedOverviewEndpointIds(scene, path) {
|
|
667
803
|
if (!Array.isArray(scene.stack)) {
|
|
668
804
|
throw new Error(`${path}.stack must be an array.`);
|
|
669
805
|
}
|
|
670
|
-
return scene.stack.filter(
|
|
806
|
+
return scene.stack.filter(isRecord2).map((entry) => typeof entry.id === "string" ? entry.id : "").filter((id) => id.length > 0);
|
|
671
807
|
}
|
|
672
808
|
function deconstructedOverviewEndpointIds(scene, path) {
|
|
673
809
|
if (!Array.isArray(scene.stages)) {
|
|
@@ -675,78 +811,11 @@ function deconstructedOverviewEndpointIds(scene, path) {
|
|
|
675
811
|
}
|
|
676
812
|
return [
|
|
677
813
|
"explanation-baseline",
|
|
678
|
-
...scene.stages.filter(
|
|
814
|
+
...scene.stages.filter(isRecord2).map((stage) => typeof stage.id === "string" ? `explanation-stage-${stage.id}` : "").filter((id) => id.length > 0)
|
|
679
815
|
];
|
|
680
816
|
}
|
|
681
|
-
function validateChangeTourReview(value, context = {}) {
|
|
682
|
-
if (!isRecord(value)) {
|
|
683
|
-
throw new Error("review must be an object.");
|
|
684
|
-
}
|
|
685
|
-
requireOnlyKeys(value, ["baseOid", "headOid", "items"], "review");
|
|
686
|
-
requireFullGitOid(value.baseOid, "review.baseOid");
|
|
687
|
-
requireFullGitOid(value.headOid, "review.headOid");
|
|
688
|
-
if (!Array.isArray(value.items) || value.items.length === 0) {
|
|
689
|
-
throw new Error("review.items must be a non-empty array.");
|
|
690
|
-
}
|
|
691
|
-
if (context.expectedRange) {
|
|
692
|
-
if (normalizeGitOid(value.baseOid) !== normalizeGitOid(context.expectedRange.baseOid)) {
|
|
693
|
-
throw new Error(
|
|
694
|
-
`Stale review: review.baseOid ${value.baseOid} does not match the resolved range.mergeBaseOid ${context.expectedRange.baseOid}.`
|
|
695
|
-
);
|
|
696
|
-
}
|
|
697
|
-
if (normalizeGitOid(value.headOid) !== normalizeGitOid(context.expectedRange.headOid)) {
|
|
698
|
-
throw new Error(
|
|
699
|
-
`Stale review: review.headOid ${value.headOid} does not match the resolved range.headOid ${context.expectedRange.headOid}.`
|
|
700
|
-
);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
const itemIds = /* @__PURE__ */ new Set();
|
|
704
|
-
for (const [index, item] of value.items.entries()) {
|
|
705
|
-
const itemPath = `review.items[${index}]`;
|
|
706
|
-
if (!isRecord(item)) throw new Error(`${itemPath} must be an object.`);
|
|
707
|
-
requireOnlyKeys(item, ["id", "kind", "title", "body", "evidence", "nextCheck"], itemPath);
|
|
708
|
-
requireReviewString(item.id, `${itemPath}.id`);
|
|
709
|
-
if (itemIds.has(item.id)) throw new Error(`Duplicate review item id: ${item.id}`);
|
|
710
|
-
itemIds.add(item.id);
|
|
711
|
-
if (typeof item.kind !== "string" || !["concept", "boundary", "tradeoff", "question"].includes(item.kind)) {
|
|
712
|
-
throw new Error(`${itemPath}.kind must be concept, boundary, tradeoff, or question.`);
|
|
713
|
-
}
|
|
714
|
-
requireReviewString(item.title, `${itemPath}.title`);
|
|
715
|
-
requireReviewString(item.body, `${itemPath}.body`);
|
|
716
|
-
if (!Array.isArray(item.evidence)) {
|
|
717
|
-
throw new Error(`${itemPath}.evidence must be an array.`);
|
|
718
|
-
}
|
|
719
|
-
if (item.kind !== "question" && item.evidence.length === 0) {
|
|
720
|
-
throw new Error(`${itemPath}.evidence must contain at least one reference for claims.`);
|
|
721
|
-
}
|
|
722
|
-
if (item.kind === "question" && item.nextCheck === void 0) {
|
|
723
|
-
throw new Error(`${itemPath}.nextCheck must be a non-empty string for question items.`);
|
|
724
|
-
}
|
|
725
|
-
if (item.nextCheck !== void 0) requireReviewString(item.nextCheck, `${itemPath}.nextCheck`);
|
|
726
|
-
for (const [evidenceIndex, evidence] of item.evidence.entries()) {
|
|
727
|
-
const evidencePath = `${itemPath}.evidence[${evidenceIndex}]`;
|
|
728
|
-
if (!isRecord(evidence)) throw new Error(`${evidencePath} must be an object.`);
|
|
729
|
-
requireOnlyKeys(evidence, ["sceneId", "stepId"], evidencePath);
|
|
730
|
-
requireReviewString(evidence.sceneId, `${evidencePath}.sceneId`);
|
|
731
|
-
requireReviewString(evidence.stepId, `${evidencePath}.stepId`);
|
|
732
|
-
if (context.authoredWalkthroughSteps) {
|
|
733
|
-
const stepIds = context.authoredWalkthroughSteps.get(evidence.sceneId);
|
|
734
|
-
if (!stepIds) {
|
|
735
|
-
throw new Error(
|
|
736
|
-
`${evidencePath} references unknown authored walkthrough scene ${evidence.sceneId}.`
|
|
737
|
-
);
|
|
738
|
-
}
|
|
739
|
-
if (!stepIds.has(evidence.stepId)) {
|
|
740
|
-
throw new Error(
|
|
741
|
-
`${evidencePath} references unknown authored walkthrough step ${evidence.sceneId}/${evidence.stepId}.`
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
817
|
function validateDeconstructedScene(scene, path) {
|
|
749
|
-
|
|
818
|
+
requireOnlyKeys2(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "base", "target", "stack", "steps", "stages", "exclusions"], path);
|
|
750
819
|
optionalString(scene.base, `${path}.base`);
|
|
751
820
|
optionalString(scene.target, `${path}.target`);
|
|
752
821
|
if (!Array.isArray(scene.stages) || scene.stages.length === 0 || scene.stages.length > 12) {
|
|
@@ -755,11 +824,11 @@ function validateDeconstructedScene(scene, path) {
|
|
|
755
824
|
const stageIds = /* @__PURE__ */ new Set();
|
|
756
825
|
for (const [stageIndex, stage] of scene.stages.entries()) {
|
|
757
826
|
const stagePath = `${path}.stages[${stageIndex}]`;
|
|
758
|
-
if (!
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
827
|
+
if (!isRecord2(stage)) throw new Error(`${stagePath} must be an object.`);
|
|
828
|
+
requireOnlyKeys2(stage, ["id", "title", "narration", "changes"], stagePath);
|
|
829
|
+
requireString2(stage.id, `${stagePath}.id`);
|
|
830
|
+
requireString2(stage.title, `${stagePath}.title`);
|
|
831
|
+
requireString2(stage.narration, `${stagePath}.narration`);
|
|
763
832
|
if (!Array.isArray(stage.changes) || stage.changes.length === 0) {
|
|
764
833
|
throw new Error(`${stagePath}.changes must be a non-empty array.`);
|
|
765
834
|
}
|
|
@@ -787,16 +856,17 @@ function validateWalkthroughSteps(scene, path, anchors, connectionIds) {
|
|
|
787
856
|
const stepIds = /* @__PURE__ */ new Set();
|
|
788
857
|
for (const [stepIndex, step] of scene.steps.entries()) {
|
|
789
858
|
const stepPath = `${path}.steps[${stepIndex}]`;
|
|
790
|
-
if (!
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
859
|
+
if (!isRecord2(step)) throw new Error(`${stepPath} must be an object.`);
|
|
860
|
+
requireString2(step.id, `${stepPath}.id`);
|
|
861
|
+
requireString2(step.title, `${stepPath}.title`);
|
|
862
|
+
requireString2(step.body, `${stepPath}.body`);
|
|
863
|
+
requireString2(step.focus, `${stepPath}.focus`);
|
|
864
|
+
requireOnlyKeys2(step, ["id", "title", "body", "focus", "connection", "depth", "requirement", "image"], stepPath);
|
|
796
865
|
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
797
866
|
throw new Error(`${stepPath}.depth must be mentioned, explained, or contextualized.`);
|
|
798
867
|
}
|
|
799
868
|
validateStepRequirement(step.requirement, `${stepPath}.requirement`);
|
|
869
|
+
validateTourImageSource(step.image, `${stepPath}.image`);
|
|
800
870
|
if (stepIds.has(step.id)) throw new Error(`Duplicate step id in scene ${scene.id}: ${step.id}`);
|
|
801
871
|
stepIds.add(step.id);
|
|
802
872
|
if (!anchors[step.focus]) throw new Error(`${stepPath} references unknown anchor ${step.focus}.`);
|
|
@@ -807,14 +877,14 @@ function validateWalkthroughSteps(scene, path, anchors, connectionIds) {
|
|
|
807
877
|
}
|
|
808
878
|
}
|
|
809
879
|
function validateDeconstructedSelection(value, path, requiresReason) {
|
|
810
|
-
if (!
|
|
811
|
-
|
|
812
|
-
|
|
880
|
+
if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
|
|
881
|
+
requireOnlyKeys2(value, requiresReason ? ["file", "hunks", "reason"] : ["file", "hunks"], path);
|
|
882
|
+
requireString2(value.file, `${path}.file`);
|
|
813
883
|
if (!requiresReason || value.hunks !== void 0) {
|
|
814
|
-
|
|
884
|
+
requireStringArray2(value.hunks, `${path}.hunks`);
|
|
815
885
|
if (value.hunks.length === 0) throw new Error(`${path}.hunks must not be empty.`);
|
|
816
886
|
}
|
|
817
|
-
if (requiresReason)
|
|
887
|
+
if (requiresReason) requireString2(value.reason, `${path}.reason`);
|
|
818
888
|
}
|
|
819
889
|
function validateRealStack(stack, path, minimum) {
|
|
820
890
|
if (!Array.isArray(stack) || stack.length < minimum || stack.length > 6) {
|
|
@@ -822,42 +892,42 @@ function validateRealStack(stack, path, minimum) {
|
|
|
822
892
|
}
|
|
823
893
|
const ids = /* @__PURE__ */ new Set();
|
|
824
894
|
for (const [index, entry] of stack.entries()) {
|
|
825
|
-
if (!
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
895
|
+
if (!isRecord2(entry)) throw new Error(`${path}[${index}] must be an object.`);
|
|
896
|
+
requireOnlyKeys2(entry, ["id", "ref", "label"], `${path}[${index}]`);
|
|
897
|
+
requireString2(entry.id, `${path}[${index}].id`);
|
|
898
|
+
requireString2(entry.ref, `${path}[${index}].ref`);
|
|
829
899
|
optionalString(entry.label, `${path}[${index}].label`);
|
|
830
900
|
if (ids.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
831
901
|
ids.add(entry.id);
|
|
832
902
|
}
|
|
833
903
|
}
|
|
834
904
|
function validateStackedScene(scene, path, minimum = 3) {
|
|
835
|
-
|
|
905
|
+
requireOnlyKeys2(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "stack", "files", "steps"], path);
|
|
836
906
|
if (!Array.isArray(scene.stack) || scene.stack.length < minimum || scene.stack.length > 6) {
|
|
837
907
|
throw new Error(`${path}.stack must contain between ${minimum} and 6 revisions.`);
|
|
838
908
|
}
|
|
839
909
|
const stackIds = /* @__PURE__ */ new Set();
|
|
840
910
|
for (const [index, entry] of scene.stack.entries()) {
|
|
841
911
|
const entryPath = `${path}.stack[${index}]`;
|
|
842
|
-
if (!
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
912
|
+
if (!isRecord2(entry)) throw new Error(`${entryPath} must be an object.`);
|
|
913
|
+
requireOnlyKeys2(entry, ["id", "ref", "label"], entryPath);
|
|
914
|
+
requireString2(entry.id, `${entryPath}.id`);
|
|
915
|
+
requireString2(entry.ref, `${entryPath}.ref`);
|
|
846
916
|
optionalString(entry.label, `${entryPath}.label`);
|
|
847
917
|
if (stackIds.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
848
918
|
stackIds.add(entry.id);
|
|
849
919
|
}
|
|
850
|
-
if (scene.files !== void 0)
|
|
920
|
+
if (scene.files !== void 0) requireStringArray2(scene.files, `${path}.files`);
|
|
851
921
|
if (!Array.isArray(scene.steps) || scene.steps.length === 0) throw new Error(`${path}.steps must be a non-empty array.`);
|
|
852
922
|
const stepIds = /* @__PURE__ */ new Set();
|
|
853
923
|
for (const [index, step] of scene.steps.entries()) {
|
|
854
924
|
const stepPath = `${path}.steps[${index}]`;
|
|
855
|
-
if (!
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
925
|
+
if (!isRecord2(step)) throw new Error(`${stepPath} must be an object.`);
|
|
926
|
+
requireOnlyKeys2(step, ["id", "title", "body", "file", "pair", "side", "lines"], stepPath);
|
|
927
|
+
requireString2(step.id, `${stepPath}.id`);
|
|
928
|
+
requireString2(step.title, `${stepPath}.title`);
|
|
929
|
+
requireString2(step.body, `${stepPath}.body`);
|
|
930
|
+
requireString2(step.file, `${stepPath}.file`);
|
|
861
931
|
const stepId = step.id;
|
|
862
932
|
if (stepIds.has(stepId)) throw new Error(`Duplicate step id in scene ${scene.id}: ${stepId}`);
|
|
863
933
|
stepIds.add(stepId);
|
|
@@ -865,8 +935,8 @@ function validateStackedScene(scene, path, minimum = 3) {
|
|
|
865
935
|
throw new Error(`${stepPath}.pair must contain two stack entry ids.`);
|
|
866
936
|
}
|
|
867
937
|
const pair = step.pair;
|
|
868
|
-
const leftIndex = scene.stack.findIndex((entry) =>
|
|
869
|
-
const rightIndex = scene.stack.findIndex((entry) =>
|
|
938
|
+
const leftIndex = scene.stack.findIndex((entry) => isRecord2(entry) && entry.id === pair[0]);
|
|
939
|
+
const rightIndex = scene.stack.findIndex((entry) => isRecord2(entry) && entry.id === pair[1]);
|
|
870
940
|
if (leftIndex < 0 || rightIndex !== leftIndex + 1) throw new Error(`${stepPath}.pair must reference adjacent stack entries in order.`);
|
|
871
941
|
if (step.side !== void 0 && step.side !== "left" && step.side !== "right") throw new Error(`${stepPath}.side must be left or right.`);
|
|
872
942
|
if (step.lines !== void 0 && (!Array.isArray(step.lines) || step.lines.length !== 2 || step.lines.some((line) => !Number.isInteger(line) || Number(line) < 1) || Number(step.lines[1]) < Number(step.lines[0]))) {
|
|
@@ -876,10 +946,10 @@ function validateStackedScene(scene, path, minimum = 3) {
|
|
|
876
946
|
}
|
|
877
947
|
function validateStepRequirement(value, path) {
|
|
878
948
|
if (value === void 0) return;
|
|
879
|
-
if (!
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
949
|
+
if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
|
|
950
|
+
requireOnlyKeys2(value, ["id", "text", "status", "source", "confidence"], path);
|
|
951
|
+
requireString2(value.id, `${path}.id`);
|
|
952
|
+
requireString2(value.text, `${path}.text`);
|
|
883
953
|
if (value.status !== void 0 && !["fulfilled", "gap"].includes(String(value.status))) {
|
|
884
954
|
throw new Error(`${path}.status must be fulfilled or gap.`);
|
|
885
955
|
}
|
|
@@ -890,20 +960,21 @@ function validateStepRequirement(value, path) {
|
|
|
890
960
|
}
|
|
891
961
|
function validateSceneOverview(value, path, options = {}) {
|
|
892
962
|
if (value === void 0) return;
|
|
893
|
-
if (!
|
|
894
|
-
|
|
963
|
+
if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
|
|
964
|
+
requireOnlyKeys2(value, ["kind", "path", "comparison", "purpose"], path);
|
|
895
965
|
if (value.kind !== "directory-diff") {
|
|
896
966
|
throw new Error(`${path}.kind must be directory-diff.`);
|
|
897
967
|
}
|
|
968
|
+
if (value.purpose !== void 0) requireString2(value.purpose, `${path}.purpose`);
|
|
898
969
|
if (value.path !== void 0) validateDirectoryOverviewPath(value.path, `${path}.path`);
|
|
899
970
|
if (value.comparison === void 0) return;
|
|
900
971
|
if (options.allowComparison === false) {
|
|
901
972
|
throw new Error(`${path}.comparison is not supported for walkthrough scenes; use the fixed base-to-head comparison.`);
|
|
902
973
|
}
|
|
903
|
-
if (!
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
974
|
+
if (!isRecord2(value.comparison)) throw new Error(`${path}.comparison must be an object.`);
|
|
975
|
+
requireOnlyKeys2(value.comparison, ["from", "to"], `${path}.comparison`);
|
|
976
|
+
requireString2(value.comparison.from, `${path}.comparison.from`);
|
|
977
|
+
requireString2(value.comparison.to, `${path}.comparison.to`);
|
|
907
978
|
if (value.comparison.from === value.comparison.to) {
|
|
908
979
|
throw new Error(`${path}.comparison.from and to must identify distinct endpoints.`);
|
|
909
980
|
}
|
|
@@ -916,7 +987,7 @@ function validateSceneOverview(value, path, options = {}) {
|
|
|
916
987
|
}
|
|
917
988
|
}
|
|
918
989
|
function validateDirectoryOverviewPath(value, path) {
|
|
919
|
-
|
|
990
|
+
requireString2(value, path);
|
|
920
991
|
if (value.includes("\\")) {
|
|
921
992
|
throw new Error(`${path} must use a relative POSIX directory path without backslashes.`);
|
|
922
993
|
}
|
|
@@ -927,53 +998,36 @@ function validateDirectoryOverviewPath(value, path) {
|
|
|
927
998
|
throw new Error(`${path} must not contain parent-directory traversal.`);
|
|
928
999
|
}
|
|
929
1000
|
}
|
|
930
|
-
function collectStepIds(value) {
|
|
931
|
-
const ids = /* @__PURE__ */ new Set();
|
|
932
|
-
if (!Array.isArray(value)) return ids;
|
|
933
|
-
for (const step of value) {
|
|
934
|
-
if (isRecord(step) && typeof step.id === "string") ids.add(step.id);
|
|
935
|
-
}
|
|
936
|
-
return ids;
|
|
937
|
-
}
|
|
938
1001
|
function validateNarrative(value, path) {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1002
|
+
requireString2(value.summary, `${path}.summary`);
|
|
1003
|
+
requireStringArray2(value.bullets, `${path}.bullets`);
|
|
1004
|
+
requireStringArray2(value.tags, `${path}.tags`);
|
|
1005
|
+
requireString2(value.takeaway, `${path}.takeaway`);
|
|
943
1006
|
}
|
|
944
|
-
function
|
|
1007
|
+
function isRecord2(value) {
|
|
945
1008
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
946
1009
|
}
|
|
947
|
-
function
|
|
1010
|
+
function requireString2(value, path) {
|
|
948
1011
|
if (typeof value !== "string" || value.length === 0) {
|
|
949
1012
|
throw new Error(`${path} must be a non-empty string.`);
|
|
950
1013
|
}
|
|
951
1014
|
}
|
|
952
|
-
function
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
throw new Error(`${path} must be a full 40- or 64-character hexadecimal Git OID.`);
|
|
1015
|
+
function optionalString(value, path) {
|
|
1016
|
+
if (value !== void 0) {
|
|
1017
|
+
requireString2(value, path);
|
|
956
1018
|
}
|
|
957
1019
|
}
|
|
958
|
-
function
|
|
959
|
-
if (typeof value !== "string" || value.trim()
|
|
1020
|
+
function requireProse2(value, path) {
|
|
1021
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
960
1022
|
throw new Error(`${path} must be a non-empty string.`);
|
|
961
1023
|
}
|
|
962
1024
|
}
|
|
963
|
-
function
|
|
964
|
-
return value.toLowerCase();
|
|
965
|
-
}
|
|
966
|
-
function optionalString(value, path) {
|
|
967
|
-
if (value !== void 0) {
|
|
968
|
-
requireString(value, path);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
function requireStringArray(value, path) {
|
|
1025
|
+
function requireStringArray2(value, path) {
|
|
972
1026
|
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
973
1027
|
throw new Error(`${path} must be an array of strings.`);
|
|
974
1028
|
}
|
|
975
1029
|
}
|
|
976
|
-
function
|
|
1030
|
+
function requireOnlyKeys2(value, allowed, path) {
|
|
977
1031
|
const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
978
1032
|
if (unexpected.length > 0) {
|
|
979
1033
|
throw new Error(`${path} contains unknown field${unexpected.length === 1 ? "" : "s"}: ${unexpected.join(", ")}`);
|
|
@@ -981,37 +1035,42 @@ function requireOnlyKeys(value, allowed, path) {
|
|
|
981
1035
|
}
|
|
982
1036
|
|
|
983
1037
|
// src/changeTourManifest.ts
|
|
984
|
-
var CHANGE_TOUR_MANIFEST_VERSION =
|
|
985
|
-
function parseChangeTourManifest(value) {
|
|
986
|
-
if (
|
|
1038
|
+
var CHANGE_TOUR_MANIFEST_VERSION = 5;
|
|
1039
|
+
function parseChangeTourManifest(value, options = {}) {
|
|
1040
|
+
if (isRecord3(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_MANIFEST_VERSION) {
|
|
987
1041
|
throw new Error(
|
|
988
1042
|
`This tour uses manifest format version ${value.version}, but this version of Bygone supports up to version ${CHANGE_TOUR_MANIFEST_VERSION}. Upgrade Bygone to open it.`
|
|
989
1043
|
);
|
|
990
1044
|
}
|
|
991
|
-
if (
|
|
1045
|
+
if (isRecord3(value) && value.version === 3) {
|
|
1046
|
+
throw new Error("Tour manifest format version 3 is retired. Remove the review block, set source version to 4, and recompile the tour.");
|
|
1047
|
+
}
|
|
1048
|
+
if (!isRecord3(value) || value.version !== 1 && value.version !== 2 && value.version !== 4 && value.version !== CHANGE_TOUR_MANIFEST_VERSION) {
|
|
992
1049
|
throw new Error(`Unsupported or missing change-tour manifest version.`);
|
|
993
1050
|
}
|
|
994
|
-
|
|
995
|
-
|
|
1051
|
+
if ("review" in value) throw new Error("Change-tour manifests do not support the review field. Put observations in scene and step narrative.");
|
|
1052
|
+
validateTourFraming(value, value.version, "manifest");
|
|
1053
|
+
requireString3(value.title, "title");
|
|
1054
|
+
requireString3(value.generatedAt, "generatedAt");
|
|
996
1055
|
if (value.windowTitle !== void 0) {
|
|
997
|
-
|
|
1056
|
+
requireString3(value.windowTitle, "windowTitle");
|
|
998
1057
|
}
|
|
999
1058
|
if (value.sourceUrl !== void 0) {
|
|
1000
|
-
|
|
1059
|
+
requireString3(value.sourceUrl, "sourceUrl");
|
|
1001
1060
|
}
|
|
1002
|
-
if (!
|
|
1061
|
+
if (!isRecord3(value.range)) {
|
|
1003
1062
|
throw new Error("Change-tour manifest range must be an object.");
|
|
1004
1063
|
}
|
|
1005
1064
|
for (const key of ["baseRef", "headRef", "mergeBaseOid", "headOid"]) {
|
|
1006
|
-
|
|
1065
|
+
requireString3(value.range[key], `range.${key}`);
|
|
1007
1066
|
}
|
|
1008
|
-
if (!
|
|
1067
|
+
if (!isRecord3(value.summary)) {
|
|
1009
1068
|
throw new Error("Change-tour manifest summary must be an object.");
|
|
1010
1069
|
}
|
|
1011
1070
|
for (const key of ["changedFiles", "includedScenes", "additions", "deletions", "commitCount"]) {
|
|
1012
1071
|
requireNonNegativeInteger(value.summary[key], `summary.${key}`);
|
|
1013
1072
|
}
|
|
1014
|
-
|
|
1073
|
+
requireStringArray3(value.summary.omittedFiles, "summary.omittedFiles");
|
|
1015
1074
|
if (!Array.isArray(value.commits) || !Array.isArray(value.chapters) || !Array.isArray(value.scenes)) {
|
|
1016
1075
|
throw new Error("Change-tour manifest commits, chapters, and scenes must be arrays.");
|
|
1017
1076
|
}
|
|
@@ -1027,12 +1086,13 @@ function parseChangeTourManifest(value) {
|
|
|
1027
1086
|
sceneIds.add(candidate.id);
|
|
1028
1087
|
}
|
|
1029
1088
|
for (const [index, candidate] of value.chapters.entries()) {
|
|
1030
|
-
if (!
|
|
1089
|
+
if (!isRecord3(candidate)) {
|
|
1031
1090
|
throw new Error(`chapters[${index}] must be an object.`);
|
|
1032
1091
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1092
|
+
requireString3(candidate.id, `chapters[${index}].id`);
|
|
1093
|
+
requireString3(candidate.title, `chapters[${index}].title`);
|
|
1094
|
+
validateChapterSummary(candidate.summary, value.version, `chapters[${index}].summary`);
|
|
1095
|
+
requireStringArray3(candidate.sceneIds, `chapters[${index}].sceneIds`);
|
|
1036
1096
|
for (const sceneId of candidate.sceneIds) {
|
|
1037
1097
|
if (!sceneIds.has(sceneId)) {
|
|
1038
1098
|
throw new Error(`Chapter references unknown scene id: ${sceneId}`);
|
|
@@ -1042,6 +1102,10 @@ function parseChangeTourManifest(value) {
|
|
|
1042
1102
|
if (value.summary.includedScenes !== value.scenes.length) {
|
|
1043
1103
|
throw new Error("summary.includedScenes must match the number of scenes.");
|
|
1044
1104
|
}
|
|
1105
|
+
if (value.details !== void 0) {
|
|
1106
|
+
if (value.version !== 5) throw new Error("details requires manifest version 5.");
|
|
1107
|
+
validateChangeTourDetails(value.details, "details");
|
|
1108
|
+
}
|
|
1045
1109
|
const files = value.files === void 0 ? collectLegacyTourFiles(value.scenes) : value.files.map((candidate, index) => {
|
|
1046
1110
|
validateTourFile(candidate, index);
|
|
1047
1111
|
return candidate;
|
|
@@ -1053,40 +1117,14 @@ function parseChangeTourManifest(value) {
|
|
|
1053
1117
|
validateAuthoringCoverage(value.authoringCoverage, value.version);
|
|
1054
1118
|
}
|
|
1055
1119
|
if (value.tours !== void 0) {
|
|
1056
|
-
if (value.version !==
|
|
1120
|
+
if (value.version !== 4 && value.version !== 5) throw new Error("Independent tours require manifest version 4.");
|
|
1057
1121
|
validateModeTours(value.tours, value.version);
|
|
1058
1122
|
}
|
|
1059
|
-
if (value.version !== 1) validateZoom(value, value.version);
|
|
1060
|
-
if (value.review !== void 0) {
|
|
1061
|
-
if (value.version !== 3) throw new Error("Review notes require manifest version 3.");
|
|
1062
|
-
const evidenceScenes = value.version >= 2 && isRecord2(value.zoom) && isRecord2(value.zoom.final) ? value.zoom.final.scenes : value.scenes;
|
|
1063
|
-
validateChangeTourReview(value.review, {
|
|
1064
|
-
expectedRange: {
|
|
1065
|
-
baseOid: String(value.range.mergeBaseOid),
|
|
1066
|
-
headOid: String(value.range.headOid)
|
|
1067
|
-
},
|
|
1068
|
-
authoredWalkthroughSteps: collectAuthoredWalkthroughStepIds(evidenceScenes)
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1123
|
+
if (value.version !== 1) validateZoom(value, value.version, Boolean(options.exported));
|
|
1071
1124
|
return { ...value, files };
|
|
1072
1125
|
}
|
|
1073
|
-
function collectAuthoredWalkthroughStepIds(value) {
|
|
1074
|
-
const scenes = /* @__PURE__ */ new Map();
|
|
1075
|
-
if (!Array.isArray(value)) return scenes;
|
|
1076
|
-
for (const scene of value) {
|
|
1077
|
-
if (!isRecord2(scene) || scene.kind !== "walkthrough" || !Array.isArray(scene.steps) || typeof scene.id !== "string") {
|
|
1078
|
-
continue;
|
|
1079
|
-
}
|
|
1080
|
-
const stepIds = /* @__PURE__ */ new Set();
|
|
1081
|
-
for (const step of scene.steps) {
|
|
1082
|
-
if (isRecord2(step) && typeof step.id === "string") stepIds.add(step.id);
|
|
1083
|
-
}
|
|
1084
|
-
scenes.set(scene.id, stepIds);
|
|
1085
|
-
}
|
|
1086
|
-
return scenes;
|
|
1087
|
-
}
|
|
1088
1126
|
function validateAuthoringCoverage(value, version) {
|
|
1089
|
-
if (!
|
|
1127
|
+
if (!isRecord3(value) || !isRecord3(value.walkthrough) || !Array.isArray(value.explanationAssignments)) {
|
|
1090
1128
|
throw new Error("authoringCoverage must contain walkthrough and explanationAssignments.");
|
|
1091
1129
|
}
|
|
1092
1130
|
const expectedScope = version >= 2 ? "final" : "tour";
|
|
@@ -1101,8 +1139,8 @@ function validateAuthoringCoverage(value, version) {
|
|
|
1101
1139
|
}
|
|
1102
1140
|
for (const [index, assignment] of value.explanationAssignments.entries()) {
|
|
1103
1141
|
const path = `authoringCoverage.explanationAssignments[${index}]`;
|
|
1104
|
-
if (!
|
|
1105
|
-
|
|
1142
|
+
if (!isRecord3(assignment)) throw new Error(`${path} must be an object.`);
|
|
1143
|
+
requireString3(assignment.sceneId, `${path}.sceneId`);
|
|
1106
1144
|
requireNonNegativeInteger(assignment.assignedUnits, `${path}.assignedUnits`);
|
|
1107
1145
|
requireNonNegativeInteger(assignment.excludedUnits, `${path}.excludedUnits`);
|
|
1108
1146
|
requireNonNegativeInteger(assignment.completeUnits, `${path}.completeUnits`);
|
|
@@ -1114,16 +1152,17 @@ function validateAuthoringCoverage(value, version) {
|
|
|
1114
1152
|
}
|
|
1115
1153
|
}
|
|
1116
1154
|
function validateModeTours(value, version) {
|
|
1117
|
-
if (!
|
|
1155
|
+
if (!isRecord3(value)) throw new Error("Change-tour manifest tours must be an object.");
|
|
1118
1156
|
if (value.historical === void 0 && value.deconstructed === void 0) {
|
|
1119
1157
|
throw new Error("Change-tour manifest tours must contain a historical or deconstructed tour.");
|
|
1120
1158
|
}
|
|
1121
1159
|
for (const mode of ["historical", "deconstructed"]) {
|
|
1122
1160
|
const tour = value[mode];
|
|
1123
1161
|
if (tour === void 0) continue;
|
|
1124
|
-
if (!
|
|
1162
|
+
if (!isRecord3(tour) || !Array.isArray(tour.chapters) || !Array.isArray(tour.scenes) || tour.chapters.length === 0 || tour.scenes.length === 0) {
|
|
1125
1163
|
throw new Error(`Change-tour manifest tours.${mode} must contain non-empty chapters and scenes arrays.`);
|
|
1126
1164
|
}
|
|
1165
|
+
validateTourFraming(tour, version, `tours.${mode}`);
|
|
1127
1166
|
const sceneIds = /* @__PURE__ */ new Set();
|
|
1128
1167
|
let hasDeconstructed = false;
|
|
1129
1168
|
for (const [index, scene] of tour.scenes.entries()) {
|
|
@@ -1137,16 +1176,21 @@ function validateModeTours(value, version) {
|
|
|
1137
1176
|
throw new Error("Historical tours cannot contain synthetic deconstructed scenes.");
|
|
1138
1177
|
}
|
|
1139
1178
|
}
|
|
1179
|
+
if (tour.details !== void 0) {
|
|
1180
|
+
if (version !== 5) throw new Error(`tours.${mode}.details requires manifest version 5.`);
|
|
1181
|
+
validateChangeTourDetails(tour.details, `tours.${mode}.details`);
|
|
1182
|
+
}
|
|
1140
1183
|
if (mode === "deconstructed" && !hasDeconstructed) {
|
|
1141
1184
|
throw new Error("Deconstructed tours must contain a constructed deconstructed-diff scene.");
|
|
1142
1185
|
}
|
|
1143
1186
|
for (const [index, chapter] of tour.chapters.entries()) {
|
|
1144
|
-
if (!
|
|
1187
|
+
if (!isRecord3(chapter)) {
|
|
1145
1188
|
throw new Error(`tours.${mode}.chapters[${index}] must be an object.`);
|
|
1146
1189
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1190
|
+
requireString3(chapter.id, `tours.${mode}.chapters[${index}].id`);
|
|
1191
|
+
requireString3(chapter.title, `tours.${mode}.chapters[${index}].title`);
|
|
1192
|
+
validateChapterSummary(chapter.summary, version, `tours.${mode}.chapters[${index}].summary`);
|
|
1193
|
+
requireStringArray3(chapter.sceneIds, `tours.${mode}.chapters[${index}].sceneIds`);
|
|
1150
1194
|
for (const sceneId of chapter.sceneIds) {
|
|
1151
1195
|
if (!sceneIds.has(sceneId)) {
|
|
1152
1196
|
throw new Error(`tours.${mode} chapter references unknown scene id: ${sceneId}`);
|
|
@@ -1155,11 +1199,11 @@ function validateModeTours(value, version) {
|
|
|
1155
1199
|
}
|
|
1156
1200
|
}
|
|
1157
1201
|
}
|
|
1158
|
-
function validateZoom(value, version) {
|
|
1159
|
-
if (!
|
|
1202
|
+
function validateZoom(value, version, exported) {
|
|
1203
|
+
if (!exported && (!isRecord3(value.repository) || typeof value.repository.root !== "string" || !value.repository.root)) {
|
|
1160
1204
|
throw new Error("A v2 tour requires its originating repository.root.");
|
|
1161
1205
|
}
|
|
1162
|
-
if (!
|
|
1206
|
+
if (!isRecord3(value.zoom) || !Array.isArray(value.zoom.revisions) || !isRecord3(value.zoom.final)) {
|
|
1163
1207
|
throw new Error("A v2 tour requires zoom metadata and real revision evidence.");
|
|
1164
1208
|
}
|
|
1165
1209
|
const scenes = value.scenes;
|
|
@@ -1204,27 +1248,29 @@ function validateZoom(value, version) {
|
|
|
1204
1248
|
finalSceneIds.add(scene.id);
|
|
1205
1249
|
}
|
|
1206
1250
|
for (const [index, chapter] of value.zoom.final.chapters.entries()) {
|
|
1207
|
-
if (!
|
|
1251
|
+
if (!isRecord3(chapter) || !Array.isArray(chapter.sceneIds)) {
|
|
1208
1252
|
throw new Error(`zoom.final.chapters[${index}] must contain sceneIds.`);
|
|
1209
1253
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1254
|
+
requireString3(chapter.id, `zoom.final.chapters[${index}].id`);
|
|
1255
|
+
requireString3(chapter.title, `zoom.final.chapters[${index}].title`);
|
|
1256
|
+
validateChapterSummary(chapter.summary, version, `zoom.final.chapters[${index}].summary`);
|
|
1212
1257
|
for (const sceneId of chapter.sceneIds) {
|
|
1213
|
-
|
|
1258
|
+
requireString3(sceneId, `zoom.final.chapters[${index}].sceneIds`);
|
|
1214
1259
|
if (!finalSceneIds.has(sceneId)) throw new Error(`Final tour chapter references unknown scene: ${sceneId}`);
|
|
1215
1260
|
}
|
|
1216
1261
|
}
|
|
1217
1262
|
}
|
|
1218
1263
|
function validateScene(value, index, version) {
|
|
1219
|
-
if (!
|
|
1264
|
+
if (!isRecord3(value) || !["text-diff", "discussion", "walkthrough", "stacked-diff", "deconstructed-diff"].includes(String(value.kind))) {
|
|
1220
1265
|
throw new Error(`scenes[${index}] must be a text-diff, discussion, walkthrough, stacked-diff, or deconstructed-diff scene.`);
|
|
1221
1266
|
}
|
|
1222
1267
|
for (const key of ["id", "title"]) {
|
|
1223
|
-
|
|
1268
|
+
requireString3(value[key], `scenes[${index}].${key}`);
|
|
1224
1269
|
}
|
|
1225
|
-
if (value.overview !== void 0 && version
|
|
1226
|
-
throw new Error(`scenes[${index}].overview requires manifest version
|
|
1270
|
+
if (value.overview !== void 0 && version < 4) {
|
|
1271
|
+
throw new Error(`scenes[${index}].overview requires manifest version 4.`);
|
|
1227
1272
|
}
|
|
1273
|
+
validateOverviewPurpose(value.overview, version, `scenes[${index}].overview`);
|
|
1228
1274
|
validateNarrative2(value, `scenes[${index}]`);
|
|
1229
1275
|
if (value.kind === "discussion") {
|
|
1230
1276
|
validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
|
|
@@ -1237,21 +1283,22 @@ function validateScene(value, index, version) {
|
|
|
1237
1283
|
}
|
|
1238
1284
|
for (const [stepIndex, step] of value.steps.entries()) {
|
|
1239
1285
|
const path = `scenes[${index}].steps[${stepIndex}]`;
|
|
1240
|
-
if (!
|
|
1286
|
+
if (!isRecord3(step) || !isRecord3(step.focus) || !isRecord3(step.diff)) {
|
|
1241
1287
|
throw new Error(`${path} must contain focus and diff objects.`);
|
|
1242
1288
|
}
|
|
1243
|
-
for (const key of ["id", "title", "body"])
|
|
1289
|
+
for (const key of ["id", "title", "body"]) requireString3(step[key], `${path}.${key}`);
|
|
1244
1290
|
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
1245
1291
|
throw new Error(`${path}.depth must be mentioned, explained, or contextualized.`);
|
|
1246
1292
|
}
|
|
1247
1293
|
validateStepRequirement(step.requirement, `${path}.requirement`);
|
|
1294
|
+
validateTourImage(step.image, `${path}.image`);
|
|
1248
1295
|
validateResolvedAnchor(step.focus, `${path}.focus`);
|
|
1249
1296
|
validateScene(step.diff, index, version);
|
|
1250
1297
|
if (step.diff.kind !== "text-diff") throw new Error(`${path}.diff must be a text-diff scene.`);
|
|
1251
1298
|
if (step.connection !== void 0) {
|
|
1252
|
-
if (!
|
|
1253
|
-
|
|
1254
|
-
|
|
1299
|
+
if (!isRecord3(step.connection)) throw new Error(`${path}.connection must be an object.`);
|
|
1300
|
+
requireString3(step.connection.id, `${path}.connection.id`);
|
|
1301
|
+
requireString3(step.connection.label, `${path}.connection.label`);
|
|
1255
1302
|
validateResolvedAnchor(step.connection.from, `${path}.connection.from`);
|
|
1256
1303
|
validateResolvedAnchor(step.connection.to, `${path}.connection.to`);
|
|
1257
1304
|
}
|
|
@@ -1274,12 +1321,12 @@ function validateScene(value, index, version) {
|
|
|
1274
1321
|
}
|
|
1275
1322
|
validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
|
|
1276
1323
|
for (const key of ["path", "leftLabel", "rightLabel", "leftContent", "rightContent"]) {
|
|
1277
|
-
|
|
1324
|
+
requireString3(value[key], `scenes[${index}].${key}`);
|
|
1278
1325
|
}
|
|
1279
1326
|
if (value.previousPath !== void 0) {
|
|
1280
|
-
|
|
1327
|
+
requireString3(value.previousPath, `scenes[${index}].previousPath`);
|
|
1281
1328
|
}
|
|
1282
|
-
|
|
1329
|
+
requireString3(value.changeKind, `scenes[${index}].changeKind`);
|
|
1283
1330
|
requireNonNegativeInteger(value.additions, `scenes[${index}].additions`);
|
|
1284
1331
|
requireNonNegativeInteger(value.deletions, `scenes[${index}].deletions`);
|
|
1285
1332
|
if (value.focusChangeIndex !== void 0) {
|
|
@@ -1291,8 +1338,8 @@ function validateStackedScene2(value, index) {
|
|
|
1291
1338
|
throw new Error(`scenes[${index}].stack must contain between 2 and 6 revisions.`);
|
|
1292
1339
|
}
|
|
1293
1340
|
for (const [panelIndex, panel] of value.stack.entries()) {
|
|
1294
|
-
if (!
|
|
1295
|
-
for (const key of ["id", "ref", "oid", "label"])
|
|
1341
|
+
if (!isRecord3(panel)) throw new Error(`scenes[${index}].stack[${panelIndex}] must be an object.`);
|
|
1342
|
+
for (const key of ["id", "ref", "oid", "label"]) requireString3(panel[key], `scenes[${index}].stack[${panelIndex}].${key}`);
|
|
1296
1343
|
}
|
|
1297
1344
|
validateMultiPanelFiles(value.files, value.stack.length, index);
|
|
1298
1345
|
validateMultiPanelSteps(value.steps, value.stack.length, index, false);
|
|
@@ -1301,33 +1348,33 @@ function validateDeconstructedScene2(value, index) {
|
|
|
1301
1348
|
if (value.stageLabel !== "Explanation stages") {
|
|
1302
1349
|
throw new Error(`scenes[${index}].stageLabel must be Explanation stages.`);
|
|
1303
1350
|
}
|
|
1304
|
-
if (!
|
|
1351
|
+
if (!isRecord3(value.realRange)) throw new Error(`scenes[${index}].realRange must be an object.`);
|
|
1305
1352
|
for (const key of ["baseRef", "targetRef", "baseOid", "targetOid"]) {
|
|
1306
|
-
|
|
1353
|
+
requireString3(value.realRange[key], `scenes[${index}].realRange.${key}`);
|
|
1307
1354
|
}
|
|
1308
1355
|
if (!Array.isArray(value.panels) || value.panels.length < 2 || value.panels.length > 13) {
|
|
1309
1356
|
throw new Error(`scenes[${index}].panels must contain a baseline and between 1 and 12 stages.`);
|
|
1310
1357
|
}
|
|
1311
1358
|
const panels = value.panels;
|
|
1312
1359
|
for (const [panelIndex, panel] of panels.entries()) {
|
|
1313
|
-
if (!
|
|
1314
|
-
for (const key of ["id", "label", "role"])
|
|
1360
|
+
if (!isRecord3(panel)) throw new Error(`scenes[${index}].panels[${panelIndex}] must be an object.`);
|
|
1361
|
+
for (const key of ["id", "label", "role"]) requireString3(panel[key], `scenes[${index}].panels[${panelIndex}].${key}`);
|
|
1315
1362
|
const expectedRole = panelIndex === 0 ? "baseline" : "stage";
|
|
1316
1363
|
if (panel.role !== expectedRole) throw new Error(`scenes[${index}].panels[${panelIndex}].role must be ${expectedRole}.`);
|
|
1317
|
-
if (panelIndex > 0)
|
|
1364
|
+
if (panelIndex > 0) requireString3(panel.stageId, `scenes[${index}].panels[${panelIndex}].stageId`);
|
|
1318
1365
|
}
|
|
1319
1366
|
validateMultiPanelFiles(value.files, panels.length, index);
|
|
1320
1367
|
validateMultiPanelSteps(value.steps, panels.length, index, true);
|
|
1321
1368
|
if (Array.isArray(value.steps)) {
|
|
1322
1369
|
let previousPairIndex = -1;
|
|
1323
1370
|
value.steps.forEach((step, stepIndex) => {
|
|
1324
|
-
if (!
|
|
1371
|
+
if (!isRecord3(step)) return;
|
|
1325
1372
|
const pairIndex = Number(step.pairIndex);
|
|
1326
1373
|
if (pairIndex < previousPairIndex) {
|
|
1327
1374
|
throw new Error(`scenes[${index}].steps must remain grouped in explanation stage order.`);
|
|
1328
1375
|
}
|
|
1329
1376
|
previousPairIndex = pairIndex;
|
|
1330
|
-
if (step.stageId !== void 0)
|
|
1377
|
+
if (step.stageId !== void 0) requireString3(step.stageId, `scenes[${index}].steps[${stepIndex}].stageId`);
|
|
1331
1378
|
if (step.stageIndex !== void 0) requireNonNegativeInteger(step.stageIndex, `scenes[${index}].steps[${stepIndex}].stageIndex`);
|
|
1332
1379
|
if (step.focusIndex !== void 0) requireNonNegativeInteger(step.focusIndex, `scenes[${index}].steps[${stepIndex}].focusIndex`);
|
|
1333
1380
|
if (step.focusCount !== void 0) requirePositiveInteger(step.focusCount, `scenes[${index}].steps[${stepIndex}].focusCount`);
|
|
@@ -1340,14 +1387,14 @@ function validateDeconstructedScene2(value, index) {
|
|
|
1340
1387
|
});
|
|
1341
1388
|
for (let stageIndex = 0; stageIndex < panels.length - 1; stageIndex += 1) {
|
|
1342
1389
|
const panel = panels[stageIndex + 1];
|
|
1343
|
-
const stageSteps = value.steps.filter((step) =>
|
|
1344
|
-
if (!
|
|
1390
|
+
const stageSteps = value.steps.filter((step) => isRecord3(step) && Number(step.pairIndex) === stageIndex);
|
|
1391
|
+
if (!isRecord3(panel) || stageSteps.length === 0) {
|
|
1345
1392
|
throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
|
|
1346
1393
|
}
|
|
1347
1394
|
const firstStep = stageSteps[0];
|
|
1348
|
-
const fallbackStageId =
|
|
1395
|
+
const fallbackStageId = isRecord3(firstStep) ? firstStep.id : void 0;
|
|
1349
1396
|
if (stageSteps.some((step) => {
|
|
1350
|
-
if (!
|
|
1397
|
+
if (!isRecord3(step)) return true;
|
|
1351
1398
|
return (step.stageId ?? fallbackStageId) !== panel.stageId;
|
|
1352
1399
|
})) {
|
|
1353
1400
|
throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
|
|
@@ -1358,15 +1405,15 @@ function validateDeconstructedScene2(value, index) {
|
|
|
1358
1405
|
function validateMultiPanelFiles(value, panelCount, index) {
|
|
1359
1406
|
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].files must be non-empty.`);
|
|
1360
1407
|
for (const [fileIndex, file] of value.entries()) {
|
|
1361
|
-
if (!
|
|
1362
|
-
|
|
1408
|
+
if (!isRecord3(file)) throw new Error(`scenes[${index}].files[${fileIndex}] must be an object.`);
|
|
1409
|
+
requireString3(file.path, `scenes[${index}].files[${fileIndex}].path`);
|
|
1363
1410
|
if (!Array.isArray(file.panels) || file.panels.length !== panelCount) {
|
|
1364
1411
|
throw new Error(`scenes[${index}].files[${fileIndex}].panels must match panel count.`);
|
|
1365
1412
|
}
|
|
1366
1413
|
for (const [panelIndex, panel] of file.panels.entries()) {
|
|
1367
|
-
if (!
|
|
1368
|
-
for (const key of ["id", "label", "content"])
|
|
1369
|
-
if (panel.path !== void 0)
|
|
1414
|
+
if (!isRecord3(panel)) throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}] must be an object.`);
|
|
1415
|
+
for (const key of ["id", "label", "content"]) requireString3(panel[key], `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].${key}`);
|
|
1416
|
+
if (panel.path !== void 0) requireString3(panel.path, `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].path`);
|
|
1370
1417
|
if (typeof panel.exists !== "boolean") throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}].exists must be boolean.`);
|
|
1371
1418
|
}
|
|
1372
1419
|
}
|
|
@@ -1374,12 +1421,12 @@ function validateMultiPanelFiles(value, panelCount, index) {
|
|
|
1374
1421
|
function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
1375
1422
|
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].steps must be non-empty.`);
|
|
1376
1423
|
for (const [stepIndex, step] of value.entries()) {
|
|
1377
|
-
if (!
|
|
1378
|
-
for (const key of ["id", "title", "body", "file", "side"])
|
|
1424
|
+
if (!isRecord3(step)) throw new Error(`scenes[${index}].steps[${stepIndex}] must be an object.`);
|
|
1425
|
+
for (const key of ["id", "title", "body", "file", "side"]) requireString3(step[key], `scenes[${index}].steps[${stepIndex}].${key}`);
|
|
1379
1426
|
requireNonNegativeInteger(step.pairIndex, `scenes[${index}].steps[${stepIndex}].pairIndex`);
|
|
1380
1427
|
if (Number(step.pairIndex) >= panelCount - 1) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex is outside the panels.`);
|
|
1381
1428
|
if (deconstructed) {
|
|
1382
|
-
|
|
1429
|
+
requireStringArray3(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
|
|
1383
1430
|
if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
|
|
1384
1431
|
}
|
|
1385
1432
|
if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
|
|
@@ -1388,7 +1435,7 @@ function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
|
1388
1435
|
}
|
|
1389
1436
|
}
|
|
1390
1437
|
function validateTourFile(value, index) {
|
|
1391
|
-
if (!
|
|
1438
|
+
if (!isRecord3(value)) throw new Error(`files[${index}] must be an object.`);
|
|
1392
1439
|
if (value.kind === "text-diff") {
|
|
1393
1440
|
validateScene(value, index, 1);
|
|
1394
1441
|
return;
|
|
@@ -1397,9 +1444,9 @@ function validateTourFile(value, index) {
|
|
|
1397
1444
|
throw new Error(`files[${index}].kind must be text-diff or omitted.`);
|
|
1398
1445
|
}
|
|
1399
1446
|
for (const key of ["id", "title", "path", "changeKind", "reason"]) {
|
|
1400
|
-
|
|
1447
|
+
requireString3(value[key], `files[${index}].${key}`);
|
|
1401
1448
|
}
|
|
1402
|
-
if (value.previousPath !== void 0)
|
|
1449
|
+
if (value.previousPath !== void 0) requireString3(value.previousPath, `files[${index}].previousPath`);
|
|
1403
1450
|
requireNonNegativeInteger(value.additions, `files[${index}].additions`);
|
|
1404
1451
|
requireNonNegativeInteger(value.deletions, `files[${index}].deletions`);
|
|
1405
1452
|
}
|
|
@@ -1419,28 +1466,35 @@ function collectLegacyTourFiles(scenes) {
|
|
|
1419
1466
|
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
1420
1467
|
}
|
|
1421
1468
|
function validateResolvedAnchor(value, path) {
|
|
1422
|
-
if (!
|
|
1423
|
-
for (const key of ["id", "path", "revision", "excerpt"])
|
|
1469
|
+
if (!isRecord3(value)) throw new Error(`${path} must be an object.`);
|
|
1470
|
+
for (const key of ["id", "path", "revision", "excerpt"]) requireString3(value[key], `${path}.${key}`);
|
|
1424
1471
|
if (value.revision !== "base" && value.revision !== "head") throw new Error(`${path}.revision must be base or head.`);
|
|
1425
1472
|
requirePositiveInteger(value.startLine, `${path}.startLine`);
|
|
1426
1473
|
requirePositiveInteger(value.endLine, `${path}.endLine`);
|
|
1427
1474
|
if (Number(value.endLine) < Number(value.startLine)) throw new Error(`${path}.endLine must not precede startLine.`);
|
|
1428
1475
|
}
|
|
1429
1476
|
function validateNarrative2(value, path) {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1477
|
+
requireString3(value.summary, `${path}.summary`);
|
|
1478
|
+
requireStringArray3(value.bullets, `${path}.bullets`);
|
|
1479
|
+
requireStringArray3(value.tags, `${path}.tags`);
|
|
1480
|
+
requireString3(value.takeaway, `${path}.takeaway`);
|
|
1434
1481
|
}
|
|
1435
|
-
function
|
|
1482
|
+
function validateChapterSummary(value, version, path) {
|
|
1483
|
+
if (value === void 0) return;
|
|
1484
|
+
if (version < 5) throw new Error(`${path} requires manifest version 5.`);
|
|
1485
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1486
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function isRecord3(value) {
|
|
1436
1490
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1437
1491
|
}
|
|
1438
|
-
function
|
|
1492
|
+
function requireString3(value, path) {
|
|
1439
1493
|
if (typeof value !== "string") {
|
|
1440
1494
|
throw new Error(`${path} must be a string.`);
|
|
1441
1495
|
}
|
|
1442
1496
|
}
|
|
1443
|
-
function
|
|
1497
|
+
function requireStringArray3(value, path) {
|
|
1444
1498
|
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
1445
1499
|
throw new Error(`${path} must be an array of strings.`);
|
|
1446
1500
|
}
|
|
@@ -1982,7 +2036,7 @@ function diffArrays(oldArr, newArr, callback) {
|
|
|
1982
2036
|
}
|
|
1983
2037
|
|
|
1984
2038
|
// src/diffEngine.ts
|
|
1985
|
-
var DEFAULT_MAX_INLINE_HIGHLIGHT_LINE_LENGTH =
|
|
2039
|
+
var DEFAULT_MAX_INLINE_HIGHLIGHT_LINE_LENGTH = 2e4;
|
|
1986
2040
|
var DEFAULT_DIFF_TIMEOUT_MS = 100;
|
|
1987
2041
|
var MAX_INLINE_HIGHLIGHT_LINE_LENGTH = readPositiveIntegerEnv(
|
|
1988
2042
|
"BYGONE_MAX_INLINE_HIGHLIGHT_LINE_LENGTH",
|
|
@@ -1995,6 +2049,22 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
|
1995
2049
|
if (leftContent === rightContent) {
|
|
1996
2050
|
return buildIdenticalTwoWayDiffModel(leftLines);
|
|
1997
2051
|
}
|
|
2052
|
+
if (leftLines.length === rightLines.length && leftLines.every((line, index) => line === rightLines[index])) {
|
|
2053
|
+
const model = buildIdenticalTwoWayDiffModel(leftLines);
|
|
2054
|
+
const rawLeft = leftContent.match(/[^\n]*\n|[^\n]+$/g) || [];
|
|
2055
|
+
const rawRight = rightContent.match(/[^\n]*\n|[^\n]+$/g) || [];
|
|
2056
|
+
leftLines.forEach((_, index) => {
|
|
2057
|
+
if (rawLeft[index] === rawRight[index]) return;
|
|
2058
|
+
model.leftLines[index].kind = model.rows[index].left.kind = "removed";
|
|
2059
|
+
model.rightLines[index].kind = model.rows[index].right.kind = "added";
|
|
2060
|
+
model.leftLines[index].inlineDetail = model.rightLines[index].inlineDetail = "line-ending";
|
|
2061
|
+
model.blocks.push(makeDiffBlock("replace", index, index + 1, index, index + 1));
|
|
2062
|
+
});
|
|
2063
|
+
model.blocks = mergeTouchingBlocks(model.blocks);
|
|
2064
|
+
model.hasChanges = true;
|
|
2065
|
+
model.formattingOnly = true;
|
|
2066
|
+
return model;
|
|
2067
|
+
}
|
|
1998
2068
|
const interned = internDiffLines(leftLines, rightLines);
|
|
1999
2069
|
const internedChanges = diffArrays(interned.left, interned.right, {
|
|
2000
2070
|
timeout: options.timeoutMs ?? DIFF_TIMEOUT_MS
|
|
@@ -2071,14 +2141,54 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
|
2071
2141
|
}
|
|
2072
2142
|
}
|
|
2073
2143
|
const hasChanges = rows.some((row) => row.left.kind !== "context" || row.right.kind !== "context");
|
|
2074
|
-
return {
|
|
2144
|
+
return markUndetailedReplacements({
|
|
2075
2145
|
rows,
|
|
2076
2146
|
leftLines: renderedLeftLines,
|
|
2077
2147
|
rightLines: renderedRightLines,
|
|
2078
|
-
blocks,
|
|
2148
|
+
blocks: mergeTouchingBlocks(blocks),
|
|
2149
|
+
reflows: blocks.filter((block) => block.reflow),
|
|
2079
2150
|
hasChanges,
|
|
2080
2151
|
quality: "exact"
|
|
2081
|
-
};
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
function markUndetailedReplacements(model) {
|
|
2155
|
+
for (const block of model.blocks) {
|
|
2156
|
+
if (block.kind !== "replace") continue;
|
|
2157
|
+
for (const side of ["left", "right"]) {
|
|
2158
|
+
for (let index = block[`${side}Start`]; index < block[`${side}End`]; index++) {
|
|
2159
|
+
const line = model[`${side}Lines`][index];
|
|
2160
|
+
if (line.kind !== "context" && !line.segments) line.inlineDetail = "omitted";
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
return model;
|
|
2165
|
+
}
|
|
2166
|
+
function mergeTouchingBlocks(blocks) {
|
|
2167
|
+
const regions = [];
|
|
2168
|
+
let oneSidedRun = false;
|
|
2169
|
+
for (const original of blocks) {
|
|
2170
|
+
const oneSided = original.kind !== "replace";
|
|
2171
|
+
const previous = regions[regions.length - 1];
|
|
2172
|
+
if (oneSided && oneSidedRun && previous.leftEnd === original.leftStart && previous.rightEnd === original.rightStart) {
|
|
2173
|
+
previous.leftEnd = original.leftEnd;
|
|
2174
|
+
previous.rightEnd = original.rightEnd;
|
|
2175
|
+
if (previous.leftEnd > previous.leftStart && previous.rightEnd > previous.rightStart) {
|
|
2176
|
+
previous.kind = "replace";
|
|
2177
|
+
}
|
|
2178
|
+
} else regions.push({ ...original });
|
|
2179
|
+
oneSidedRun = oneSided;
|
|
2180
|
+
}
|
|
2181
|
+
const merged = [];
|
|
2182
|
+
for (const original of regions) {
|
|
2183
|
+
const block = { ...original };
|
|
2184
|
+
delete block.reflow;
|
|
2185
|
+
const previous = merged[merged.length - 1];
|
|
2186
|
+
if ((previous == null ? void 0 : previous.kind) === block.kind && previous.leftEnd === block.leftStart && previous.rightEnd === block.rightStart) {
|
|
2187
|
+
previous.leftEnd = block.leftEnd;
|
|
2188
|
+
previous.rightEnd = block.rightEnd;
|
|
2189
|
+
} else merged.push(block);
|
|
2190
|
+
}
|
|
2191
|
+
return merged;
|
|
2082
2192
|
}
|
|
2083
2193
|
function internDiffLines(leftLines, rightLines) {
|
|
2084
2194
|
const lineIds = /* @__PURE__ */ new Map();
|
|
@@ -2131,14 +2241,14 @@ function buildConservativeTwoWayDiffModel(leftLines, rightLines) {
|
|
|
2131
2241
|
} else if (rightLines.length === 0) {
|
|
2132
2242
|
blockKind = "delete";
|
|
2133
2243
|
}
|
|
2134
|
-
return {
|
|
2244
|
+
return markUndetailedReplacements({
|
|
2135
2245
|
rows,
|
|
2136
2246
|
leftLines: renderedLeftLines,
|
|
2137
2247
|
rightLines: renderedRightLines,
|
|
2138
2248
|
blocks: rows.length === 0 ? [] : [makeDiffBlock(blockKind, 0, leftLines.length, 0, rightLines.length)],
|
|
2139
2249
|
hasChanges: rows.length > 0,
|
|
2140
2250
|
quality: "fallback"
|
|
2141
|
-
};
|
|
2251
|
+
});
|
|
2142
2252
|
}
|
|
2143
2253
|
function normalizeLines(content) {
|
|
2144
2254
|
if (content.length === 0) {
|
|
@@ -2153,33 +2263,64 @@ function normalizeLines(content) {
|
|
|
2153
2263
|
function appendReplacementCandidate(leftLines, rightLines, initialLeftLineNumber, initialRightLineNumber, rows, renderedLeftLines, renderedRightLines, blocks) {
|
|
2154
2264
|
const aligned = alignReplacementLines(leftLines, rightLines);
|
|
2155
2265
|
const crediblePairs = aligned.filter((row) => row.left !== void 0 && row.right !== void 0 && hasCredibleLineCorrespondence(row.left, row.right)).length;
|
|
2156
|
-
const coherentBlock = hasCoherentBlockCorrespondence(leftLines, rightLines, crediblePairs);
|
|
2157
|
-
const
|
|
2158
|
-
if (row.left === void 0 || row.right === void 0 || hasCredibleLineCorrespondence(row.left, row.right)) {
|
|
2266
|
+
const coherentBlock = !aligned.some((row) => row.reflow) && hasCoherentBlockCorrespondence(leftLines, rightLines, crediblePairs);
|
|
2267
|
+
const candidates = aligned.flatMap((row) => {
|
|
2268
|
+
if (row.reflow || row.left === void 0 || row.right === void 0 || hasCredibleLineCorrespondence(row.left, row.right)) {
|
|
2159
2269
|
return [row];
|
|
2160
2270
|
}
|
|
2161
2271
|
return [{ left: row.left }, { right: row.right }];
|
|
2162
2272
|
});
|
|
2273
|
+
const normalized = [];
|
|
2274
|
+
for (let index = 0; index < candidates.length; ) {
|
|
2275
|
+
const row = candidates[index];
|
|
2276
|
+
if (row.reflow || row.left !== void 0 && row.right !== void 0) {
|
|
2277
|
+
normalized.push(row);
|
|
2278
|
+
index++;
|
|
2279
|
+
continue;
|
|
2280
|
+
}
|
|
2281
|
+
const start = index;
|
|
2282
|
+
while (index < candidates.length && !candidates[index].reflow && (candidates[index].left === void 0 || candidates[index].right === void 0)) index++;
|
|
2283
|
+
const gap = candidates.slice(start, index);
|
|
2284
|
+
const left = gap.filter((item) => item.left !== void 0);
|
|
2285
|
+
const right = gap.filter((item) => item.right !== void 0);
|
|
2286
|
+
if (left.length === 1 && right.length === 1) {
|
|
2287
|
+
normalized.push({ left: left[0].left, right: right[0].right });
|
|
2288
|
+
} else {
|
|
2289
|
+
for (const item of gap) normalized.push(item);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2163
2292
|
let leftLineNumber = initialLeftLineNumber;
|
|
2164
2293
|
let rightLineNumber = initialRightLineNumber;
|
|
2294
|
+
let unmatchedLeft = [];
|
|
2295
|
+
let unmatchedRight = [];
|
|
2296
|
+
const flushUnmatched = () => {
|
|
2297
|
+
applyWrappedReplacementHighlights(unmatchedLeft, unmatchedRight);
|
|
2298
|
+
unmatchedLeft = [];
|
|
2299
|
+
unmatchedRight = [];
|
|
2300
|
+
};
|
|
2165
2301
|
let blockKind;
|
|
2302
|
+
let blockReflow = false;
|
|
2166
2303
|
let blockLeftStart = renderedLeftLines.length;
|
|
2167
2304
|
let blockRightStart = renderedRightLines.length;
|
|
2168
2305
|
const flushBlock = () => {
|
|
2169
2306
|
if (!blockKind) return;
|
|
2170
|
-
|
|
2307
|
+
const block = makeDiffBlock(
|
|
2171
2308
|
blockKind,
|
|
2172
2309
|
blockLeftStart,
|
|
2173
2310
|
renderedLeftLines.length,
|
|
2174
2311
|
blockRightStart,
|
|
2175
2312
|
renderedRightLines.length
|
|
2176
|
-
)
|
|
2313
|
+
);
|
|
2314
|
+
if (blockReflow) block.reflow = true;
|
|
2315
|
+
blocks.push(block);
|
|
2177
2316
|
};
|
|
2178
|
-
for (const { left, right } of normalized) {
|
|
2179
|
-
|
|
2180
|
-
|
|
2317
|
+
for (const { left, right, reflow } of normalized) {
|
|
2318
|
+
if (reflow || left !== void 0 && right !== void 0) flushUnmatched();
|
|
2319
|
+
const nextKind = coherentBlock || reflow ? "replace" : left !== void 0 && right !== void 0 ? "replace" : left !== void 0 ? "delete" : "insert";
|
|
2320
|
+
if (blockKind !== nextKind || blockReflow !== Boolean(reflow) || (reflow == null ? void 0 : reflow.start)) {
|
|
2181
2321
|
flushBlock();
|
|
2182
2322
|
blockKind = nextKind;
|
|
2323
|
+
blockReflow = Boolean(reflow);
|
|
2183
2324
|
blockLeftStart = renderedLeftLines.length;
|
|
2184
2325
|
blockRightStart = renderedRightLines.length;
|
|
2185
2326
|
}
|
|
@@ -2193,21 +2334,30 @@ function appendReplacementCandidate(leftLines, rightLines, initialLeftLineNumber
|
|
|
2193
2334
|
renderedRightLine = makeDiffLine("added", right, rightLineNumber);
|
|
2194
2335
|
renderedRightLines.push(renderedRightLine);
|
|
2195
2336
|
}
|
|
2196
|
-
if (
|
|
2337
|
+
if (reflow) {
|
|
2338
|
+
if (renderedLeftLine) renderedLeftLine.segments = reflow.leftSegments;
|
|
2339
|
+
if (renderedRightLine) renderedRightLine.segments = reflow.rightSegments;
|
|
2340
|
+
} else if (renderedLeftLine && renderedRightLine) {
|
|
2197
2341
|
applyInlineHighlightPair(renderedLeftLine, renderedRightLine);
|
|
2198
2342
|
} else if (coherentBlock) {
|
|
2199
2343
|
if (renderedLeftLine) applyFullLineHighlight(renderedLeftLine);
|
|
2200
2344
|
if (renderedRightLine) applyFullLineHighlight(renderedRightLine);
|
|
2201
2345
|
}
|
|
2346
|
+
if (!reflow && (left === void 0 || right === void 0)) {
|
|
2347
|
+
if (renderedLeftLine) unmatchedLeft.push(renderedLeftLine);
|
|
2348
|
+
if (renderedRightLine) unmatchedRight.push(renderedRightLine);
|
|
2349
|
+
}
|
|
2202
2350
|
rows.push(makeDiffRow(
|
|
2203
2351
|
left === void 0 ? makePlaceholder() : makeDiffCell("removed", left, leftLineNumber++),
|
|
2204
2352
|
right === void 0 ? makePlaceholder() : makeDiffCell("added", right, rightLineNumber++)
|
|
2205
2353
|
));
|
|
2206
2354
|
}
|
|
2207
2355
|
flushBlock();
|
|
2356
|
+
flushUnmatched();
|
|
2208
2357
|
return { left: leftLineNumber, right: rightLineNumber };
|
|
2209
2358
|
}
|
|
2210
2359
|
function hasCredibleLineCorrespondence(left, right) {
|
|
2360
|
+
if (!left.trim() && !right.trim()) return true;
|
|
2211
2361
|
const score = scoreReplacementLinePair(left, right);
|
|
2212
2362
|
if (score.eligible) return true;
|
|
2213
2363
|
const leftAnchor = declarationAnchorKey(left);
|
|
@@ -2260,24 +2410,101 @@ var MAX_ALIGNMENT_CHARACTER_CELLS = 5e7;
|
|
|
2260
2410
|
var MAX_RARE_TOKEN_OCCURRENCES = 4;
|
|
2261
2411
|
var MAX_CONTEXTUAL_HUNK_LINES = 8;
|
|
2262
2412
|
var MAX_CONTEXTUAL_LENGTH_DELTA = 4;
|
|
2413
|
+
function wrappedTextUnits(lines) {
|
|
2414
|
+
const units = [];
|
|
2415
|
+
for (let index = 0; index < lines.length; index++) {
|
|
2416
|
+
const first = lines[index];
|
|
2417
|
+
const paragraph = [first];
|
|
2418
|
+
const start = first.match(/^(\s*)(?:[\w-]+:\s+|[-*+]\s+)(\S.*)$/);
|
|
2419
|
+
if (start && !/^[|>\[{]/.test(start[2])) {
|
|
2420
|
+
while (index + 1 < lines.length) {
|
|
2421
|
+
const next = lines[index + 1];
|
|
2422
|
+
if (!next.trim() || next.length - next.trimStart().length <= start[1].length || /^\s*(?:[\w-]+:|[-*+]\s|\d+[.)]\s|#)/.test(next)) break;
|
|
2423
|
+
paragraph.push(next);
|
|
2424
|
+
index++;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
units.push(paragraph.join("\n"));
|
|
2428
|
+
}
|
|
2429
|
+
return units;
|
|
2430
|
+
}
|
|
2263
2431
|
function alignReplacementLines(leftLines, rightLines) {
|
|
2432
|
+
if ([...leftLines, ...rightLines].some((line) => line.includes("\n"))) {
|
|
2433
|
+
return alignPhysicalReplacementLines(leftLines, rightLines);
|
|
2434
|
+
}
|
|
2435
|
+
const leftUnits = wrappedTextUnits(leftLines);
|
|
2436
|
+
const rightUnits = wrappedTextUnits(rightLines);
|
|
2437
|
+
if (leftUnits.length === leftLines.length && rightUnits.length === rightLines.length) {
|
|
2438
|
+
return alignPhysicalReplacementLines(leftLines, rightLines);
|
|
2439
|
+
}
|
|
2440
|
+
return alignPhysicalReplacementLines(leftUnits, rightUnits).flatMap((row) => {
|
|
2441
|
+
var _a, _b;
|
|
2442
|
+
const left = ((_a = row.left) == null ? void 0 : _a.split("\n")) || [];
|
|
2443
|
+
const right = ((_b = row.right) == null ? void 0 : _b.split("\n")) || [];
|
|
2444
|
+
if (!left.length || !right.length) {
|
|
2445
|
+
return [...left.map((left2) => ({ left: left2 })), ...right.map((right2) => ({ right: right2 }))];
|
|
2446
|
+
}
|
|
2447
|
+
if (left.length === 1 && right.length === 1) return [row];
|
|
2448
|
+
const whitespace = buildWhitespaceOnlySegments(row.left, row.right);
|
|
2449
|
+
const segments = whitespace || buildInlineSegments(row.left, row.right);
|
|
2450
|
+
const leftSegments = segments && splitSegmentsIntoLines(segments.leftSegments);
|
|
2451
|
+
const rightSegments = segments && splitSegmentsIntoLines(segments.rightSegments);
|
|
2452
|
+
return Array.from({ length: Math.max(left.length, right.length) }, (_, index) => ({
|
|
2453
|
+
left: left[index],
|
|
2454
|
+
right: right[index],
|
|
2455
|
+
reflow: { start: index === 0, leftSegments: leftSegments == null ? void 0 : leftSegments[index], rightSegments: rightSegments == null ? void 0 : rightSegments[index] }
|
|
2456
|
+
}));
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
function alignPhysicalReplacementLines(leftLines, rightLines) {
|
|
2264
2460
|
if (leftLines.length === 0) {
|
|
2265
2461
|
return rightLines.map((right) => ({ right }));
|
|
2266
2462
|
}
|
|
2267
2463
|
if (rightLines.length === 0) {
|
|
2268
2464
|
return leftLines.map((left) => ({ left }));
|
|
2269
2465
|
}
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
if (
|
|
2466
|
+
let prefix = 0;
|
|
2467
|
+
while (prefix < Math.min(leftLines.length, rightLines.length) && leftLines[prefix].trim() === rightLines[prefix].trim()) prefix++;
|
|
2468
|
+
if (prefix) {
|
|
2273
2469
|
return [
|
|
2274
|
-
...leftLines.map((left) => ({ left })),
|
|
2275
|
-
...
|
|
2470
|
+
...leftLines.slice(0, prefix).map((left, index) => ({ left, right: rightLines[index] })),
|
|
2471
|
+
...alignReplacementLines(leftLines.slice(prefix), rightLines.slice(prefix))
|
|
2276
2472
|
];
|
|
2277
2473
|
}
|
|
2474
|
+
let leftEnd = 1;
|
|
2475
|
+
let rightEnd = 1;
|
|
2476
|
+
let leftText = leftLines[0].trim().replace(/\s+/g, " ");
|
|
2477
|
+
let rightText = rightLines[0].trim().replace(/\s+/g, " ");
|
|
2478
|
+
while (leftText && rightText && leftText !== rightText) {
|
|
2479
|
+
if (rightText.startsWith(leftText + " ") && leftEnd < leftLines.length) {
|
|
2480
|
+
leftText += " " + leftLines[leftEnd++].trim().replace(/\s+/g, " ");
|
|
2481
|
+
} else if (leftText.startsWith(rightText + " ") && rightEnd < rightLines.length) {
|
|
2482
|
+
rightText += " " + rightLines[rightEnd++].trim().replace(/\s+/g, " ");
|
|
2483
|
+
} else break;
|
|
2484
|
+
}
|
|
2485
|
+
if (leftText === rightText && (leftEnd > 1 || rightEnd > 1) && ![...leftLines, ...rightLines].some((line) => line.includes("\n"))) {
|
|
2486
|
+
const reflow = buildWhitespaceOnlySegments(leftLines.slice(0, leftEnd).join("\n"), rightLines.slice(0, rightEnd).join("\n"));
|
|
2487
|
+
if (reflow) {
|
|
2488
|
+
const leftSegments = splitSegmentsIntoLines(reflow.leftSegments);
|
|
2489
|
+
const rightSegments = splitSegmentsIntoLines(reflow.rightSegments);
|
|
2490
|
+
return [
|
|
2491
|
+
...Array.from({ length: Math.max(leftEnd, rightEnd) }, (_, index) => ({
|
|
2492
|
+
left: index < leftEnd ? leftLines[index] : void 0,
|
|
2493
|
+
right: index < rightEnd ? rightLines[index] : void 0,
|
|
2494
|
+
reflow: { start: index === 0, leftSegments: leftSegments[index], rightSegments: rightSegments[index] }
|
|
2495
|
+
})),
|
|
2496
|
+
...alignReplacementLines(leftLines.slice(leftEnd), rightLines.slice(rightEnd))
|
|
2497
|
+
];
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
const leftCharacterCount = leftLines.reduce((total, line) => total + Math.min(line.length, MAX_SCORING_LINE_LENGTH), 0);
|
|
2501
|
+
const rightCharacterCount = rightLines.reduce((total, line) => total + Math.min(line.length, MAX_SCORING_LINE_LENGTH), 0);
|
|
2502
|
+
if (leftCharacterCount * rightCharacterCount > MAX_ALIGNMENT_CHARACTER_CELLS) {
|
|
2503
|
+
return alignLargeReplacementLines(leftLines, rightLines, true);
|
|
2504
|
+
}
|
|
2278
2505
|
if (leftLines.length === 1 && rightLines.length === 1) {
|
|
2279
2506
|
const singletonScore = scoreReplacementLinePair(leftLines[0], rightLines[0]);
|
|
2280
|
-
if ((singletonScore.eligible || singletonScore.score >= MINIMUM_SINGLE_PAIR_SCORE) && hasCredibleLineCorrespondence(leftLines[0], rightLines[0]) && haveCompatibleLineRoles(leftLines[0], rightLines[0]) && isInformativeLine(normalizeMatchingContent(leftLines[0])) && isInformativeLine(normalizeMatchingContent(rightLines[0]))) {
|
|
2507
|
+
if (!leftLines[0].trim() && !rightLines[0].trim() || (singletonScore.eligible || singletonScore.score >= MINIMUM_SINGLE_PAIR_SCORE) && hasCredibleLineCorrespondence(leftLines[0], rightLines[0]) && haveCompatibleLineRoles(leftLines[0], rightLines[0]) && isInformativeLine(normalizeMatchingContent(leftLines[0])) && isInformativeLine(normalizeMatchingContent(rightLines[0]))) {
|
|
2281
2508
|
return [{ left: leftLines[0], right: rightLines[0] }];
|
|
2282
2509
|
}
|
|
2283
2510
|
return [{ left: leftLines[0] }, { right: rightLines[0] }];
|
|
@@ -2339,16 +2566,7 @@ function scoreReplacementLinePair(left, right) {
|
|
|
2339
2566
|
const normalizedRight = normalizeMatchingContent(right);
|
|
2340
2567
|
const leftInformative = isInformativeLine(normalizedLeft);
|
|
2341
2568
|
const rightInformative = isInformativeLine(normalizedRight);
|
|
2342
|
-
if (
|
|
2343
|
-
return {
|
|
2344
|
-
score: 0,
|
|
2345
|
-
eligible: false,
|
|
2346
|
-
characterSimilarity: 0,
|
|
2347
|
-
tokenSimilarity: 0,
|
|
2348
|
-
lengthSimilarity: 0
|
|
2349
|
-
};
|
|
2350
|
-
}
|
|
2351
|
-
if (normalizedLeft === normalizedRight) {
|
|
2569
|
+
if (leftInformative && rightInformative && normalizedLeft === normalizedRight) {
|
|
2352
2570
|
return {
|
|
2353
2571
|
score: 1,
|
|
2354
2572
|
eligible: true,
|
|
@@ -2357,14 +2575,30 @@ function scoreReplacementLinePair(left, right) {
|
|
|
2357
2575
|
lengthSimilarity: 1
|
|
2358
2576
|
};
|
|
2359
2577
|
}
|
|
2578
|
+
if (!leftInformative || !rightInformative) {
|
|
2579
|
+
return {
|
|
2580
|
+
score: 0,
|
|
2581
|
+
eligible: false,
|
|
2582
|
+
characterSimilarity: 0,
|
|
2583
|
+
tokenSimilarity: 0,
|
|
2584
|
+
lengthSimilarity: 0
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2360
2587
|
const leftTokens = tokenizeMatchingContent(normalizedLeft);
|
|
2361
2588
|
const rightTokens = tokenizeMatchingContent(normalizedRight);
|
|
2362
|
-
const
|
|
2589
|
+
const sample = (text) => text.length <= MAX_SCORING_LINE_LENGTH ? text : [0, Math.floor(text.length / 2) - 300, text.length - 600].map((start) => text.slice(start, start + 600)).join(" ");
|
|
2590
|
+
const characterSimilarity = lineSimilarity(sample(normalizedLeft), sample(normalizedRight));
|
|
2363
2591
|
const tokenSimilarity = multisetDiceSimilarity(leftTokens, rightTokens);
|
|
2364
2592
|
const lengthSimilarity = Math.min(normalizedLeft.length, normalizedRight.length) / Math.max(normalizedLeft.length, normalizedRight.length);
|
|
2365
2593
|
const boundarySimilarity = tokenBoundarySimilarity(leftTokens, rightTokens);
|
|
2366
|
-
const
|
|
2367
|
-
|
|
2594
|
+
const prefix = (text) => {
|
|
2595
|
+
var _a;
|
|
2596
|
+
return (_a = text.match(/^(?:(?:const|let|var)\s+)?([\p{L}_$][\p{L}\p{N}_$.-]*\s*(?::|=(?!=)))\s*/u)) == null ? void 0 : _a[1].replace(/\s+/g, "");
|
|
2597
|
+
};
|
|
2598
|
+
const leftPrefix = prefix(normalizedLeft);
|
|
2599
|
+
const prefixBonus = leftPrefix && leftPrefix === prefix(normalizedRight) ? 0.12 : 0;
|
|
2600
|
+
const score = Math.min(1, characterSimilarity * 0.52 + tokenSimilarity * 0.32 + lengthSimilarity * 0.1 + boundarySimilarity * 0.06 + prefixBonus);
|
|
2601
|
+
const hasContentEvidence = prefixBonus > 0 || characterSimilarity >= 0.52 || tokenSimilarity >= 0.45 || characterSimilarity >= 0.48 && tokenSimilarity >= 0.28;
|
|
2368
2602
|
return {
|
|
2369
2603
|
score,
|
|
2370
2604
|
eligible: hasContentEvidence && score >= MINIMUM_MATCH_SCORE,
|
|
@@ -2514,8 +2748,11 @@ function rankEligibleScores(scores) {
|
|
|
2514
2748
|
}
|
|
2515
2749
|
return { best, second };
|
|
2516
2750
|
}
|
|
2517
|
-
function alignLargeReplacementLines(leftLines, rightLines) {
|
|
2518
|
-
const anchors = collectLargeHunkAnchors(leftLines, rightLines);
|
|
2751
|
+
function alignLargeReplacementLines(leftLines, rightLines, exactOnly = false) {
|
|
2752
|
+
const anchors = collectLargeHunkAnchors(leftLines, rightLines, exactOnly);
|
|
2753
|
+
if (anchors.length === 0) {
|
|
2754
|
+
return [...leftLines.map((left) => ({ left })), ...rightLines.map((right) => ({ right }))];
|
|
2755
|
+
}
|
|
2519
2756
|
const aligned = [];
|
|
2520
2757
|
let leftStart = 0;
|
|
2521
2758
|
let rightStart = 0;
|
|
@@ -2532,8 +2769,8 @@ function alignLargeReplacementLines(leftLines, rightLines) {
|
|
|
2532
2769
|
appendBoundedSegment(aligned, leftLines.slice(leftStart), rightLines.slice(rightStart));
|
|
2533
2770
|
return aligned;
|
|
2534
2771
|
}
|
|
2535
|
-
function collectLargeHunkAnchors(leftLines, rightLines) {
|
|
2536
|
-
const candidatePairs = collectRareTokenCandidatePairs(leftLines, rightLines);
|
|
2772
|
+
function collectLargeHunkAnchors(leftLines, rightLines, exactOnly) {
|
|
2773
|
+
const candidatePairs = collectRareTokenCandidatePairs(leftLines, rightLines, exactOnly);
|
|
2537
2774
|
if (candidatePairs === null) {
|
|
2538
2775
|
return [];
|
|
2539
2776
|
}
|
|
@@ -2562,7 +2799,7 @@ function collectLargeHunkAnchors(leftLines, rightLines) {
|
|
|
2562
2799
|
const anchors = selectMaximumWeightMonotonicAnchors(confident, rightLines.length);
|
|
2563
2800
|
return anchors.length <= MAX_LARGE_HUNK_ANCHORS ? anchors : [];
|
|
2564
2801
|
}
|
|
2565
|
-
function collectRareTokenCandidatePairs(leftLines, rightLines) {
|
|
2802
|
+
function collectRareTokenCandidatePairs(leftLines, rightLines, exactOnly) {
|
|
2566
2803
|
const leftExact = buildLinePostings(leftLines, normalizeMatchingContent);
|
|
2567
2804
|
const rightExact = buildLinePostings(rightLines, normalizeMatchingContent);
|
|
2568
2805
|
const pairs = /* @__PURE__ */ new Map();
|
|
@@ -2578,6 +2815,9 @@ function collectRareTokenCandidatePairs(leftLines, rightLines) {
|
|
|
2578
2815
|
return null;
|
|
2579
2816
|
}
|
|
2580
2817
|
}
|
|
2818
|
+
if (exactOnly) {
|
|
2819
|
+
return [...pairs.values()];
|
|
2820
|
+
}
|
|
2581
2821
|
const leftTokens = buildTokenPostings(leftLines);
|
|
2582
2822
|
const rightTokens = buildTokenPostings(rightLines);
|
|
2583
2823
|
for (const [token, leftIndices] of leftTokens) {
|
|
@@ -2790,22 +3030,113 @@ function lineSimilarity(left, right) {
|
|
|
2790
3030
|
return 2 * commonLength / maxLength;
|
|
2791
3031
|
}
|
|
2792
3032
|
function applyInlineHighlightPair(leftLine, rightLine) {
|
|
2793
|
-
|
|
3033
|
+
const whitespace = buildWhitespaceOnlySegments(leftLine.content, rightLine.content);
|
|
3034
|
+
if (whitespace) {
|
|
3035
|
+
leftLine.segments = whitespace.leftSegments;
|
|
3036
|
+
rightLine.segments = whitespace.rightSegments;
|
|
2794
3037
|
return;
|
|
2795
3038
|
}
|
|
2796
|
-
const
|
|
2797
|
-
if (
|
|
2798
|
-
leftLine.segments = leftSegments;
|
|
2799
|
-
rightLine.segments = rightSegments;
|
|
2800
|
-
}
|
|
3039
|
+
const segments = buildInlineSegments(leftLine.content, rightLine.content);
|
|
3040
|
+
if (segments) {
|
|
3041
|
+
leftLine.segments = segments.leftSegments;
|
|
3042
|
+
rightLine.segments = segments.rightSegments;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
function applyWrappedReplacementHighlights(leftLines, rightLines) {
|
|
3046
|
+
if (!leftLines.length || !rightLines.length || leftLines.length === 1 && rightLines.length === 1) return;
|
|
3047
|
+
const length = (lines) => lines.reduce((total, line) => total + line.content.length + 1, -1);
|
|
3048
|
+
if (length(leftLines) > MAX_INLINE_HIGHLIGHT_LINE_LENGTH || length(rightLines) > MAX_INLINE_HIGHLIGHT_LINE_LENGTH) return;
|
|
3049
|
+
const left = leftLines.map((line) => line.content).join("\n");
|
|
3050
|
+
const right = rightLines.map((line) => line.content).join("\n");
|
|
3051
|
+
const segments = buildInlineSegments(left, right);
|
|
3052
|
+
if (!segments) return;
|
|
3053
|
+
const { leftSegments, rightSegments } = segments;
|
|
3054
|
+
const leftParts = splitSegmentsIntoLines(leftSegments);
|
|
3055
|
+
const rightParts = splitSegmentsIntoLines(rightSegments);
|
|
3056
|
+
leftLines.forEach((line, index) => {
|
|
3057
|
+
line.segments = leftParts[index];
|
|
3058
|
+
});
|
|
3059
|
+
rightLines.forEach((line, index) => {
|
|
3060
|
+
line.segments = rightParts[index];
|
|
3061
|
+
});
|
|
2801
3062
|
}
|
|
2802
3063
|
function applyFullLineHighlight(line) {
|
|
2803
3064
|
if (/[^\s]/.test(line.content)) {
|
|
2804
3065
|
line.segments = [{ kind: line.kind, text: line.content, emphasis: true }];
|
|
2805
3066
|
}
|
|
2806
3067
|
}
|
|
3068
|
+
var inlineGraphemes = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
3069
|
+
function trimReplacementEdges(left, right) {
|
|
3070
|
+
if (left === right) return left ? [{ value: left }] : [];
|
|
3071
|
+
const leftChars = Array.from(inlineGraphemes.segment(left), (part) => part.segment);
|
|
3072
|
+
const rightChars = Array.from(inlineGraphemes.segment(right), (part) => part.segment);
|
|
3073
|
+
const limit = Math.min(leftChars.length, rightChars.length);
|
|
3074
|
+
let prefix = 0;
|
|
3075
|
+
let suffix = 0;
|
|
3076
|
+
while (prefix < limit && leftChars[prefix] === rightChars[prefix]) prefix++;
|
|
3077
|
+
while (suffix < limit && leftChars[leftChars.length - suffix - 1] === rightChars[rightChars.length - suffix - 1]) suffix++;
|
|
3078
|
+
if (suffix === limit && prefix < limit) prefix = 0;
|
|
3079
|
+
suffix = Math.min(suffix, limit - prefix);
|
|
3080
|
+
return [
|
|
3081
|
+
{ value: leftChars.slice(0, prefix).join("") },
|
|
3082
|
+
{ value: leftChars.slice(prefix, leftChars.length - suffix).join(""), removed: true },
|
|
3083
|
+
{ value: rightChars.slice(prefix, rightChars.length - suffix).join(""), added: true },
|
|
3084
|
+
{ value: suffix ? leftChars.slice(-suffix).join("") : "" }
|
|
3085
|
+
].filter((change) => change.value.length > 0);
|
|
3086
|
+
}
|
|
3087
|
+
function edgeTrimmedWordChanges(left, right) {
|
|
3088
|
+
const words = diffWordsWithSpace(left, right, { timeout: DIFF_TIMEOUT_MS, maxEditLength: 2e3 });
|
|
3089
|
+
if (!words) return void 0;
|
|
3090
|
+
const boundaries = (text) => /* @__PURE__ */ new Set([
|
|
3091
|
+
...Array.from(inlineGraphemes.segment(text), (part) => part.index),
|
|
3092
|
+
text.length
|
|
3093
|
+
]);
|
|
3094
|
+
const leftBoundaries = boundaries(left);
|
|
3095
|
+
const rightBoundaries = boundaries(right);
|
|
3096
|
+
const result = [];
|
|
3097
|
+
let leftOffset = 0;
|
|
3098
|
+
let rightOffset = 0;
|
|
3099
|
+
let pendingLeft = "";
|
|
3100
|
+
let pendingRight = "";
|
|
3101
|
+
for (let index = 0; index < words.length; ) {
|
|
3102
|
+
const word = words[index];
|
|
3103
|
+
if (!word.added && !word.removed) {
|
|
3104
|
+
pendingLeft += word.value;
|
|
3105
|
+
pendingRight += word.value;
|
|
3106
|
+
leftOffset += word.value.length;
|
|
3107
|
+
rightOffset += word.value.length;
|
|
3108
|
+
index++;
|
|
3109
|
+
} else {
|
|
3110
|
+
while (index < words.length && (words[index].added || words[index].removed)) {
|
|
3111
|
+
const change = words[index++];
|
|
3112
|
+
if (change.removed) {
|
|
3113
|
+
pendingLeft += change.value;
|
|
3114
|
+
leftOffset += change.value.length;
|
|
3115
|
+
} else {
|
|
3116
|
+
pendingRight += change.value;
|
|
3117
|
+
rightOffset += change.value.length;
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
if (leftBoundaries.has(leftOffset) && rightBoundaries.has(rightOffset)) {
|
|
3122
|
+
for (const change of trimReplacementEdges(pendingLeft, pendingRight)) {
|
|
3123
|
+
const previous = result[result.length - 1];
|
|
3124
|
+
if (previous && previous.added === change.added && previous.removed === change.removed) {
|
|
3125
|
+
previous.value += change.value;
|
|
3126
|
+
} else {
|
|
3127
|
+
result.push(change);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
pendingLeft = "";
|
|
3131
|
+
pendingRight = "";
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
return result;
|
|
3135
|
+
}
|
|
2807
3136
|
function buildInlineSegments(leftContent, rightContent) {
|
|
2808
|
-
|
|
3137
|
+
if (leftContent.length > MAX_INLINE_HIGHLIGHT_LINE_LENGTH || rightContent.length > MAX_INLINE_HIGHLIGHT_LINE_LENGTH) return void 0;
|
|
3138
|
+
const changes = edgeTrimmedWordChanges(leftContent, rightContent);
|
|
3139
|
+
if (!changes) return void 0;
|
|
2809
3140
|
const leftSegments = [];
|
|
2810
3141
|
const rightSegments = [];
|
|
2811
3142
|
let hasInlineChanges = false;
|
|
@@ -2821,7 +3152,7 @@ function buildInlineSegments(leftContent, rightContent) {
|
|
|
2821
3152
|
rightSegments.push(contextSegment);
|
|
2822
3153
|
continue;
|
|
2823
3154
|
}
|
|
2824
|
-
const emphasis =
|
|
3155
|
+
const emphasis = value.length > 0;
|
|
2825
3156
|
hasInlineChanges = hasInlineChanges || emphasis;
|
|
2826
3157
|
if (change.removed) {
|
|
2827
3158
|
leftSegments.push({
|
|
@@ -2844,6 +3175,48 @@ function buildInlineSegments(leftContent, rightContent) {
|
|
|
2844
3175
|
hasInlineChanges
|
|
2845
3176
|
};
|
|
2846
3177
|
}
|
|
3178
|
+
function buildWhitespaceOnlySegments(left, right) {
|
|
3179
|
+
const leftParts = left.split(/(\S+)/);
|
|
3180
|
+
const rightParts = right.split(/(\S+)/);
|
|
3181
|
+
if (leftParts.length !== rightParts.length || leftParts.some((part, index) => index % 2 === 1 && part !== rightParts[index])) return void 0;
|
|
3182
|
+
const leftSegments = [];
|
|
3183
|
+
const rightSegments = [];
|
|
3184
|
+
const append = (segments, kind, text) => {
|
|
3185
|
+
if (!text) return;
|
|
3186
|
+
const previous = segments[segments.length - 1];
|
|
3187
|
+
if ((previous == null ? void 0 : previous.kind) === kind) previous.text += text;
|
|
3188
|
+
else segments.push({ kind, text, emphasis: kind !== "context" });
|
|
3189
|
+
};
|
|
3190
|
+
leftParts.forEach((part, index) => {
|
|
3191
|
+
const other = rightParts[index];
|
|
3192
|
+
let prefix = 0;
|
|
3193
|
+
if (index > 0) {
|
|
3194
|
+
while (prefix < Math.min(part.length, other.length) && part[prefix] === other[prefix]) prefix++;
|
|
3195
|
+
}
|
|
3196
|
+
let suffix = 0;
|
|
3197
|
+
while (suffix < Math.min(part.length, other.length) - prefix && part[part.length - suffix - 1] === other[other.length - suffix - 1]) suffix++;
|
|
3198
|
+
if (index === 0) {
|
|
3199
|
+
while (prefix < Math.min(part.length, other.length) - suffix && part[prefix] === other[prefix]) prefix++;
|
|
3200
|
+
}
|
|
3201
|
+
append(leftSegments, "context", part.slice(0, prefix));
|
|
3202
|
+
append(rightSegments, "context", other.slice(0, prefix));
|
|
3203
|
+
append(leftSegments, "removed", part.slice(prefix, part.length - suffix));
|
|
3204
|
+
append(rightSegments, "added", other.slice(prefix, other.length - suffix));
|
|
3205
|
+
append(leftSegments, "context", part.slice(part.length - suffix));
|
|
3206
|
+
append(rightSegments, "context", other.slice(other.length - suffix));
|
|
3207
|
+
});
|
|
3208
|
+
return { leftSegments, rightSegments };
|
|
3209
|
+
}
|
|
3210
|
+
function splitSegmentsIntoLines(segments) {
|
|
3211
|
+
const lines = [[]];
|
|
3212
|
+
for (const segment of segments) {
|
|
3213
|
+
segment.text.split("\n").forEach((text, index) => {
|
|
3214
|
+
if (index > 0) lines.push([]);
|
|
3215
|
+
if (text) lines[lines.length - 1].push({ ...segment, text });
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
return lines;
|
|
3219
|
+
}
|
|
2847
3220
|
function makePlaceholder() {
|
|
2848
3221
|
return {
|
|
2849
3222
|
kind: "placeholder",
|
|
@@ -3093,20 +3466,16 @@ var CHAPTERS = {
|
|
|
3093
3466
|
};
|
|
3094
3467
|
function buildChangeTourManifest(startPath, options = {}) {
|
|
3095
3468
|
var _a, _b, _c, _d;
|
|
3096
|
-
|
|
3469
|
+
let source = options.source ? parseChangeTourSource(options.source) : void 0;
|
|
3097
3470
|
const range = resolveBranchReviewRange(
|
|
3098
3471
|
startPath,
|
|
3099
3472
|
options.headRef ?? ((_a = source == null ? void 0 : source.range) == null ? void 0 : _a.head),
|
|
3100
3473
|
options.baseRef ?? ((_b = source == null ? void 0 : source.range) == null ? void 0 : _b.base)
|
|
3101
3474
|
);
|
|
3102
|
-
if (source
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
headOid: range.headOid
|
|
3107
|
-
}
|
|
3108
|
-
});
|
|
3109
|
-
}
|
|
3475
|
+
if (source) source = pinTourSource(source, range.repoRoot, /* @__PURE__ */ new Map([
|
|
3476
|
+
[range.headRef, range.headOid],
|
|
3477
|
+
[range.baseRef, range.baseOid]
|
|
3478
|
+
]), range.baseOid, range.headOid);
|
|
3110
3479
|
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_TOUR_FILE_BYTES;
|
|
3111
3480
|
const maxLineBytes = options.maxLineBytes ?? DEFAULT_MAX_TOUR_LINE_BYTES;
|
|
3112
3481
|
const omittedFiles = [];
|
|
@@ -3187,7 +3556,7 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
3187
3556
|
};
|
|
3188
3557
|
defaultScenes = [...defaultScenes].sort((left, right) => Number(right.path === attention.entryPath) - Number(left.path === attention.entryPath) || focusRank(left.path) - focusRank(right.path) || left.id.localeCompare(right.id));
|
|
3189
3558
|
}
|
|
3190
|
-
const authored = source ?
|
|
3559
|
+
const authored = source ? applySourceChapters(source, source.chapters, defaultScenes, range.repoRoot, { base: range.mergeBaseOid, head: range.headOid }) : options.story ? applyStory(options.story, defaultScenes) : buildGeneratedTourLanding(
|
|
3191
3560
|
defaultScenes,
|
|
3192
3561
|
buildChapters(sceneRecords, new Map(defaultScenes.map((scene) => [scene.path, scene.id]))),
|
|
3193
3562
|
attention,
|
|
@@ -3209,6 +3578,8 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
3209
3578
|
version: (source == null ? void 0 : source.version) ?? 1,
|
|
3210
3579
|
title: (source == null ? void 0 : source.title) || ((_c = options.story) == null ? void 0 : _c.title) || options.title || `${range.headRef} against ${range.baseRef}`,
|
|
3211
3580
|
windowTitle: source == null ? void 0 : source.windowTitle,
|
|
3581
|
+
opening: source == null ? void 0 : source.opening,
|
|
3582
|
+
conclusion: source == null ? void 0 : source.conclusion,
|
|
3212
3583
|
sourceUrl: (source == null ? void 0 : source.sourceUrl) || ((_d = options.story) == null ? void 0 : _d.sourceUrl) || options.sourceUrl,
|
|
3213
3584
|
generatedAt: options.generatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
3214
3585
|
range: {
|
|
@@ -3229,9 +3600,9 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
3229
3600
|
files,
|
|
3230
3601
|
chapters,
|
|
3231
3602
|
scenes,
|
|
3232
|
-
...(source == null ? void 0 : source.
|
|
3603
|
+
...(source == null ? void 0 : source.details) !== void 0 ? { details: source.details } : {}
|
|
3233
3604
|
};
|
|
3234
|
-
const authoredModeTours =
|
|
3605
|
+
const authoredModeTours = source && source.version >= 4 ? buildAuthoredModeTours(
|
|
3235
3606
|
source,
|
|
3236
3607
|
authored,
|
|
3237
3608
|
defaultScenes,
|
|
@@ -3286,10 +3657,7 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
3286
3657
|
}
|
|
3287
3658
|
return parseChangeTourManifest(manifest);
|
|
3288
3659
|
}
|
|
3289
|
-
function
|
|
3290
|
-
return applySourceChapters(source, source.chapters, defaultScenes, repoRoot);
|
|
3291
|
-
}
|
|
3292
|
-
function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot) {
|
|
3660
|
+
function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot, imageRevisions) {
|
|
3293
3661
|
var _a, _b;
|
|
3294
3662
|
const available = new Map(defaultScenes.map((scene) => [scene.path, scene]));
|
|
3295
3663
|
const resolvedAnchors = /* @__PURE__ */ new Map();
|
|
@@ -3330,7 +3698,8 @@ function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot)
|
|
|
3330
3698
|
{ ...authoredScene, steps: authoredScene.steps },
|
|
3331
3699
|
available,
|
|
3332
3700
|
resolvedAnchors,
|
|
3333
|
-
connections
|
|
3701
|
+
connections,
|
|
3702
|
+
(image) => compileTourImage(repoRoot, image, imageRevisions[image.revision])
|
|
3334
3703
|
);
|
|
3335
3704
|
finalScenes.push(finalScene);
|
|
3336
3705
|
finalSceneIds.push(finalScene.id);
|
|
@@ -3354,14 +3723,30 @@ function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot)
|
|
|
3354
3723
|
}
|
|
3355
3724
|
continue;
|
|
3356
3725
|
}
|
|
3357
|
-
const scene = buildWalkthroughManifestScene(
|
|
3726
|
+
const scene = buildWalkthroughManifestScene(
|
|
3727
|
+
authoredScene,
|
|
3728
|
+
available,
|
|
3729
|
+
resolvedAnchors,
|
|
3730
|
+
connections,
|
|
3731
|
+
(image) => compileTourImage(repoRoot, image, imageRevisions[image.revision])
|
|
3732
|
+
);
|
|
3358
3733
|
scenes.push(scene);
|
|
3359
3734
|
sceneIds.push(scene.id);
|
|
3360
3735
|
finalScenes.push(scene);
|
|
3361
3736
|
finalSceneIds.push(scene.id);
|
|
3362
3737
|
}
|
|
3363
|
-
chapters.push({
|
|
3364
|
-
|
|
3738
|
+
chapters.push({
|
|
3739
|
+
id: chapter.id,
|
|
3740
|
+
title: chapter.title,
|
|
3741
|
+
...chapter.summary === void 0 ? {} : { summary: chapter.summary },
|
|
3742
|
+
sceneIds
|
|
3743
|
+
});
|
|
3744
|
+
finalChapters.push({
|
|
3745
|
+
id: chapter.id,
|
|
3746
|
+
title: chapter.title,
|
|
3747
|
+
...chapter.summary === void 0 ? {} : { summary: chapter.summary },
|
|
3748
|
+
sceneIds: finalSceneIds
|
|
3749
|
+
});
|
|
3365
3750
|
}
|
|
3366
3751
|
return { scenes, chapters, finalScenes, finalChapters };
|
|
3367
3752
|
}
|
|
@@ -3369,18 +3754,41 @@ function buildAuthoredModeTours(source, root, defaultScenes, repoRoot, mergeBase
|
|
|
3369
3754
|
var _a, _b;
|
|
3370
3755
|
const tours = {};
|
|
3371
3756
|
if ((_a = source.tours) == null ? void 0 : _a.historical) {
|
|
3372
|
-
const compiled = applySourceChapters(source, source.tours.historical.chapters, defaultScenes, repoRoot);
|
|
3757
|
+
const compiled = applySourceChapters(source, source.tours.historical.chapters, defaultScenes, repoRoot, { base: mergeBaseOid, head: headOid });
|
|
3373
3758
|
validateHistoricalStackEndpoints(compiled.scenes, mergeBaseOid, headOid);
|
|
3374
|
-
tours.historical = {
|
|
3375
|
-
|
|
3759
|
+
tours.historical = {
|
|
3760
|
+
opening: source.tours.historical.opening,
|
|
3761
|
+
conclusion: source.tours.historical.conclusion,
|
|
3762
|
+
chapters: compiled.chapters,
|
|
3763
|
+
scenes: compiled.scenes,
|
|
3764
|
+
...source.tours.historical.details !== void 0 ? { details: source.tours.historical.details } : {}
|
|
3765
|
+
};
|
|
3766
|
+
} else if (source.version >= 4) {
|
|
3376
3767
|
const fallback = buildHistoricalFallback(source.chapters, root);
|
|
3377
|
-
if (fallback)
|
|
3768
|
+
if (fallback) {
|
|
3769
|
+
tours.historical = {
|
|
3770
|
+
...fallback,
|
|
3771
|
+
...root.scenes.every((scene) => scene.kind !== "deconstructed-diff") ? { opening: source.opening, conclusion: source.conclusion, details: source.details } : {}
|
|
3772
|
+
};
|
|
3773
|
+
}
|
|
3378
3774
|
}
|
|
3379
3775
|
if ((_b = source.tours) == null ? void 0 : _b.deconstructed) {
|
|
3380
|
-
const compiled = applySourceChapters(source, source.tours.deconstructed.chapters, defaultScenes, repoRoot);
|
|
3381
|
-
tours.deconstructed = {
|
|
3382
|
-
|
|
3383
|
-
|
|
3776
|
+
const compiled = applySourceChapters(source, source.tours.deconstructed.chapters, defaultScenes, repoRoot, { base: mergeBaseOid, head: headOid });
|
|
3777
|
+
tours.deconstructed = {
|
|
3778
|
+
opening: source.tours.deconstructed.opening,
|
|
3779
|
+
conclusion: source.tours.deconstructed.conclusion,
|
|
3780
|
+
chapters: compiled.chapters,
|
|
3781
|
+
scenes: compiled.scenes,
|
|
3782
|
+
...source.tours.deconstructed.details !== void 0 ? { details: source.tours.deconstructed.details } : {}
|
|
3783
|
+
};
|
|
3784
|
+
} else if (source.version >= 4 && source.chapters.some((chapter) => chapter.scenes.some((scene) => scene.kind === "deconstructed-diff"))) {
|
|
3785
|
+
tours.deconstructed = {
|
|
3786
|
+
opening: source.opening,
|
|
3787
|
+
conclusion: source.conclusion,
|
|
3788
|
+
details: source.details,
|
|
3789
|
+
chapters: root.chapters,
|
|
3790
|
+
scenes: root.scenes
|
|
3791
|
+
};
|
|
3384
3792
|
}
|
|
3385
3793
|
return tours.historical || tours.deconstructed ? tours : void 0;
|
|
3386
3794
|
}
|
|
@@ -3405,7 +3813,14 @@ function buildHistoricalFallback(sourceChapters, root) {
|
|
|
3405
3813
|
scenes.push(scene);
|
|
3406
3814
|
sceneIds.push(scene.id);
|
|
3407
3815
|
}
|
|
3408
|
-
if (sceneIds.length > 0)
|
|
3816
|
+
if (sceneIds.length > 0) {
|
|
3817
|
+
chapters.push({
|
|
3818
|
+
id: chapter.id,
|
|
3819
|
+
title: chapter.title,
|
|
3820
|
+
...chapter.summary === void 0 ? {} : { summary: chapter.summary },
|
|
3821
|
+
sceneIds
|
|
3822
|
+
});
|
|
3823
|
+
}
|
|
3409
3824
|
}
|
|
3410
3825
|
return scenes.length > 0 ? { chapters, scenes } : void 0;
|
|
3411
3826
|
}
|
|
@@ -3413,11 +3828,12 @@ function compileSceneOverview(overview, includeComparison = true) {
|
|
|
3413
3828
|
if (!overview) return void 0;
|
|
3414
3829
|
return {
|
|
3415
3830
|
kind: overview.kind,
|
|
3831
|
+
...overview.purpose === void 0 ? {} : { purpose: overview.purpose },
|
|
3416
3832
|
...overview.path === void 0 ? {} : { path: overview.path },
|
|
3417
3833
|
...includeComparison && overview.comparison ? { comparison: { ...overview.comparison } } : {}
|
|
3418
3834
|
};
|
|
3419
3835
|
}
|
|
3420
|
-
function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections) {
|
|
3836
|
+
function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections, resolveImage) {
|
|
3421
3837
|
const overview = compileSceneOverview(authoredScene.overview, false);
|
|
3422
3838
|
return {
|
|
3423
3839
|
id: authoredScene.id,
|
|
@@ -3438,6 +3854,7 @@ function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors
|
|
|
3438
3854
|
body: step.body,
|
|
3439
3855
|
depth: step.depth,
|
|
3440
3856
|
requirement: step.requirement,
|
|
3857
|
+
...step.image ? { image: resolveImage(step.image) } : {},
|
|
3441
3858
|
focus,
|
|
3442
3859
|
connection: step.connection ? connections.get(step.connection) : void 0,
|
|
3443
3860
|
diff: { ...diff2, id: `${authoredScene.id}-${step.id}` }
|
|
@@ -3550,8 +3967,10 @@ function buildDeconstructedFocusSteps(sceneId, stages, files) {
|
|
|
3550
3967
|
});
|
|
3551
3968
|
return focuses.map((focus, focusIndex) => ({
|
|
3552
3969
|
id: focusIndex === 0 ? claimDeconstructedStageId(stage.id, usedIds) : claimDeconstructedFocusId(`${stage.id}-focus-${focusIndex + 1}`, reservedIds, usedIds),
|
|
3553
|
-
title:
|
|
3554
|
-
|
|
3970
|
+
title: focusIndex === 0 ? stage.title : `${focus.file} \xB7 Evidence ${focusIndex + 1} of ${focuses.length}`,
|
|
3971
|
+
// A stage is one argument supported by multiple code focuses.
|
|
3972
|
+
// Keep its explanation once in the reading/narration sequence.
|
|
3973
|
+
body: focusIndex === 0 ? stage.narration : "",
|
|
3555
3974
|
file: focus.file,
|
|
3556
3975
|
pairIndex: stageIndex,
|
|
3557
3976
|
side: focus.side,
|
|
@@ -3854,6 +4273,8 @@ function readGitText(repoRoot, oid, relativePath, maxBytes, maxLineBytes) {
|
|
|
3854
4273
|
if (!relativePath) {
|
|
3855
4274
|
return { kind: "text", content: "" };
|
|
3856
4275
|
}
|
|
4276
|
+
const size = Number((0, import_child_process3.execFileSync)("git", ["cat-file", "-s", `${oid}:${relativePath}`], { cwd: repoRoot, encoding: "utf8" }).trim());
|
|
4277
|
+
if (size > maxBytes) return { kind: "binary-or-large", content: "" };
|
|
3857
4278
|
const content = (0, import_child_process3.execFileSync)("git", ["show", `${oid}:${relativePath}`], {
|
|
3858
4279
|
cwd: repoRoot,
|
|
3859
4280
|
encoding: "buffer",
|
|
@@ -3895,6 +4316,49 @@ function readGitLineStats(repoRoot, baseOid, headOid, filePath, previousPath) {
|
|
|
3895
4316
|
binary: additionsText === "-" || deletionsText === "-"
|
|
3896
4317
|
};
|
|
3897
4318
|
}
|
|
4319
|
+
function pinTourSource(source, repoRoot, resolved, baseOid, headOid) {
|
|
4320
|
+
const copy = JSON.parse(JSON.stringify(source));
|
|
4321
|
+
const pin = (ref) => {
|
|
4322
|
+
let oid = resolved.get(ref);
|
|
4323
|
+
if (!oid) {
|
|
4324
|
+
oid = runGitText(repoRoot, ["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`]);
|
|
4325
|
+
resolved.set(ref, oid);
|
|
4326
|
+
}
|
|
4327
|
+
return oid;
|
|
4328
|
+
};
|
|
4329
|
+
copy.range = { base: baseOid, head: headOid };
|
|
4330
|
+
const chapters = [...copy.chapters, ...Object.values(copy.tours || {}).flatMap((tour) => (tour == null ? void 0 : tour.chapters) || [])];
|
|
4331
|
+
for (const chapter of chapters) for (const scene of chapter.scenes) {
|
|
4332
|
+
if (scene.kind === "stacked-diff") for (const entry of scene.stack) {
|
|
4333
|
+
entry.label ||= entry.ref;
|
|
4334
|
+
entry.ref = pin(entry.ref);
|
|
4335
|
+
}
|
|
4336
|
+
if (scene.kind === "deconstructed-diff") {
|
|
4337
|
+
scene.base = scene.base ? pin(scene.base) : baseOid;
|
|
4338
|
+
scene.target = scene.target ? pin(scene.target) : headOid;
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
return copy;
|
|
4342
|
+
}
|
|
4343
|
+
function compileTourImage(repoRoot, source, revision) {
|
|
4344
|
+
const object = `${revision}:${source.file}`;
|
|
4345
|
+
let size;
|
|
4346
|
+
try {
|
|
4347
|
+
size = Number((0, import_child_process3.execFileSync)("git", ["cat-file", "-s", object], {
|
|
4348
|
+
cwd: repoRoot,
|
|
4349
|
+
encoding: "utf8",
|
|
4350
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4351
|
+
}).trim());
|
|
4352
|
+
} catch {
|
|
4353
|
+
throw new Error(`Image ${source.file} is unavailable at pinned revision ${revision}.`);
|
|
4354
|
+
}
|
|
4355
|
+
if (!Number.isSafeInteger(size) || size < 33 || size > MAX_TOUR_IMAGE_BYTES) {
|
|
4356
|
+
throw new Error(`Image ${source.file} must be a PNG of at most 8 MiB.`);
|
|
4357
|
+
}
|
|
4358
|
+
const bytes = (0, import_child_process3.execFileSync)("git", ["cat-file", "blob", object], { cwd: repoRoot, maxBuffer: MAX_TOUR_IMAGE_BYTES });
|
|
4359
|
+
validatePngHeader(bytes, source.file);
|
|
4360
|
+
return { path: source.file, revision, alt: source.alt, dataUrl: `data:image/png;base64,${bytes.toString("base64")}` };
|
|
4361
|
+
}
|
|
3898
4362
|
|
|
3899
4363
|
// src/tourCoverage.ts
|
|
3900
4364
|
var TOUR_COVERAGE_REPORT_VERSION = 2;
|