@davmash/bygone 0.10.0 → 0.11.0

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.
@@ -44,11 +44,13 @@ var import_child_process2 = require("child_process");
44
44
  var import_child_process = require("child_process");
45
45
  var fs = __toESM(require("fs"));
46
46
  var DEFAULT_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
47
+ var DEFAULT_GIT_TIMEOUT_MS = 3e4;
47
48
  function createGitRunner(maxBuffer = readGitMaxBuffer()) {
48
49
  return (args, cwd) => (0, import_child_process.execFileSync)("git", [...args], {
49
50
  cwd,
50
51
  encoding: "utf8",
51
52
  maxBuffer,
53
+ timeout: DEFAULT_GIT_TIMEOUT_MS,
52
54
  stdio: ["ignore", "pipe", "pipe"]
53
55
  }).trimEnd();
54
56
  }
@@ -115,12 +117,12 @@ function parseNameStatusZ(output) {
115
117
  }
116
118
  const changedPaths = [];
117
119
  for (let index = 0; index < fields.length; ) {
118
- const status = fields[index++];
119
- if (!status) {
120
+ const status2 = fields[index++];
121
+ if (!status2) {
120
122
  continue;
121
123
  }
122
- const code = status[0];
123
- const similarityText = status.slice(1);
124
+ const code = status2[0];
125
+ const similarityText = status2.slice(1);
124
126
  const similarity = similarityText ? Number.parseInt(similarityText, 10) : void 0;
125
127
  if (code === "R" || code === "C") {
126
128
  const previousPath = fields[index++];
@@ -471,1568 +473,1878 @@ function runGit(repoRoot, args) {
471
473
  });
472
474
  }
473
475
 
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
-
511
476
  // src/changeTour.ts
512
- var import_child_process3 = require("child_process");
477
+ var import_fs2 = require("fs");
478
+ var import_path = require("path");
513
479
 
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
- }
480
+ // src/changeCorrespondence.ts
481
+ var import_crypto2 = require("crypto");
482
+ var import_child_process3 = require("child_process");
483
+ var import_fs = require("fs");
545
484
 
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
- }
485
+ // node_modules/diff/lib/index.mjs
486
+ function Diff() {
601
487
  }
602
-
603
- // src/changeTourSource.ts
604
- var CHANGE_TOUR_SOURCE_VERSION = 5;
605
- function parseChangeTourSource(value) {
606
- if (isRecord2(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_SOURCE_VERSION) {
607
- throw new Error(
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.`
609
- );
610
- }
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) {
615
- throw new Error("Unsupported or missing change-tour source version.");
616
- }
617
- requireOnlyKeys2(value, [
618
- "version",
619
- "title",
620
- "windowTitle",
621
- "sourceUrl",
622
- "range",
623
- "anchors",
624
- "connections",
625
- "chapters",
626
- "tours",
627
- "coverage",
628
- "opening",
629
- "conclusion",
630
- "details"
631
- ], "source");
632
- validateTourFraming(value, value.version, "source");
633
- optionalString(value.title, "title");
634
- optionalString(value.windowTitle, "windowTitle");
635
- optionalString(value.sourceUrl, "sourceUrl");
636
- if (value.range !== void 0) {
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");
641
- }
642
- if (value.coverage !== void 0) {
643
- if (!isRecord2(value.coverage) || !Array.isArray(value.coverage.exclusions)) {
644
- throw new Error("coverage must contain an exclusions array.");
645
- }
646
- requireOnlyKeys2(value.coverage, ["exclusions"], "coverage");
647
- for (const [index, exclusion] of value.coverage.exclusions.entries()) {
648
- const exclusionPath = `coverage.exclusions[${index}]`;
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`);
654
- }
655
- }
656
- if (!isRecord2(value.anchors)) {
657
- throw new Error("anchors must be an object.");
658
- }
659
- for (const [id, anchor] of Object.entries(value.anchors)) {
660
- if (!isRecord2(anchor)) {
661
- throw new Error(`anchors.${id} must be an object.`);
662
- }
663
- requireOnlyKeys2(anchor, ["file", "revision", "contains", "occurrence"], `anchors.${id}`);
664
- requireString2(anchor.file, `anchors.${id}.file`);
665
- if (anchor.revision !== "base" && anchor.revision !== "head") {
666
- throw new Error(`anchors.${id}.revision must be base or head.`);
667
- }
668
- requireString2(anchor.contains, `anchors.${id}.contains`);
669
- if (anchor.occurrence !== void 0 && (!Number.isInteger(anchor.occurrence) || Number(anchor.occurrence) < 1)) {
670
- throw new Error(`anchors.${id}.occurrence must be a positive integer.`);
671
- }
672
- }
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;
674
- if (!rawConnections || !Array.isArray(value.chapters) || value.chapters.length === 0) {
675
- throw new Error("connections must be an array or object, and chapters must be a non-empty array.");
676
- }
677
- const connectionIds = /* @__PURE__ */ new Set();
678
- for (const [index, connection] of rawConnections.entries()) {
679
- if (!isRecord2(connection)) {
680
- throw new Error(`connections[${index}] must be an object.`);
681
- }
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`);
687
- const { id, from, to } = connection;
688
- if (!value.anchors[from] || !value.anchors[to]) {
689
- throw new Error(`Connection ${id} references an unknown anchor.`);
690
- }
691
- if (connectionIds.has(id)) {
692
- throw new Error(`Duplicate connection id: ${id}`);
693
- }
694
- connectionIds.add(id);
695
- }
696
- validateSourceChapters(
697
- value.chapters,
698
- "chapters",
699
- value.version,
700
- value.anchors,
701
- connectionIds,
702
- "root"
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
- }
708
- if (value.tours !== void 0) {
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");
712
- if (value.tours.historical === void 0 && value.tours.deconstructed === void 0) {
713
- throw new Error("tours must contain a historical or deconstructed tour.");
488
+ Diff.prototype = {
489
+ diff: function diff(oldString, newString) {
490
+ var _options$timeout;
491
+ var options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
492
+ var callback = options.callback;
493
+ if (typeof options === "function") {
494
+ callback = options;
495
+ options = {};
714
496
  }
715
- for (const mode of ["historical", "deconstructed"]) {
716
- const tour = value.tours[mode];
717
- if (tour === void 0) continue;
718
- const path = `tours.${mode}`;
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);
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`);
497
+ this.options = options;
498
+ var self = this;
499
+ function done(value) {
500
+ if (callback) {
501
+ setTimeout(function() {
502
+ callback(void 0, value);
503
+ }, 0);
504
+ return true;
505
+ } else {
506
+ return value;
726
507
  }
727
508
  }
728
- }
729
- return { ...value, connections: rawConnections };
730
- }
731
- function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, connectionIds, mode) {
732
- if (!Array.isArray(chaptersValue) || chaptersValue.length === 0) {
733
- throw new Error(`${chaptersPath} must be a non-empty array.`);
734
- }
735
- const chapterIds = /* @__PURE__ */ new Set();
736
- const sceneIds = /* @__PURE__ */ new Set();
737
- for (const [chapterIndex, chapter] of chaptersValue.entries()) {
738
- const chapterPath = `${chaptersPath}[${chapterIndex}]`;
739
- if (!isRecord2(chapter) || !Array.isArray(chapter.scenes) || chapter.scenes.length === 0) {
740
- throw new Error(`${chapterPath} must contain a non-empty scenes array.`);
509
+ oldString = this.castInput(oldString);
510
+ newString = this.castInput(newString);
511
+ oldString = this.removeEmpty(this.tokenize(oldString));
512
+ newString = this.removeEmpty(this.tokenize(newString));
513
+ var newLen = newString.length, oldLen = oldString.length;
514
+ var editLength = 1;
515
+ var maxEditLength = newLen + oldLen;
516
+ if (options.maxEditLength) {
517
+ maxEditLength = Math.min(maxEditLength, options.maxEditLength);
741
518
  }
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`);
519
+ var maxExecutionTime = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : Infinity;
520
+ var abortAfterTimestamp = Date.now() + maxExecutionTime;
521
+ var bestPath = [{
522
+ oldPos: -1,
523
+ lastComponent: void 0
524
+ }];
525
+ var newPos = this.extractCommon(bestPath[0], newString, oldString, 0);
526
+ if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
527
+ return done([{
528
+ value: this.join(newString),
529
+ count: newString.length
530
+ }]);
747
531
  }
748
- requireOnlyKeys2(chapter, ["id", "title", "summary", "scenes"], chapterPath);
749
- if (chapterIds.has(chapter.id)) throw new Error(`Duplicate chapter id: ${chapter.id}`);
750
- chapterIds.add(chapter.id);
751
- for (const [sceneIndex, scene] of chapter.scenes.entries()) {
752
- const path = `${chapterPath}.scenes[${sceneIndex}]`;
753
- if (!isRecord2(scene)) throw new Error(`${path} must be an object.`);
754
- requireString2(scene.id, `${path}.id`);
755
- requireString2(scene.title, `${path}.title`);
756
- if (sceneIds.has(scene.id)) throw new Error(`Duplicate scene id: ${scene.id}`);
757
- sceneIds.add(scene.id);
758
- if (scene.overview !== void 0 && version < 4) {
759
- throw new Error(`${path}.overview requires version 4.`);
760
- }
761
- validateOverviewPurpose(scene.overview, version, `${path}.overview`);
762
- validateNarrative(scene, path);
763
- if (scene.kind === "deconstructed-diff") {
764
- if (mode === "historical") {
765
- throw new Error(`${path} cannot contain a deconstructed-diff scene in the historical tour.`);
532
+ var minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
533
+ function execEditLength() {
534
+ for (var diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
535
+ var basePath = void 0;
536
+ var removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
537
+ if (removePath) {
538
+ bestPath[diagonalPath - 1] = void 0;
766
539
  }
767
- validateDeconstructedScene(scene, path);
768
- validateSceneOverview(scene.overview, `${path}.overview`, {
769
- endpointIds: deconstructedOverviewEndpointIds(scene, path)
770
- });
771
- const requireRealEvidence = mode === "root" && version >= 2;
772
- if (requireRealEvidence || scene.stack !== void 0) {
773
- validateRealStack(scene.stack, `${path}.stack`, 2);
540
+ var canAdd = false;
541
+ if (addPath) {
542
+ var addPathNewPos = addPath.oldPos - diagonalPath;
543
+ canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
774
544
  }
775
- if (requireRealEvidence || scene.steps !== void 0) {
776
- validateWalkthroughSteps(scene, path, anchors, connectionIds);
545
+ var canRemove = removePath && removePath.oldPos + 1 < oldLen;
546
+ if (!canAdd && !canRemove) {
547
+ bestPath[diagonalPath] = void 0;
548
+ continue;
549
+ }
550
+ if (!canRemove || canAdd && removePath.oldPos + 1 < addPath.oldPos) {
551
+ basePath = self.addToPath(addPath, true, void 0, 0);
552
+ } else {
553
+ basePath = self.addToPath(removePath, void 0, true, 1);
554
+ }
555
+ newPos = self.extractCommon(basePath, newString, oldString, diagonalPath);
556
+ if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
557
+ return done(buildValues(self, basePath.lastComponent, newString, oldString, self.useLongestToken));
558
+ } else {
559
+ bestPath[diagonalPath] = basePath;
560
+ if (basePath.oldPos + 1 >= oldLen) {
561
+ maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
562
+ }
563
+ if (newPos + 1 >= newLen) {
564
+ minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
565
+ }
777
566
  }
778
- continue;
779
- }
780
- if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
781
- throw new Error(`${path} must contain a non-empty steps array.`);
782
- }
783
- if (scene.kind === "stacked-diff") {
784
- validateStackedScene(scene, path, mode === "root" ? version >= 2 ? 2 : 3 : 2);
785
- validateSceneOverview(scene.overview, `${path}.overview`, {
786
- endpointIds: stackedOverviewEndpointIds(scene, path)
787
- });
788
- continue;
789
567
  }
790
- if (scene.kind !== void 0 && scene.kind !== "walkthrough") {
791
- throw new Error(`${path}.kind must be walkthrough, stacked-diff, or deconstructed-diff.`);
568
+ editLength++;
569
+ }
570
+ if (callback) {
571
+ (function exec() {
572
+ setTimeout(function() {
573
+ if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
574
+ return callback();
575
+ }
576
+ if (!execEditLength()) {
577
+ exec();
578
+ }
579
+ }, 0);
580
+ })();
581
+ } else {
582
+ while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
583
+ var ret = execEditLength();
584
+ if (ret) {
585
+ return ret;
586
+ }
792
587
  }
793
- requireOnlyKeys2(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "steps"], path);
794
- validateSceneOverview(scene.overview, `${path}.overview`, { allowComparison: false });
795
- validateWalkthroughSteps(scene, path, anchors, connectionIds);
796
588
  }
797
- }
798
- if (mode === "deconstructed" && !chaptersValue.some((chapter) => isRecord2(chapter) && Array.isArray(chapter.scenes) && chapter.scenes.some((scene) => isRecord2(scene) && scene.kind === "deconstructed-diff"))) {
799
- throw new Error(`${chaptersPath} must contain at least one deconstructed-diff scene.`);
800
- }
801
- }
802
- function stackedOverviewEndpointIds(scene, path) {
803
- if (!Array.isArray(scene.stack)) {
804
- throw new Error(`${path}.stack must be an array.`);
805
- }
806
- return scene.stack.filter(isRecord2).map((entry) => typeof entry.id === "string" ? entry.id : "").filter((id) => id.length > 0);
807
- }
808
- function deconstructedOverviewEndpointIds(scene, path) {
809
- if (!Array.isArray(scene.stages)) {
810
- throw new Error(`${path}.stages must be an array.`);
811
- }
812
- return [
813
- "explanation-baseline",
814
- ...scene.stages.filter(isRecord2).map((stage) => typeof stage.id === "string" ? `explanation-stage-${stage.id}` : "").filter((id) => id.length > 0)
815
- ];
816
- }
817
- function validateDeconstructedScene(scene, path) {
818
- requireOnlyKeys2(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "base", "target", "stack", "steps", "stages", "exclusions"], path);
819
- optionalString(scene.base, `${path}.base`);
820
- optionalString(scene.target, `${path}.target`);
821
- if (!Array.isArray(scene.stages) || scene.stages.length === 0 || scene.stages.length > 12) {
822
- throw new Error(`${path}.stages must contain between 1 and 12 stages.`);
823
- }
824
- const stageIds = /* @__PURE__ */ new Set();
825
- for (const [stageIndex, stage] of scene.stages.entries()) {
826
- const stagePath = `${path}.stages[${stageIndex}]`;
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`);
832
- if (!Array.isArray(stage.changes) || stage.changes.length === 0) {
833
- throw new Error(`${stagePath}.changes must be a non-empty array.`);
589
+ },
590
+ addToPath: function addToPath(path, added, removed, oldPosInc) {
591
+ var last = path.lastComponent;
592
+ if (last && last.added === added && last.removed === removed) {
593
+ return {
594
+ oldPos: path.oldPos + oldPosInc,
595
+ lastComponent: {
596
+ count: last.count + 1,
597
+ added,
598
+ removed,
599
+ previousComponent: last.previousComponent
600
+ }
601
+ };
602
+ } else {
603
+ return {
604
+ oldPos: path.oldPos + oldPosInc,
605
+ lastComponent: {
606
+ count: 1,
607
+ added,
608
+ removed,
609
+ previousComponent: last
610
+ }
611
+ };
834
612
  }
835
- if (stageIds.has(stage.id)) throw new Error(`Duplicate deconstructed stage id: ${stage.id}`);
836
- stageIds.add(stage.id);
837
- stage.changes.forEach((change, changeIndex) => validateDeconstructedSelection(
838
- change,
839
- `${stagePath}.changes[${changeIndex}]`,
840
- false
841
- ));
842
- }
843
- if (scene.exclusions !== void 0) {
844
- if (!Array.isArray(scene.exclusions)) throw new Error(`${path}.exclusions must be an array.`);
845
- scene.exclusions.forEach((exclusion, exclusionIndex) => validateDeconstructedSelection(
846
- exclusion,
847
- `${path}.exclusions[${exclusionIndex}]`,
848
- true
849
- ));
850
- }
851
- }
852
- function validateWalkthroughSteps(scene, path, anchors, connectionIds) {
853
- if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
854
- throw new Error(`${path} must contain a non-empty steps array.`);
855
- }
856
- const stepIds = /* @__PURE__ */ new Set();
857
- for (const [stepIndex, step] of scene.steps.entries()) {
858
- const stepPath = `${path}.steps[${stepIndex}]`;
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);
865
- if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
866
- throw new Error(`${stepPath}.depth must be mentioned, explained, or contextualized.`);
613
+ },
614
+ extractCommon: function extractCommon(basePath, newString, oldString, diagonalPath) {
615
+ var newLen = newString.length, oldLen = oldString.length, oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
616
+ while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newString[newPos + 1], oldString[oldPos + 1])) {
617
+ newPos++;
618
+ oldPos++;
619
+ commonCount++;
867
620
  }
868
- validateStepRequirement(step.requirement, `${stepPath}.requirement`);
869
- validateTourImageSource(step.image, `${stepPath}.image`);
870
- if (stepIds.has(step.id)) throw new Error(`Duplicate step id in scene ${scene.id}: ${step.id}`);
871
- stepIds.add(step.id);
872
- if (!anchors[step.focus]) throw new Error(`${stepPath} references unknown anchor ${step.focus}.`);
873
- optionalString(step.connection, `${stepPath}.connection`);
874
- if (typeof step.connection === "string" && !connectionIds.has(step.connection)) {
875
- throw new Error(`${stepPath} references unknown connection ${step.connection}.`);
621
+ if (commonCount) {
622
+ basePath.lastComponent = {
623
+ count: commonCount,
624
+ previousComponent: basePath.lastComponent
625
+ };
626
+ }
627
+ basePath.oldPos = oldPos;
628
+ return newPos;
629
+ },
630
+ equals: function equals(left, right) {
631
+ if (this.options.comparator) {
632
+ return this.options.comparator(left, right);
633
+ } else {
634
+ return left === right || this.options.ignoreCase && left.toLowerCase() === right.toLowerCase();
635
+ }
636
+ },
637
+ removeEmpty: function removeEmpty(array) {
638
+ var ret = [];
639
+ for (var i = 0; i < array.length; i++) {
640
+ if (array[i]) {
641
+ ret.push(array[i]);
642
+ }
643
+ }
644
+ return ret;
645
+ },
646
+ castInput: function castInput(value) {
647
+ return value;
648
+ },
649
+ tokenize: function tokenize(value) {
650
+ return value.split("");
651
+ },
652
+ join: function join(chars) {
653
+ return chars.join("");
654
+ }
655
+ };
656
+ function buildValues(diff2, lastComponent, newString, oldString, useLongestToken) {
657
+ var components = [];
658
+ var nextComponent;
659
+ while (lastComponent) {
660
+ components.push(lastComponent);
661
+ nextComponent = lastComponent.previousComponent;
662
+ delete lastComponent.previousComponent;
663
+ lastComponent = nextComponent;
664
+ }
665
+ components.reverse();
666
+ var componentPos = 0, componentLen = components.length, newPos = 0, oldPos = 0;
667
+ for (; componentPos < componentLen; componentPos++) {
668
+ var component = components[componentPos];
669
+ if (!component.removed) {
670
+ if (!component.added && useLongestToken) {
671
+ var value = newString.slice(newPos, newPos + component.count);
672
+ value = value.map(function(value2, i) {
673
+ var oldValue = oldString[oldPos + i];
674
+ return oldValue.length > value2.length ? oldValue : value2;
675
+ });
676
+ component.value = diff2.join(value);
677
+ } else {
678
+ component.value = diff2.join(newString.slice(newPos, newPos + component.count));
679
+ }
680
+ newPos += component.count;
681
+ if (!component.added) {
682
+ oldPos += component.count;
683
+ }
684
+ } else {
685
+ component.value = diff2.join(oldString.slice(oldPos, oldPos + component.count));
686
+ oldPos += component.count;
687
+ if (componentPos && components[componentPos - 1].added) {
688
+ var tmp = components[componentPos - 1];
689
+ components[componentPos - 1] = components[componentPos];
690
+ components[componentPos] = tmp;
691
+ }
876
692
  }
877
693
  }
878
- }
879
- function validateDeconstructedSelection(value, path, requiresReason) {
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`);
883
- if (!requiresReason || value.hunks !== void 0) {
884
- requireStringArray2(value.hunks, `${path}.hunks`);
885
- if (value.hunks.length === 0) throw new Error(`${path}.hunks must not be empty.`);
694
+ var finalComponent = components[componentLen - 1];
695
+ if (componentLen > 1 && typeof finalComponent.value === "string" && (finalComponent.added || finalComponent.removed) && diff2.equals("", finalComponent.value)) {
696
+ components[componentLen - 2].value += finalComponent.value;
697
+ components.pop();
886
698
  }
887
- if (requiresReason) requireString2(value.reason, `${path}.reason`);
699
+ return components;
888
700
  }
889
- function validateRealStack(stack, path, minimum) {
890
- if (!Array.isArray(stack) || stack.length < minimum || stack.length > 6) {
891
- throw new Error(`${path} must contain between ${minimum} and 6 explicit real revisions.`);
701
+ var characterDiff = new Diff();
702
+ function diffChars(oldStr, newStr, options) {
703
+ return characterDiff.diff(oldStr, newStr, options);
704
+ }
705
+ var extendedWordChars = /^[A-Za-z\xC0-\u02C6\u02C8-\u02D7\u02DE-\u02FF\u1E00-\u1EFF]+$/;
706
+ var reWhitespace = /\S/;
707
+ var wordDiff = new Diff();
708
+ wordDiff.equals = function(left, right) {
709
+ if (this.options.ignoreCase) {
710
+ left = left.toLowerCase();
711
+ right = right.toLowerCase();
892
712
  }
893
- const ids = /* @__PURE__ */ new Set();
894
- for (const [index, entry] of stack.entries()) {
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`);
899
- optionalString(entry.label, `${path}[${index}].label`);
900
- if (ids.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
901
- ids.add(entry.id);
713
+ return left === right || this.options.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right);
714
+ };
715
+ wordDiff.tokenize = function(value) {
716
+ var tokens = value.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/);
717
+ for (var i = 0; i < tokens.length - 1; i++) {
718
+ if (!tokens[i + 1] && tokens[i + 2] && extendedWordChars.test(tokens[i]) && extendedWordChars.test(tokens[i + 2])) {
719
+ tokens[i] += tokens[i + 2];
720
+ tokens.splice(i + 1, 2);
721
+ i--;
722
+ }
902
723
  }
724
+ return tokens;
725
+ };
726
+ function diffWordsWithSpace(oldStr, newStr, options) {
727
+ return wordDiff.diff(oldStr, newStr, options);
903
728
  }
904
- function validateStackedScene(scene, path, minimum = 3) {
905
- requireOnlyKeys2(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "stack", "files", "steps"], path);
906
- if (!Array.isArray(scene.stack) || scene.stack.length < minimum || scene.stack.length > 6) {
907
- throw new Error(`${path}.stack must contain between ${minimum} and 6 revisions.`);
729
+ var lineDiff = new Diff();
730
+ lineDiff.tokenize = function(value) {
731
+ if (this.options.stripTrailingCr) {
732
+ value = value.replace(/\r\n/g, "\n");
908
733
  }
909
- const stackIds = /* @__PURE__ */ new Set();
910
- for (const [index, entry] of scene.stack.entries()) {
911
- const entryPath = `${path}.stack[${index}]`;
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`);
916
- optionalString(entry.label, `${entryPath}.label`);
917
- if (stackIds.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
918
- stackIds.add(entry.id);
734
+ var retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
735
+ if (!linesAndNewlines[linesAndNewlines.length - 1]) {
736
+ linesAndNewlines.pop();
919
737
  }
920
- if (scene.files !== void 0) requireStringArray2(scene.files, `${path}.files`);
921
- if (!Array.isArray(scene.steps) || scene.steps.length === 0) throw new Error(`${path}.steps must be a non-empty array.`);
922
- const stepIds = /* @__PURE__ */ new Set();
923
- for (const [index, step] of scene.steps.entries()) {
924
- const stepPath = `${path}.steps[${index}]`;
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`);
931
- const stepId = step.id;
932
- if (stepIds.has(stepId)) throw new Error(`Duplicate step id in scene ${scene.id}: ${stepId}`);
933
- stepIds.add(stepId);
934
- if (!Array.isArray(step.pair) || step.pair.length !== 2 || step.pair.some((id) => typeof id !== "string")) {
935
- throw new Error(`${stepPath}.pair must contain two stack entry ids.`);
936
- }
937
- const pair = step.pair;
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]);
940
- if (leftIndex < 0 || rightIndex !== leftIndex + 1) throw new Error(`${stepPath}.pair must reference adjacent stack entries in order.`);
941
- if (step.side !== void 0 && step.side !== "left" && step.side !== "right") throw new Error(`${stepPath}.side must be left or right.`);
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]))) {
943
- throw new Error(`${stepPath}.lines must contain an ordered positive line range.`);
738
+ for (var i = 0; i < linesAndNewlines.length; i++) {
739
+ var line = linesAndNewlines[i];
740
+ if (i % 2 && !this.options.newlineIsToken) {
741
+ retLines[retLines.length - 1] += line;
742
+ } else {
743
+ if (this.options.ignoreWhitespace) {
744
+ line = line.trim();
745
+ }
746
+ retLines.push(line);
944
747
  }
945
748
  }
749
+ return retLines;
750
+ };
751
+ function diffLines(oldStr, newStr, callback) {
752
+ return lineDiff.diff(oldStr, newStr, callback);
946
753
  }
947
- function validateStepRequirement(value, path) {
948
- if (value === void 0) return;
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`);
953
- if (value.status !== void 0 && !["fulfilled", "gap"].includes(String(value.status))) {
954
- throw new Error(`${path}.status must be fulfilled or gap.`);
955
- }
956
- optionalString(value.source, `${path}.source`);
957
- if (value.confidence !== void 0 && !["high", "medium", "low"].includes(String(value.confidence))) {
958
- throw new Error(`${path}.confidence must be high, medium, or low.`);
754
+ var sentenceDiff = new Diff();
755
+ sentenceDiff.tokenize = function(value) {
756
+ return value.split(/(\S.+?[.!?])(?=\s+|$)/);
757
+ };
758
+ var cssDiff = new Diff();
759
+ cssDiff.tokenize = function(value) {
760
+ return value.split(/([{}:;,]|\s+)/);
761
+ };
762
+ function _typeof(obj) {
763
+ "@babel/helpers - typeof";
764
+ if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
765
+ _typeof = function(obj2) {
766
+ return typeof obj2;
767
+ };
768
+ } else {
769
+ _typeof = function(obj2) {
770
+ return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
771
+ };
959
772
  }
773
+ return _typeof(obj);
960
774
  }
961
- function validateSceneOverview(value, path, options = {}) {
962
- if (value === void 0) return;
963
- if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
964
- requireOnlyKeys2(value, ["kind", "path", "comparison", "purpose"], path);
965
- if (value.kind !== "directory-diff") {
966
- throw new Error(`${path}.kind must be directory-diff.`);
967
- }
968
- if (value.purpose !== void 0) requireString2(value.purpose, `${path}.purpose`);
969
- if (value.path !== void 0) validateDirectoryOverviewPath(value.path, `${path}.path`);
970
- if (value.comparison === void 0) return;
971
- if (options.allowComparison === false) {
972
- throw new Error(`${path}.comparison is not supported for walkthrough scenes; use the fixed base-to-head comparison.`);
973
- }
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`);
978
- if (value.comparison.from === value.comparison.to) {
979
- throw new Error(`${path}.comparison.from and to must identify distinct endpoints.`);
775
+ var objectPrototypeToString = Object.prototype.toString;
776
+ var jsonDiff = new Diff();
777
+ jsonDiff.useLongestToken = true;
778
+ jsonDiff.tokenize = lineDiff.tokenize;
779
+ jsonDiff.castInput = function(value) {
780
+ var _this$options = this.options, undefinedReplacement = _this$options.undefinedReplacement, _this$options$stringi = _this$options.stringifyReplacer, stringifyReplacer = _this$options$stringi === void 0 ? function(k, v) {
781
+ return typeof v === "undefined" ? undefinedReplacement : v;
782
+ } : _this$options$stringi;
783
+ return typeof value === "string" ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, " ");
784
+ };
785
+ jsonDiff.equals = function(left, right) {
786
+ return Diff.prototype.equals.call(jsonDiff, left.replace(/,([\r\n])/g, "$1"), right.replace(/,([\r\n])/g, "$1"));
787
+ };
788
+ function canonicalize(obj, stack, replacementStack, replacer, key) {
789
+ stack = stack || [];
790
+ replacementStack = replacementStack || [];
791
+ if (replacer) {
792
+ obj = replacer(key, obj);
980
793
  }
981
- if (options.endpointIds !== void 0) {
982
- for (const endpoint of [value.comparison.from, value.comparison.to]) {
983
- if (!options.endpointIds.includes(endpoint)) {
984
- throw new Error(`${path}.comparison endpoint ${endpoint} is not defined by this scene.`);
985
- }
794
+ var i;
795
+ for (i = 0; i < stack.length; i += 1) {
796
+ if (stack[i] === obj) {
797
+ return replacementStack[i];
986
798
  }
987
799
  }
988
- }
989
- function validateDirectoryOverviewPath(value, path) {
990
- requireString2(value, path);
991
- if (value.includes("\\")) {
992
- throw new Error(`${path} must use a relative POSIX directory path without backslashes.`);
800
+ var canonicalizedObj;
801
+ if ("[object Array]" === objectPrototypeToString.call(obj)) {
802
+ stack.push(obj);
803
+ canonicalizedObj = new Array(obj.length);
804
+ replacementStack.push(canonicalizedObj);
805
+ for (i = 0; i < obj.length; i += 1) {
806
+ canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key);
807
+ }
808
+ stack.pop();
809
+ replacementStack.pop();
810
+ return canonicalizedObj;
993
811
  }
994
- if (value.startsWith("/") || /^[A-Za-z]:/.test(value)) {
995
- throw new Error(`${path} must be relative to the repository root.`);
812
+ if (obj && obj.toJSON) {
813
+ obj = obj.toJSON();
996
814
  }
997
- if (value.split("/").some((segment) => segment === "..")) {
998
- throw new Error(`${path} must not contain parent-directory traversal.`);
815
+ if (_typeof(obj) === "object" && obj !== null) {
816
+ stack.push(obj);
817
+ canonicalizedObj = {};
818
+ replacementStack.push(canonicalizedObj);
819
+ var sortedKeys = [], _key;
820
+ for (_key in obj) {
821
+ if (obj.hasOwnProperty(_key)) {
822
+ sortedKeys.push(_key);
823
+ }
824
+ }
825
+ sortedKeys.sort();
826
+ for (i = 0; i < sortedKeys.length; i += 1) {
827
+ _key = sortedKeys[i];
828
+ canonicalizedObj[_key] = canonicalize(obj[_key], stack, replacementStack, replacer, _key);
829
+ }
830
+ stack.pop();
831
+ replacementStack.pop();
832
+ } else {
833
+ canonicalizedObj = obj;
999
834
  }
835
+ return canonicalizedObj;
1000
836
  }
1001
- function validateNarrative(value, path) {
1002
- requireString2(value.summary, `${path}.summary`);
1003
- requireStringArray2(value.bullets, `${path}.bullets`);
1004
- requireStringArray2(value.tags, `${path}.tags`);
1005
- requireString2(value.takeaway, `${path}.takeaway`);
837
+ var arrayDiff = new Diff();
838
+ arrayDiff.tokenize = function(value) {
839
+ return value.slice();
840
+ };
841
+ arrayDiff.join = arrayDiff.removeEmpty = function(value) {
842
+ return value;
843
+ };
844
+ function diffArrays(oldArr, newArr, callback) {
845
+ return arrayDiff.diff(oldArr, newArr, callback);
1006
846
  }
1007
- function isRecord2(value) {
1008
- return typeof value === "object" && value !== null && !Array.isArray(value);
847
+
848
+ // src/changeCorrespondence.ts
849
+ var MAX_BYTES = 16 * 1024 * 1024;
850
+ var MAX_SOURCE_BYTES = 8 * 1024 * 1024;
851
+ var digest = (text) => (0, import_crypto2.createHash)("sha256").update(text, "utf8").digest("hex");
852
+ function object(value) {
853
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid correspondence object.");
854
+ return value;
1009
855
  }
1010
- function requireString2(value, path) {
1011
- if (typeof value !== "string" || value.length === 0) {
1012
- throw new Error(`${path} must be a non-empty string.`);
1013
- }
856
+ function list(value, max = 2e4) {
857
+ if (!Array.isArray(value) || value.length > max) throw new Error("Invalid or over-budget correspondence array.");
858
+ return value;
1014
859
  }
1015
- function optionalString(value, path) {
1016
- if (value !== void 0) {
1017
- requireString2(value, path);
1018
- }
860
+ function string(value) {
861
+ if (typeof value !== "string" || !value || value.length > 4096) throw new Error("Invalid correspondence string.");
862
+ return value;
1019
863
  }
1020
- function requireProse2(value, path) {
1021
- if (typeof value !== "string" || !value.trim()) {
1022
- throw new Error(`${path} must be a non-empty string.`);
1023
- }
864
+ function integer(value) {
865
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error("Invalid correspondence offset.");
866
+ return value;
1024
867
  }
1025
- function requireStringArray2(value, path) {
1026
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
1027
- throw new Error(`${path} must be an array of strings.`);
868
+ function filePath(value) {
869
+ const name = string(value);
870
+ if (name.startsWith("/") || name.includes("\\") || name.includes("\0") || name.split("/").some((p) => !p || p === "." || p === "..")) {
871
+ throw new Error("Invalid correspondence repository path.");
1028
872
  }
873
+ return name;
1029
874
  }
1030
- function requireOnlyKeys2(value, allowed, path) {
1031
- const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
1032
- if (unexpected.length > 0) {
1033
- throw new Error(`${path} contains unknown field${unexpected.length === 1 ? "" : "s"}: ${unexpected.join(", ")}`);
1034
- }
875
+ function hash(value) {
876
+ const result = string(value);
877
+ if (!/^[0-9a-f]{64}$/.test(result)) throw new Error("Invalid correspondence digest.");
878
+ return result;
1035
879
  }
1036
-
1037
- // src/changeTourManifest.ts
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) {
1041
- throw new Error(
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.`
1043
- );
1044
- }
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) {
1049
- throw new Error(`Unsupported or missing change-tour manifest version.`);
1050
- }
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");
1055
- if (value.windowTitle !== void 0) {
1056
- requireString3(value.windowTitle, "windowTitle");
1057
- }
1058
- if (value.sourceUrl !== void 0) {
1059
- requireString3(value.sourceUrl, "sourceUrl");
1060
- }
1061
- if (!isRecord3(value.range)) {
1062
- throw new Error("Change-tour manifest range must be an object.");
1063
- }
1064
- for (const key of ["baseRef", "headRef", "mergeBaseOid", "headOid"]) {
1065
- requireString3(value.range[key], `range.${key}`);
1066
- }
1067
- if (!isRecord3(value.summary)) {
1068
- throw new Error("Change-tour manifest summary must be an object.");
1069
- }
1070
- for (const key of ["changedFiles", "includedScenes", "additions", "deletions", "commitCount"]) {
1071
- requireNonNegativeInteger(value.summary[key], `summary.${key}`);
1072
- }
1073
- requireStringArray3(value.summary.omittedFiles, "summary.omittedFiles");
1074
- if (!Array.isArray(value.commits) || !Array.isArray(value.chapters) || !Array.isArray(value.scenes)) {
1075
- throw new Error("Change-tour manifest commits, chapters, and scenes must be arrays.");
1076
- }
1077
- if (value.files !== void 0 && !Array.isArray(value.files)) {
1078
- throw new Error("Change-tour manifest files must be an array.");
1079
- }
1080
- const sceneIds = /* @__PURE__ */ new Set();
1081
- for (const [index, candidate] of value.scenes.entries()) {
1082
- validateScene(candidate, index, value.version);
1083
- if (sceneIds.has(candidate.id)) {
1084
- throw new Error(`Duplicate change-tour scene id: ${candidate.id}`);
1085
- }
1086
- sceneIds.add(candidate.id);
1087
- }
1088
- for (const [index, candidate] of value.chapters.entries()) {
1089
- if (!isRecord3(candidate)) {
1090
- throw new Error(`chapters[${index}] must be an object.`);
1091
- }
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`);
1096
- for (const sceneId of candidate.sceneIds) {
1097
- if (!sceneIds.has(sceneId)) {
1098
- throw new Error(`Chapter references unknown scene id: ${sceneId}`);
880
+ function status(value) {
881
+ if (value !== "complete" && value !== "partial" && value !== "unavailable") throw new Error("Invalid correspondence analysis status.");
882
+ return value;
883
+ }
884
+ function readCorrespondenceJson(file) {
885
+ if ((0, import_fs.statSync)(file).size > MAX_BYTES) throw new Error("Correspondence file exceeds 16 MiB.");
886
+ return JSON.parse((0, import_fs.readFileSync)(file, "utf8"));
887
+ }
888
+ function validateRange(value, expected) {
889
+ const input = object(value);
890
+ for (const key of ["baseOid", "headOid"]) {
891
+ if (input[key] !== expected[key] || !/^[0-9a-f]{40,64}$/.test(expected[key])) {
892
+ throw new Error("Correspondence revisions do not match the selected merge-base/head comparison.");
893
+ }
894
+ }
895
+ return { ...expected };
896
+ }
897
+ function validateChangeCorrespondence(repoRoot, input, expected) {
898
+ const data = object(input);
899
+ if (data.version !== 1) throw new Error("Unsupported correspondence version.");
900
+ const range = validateRange(data.range, expected);
901
+ const documents = object(data.documents), symbols = object(data.symbols);
902
+ const result = {
903
+ version: 1,
904
+ range,
905
+ producer: string(data.producer),
906
+ status: status(data.status),
907
+ documents: { before: [], after: [] },
908
+ symbols: { before: [], after: [] },
909
+ relations: [],
910
+ unresolved: [],
911
+ limitations: list(data.limitations, 100).map(string)
912
+ };
913
+ const entries = /* @__PURE__ */ new Map();
914
+ const documentsBySide = { before: /* @__PURE__ */ new Map(), after: /* @__PURE__ */ new Map() };
915
+ const lineIndexes = /* @__PURE__ */ new Map();
916
+ const lineAt = (side, path, start) => {
917
+ const key = `${side}:${path}`;
918
+ let lines = lineIndexes.get(key);
919
+ if (!lines) {
920
+ const text = documentsBySide[side].get(path);
921
+ lines = [0];
922
+ for (let at = text.indexOf("\n"); at !== -1; at = text.indexOf("\n", at + 1)) lines.push(at + 1);
923
+ lineIndexes.set(key, lines);
924
+ }
925
+ let low = 0, high = lines.length;
926
+ while (low < high) {
927
+ const mid = Math.floor((low + high) / 2);
928
+ if (lines[mid] <= start) low = mid + 1;
929
+ else high = mid;
930
+ }
931
+ return low;
932
+ };
933
+ let sourceBytes = 0, symbolBytes = 0;
934
+ for (const side of ["before", "after"]) {
935
+ const texts = /* @__PURE__ */ new Map();
936
+ const oid = side === "before" ? range.baseOid : range.headOid;
937
+ for (const value of list(documents[side], 128)) {
938
+ const doc = object(value), name = filePath(doc.path), expectedDigest = hash(doc.digest);
939
+ const documentStatus = string(doc.status);
940
+ if (!["complete", "invalid", "unsupported"].includes(documentStatus) || texts.has(name)) throw new Error("Invalid or duplicate correspondence document.");
941
+ const bytes = (0, import_child_process3.execFileSync)("git", ["show", `${oid}:${name}`], { cwd: repoRoot, timeout: 1e4, maxBuffer: 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
942
+ const text = bytes.toString("utf8");
943
+ sourceBytes += bytes.length;
944
+ if (sourceBytes > MAX_SOURCE_BYTES || !Buffer.from(text, "utf8").equals(bytes) || digest(text) !== expectedDigest) {
945
+ throw new Error(`Correspondence source digest mismatch or source budget exceeded: ${side}/${name}`);
1099
946
  }
1100
- }
1101
- }
1102
- if (value.summary.includedScenes !== value.scenes.length) {
1103
- throw new Error("summary.includedScenes must match the number of scenes.");
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
- }
1109
- const files = value.files === void 0 ? collectLegacyTourFiles(value.scenes) : value.files.map((candidate, index) => {
1110
- validateTourFile(candidate, index);
1111
- return candidate;
1112
- });
1113
- if (value.files !== void 0 && value.summary.changedFiles !== files.length) {
1114
- throw new Error("summary.changedFiles must match the number of files.");
947
+ texts.set(name, { text, status: documentStatus });
948
+ documentsBySide[side].set(name, text);
949
+ result.documents[side].push({ path: name, digest: expectedDigest, status: documentStatus, text });
950
+ }
951
+ for (const value of list(symbols[side])) {
952
+ const entry = object(value), name = filePath(entry.path), doc = texts.get(name);
953
+ const id = string(entry.id), start = integer(entry.start), end = integer(entry.end);
954
+ if (!doc || start >= end || end > doc.text.length || entries.has(id)) throw new Error("Invalid correspondence symbol ownership/range/ID.");
955
+ const text = doc.text.slice(start, end);
956
+ symbolBytes += Buffer.byteLength(text);
957
+ if (symbolBytes > MAX_SOURCE_BYTES || digest(text) !== hash(entry.digest)) throw new Error(`Correspondence symbol digest mismatch or budget exceeded: ${name}`);
958
+ const symbol = {
959
+ id,
960
+ path: name,
961
+ name: string(entry.name),
962
+ kind: string(entry.kind),
963
+ start,
964
+ end,
965
+ digest: digest(text),
966
+ text,
967
+ startLine: lineAt(side, name, start),
968
+ endLine: lineAt(side, name, end - 1)
969
+ };
970
+ entries.set(id, { side, symbol, documentStatus: doc.status });
971
+ result.symbols[side].push(symbol);
972
+ }
973
+ }
974
+ const ids = /* @__PURE__ */ new Set(), sources = /* @__PURE__ */ new Set(), destinations = /* @__PURE__ */ new Set();
975
+ for (const value of list(data.relations)) {
976
+ const relation = object(value), id = string(relation.id), sourceId = string(relation.sourceId), destinationId = string(relation.destinationId);
977
+ const source = entries.get(sourceId), destination = entries.get(destinationId);
978
+ if (ids.has(id) || sources.has(sourceId) || destinations.has(destinationId)) throw new Error("Conflicting correspondence continuations; preserve them as unresolved candidates.");
979
+ if ((source == null ? void 0 : source.side) !== "before" || (destination == null ? void 0 : destination.side) !== "after" || source.documentStatus !== "complete" || destination.documentStatus !== "complete") {
980
+ throw new Error("Correspondence endpoints must resolve to valid, parsed before/after documents.");
981
+ }
982
+ ids.add(id);
983
+ sources.add(sourceId);
984
+ destinations.add(destinationId);
985
+ const producers = list(relation.producers, 10).map(string);
986
+ if (relation.coverage !== void 0 && relation.coverage !== "complete") throw new Error("Invalid correspondence relation coverage.");
987
+ const covered = result.status === "partial" && relation.coverage === "complete" && producers.some((producer) => ["refactoring-miner", "bygone-strand"].includes(producer));
988
+ if (result.status !== "complete" && !covered && !(producers.includes("exact-declaration") && source.symbol.text === destination.symbol.text)) continue;
989
+ const a = source.symbol, b = destination.symbol;
990
+ result.relations.push({
991
+ id,
992
+ sourceId,
993
+ destinationId,
994
+ producers,
995
+ pathChanged: a.path !== b.path,
996
+ nameChanged: a.name !== b.name,
997
+ ...relation.coverage === "complete" ? { coverage: "complete" } : {},
998
+ rawEqual: a.text === b.text,
999
+ behavior: "unassessed",
1000
+ localDiff: (diffLines(a.text, b.text, { timeout: 100, maxEditLength: 2e3 }) || [{ value: a.text, removed: true }, { value: b.text, added: true }]).map((part) => ({
1001
+ value: part.value,
1002
+ ...part.added ? { added: true } : {},
1003
+ ...part.removed ? { removed: true } : {}
1004
+ }))
1005
+ });
1115
1006
  }
1116
- if (value.authoringCoverage !== void 0) {
1117
- validateAuthoringCoverage(value.authoringCoverage, value.version);
1007
+ for (const value of list(data.unresolved)) {
1008
+ const candidate = object(value);
1009
+ const sourceIds = list(candidate.sourceIds).map(string), destinationIds = list(candidate.destinationIds).map(string);
1010
+ if (!sourceIds.length && !destinationIds.length) throw new Error("Unresolved correspondence needs candidate endpoints.");
1011
+ if (sourceIds.some((id) => {
1012
+ var _a;
1013
+ return ((_a = entries.get(id)) == null ? void 0 : _a.side) !== "before";
1014
+ }) || destinationIds.some((id) => {
1015
+ var _a;
1016
+ return ((_a = entries.get(id)) == null ? void 0 : _a.side) !== "after";
1017
+ })) {
1018
+ throw new Error("Unresolved correspondence references unknown endpoints.");
1019
+ }
1020
+ if (sourceIds.some((id) => sources.has(id)) || destinationIds.some((id) => destinations.has(id))) throw new Error("Ambiguous endpoints cannot also be accepted continuations.");
1021
+ result.unresolved.push({ reason: string(candidate.reason), sourceIds, destinationIds });
1022
+ }
1023
+ if (data.diagnostics !== void 0) {
1024
+ const diagnostics = object(data.diagnostics);
1025
+ if (diagnostics.version !== 1) throw new Error("Unsupported correspondence diagnostics version.");
1026
+ let locationCount = 0;
1027
+ result.diagnostics = { version: 1, scope: list(diagnostics.scope, 20).map(string), records: list(diagnostics.records).map((value) => {
1028
+ const record2 = object(value), category = string(record2.category);
1029
+ if (!["outside", "syntax", "rejected", "unmatched", "relationship", "problem"].includes(category)) throw new Error("Invalid correspondence diagnostic category.");
1030
+ return {
1031
+ category,
1032
+ reason: string(record2.reason),
1033
+ ...record2.message !== void 0 ? { message: string(record2.message) } : {},
1034
+ ...record2.producerId !== void 0 ? { producerId: string(record2.producerId) } : {},
1035
+ locations: list(record2.locations, 100).map((value2) => {
1036
+ var _a, _b;
1037
+ if (++locationCount > 4e4) throw new Error("Correspondence diagnostic location budget exceeded.");
1038
+ const location = object(value2), path = filePath(location.path);
1039
+ const side = location.side;
1040
+ if (side !== void 0 && side !== "before" && side !== "after") throw new Error("Invalid diagnostic revision side.");
1041
+ const symbolId = location.symbolId === void 0 ? void 0 : string(location.symbolId);
1042
+ if (symbolId && (((_a = entries.get(symbolId)) == null ? void 0 : _a.side) !== side || ((_b = entries.get(symbolId)) == null ? void 0 : _b.symbol.path) !== path)) throw new Error("Invalid diagnostic symbol reference.");
1043
+ const start = location.start === void 0 ? symbolId ? entries.get(symbolId).symbol.start : void 0 : integer(location.start);
1044
+ const text = side ? documentsBySide[side].get(path) : void 0;
1045
+ if (start !== void 0 && (text === void 0 || start > text.length)) throw new Error("Invalid diagnostic source location.");
1046
+ return {
1047
+ path,
1048
+ ...side ? { side } : {},
1049
+ ...symbolId ? { symbolId } : {},
1050
+ ...start !== void 0 ? { start, line: lineAt(side, path, start) } : {},
1051
+ ...location.reason !== void 0 ? { reason: string(location.reason) } : {}
1052
+ };
1053
+ })
1054
+ };
1055
+ }) };
1118
1056
  }
1119
- if (value.tours !== void 0) {
1120
- if (value.version !== 4 && value.version !== 5) throw new Error("Independent tours require manifest version 4.");
1121
- validateModeTours(value.tours, value.version);
1057
+ return result;
1058
+ }
1059
+ function loadChangeCorrespondence(repoRoot, file, range) {
1060
+ return validateChangeCorrespondence(repoRoot, readCorrespondenceJson(file), range);
1061
+ }
1062
+
1063
+ // src/tourSymbol.ts
1064
+ function compileTourSymbol(evidence, selector, focus) {
1065
+ let before, after;
1066
+ let status2 = "inferred";
1067
+ if (selector.relation) {
1068
+ const relation = evidence.relations.find((item) => item.id === selector.relation);
1069
+ if (!relation) throw new Error(`No accepted correspondence: ${selector.relation}`);
1070
+ before = evidence.symbols.before.find((item) => item.id === relation.sourceId);
1071
+ after = evidence.symbols.after.find((item) => item.id === relation.destinationId);
1072
+ } else {
1073
+ const id = selector.symbol;
1074
+ if (evidence.relations.some((item) => item.sourceId === id || item.destinationId === id)) throw new Error("Matched symbols must use their accepted relation ID.");
1075
+ before = evidence.symbols.before.find((item) => item.id === id);
1076
+ after = evidence.symbols.after.find((item) => item.id === id);
1077
+ if (!before && !after) throw new Error(`Unknown correspondence symbol: ${id}`);
1078
+ status2 = evidence.unresolved.some((group) => [...group.sourceIds, ...group.destinationIds].includes(id)) ? "ambiguous" : "unmatched";
1079
+ }
1080
+ const endpoint = focus.revision === "base" ? before : after;
1081
+ if (!endpoint || endpoint.path !== focus.path || focus.startLine < endpoint.startLine || focus.endLine > endpoint.endLine) throw new Error("Symbol stop focus must lie within the selected declaration on its revision side.");
1082
+ const text = (side, entry) => entry ? evidence.documents[side].find((doc) => doc.path === entry.path).text : "";
1083
+ const result = {
1084
+ id: selector.relation || selector.symbol,
1085
+ range: evidence.range,
1086
+ status: status2,
1087
+ before,
1088
+ after,
1089
+ context: { leftContent: text("before", before), rightContent: text("after", after) },
1090
+ behavior: "unassessed"
1091
+ };
1092
+ validateTourSymbol(result);
1093
+ return result;
1094
+ }
1095
+ function validateTourSymbol(value) {
1096
+ const fail = () => {
1097
+ throw new Error("Invalid pinned symbol tour evidence.");
1098
+ };
1099
+ if (!value || typeof value !== "object") fail();
1100
+ const data = value;
1101
+ if (typeof data.id !== "string" || !data.id.trim() || !data.range || ![data.range.baseOid, data.range.headOid].every((oid) => typeof oid === "string" && /^[0-9a-f]{40,64}$/.test(oid)) || !["inferred", "unmatched", "ambiguous"].includes(data.status) || data.behavior !== "unassessed" || !data.context) fail();
1102
+ const count = Number(!!data.before) + Number(!!data.after);
1103
+ if (count !== (data.status === "inferred" ? 2 : 1)) fail();
1104
+ for (const [entry, content] of [[data.before, data.context.leftContent], [data.after, data.context.rightContent]]) {
1105
+ if (typeof content !== "string") fail();
1106
+ if (!entry) {
1107
+ if (content !== "") fail();
1108
+ continue;
1109
+ }
1110
+ if (typeof entry.id !== "string" || !entry.id || typeof entry.name !== "string" || !entry.name || typeof entry.kind !== "string" || !entry.kind || typeof entry.path !== "string" || entry.path.includes("\0") || entry.path.startsWith("/") || entry.path.includes("\\") || entry.path.split("/").some((part) => !part || part === "." || part === "..") || typeof entry.digest !== "string" || !/^[0-9a-f]{64}$/.test(entry.digest) || !Number.isSafeInteger(entry.start) || !Number.isSafeInteger(entry.end) || entry.start < 0 || entry.end <= entry.start || entry.end > content.length || content.slice(entry.start, entry.end) !== entry.text || entry.startLine !== content.slice(0, entry.start).split("\n").length || entry.endLine !== content.slice(0, entry.end - 1).split("\n").length) fail();
1122
1111
  }
1123
- if (value.version !== 1) validateZoom(value, value.version, Boolean(options.exported));
1124
- return { ...value, files };
1125
1112
  }
1126
- function validateAuthoringCoverage(value, version) {
1127
- if (!isRecord3(value) || !isRecord3(value.walkthrough) || !Array.isArray(value.explanationAssignments)) {
1128
- throw new Error("authoringCoverage must contain walkthrough and explanationAssignments.");
1113
+
1114
+ // src/tourImage.ts
1115
+ var MAX_TOUR_IMAGE_BYTES = 8 * 1024 * 1024;
1116
+ function record(value) {
1117
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1118
+ }
1119
+ function validPath(value) {
1120
+ return typeof value === "string" && value.length > 0 && !/[\\\x00-\x1f:]/.test(value) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
1121
+ }
1122
+ function validateTourImageSource(value, label) {
1123
+ if (value === void 0) return;
1124
+ 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()) {
1125
+ throw new Error(`${label} must contain a repository-relative file, base/head revision, and non-empty alt text.`);
1129
1126
  }
1130
- const expectedScope = version >= 2 ? "final" : "tour";
1131
- if (value.walkthrough.scope !== expectedScope) {
1132
- throw new Error(`authoringCoverage.walkthrough.scope must be ${expectedScope}.`);
1127
+ }
1128
+ function validateTourImage(value, label) {
1129
+ if (value === void 0) return;
1130
+ 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,")) {
1131
+ throw new Error(`${label} must contain pinned, embedded PNG evidence and alt text.`);
1133
1132
  }
1134
- requireNonNegativeInteger(value.walkthrough.coveredUnits, "authoringCoverage.walkthrough.coveredUnits");
1135
- requireNonNegativeInteger(value.walkthrough.includedUnits, "authoringCoverage.walkthrough.includedUnits");
1136
- requirePercentage(value.walkthrough.coveragePercent, "authoringCoverage.walkthrough.coveragePercent");
1137
- if (value.walkthrough.coveredUnits > value.walkthrough.includedUnits) {
1138
- throw new Error("authoringCoverage.walkthrough.coveredUnits must not exceed includedUnits.");
1133
+ const base64 = value.dataUrl.slice(22);
1134
+ const decodedBytes = base64.length / 4 * 3 - (base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0);
1135
+ 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.`);
1136
+ const header = Uint8Array.from(atob(base64.slice(0, 44)), (character) => character.charCodeAt(0));
1137
+ validatePngHeader(header, label);
1138
+ }
1139
+ function validatePngHeader(bytes, label) {
1140
+ const signature = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82];
1141
+ if (bytes.length < 33 || signature.some((byte, index) => bytes[index] !== byte)) {
1142
+ throw new Error(`${label} must be a PNG with an IHDR header.`);
1139
1143
  }
1140
- for (const [index, assignment] of value.explanationAssignments.entries()) {
1141
- const path = `authoringCoverage.explanationAssignments[${index}]`;
1142
- if (!isRecord3(assignment)) throw new Error(`${path} must be an object.`);
1143
- requireString3(assignment.sceneId, `${path}.sceneId`);
1144
- requireNonNegativeInteger(assignment.assignedUnits, `${path}.assignedUnits`);
1145
- requireNonNegativeInteger(assignment.excludedUnits, `${path}.excludedUnits`);
1146
- requireNonNegativeInteger(assignment.completeUnits, `${path}.completeUnits`);
1147
- requireNonNegativeInteger(assignment.totalUnits, `${path}.totalUnits`);
1148
- requirePercentage(assignment.assignmentPercent, `${path}.assignmentPercent`);
1149
- if (assignment.assignedUnits + assignment.excludedUnits !== assignment.completeUnits || assignment.completeUnits > assignment.totalUnits) {
1150
- throw new Error(`${path} contains inconsistent unit counts.`);
1151
- }
1144
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1145
+ const width = view.getUint32(16), height = view.getUint32(20);
1146
+ if (!width || !height || width > 16384 || height > 16384 || width * height > 64 * 1024 * 1024) {
1147
+ throw new Error(`${label} exceeds the PNG dimension limit.`);
1152
1148
  }
1153
1149
  }
1154
- function validateModeTours(value, version) {
1155
- if (!isRecord3(value)) throw new Error("Change-tour manifest tours must be an object.");
1156
- if (value.historical === void 0 && value.deconstructed === void 0) {
1157
- throw new Error("Change-tour manifest tours must contain a historical or deconstructed tour.");
1158
- }
1159
- for (const mode of ["historical", "deconstructed"]) {
1160
- const tour = value[mode];
1161
- if (tour === void 0) continue;
1162
- if (!isRecord3(tour) || !Array.isArray(tour.chapters) || !Array.isArray(tour.scenes) || tour.chapters.length === 0 || tour.scenes.length === 0) {
1163
- throw new Error(`Change-tour manifest tours.${mode} must contain non-empty chapters and scenes arrays.`);
1164
- }
1165
- validateTourFraming(tour, version, `tours.${mode}`);
1166
- const sceneIds = /* @__PURE__ */ new Set();
1167
- let hasDeconstructed = false;
1168
- for (const [index, scene] of tour.scenes.entries()) {
1169
- validateScene(scene, index, version);
1170
- if (sceneIds.has(scene.id)) {
1171
- throw new Error(`Duplicate ${mode} tour scene id: ${scene.id}`);
1172
- }
1173
- sceneIds.add(scene.id);
1174
- if (scene.kind === "deconstructed-diff") hasDeconstructed = true;
1175
- if (mode === "historical" && scene.kind === "deconstructed-diff") {
1176
- throw new Error("Historical tours cannot contain synthetic deconstructed scenes.");
1177
- }
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
- }
1183
- if (mode === "deconstructed" && !hasDeconstructed) {
1184
- throw new Error("Deconstructed tours must contain a constructed deconstructed-diff scene.");
1185
- }
1186
- for (const [index, chapter] of tour.chapters.entries()) {
1187
- if (!isRecord3(chapter)) {
1188
- throw new Error(`tours.${mode}.chapters[${index}] must be an object.`);
1189
- }
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`);
1194
- for (const sceneId of chapter.sceneIds) {
1195
- if (!sceneIds.has(sceneId)) {
1196
- throw new Error(`tours.${mode} chapter references unknown scene id: ${sceneId}`);
1197
- }
1198
- }
1150
+
1151
+ // src/changeTour.ts
1152
+ var import_child_process4 = require("child_process");
1153
+
1154
+ // src/tourFraming.ts
1155
+ function validateTourFraming(value, version, path) {
1156
+ for (const key of ["opening", "conclusion"]) {
1157
+ const passage = value[key];
1158
+ if (passage === void 0) continue;
1159
+ if (Number(version) < 5) throw new Error(`${path}.${key} requires version 5.`);
1160
+ if (!passage || typeof passage !== "object" || Array.isArray(passage)) {
1161
+ throw new Error(`${path}.${key} must be an object.`);
1162
+ }
1163
+ const fields = passage;
1164
+ for (const field of Object.keys(fields)) {
1165
+ if (!["title", "summary", "bullets"].includes(field)) throw new Error(`${path}.${key}.${field} is unsupported.`);
1166
+ }
1167
+ for (const field of ["title", "summary"]) requireProse(fields[field], `${path}.${key}.${field}`);
1168
+ if (fields.bullets !== void 0) {
1169
+ if (!Array.isArray(fields.bullets)) throw new Error(`${path}.${key}.bullets must be an array.`);
1170
+ fields.bullets.forEach((bullet, index) => requireProse(bullet, `${path}.${key}.bullets[${index}]`));
1199
1171
  }
1200
1172
  }
1201
1173
  }
1202
- function validateZoom(value, version, exported) {
1203
- if (!exported && (!isRecord3(value.repository) || typeof value.repository.root !== "string" || !value.repository.root)) {
1204
- throw new Error("A v2 tour requires its originating repository.root.");
1174
+ function validateOverviewPurpose(value, version, path) {
1175
+ if (value === void 0) return;
1176
+ if (version < 5) {
1177
+ if (value && typeof value === "object" && "purpose" in value) throw new Error(`${path}.purpose requires version 5.`);
1178
+ return;
1205
1179
  }
1206
- if (!isRecord3(value.zoom) || !Array.isArray(value.zoom.revisions) || !isRecord3(value.zoom.final)) {
1207
- throw new Error("A v2 tour requires zoom metadata and real revision evidence.");
1180
+ requireProse(value == null ? void 0 : value.purpose, `${path}.purpose`);
1181
+ }
1182
+ function requireProse(value, path) {
1183
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${path} must be a non-empty string.`);
1184
+ }
1185
+
1186
+ // src/tourDetails.ts
1187
+ function validateChangeTourDetails(value, path) {
1188
+ if (value === void 0) return;
1189
+ if (!isRecord(value)) throw new Error(`${path} must be an object.`);
1190
+ requireOnlyKeys(value, ["audience"], path);
1191
+ validateAudience(value.audience, `${path}.audience`);
1192
+ }
1193
+ function validateAudience(value, path) {
1194
+ if (value === void 0) return;
1195
+ if (!isRecord(value)) throw new Error(`${path} must be an object.`);
1196
+ requireOnlyKeys(value, [
1197
+ "codebase",
1198
+ "language",
1199
+ "domain",
1200
+ "readingDemand",
1201
+ "purpose",
1202
+ "scope",
1203
+ "estimatedMinutes"
1204
+ ], path);
1205
+ if (typeof value.codebase !== "string" || !["none", "some", "maintainer"].includes(value.codebase)) {
1206
+ throw new Error(`${path}.codebase must be none, some, or maintainer.`);
1208
1207
  }
1209
- const scenes = value.scenes;
1210
- const revisions = value.zoom.revisions;
1211
- const ids = /* @__PURE__ */ new Set();
1212
- for (const [index, scene] of revisions.entries()) {
1213
- validateScene(scene, index, version);
1214
- if (scene.kind !== "stacked-diff") throw new Error("zoom.revisions must contain real stacked-diff scenes.");
1215
- if (ids.has(scene.id)) throw new Error(`Duplicate zoom revision scene: ${scene.id}`);
1216
- ids.add(scene.id);
1217
- const range = value.range;
1218
- if (scene.stack[0].oid !== range.mergeBaseOid || scene.stack[scene.stack.length - 1].oid !== range.headOid) {
1219
- throw new Error(`Revision scene ${scene.id} endpoints must match the final diff range.`);
1220
- }
1208
+ if (typeof value.language !== "string" || !["none", "basic", "fluent", "advanced"].includes(value.language)) {
1209
+ throw new Error(`${path}.language must be none, basic, fluent, or advanced.`);
1221
1210
  }
1222
- for (const scene of scenes) {
1223
- if (scene.kind !== "deconstructed-diff" && scene.kind !== "stacked-diff") continue;
1224
- const real = revisions.find((candidate) => candidate.id === scene.id);
1225
- if (!real) throw new Error(`Scene ${scene.id} is missing its real revision evidence.`);
1226
- if (scene.kind === "deconstructed-diff" && (real.stack[0].oid !== scene.realRange.baseOid || real.stack[real.stack.length - 1].oid !== scene.realRange.targetOid)) {
1227
- throw new Error(`Deconstructed scene ${scene.id} real stack endpoints must match its explanation range.`);
1228
- }
1211
+ if (value.domain !== void 0) requireStringArray(value.domain, `${path}.domain`);
1212
+ if (typeof value.readingDemand !== "string" || !["general", "technical", "specialist"].includes(value.readingDemand)) {
1213
+ throw new Error(`${path}.readingDemand must be general, technical, or specialist.`);
1229
1214
  }
1230
- const explanation = scenes.some((scene) => scene.kind === "deconstructed-diff");
1231
- const distinct = revisions.some((scene) => scene.stack.length > 2);
1232
- const depth = explanation ? "explanation" : distinct ? "revisions" : "final";
1233
- const modes = [...explanation ? ["explanation"] : [], ...distinct ? ["revisions"] : [], "final", "history"];
1234
- if (value.zoom.authoredDepth !== depth || JSON.stringify(value.zoom.modes) !== JSON.stringify(modes)) {
1235
- throw new Error("zoom depth and modes must match the maximum authored depth.");
1215
+ if (typeof value.purpose !== "string" || !["overview", "explanation", "review"].includes(value.purpose)) {
1216
+ throw new Error(`${path}.purpose must be overview, explanation, or review.`);
1236
1217
  }
1237
- if (!Array.isArray(value.files)) throw new Error("A v2 tour requires final endpoint file evidence.");
1238
- if (!Array.isArray(value.zoom.final.scenes) || !Array.isArray(value.zoom.final.chapters)) {
1239
- throw new Error("A v2 tour requires a non-stacked final tour.");
1218
+ requireString(value.scope, `${path}.scope`);
1219
+ if (value.estimatedMinutes !== void 0 && (typeof value.estimatedMinutes !== "number" || !Number.isFinite(value.estimatedMinutes) || value.estimatedMinutes <= 0)) {
1220
+ throw new Error(`${path}.estimatedMinutes must be a finite positive number.`);
1240
1221
  }
1241
- const finalSceneIds = /* @__PURE__ */ new Set();
1242
- for (const [index, scene] of value.zoom.final.scenes.entries()) {
1243
- validateScene(scene, index, version);
1244
- if (scene.kind === "stacked-diff" || scene.kind === "deconstructed-diff") {
1245
- throw new Error("zoom.final must contain only non-stacked scenes.");
1246
- }
1247
- if (finalSceneIds.has(scene.id)) throw new Error(`Duplicate final tour scene: ${scene.id}`);
1248
- finalSceneIds.add(scene.id);
1222
+ }
1223
+ function isRecord(value) {
1224
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1225
+ }
1226
+ function requireString(value, path) {
1227
+ if (typeof value !== "string" || value.trim().length === 0) {
1228
+ throw new Error(`${path} must be a non-empty string.`);
1249
1229
  }
1250
- for (const [index, chapter] of value.zoom.final.chapters.entries()) {
1251
- if (!isRecord3(chapter) || !Array.isArray(chapter.sceneIds)) {
1252
- throw new Error(`zoom.final.chapters[${index}] must contain sceneIds.`);
1253
- }
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`);
1257
- for (const sceneId of chapter.sceneIds) {
1258
- requireString3(sceneId, `zoom.final.chapters[${index}].sceneIds`);
1259
- if (!finalSceneIds.has(sceneId)) throw new Error(`Final tour chapter references unknown scene: ${sceneId}`);
1260
- }
1230
+ }
1231
+ function requireStringArray(value, path) {
1232
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) {
1233
+ throw new Error(`${path} must be an array of strings.`);
1261
1234
  }
1262
1235
  }
1263
- function validateScene(value, index, version) {
1264
- if (!isRecord3(value) || !["text-diff", "discussion", "walkthrough", "stacked-diff", "deconstructed-diff"].includes(String(value.kind))) {
1265
- throw new Error(`scenes[${index}] must be a text-diff, discussion, walkthrough, stacked-diff, or deconstructed-diff scene.`);
1236
+ function requireOnlyKeys(value, allowed, path) {
1237
+ const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
1238
+ if (unexpected.length > 0) {
1239
+ throw new Error(`${path} contains unknown field${unexpected.length === 1 ? "" : "s"}: ${unexpected.join(", ")}`);
1266
1240
  }
1267
- for (const key of ["id", "title"]) {
1268
- requireString3(value[key], `scenes[${index}].${key}`);
1241
+ }
1242
+
1243
+ // src/changeTourSource.ts
1244
+ var CHANGE_TOUR_SOURCE_VERSION = 6;
1245
+ function parseChangeTourSource(value) {
1246
+ if (isRecord2(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_SOURCE_VERSION) {
1247
+ throw new Error(
1248
+ `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.`
1249
+ );
1269
1250
  }
1270
- if (value.overview !== void 0 && version < 4) {
1271
- throw new Error(`scenes[${index}].overview requires manifest version 4.`);
1251
+ if (isRecord2(value) && value.version === 3) {
1252
+ throw new Error("Tour source format version 3 is retired. Remove the review block, set source version to 4, and recompile the tour.");
1272
1253
  }
1273
- validateOverviewPurpose(value.overview, version, `scenes[${index}].overview`);
1274
- validateNarrative2(value, `scenes[${index}]`);
1275
- if (value.kind === "discussion") {
1276
- validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
1277
- return;
1254
+ if (!isRecord2(value) || value.version !== 1 && value.version !== 2 && value.version !== 4 && value.version !== 5 && value.version !== CHANGE_TOUR_SOURCE_VERSION) {
1255
+ throw new Error("Unsupported or missing change-tour source version.");
1278
1256
  }
1279
- if (value.kind === "walkthrough") {
1280
- validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
1281
- if (!Array.isArray(value.steps) || value.steps.length === 0) {
1282
- throw new Error(`scenes[${index}].steps must be a non-empty array.`);
1257
+ requireOnlyKeys2(value, [
1258
+ "version",
1259
+ "title",
1260
+ "windowTitle",
1261
+ "sourceUrl",
1262
+ "correspondence",
1263
+ "range",
1264
+ "anchors",
1265
+ "connections",
1266
+ "chapters",
1267
+ "tours",
1268
+ "coverage",
1269
+ "opening",
1270
+ "conclusion",
1271
+ "details"
1272
+ ], "source");
1273
+ validateTourFraming(value, value.version, "source");
1274
+ optionalString(value.title, "title");
1275
+ optionalString(value.windowTitle, "windowTitle");
1276
+ optionalString(value.sourceUrl, "sourceUrl");
1277
+ if (value.range !== void 0) {
1278
+ if (!isRecord2(value.range)) throw new Error("range must be an object.");
1279
+ requireOnlyKeys2(value.range, ["base", "head"], "range");
1280
+ requireString2(value.range.base, "range.base");
1281
+ requireString2(value.range.head, "range.head");
1282
+ }
1283
+ if (value.coverage !== void 0) {
1284
+ if (!isRecord2(value.coverage) || !Array.isArray(value.coverage.exclusions)) {
1285
+ throw new Error("coverage must contain an exclusions array.");
1283
1286
  }
1284
- for (const [stepIndex, step] of value.steps.entries()) {
1285
- const path = `scenes[${index}].steps[${stepIndex}]`;
1286
- if (!isRecord3(step) || !isRecord3(step.focus) || !isRecord3(step.diff)) {
1287
- throw new Error(`${path} must contain focus and diff objects.`);
1288
- }
1289
- for (const key of ["id", "title", "body"]) requireString3(step[key], `${path}.${key}`);
1290
- if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
1291
- throw new Error(`${path}.depth must be mentioned, explained, or contextualized.`);
1292
- }
1293
- validateStepRequirement(step.requirement, `${path}.requirement`);
1294
- validateTourImage(step.image, `${path}.image`);
1295
- validateResolvedAnchor(step.focus, `${path}.focus`);
1296
- validateScene(step.diff, index, version);
1297
- if (step.diff.kind !== "text-diff") throw new Error(`${path}.diff must be a text-diff scene.`);
1298
- if (step.connection !== void 0) {
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`);
1302
- validateResolvedAnchor(step.connection.from, `${path}.connection.from`);
1303
- validateResolvedAnchor(step.connection.to, `${path}.connection.to`);
1304
- }
1287
+ requireOnlyKeys2(value.coverage, ["exclusions"], "coverage");
1288
+ for (const [index, exclusion] of value.coverage.exclusions.entries()) {
1289
+ const exclusionPath = `coverage.exclusions[${index}]`;
1290
+ if (!isRecord2(exclusion)) throw new Error(`${exclusionPath} must be an object.`);
1291
+ requireOnlyKeys2(exclusion, ["path", "hunks", "reason"], exclusionPath);
1292
+ requireString2(exclusion.path, `${exclusionPath}.path`);
1293
+ requireString2(exclusion.reason, `${exclusionPath}.reason`);
1294
+ if (exclusion.hunks !== void 0) requireStringArray2(exclusion.hunks, `${exclusionPath}.hunks`);
1305
1295
  }
1306
- return;
1307
- }
1308
- if (value.kind === "stacked-diff") {
1309
- validateStackedScene2(value, index);
1310
- validateSceneOverview(value.overview, `scenes[${index}].overview`, {
1311
- endpointIds: value.stack.map((panel) => panel.id)
1312
- });
1313
- return;
1314
- }
1315
- if (value.kind === "deconstructed-diff") {
1316
- validateDeconstructedScene2(value, index);
1317
- validateSceneOverview(value.overview, `scenes[${index}].overview`, {
1318
- endpointIds: value.panels.map((panel) => panel.id)
1319
- });
1320
- return;
1321
- }
1322
- validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
1323
- for (const key of ["path", "leftLabel", "rightLabel", "leftContent", "rightContent"]) {
1324
- requireString3(value[key], `scenes[${index}].${key}`);
1325
- }
1326
- if (value.previousPath !== void 0) {
1327
- requireString3(value.previousPath, `scenes[${index}].previousPath`);
1328
1296
  }
1329
- requireString3(value.changeKind, `scenes[${index}].changeKind`);
1330
- requireNonNegativeInteger(value.additions, `scenes[${index}].additions`);
1331
- requireNonNegativeInteger(value.deletions, `scenes[${index}].deletions`);
1332
- if (value.focusChangeIndex !== void 0) {
1333
- requireNonNegativeInteger(value.focusChangeIndex, `scenes[${index}].focusChangeIndex`);
1297
+ if (!isRecord2(value.anchors)) {
1298
+ throw new Error("anchors must be an object.");
1334
1299
  }
1335
- }
1336
- function validateStackedScene2(value, index) {
1337
- if (!Array.isArray(value.stack) || value.stack.length < 2 || value.stack.length > 6) {
1338
- throw new Error(`scenes[${index}].stack must contain between 2 and 6 revisions.`);
1300
+ for (const [id, anchor] of Object.entries(value.anchors)) {
1301
+ if (!isRecord2(anchor)) {
1302
+ throw new Error(`anchors.${id} must be an object.`);
1303
+ }
1304
+ requireOnlyKeys2(anchor, ["file", "revision", "contains", "occurrence"], `anchors.${id}`);
1305
+ requireString2(anchor.file, `anchors.${id}.file`);
1306
+ if (anchor.revision !== "base" && anchor.revision !== "head") {
1307
+ throw new Error(`anchors.${id}.revision must be base or head.`);
1308
+ }
1309
+ requireString2(anchor.contains, `anchors.${id}.contains`);
1310
+ if (anchor.occurrence !== void 0 && (!Number.isInteger(anchor.occurrence) || Number(anchor.occurrence) < 1)) {
1311
+ throw new Error(`anchors.${id}.occurrence must be a positive integer.`);
1312
+ }
1339
1313
  }
1340
- for (const [panelIndex, panel] of value.stack.entries()) {
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}`);
1314
+ if (value.correspondence !== void 0) {
1315
+ if (value.version < 6) throw new Error("correspondence requires source version 6.");
1316
+ requireString2(value.correspondence, "correspondence");
1317
+ if (value.correspondence.startsWith("/") || value.correspondence.includes("\\") || value.correspondence.includes("\0") || value.correspondence.split("/").some((part) => !part || part === "." || part === "..")) throw new Error("correspondence must be a repository-relative file path.");
1343
1318
  }
1344
- validateMultiPanelFiles(value.files, value.stack.length, index);
1345
- validateMultiPanelSteps(value.steps, value.stack.length, index, false);
1346
- }
1347
- function validateDeconstructedScene2(value, index) {
1348
- if (value.stageLabel !== "Explanation stages") {
1349
- throw new Error(`scenes[${index}].stageLabel must be Explanation stages.`);
1319
+ const rawConnections = Array.isArray(value.connections) ? value.connections : isRecord2(value.connections) ? Object.entries(value.connections).map(([id, connection]) => isRecord2(connection) ? { id, ...connection } : connection) : null;
1320
+ if (!rawConnections || !Array.isArray(value.chapters) || value.chapters.length === 0) {
1321
+ throw new Error("connections must be an array or object, and chapters must be a non-empty array.");
1350
1322
  }
1351
- if (!isRecord3(value.realRange)) throw new Error(`scenes[${index}].realRange must be an object.`);
1352
- for (const key of ["baseRef", "targetRef", "baseOid", "targetOid"]) {
1353
- requireString3(value.realRange[key], `scenes[${index}].realRange.${key}`);
1323
+ const connectionIds = /* @__PURE__ */ new Set();
1324
+ for (const [index, connection] of rawConnections.entries()) {
1325
+ if (!isRecord2(connection)) {
1326
+ throw new Error(`connections[${index}] must be an object.`);
1327
+ }
1328
+ requireOnlyKeys2(connection, ["id", "from", "to", "label"], `connections[${index}]`);
1329
+ requireString2(connection.id, `connections[${index}].id`);
1330
+ requireString2(connection.from, `connections[${index}].from`);
1331
+ requireString2(connection.to, `connections[${index}].to`);
1332
+ requireString2(connection.label, `connections[${index}].label`);
1333
+ const { id, from, to } = connection;
1334
+ if (!value.anchors[from] || !value.anchors[to]) {
1335
+ throw new Error(`Connection ${id} references an unknown anchor.`);
1336
+ }
1337
+ if (connectionIds.has(id)) {
1338
+ throw new Error(`Duplicate connection id: ${id}`);
1339
+ }
1340
+ connectionIds.add(id);
1354
1341
  }
1355
- if (!Array.isArray(value.panels) || value.panels.length < 2 || value.panels.length > 13) {
1356
- throw new Error(`scenes[${index}].panels must contain a baseline and between 1 and 12 stages.`);
1342
+ validateSourceChapters(
1343
+ value.chapters,
1344
+ "chapters",
1345
+ value.version,
1346
+ value.anchors,
1347
+ connectionIds,
1348
+ "root"
1349
+ );
1350
+ if (value.details !== void 0) {
1351
+ if (value.version < 5) throw new Error("source.details requires version 5.");
1352
+ validateChangeTourDetails(value.details, "details");
1357
1353
  }
1358
- const panels = value.panels;
1359
- for (const [panelIndex, panel] of panels.entries()) {
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}`);
1362
- const expectedRole = panelIndex === 0 ? "baseline" : "stage";
1363
- if (panel.role !== expectedRole) throw new Error(`scenes[${index}].panels[${panelIndex}].role must be ${expectedRole}.`);
1364
- if (panelIndex > 0) requireString3(panel.stageId, `scenes[${index}].panels[${panelIndex}].stageId`);
1354
+ if (value.tours !== void 0) {
1355
+ if (value.version < 4) throw new Error("Independent tours require version 4.");
1356
+ if (!isRecord2(value.tours)) throw new Error("tours must be an object.");
1357
+ requireOnlyKeys2(value.tours, ["historical", "deconstructed"], "tours");
1358
+ if (value.tours.historical === void 0 && value.tours.deconstructed === void 0) {
1359
+ throw new Error("tours must contain a historical or deconstructed tour.");
1360
+ }
1361
+ for (const mode of ["historical", "deconstructed"]) {
1362
+ const tour = value.tours[mode];
1363
+ if (tour === void 0) continue;
1364
+ const path = `tours.${mode}`;
1365
+ if (!isRecord2(tour)) throw new Error(`${path} must be an object.`);
1366
+ requireOnlyKeys2(tour, ["chapters", "opening", "conclusion", "details"], path);
1367
+ validateTourFraming(tour, value.version, path);
1368
+ validateSourceChapters(tour.chapters, `${path}.chapters`, value.version, value.anchors, connectionIds, mode);
1369
+ if (tour.details !== void 0) {
1370
+ if (value.version < 5) throw new Error(`${path}.details requires source version 5.`);
1371
+ validateChangeTourDetails(tour.details, `${path}.details`);
1372
+ }
1373
+ }
1365
1374
  }
1366
- validateMultiPanelFiles(value.files, panels.length, index);
1367
- validateMultiPanelSteps(value.steps, panels.length, index, true);
1368
- if (Array.isArray(value.steps)) {
1369
- let previousPairIndex = -1;
1370
- value.steps.forEach((step, stepIndex) => {
1371
- if (!isRecord3(step)) return;
1372
- const pairIndex = Number(step.pairIndex);
1373
- if (pairIndex < previousPairIndex) {
1374
- throw new Error(`scenes[${index}].steps must remain grouped in explanation stage order.`);
1375
+ return { ...value, connections: rawConnections };
1376
+ }
1377
+ function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, connectionIds, mode) {
1378
+ if (!Array.isArray(chaptersValue) || chaptersValue.length === 0) {
1379
+ throw new Error(`${chaptersPath} must be a non-empty array.`);
1380
+ }
1381
+ const chapterIds = /* @__PURE__ */ new Set();
1382
+ const sceneIds = /* @__PURE__ */ new Set();
1383
+ for (const [chapterIndex, chapter] of chaptersValue.entries()) {
1384
+ const chapterPath = `${chaptersPath}[${chapterIndex}]`;
1385
+ if (!isRecord2(chapter) || !Array.isArray(chapter.scenes) || chapter.scenes.length === 0) {
1386
+ throw new Error(`${chapterPath} must contain a non-empty scenes array.`);
1387
+ }
1388
+ requireString2(chapter.id, `${chapterPath}.id`);
1389
+ requireString2(chapter.title, `${chapterPath}.title`);
1390
+ if (chapter.summary !== void 0) {
1391
+ if (version < 5) throw new Error(`${chapterPath}.summary requires source version 5.`);
1392
+ requireProse2(chapter.summary, `${chapterPath}.summary`);
1393
+ }
1394
+ requireOnlyKeys2(chapter, ["id", "title", "summary", "scenes"], chapterPath);
1395
+ if (chapterIds.has(chapter.id)) throw new Error(`Duplicate chapter id: ${chapter.id}`);
1396
+ chapterIds.add(chapter.id);
1397
+ for (const [sceneIndex, scene] of chapter.scenes.entries()) {
1398
+ const path = `${chapterPath}.scenes[${sceneIndex}]`;
1399
+ if (!isRecord2(scene)) throw new Error(`${path} must be an object.`);
1400
+ requireString2(scene.id, `${path}.id`);
1401
+ requireString2(scene.title, `${path}.title`);
1402
+ if (sceneIds.has(scene.id)) throw new Error(`Duplicate scene id: ${scene.id}`);
1403
+ sceneIds.add(scene.id);
1404
+ if (scene.overview !== void 0 && version < 4) {
1405
+ throw new Error(`${path}.overview requires version 4.`);
1375
1406
  }
1376
- previousPairIndex = pairIndex;
1377
- if (step.stageId !== void 0) requireString3(step.stageId, `scenes[${index}].steps[${stepIndex}].stageId`);
1378
- if (step.stageIndex !== void 0) requireNonNegativeInteger(step.stageIndex, `scenes[${index}].steps[${stepIndex}].stageIndex`);
1379
- if (step.focusIndex !== void 0) requireNonNegativeInteger(step.focusIndex, `scenes[${index}].steps[${stepIndex}].focusIndex`);
1380
- if (step.focusCount !== void 0) requirePositiveInteger(step.focusCount, `scenes[${index}].steps[${stepIndex}].focusCount`);
1381
- if (step.stageIndex !== void 0 && Number(step.stageIndex) !== pairIndex) {
1382
- throw new Error(`scenes[${index}].steps[${stepIndex}].stageIndex must match pairIndex.`);
1407
+ validateOverviewPurpose(scene.overview, version, `${path}.overview`);
1408
+ validateNarrative(scene, path);
1409
+ if (scene.kind === "deconstructed-diff") {
1410
+ if (mode === "historical") {
1411
+ throw new Error(`${path} cannot contain a deconstructed-diff scene in the historical tour.`);
1412
+ }
1413
+ validateDeconstructedScene(scene, path);
1414
+ validateSceneOverview(scene.overview, `${path}.overview`, {
1415
+ endpointIds: deconstructedOverviewEndpointIds(scene, path)
1416
+ });
1417
+ const requireRealEvidence = mode === "root" && version >= 2;
1418
+ if (requireRealEvidence || scene.stack !== void 0) {
1419
+ validateRealStack(scene.stack, `${path}.stack`, 2);
1420
+ }
1421
+ if (requireRealEvidence || scene.steps !== void 0) {
1422
+ validateWalkthroughSteps(scene, path, anchors, connectionIds, version);
1423
+ }
1424
+ continue;
1383
1425
  }
1384
- if (step.focusIndex !== void 0 && step.focusCount !== void 0 && Number(step.focusIndex) >= Number(step.focusCount)) {
1385
- throw new Error(`scenes[${index}].steps[${stepIndex}].focusIndex must be less than focusCount.`);
1426
+ if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
1427
+ throw new Error(`${path} must contain a non-empty steps array.`);
1386
1428
  }
1387
- });
1388
- for (let stageIndex = 0; stageIndex < panels.length - 1; stageIndex += 1) {
1389
- const panel = panels[stageIndex + 1];
1390
- const stageSteps = value.steps.filter((step) => isRecord3(step) && Number(step.pairIndex) === stageIndex);
1391
- if (!isRecord3(panel) || stageSteps.length === 0) {
1392
- throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
1429
+ if (scene.kind === "stacked-diff") {
1430
+ validateStackedScene(scene, path, mode === "root" ? version >= 2 ? 2 : 3 : 2);
1431
+ validateSceneOverview(scene.overview, `${path}.overview`, {
1432
+ endpointIds: stackedOverviewEndpointIds(scene, path)
1433
+ });
1434
+ continue;
1393
1435
  }
1394
- const firstStep = stageSteps[0];
1395
- const fallbackStageId = isRecord3(firstStep) ? firstStep.id : void 0;
1396
- if (stageSteps.some((step) => {
1397
- if (!isRecord3(step)) return true;
1398
- return (step.stageId ?? fallbackStageId) !== panel.stageId;
1399
- })) {
1400
- throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
1436
+ if (scene.kind !== void 0 && scene.kind !== "walkthrough") {
1437
+ throw new Error(`${path}.kind must be walkthrough, stacked-diff, or deconstructed-diff.`);
1401
1438
  }
1439
+ requireOnlyKeys2(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "steps"], path);
1440
+ validateSceneOverview(scene.overview, `${path}.overview`, { allowComparison: false });
1441
+ validateWalkthroughSteps(scene, path, anchors, connectionIds, version);
1402
1442
  }
1403
1443
  }
1444
+ if (mode === "deconstructed" && !chaptersValue.some((chapter) => isRecord2(chapter) && Array.isArray(chapter.scenes) && chapter.scenes.some((scene) => isRecord2(scene) && scene.kind === "deconstructed-diff"))) {
1445
+ throw new Error(`${chaptersPath} must contain at least one deconstructed-diff scene.`);
1446
+ }
1404
1447
  }
1405
- function validateMultiPanelFiles(value, panelCount, index) {
1406
- if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].files must be non-empty.`);
1407
- for (const [fileIndex, file] of value.entries()) {
1408
- if (!isRecord3(file)) throw new Error(`scenes[${index}].files[${fileIndex}] must be an object.`);
1409
- requireString3(file.path, `scenes[${index}].files[${fileIndex}].path`);
1410
- if (!Array.isArray(file.panels) || file.panels.length !== panelCount) {
1411
- throw new Error(`scenes[${index}].files[${fileIndex}].panels must match panel count.`);
1412
- }
1413
- for (const [panelIndex, panel] of file.panels.entries()) {
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`);
1417
- if (typeof panel.exists !== "boolean") throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}].exists must be boolean.`);
1418
- }
1448
+ function stackedOverviewEndpointIds(scene, path) {
1449
+ if (!Array.isArray(scene.stack)) {
1450
+ throw new Error(`${path}.stack must be an array.`);
1419
1451
  }
1452
+ return scene.stack.filter(isRecord2).map((entry) => typeof entry.id === "string" ? entry.id : "").filter((id) => id.length > 0);
1420
1453
  }
1421
- function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
1422
- if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].steps must be non-empty.`);
1423
- for (const [stepIndex, step] of value.entries()) {
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}`);
1426
- requireNonNegativeInteger(step.pairIndex, `scenes[${index}].steps[${stepIndex}].pairIndex`);
1427
- if (Number(step.pairIndex) >= panelCount - 1) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex is outside the panels.`);
1428
- if (deconstructed) {
1429
- requireStringArray3(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
1430
- if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
1431
- }
1432
- if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
1433
- if (step.startLine !== void 0) requirePositiveInteger(step.startLine, `scenes[${index}].steps[${stepIndex}].startLine`);
1434
- if (step.endLine !== void 0) requirePositiveInteger(step.endLine, `scenes[${index}].steps[${stepIndex}].endLine`);
1454
+ function deconstructedOverviewEndpointIds(scene, path) {
1455
+ if (!Array.isArray(scene.stages)) {
1456
+ throw new Error(`${path}.stages must be an array.`);
1435
1457
  }
1458
+ return [
1459
+ "explanation-baseline",
1460
+ ...scene.stages.filter(isRecord2).map((stage) => typeof stage.id === "string" ? `explanation-stage-${stage.id}` : "").filter((id) => id.length > 0)
1461
+ ];
1436
1462
  }
1437
- function validateTourFile(value, index) {
1438
- if (!isRecord3(value)) throw new Error(`files[${index}] must be an object.`);
1439
- if (value.kind === "text-diff") {
1440
- validateScene(value, index, 1);
1441
- return;
1463
+ function validateDeconstructedScene(scene, path) {
1464
+ requireOnlyKeys2(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "base", "target", "stack", "steps", "stages", "exclusions"], path);
1465
+ optionalString(scene.base, `${path}.base`);
1466
+ optionalString(scene.target, `${path}.target`);
1467
+ if (!Array.isArray(scene.stages) || scene.stages.length === 0 || scene.stages.length > 12) {
1468
+ throw new Error(`${path}.stages must contain between 1 and 12 stages.`);
1442
1469
  }
1443
- if (value.kind !== "omitted") {
1444
- throw new Error(`files[${index}].kind must be text-diff or omitted.`);
1470
+ const stageIds = /* @__PURE__ */ new Set();
1471
+ for (const [stageIndex, stage] of scene.stages.entries()) {
1472
+ const stagePath = `${path}.stages[${stageIndex}]`;
1473
+ if (!isRecord2(stage)) throw new Error(`${stagePath} must be an object.`);
1474
+ requireOnlyKeys2(stage, ["id", "title", "narration", "changes"], stagePath);
1475
+ requireString2(stage.id, `${stagePath}.id`);
1476
+ requireString2(stage.title, `${stagePath}.title`);
1477
+ requireString2(stage.narration, `${stagePath}.narration`);
1478
+ if (!Array.isArray(stage.changes) || stage.changes.length === 0) {
1479
+ throw new Error(`${stagePath}.changes must be a non-empty array.`);
1480
+ }
1481
+ if (stageIds.has(stage.id)) throw new Error(`Duplicate deconstructed stage id: ${stage.id}`);
1482
+ stageIds.add(stage.id);
1483
+ stage.changes.forEach((change, changeIndex) => validateDeconstructedSelection(
1484
+ change,
1485
+ `${stagePath}.changes[${changeIndex}]`,
1486
+ false
1487
+ ));
1445
1488
  }
1446
- for (const key of ["id", "title", "path", "changeKind", "reason"]) {
1447
- requireString3(value[key], `files[${index}].${key}`);
1489
+ if (scene.exclusions !== void 0) {
1490
+ if (!Array.isArray(scene.exclusions)) throw new Error(`${path}.exclusions must be an array.`);
1491
+ scene.exclusions.forEach((exclusion, exclusionIndex) => validateDeconstructedSelection(
1492
+ exclusion,
1493
+ `${path}.exclusions[${exclusionIndex}]`,
1494
+ true
1495
+ ));
1448
1496
  }
1449
- if (value.previousPath !== void 0) requireString3(value.previousPath, `files[${index}].previousPath`);
1450
- requireNonNegativeInteger(value.additions, `files[${index}].additions`);
1451
- requireNonNegativeInteger(value.deletions, `files[${index}].deletions`);
1452
1497
  }
1453
- function collectLegacyTourFiles(scenes) {
1454
- const files = /* @__PURE__ */ new Map();
1455
- for (const scene of scenes) {
1456
- if (scene.kind === "text-diff") {
1457
- if (!files.has(scene.path)) files.set(scene.path, scene);
1458
- continue;
1498
+ function validateWalkthroughSteps(scene, path, anchors, connectionIds, version) {
1499
+ if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
1500
+ throw new Error(`${path} must contain a non-empty steps array.`);
1501
+ }
1502
+ const stepIds = /* @__PURE__ */ new Set();
1503
+ for (const [stepIndex, step] of scene.steps.entries()) {
1504
+ const stepPath = `${path}.steps[${stepIndex}]`;
1505
+ if (!isRecord2(step)) throw new Error(`${stepPath} must be an object.`);
1506
+ requireString2(step.id, `${stepPath}.id`);
1507
+ requireString2(step.title, `${stepPath}.title`);
1508
+ requireString2(step.body, `${stepPath}.body`);
1509
+ requireString2(step.focus, `${stepPath}.focus`);
1510
+ requireOnlyKeys2(step, ["id", "title", "body", "focus", "connection", "depth", "requirement", "image", "symbol"], stepPath);
1511
+ if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
1512
+ throw new Error(`${stepPath}.depth must be mentioned, explained, or contextualized.`);
1459
1513
  }
1460
- if (scene.kind === "walkthrough") {
1461
- for (const step of scene.steps) {
1462
- if (!files.has(step.diff.path)) files.set(step.diff.path, step.diff);
1463
- }
1514
+ if (step.symbol !== void 0) {
1515
+ if (version < 6 || !isRecord2(step.symbol)) throw new Error(`${stepPath}.symbol requires an object and source version 6.`);
1516
+ requireOnlyKeys2(step.symbol, ["relation", "symbol"], `${stepPath}.symbol`);
1517
+ if (step.symbol.relation === void 0 === (step.symbol.symbol === void 0)) throw new Error(`${stepPath}.symbol requires exactly one relation or symbol ID.`);
1518
+ requireString2(step.symbol.relation ?? step.symbol.symbol, `${stepPath}.symbol ID`);
1519
+ if (step.image || step.connection) throw new Error(`${stepPath}.symbol cannot combine with image or connection.`);
1520
+ }
1521
+ validateStepRequirement(step.requirement, `${stepPath}.requirement`);
1522
+ validateTourImageSource(step.image, `${stepPath}.image`);
1523
+ if (stepIds.has(step.id)) throw new Error(`Duplicate step id in scene ${scene.id}: ${step.id}`);
1524
+ stepIds.add(step.id);
1525
+ if (!anchors[step.focus]) throw new Error(`${stepPath} references unknown anchor ${step.focus}.`);
1526
+ optionalString(step.connection, `${stepPath}.connection`);
1527
+ if (typeof step.connection === "string" && !connectionIds.has(step.connection)) {
1528
+ throw new Error(`${stepPath} references unknown connection ${step.connection}.`);
1464
1529
  }
1465
1530
  }
1466
- return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
1467
- }
1468
- function validateResolvedAnchor(value, path) {
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}`);
1471
- if (value.revision !== "base" && value.revision !== "head") throw new Error(`${path}.revision must be base or head.`);
1472
- requirePositiveInteger(value.startLine, `${path}.startLine`);
1473
- requirePositiveInteger(value.endLine, `${path}.endLine`);
1474
- if (Number(value.endLine) < Number(value.startLine)) throw new Error(`${path}.endLine must not precede startLine.`);
1475
- }
1476
- function validateNarrative2(value, path) {
1477
- requireString3(value.summary, `${path}.summary`);
1478
- requireStringArray3(value.bullets, `${path}.bullets`);
1479
- requireStringArray3(value.tags, `${path}.tags`);
1480
- requireString3(value.takeaway, `${path}.takeaway`);
1481
1531
  }
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.`);
1532
+ function validateDeconstructedSelection(value, path, requiresReason) {
1533
+ if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
1534
+ requireOnlyKeys2(value, requiresReason ? ["file", "hunks", "reason"] : ["file", "hunks"], path);
1535
+ requireString2(value.file, `${path}.file`);
1536
+ if (!requiresReason || value.hunks !== void 0) {
1537
+ requireStringArray2(value.hunks, `${path}.hunks`);
1538
+ if (value.hunks.length === 0) throw new Error(`${path}.hunks must not be empty.`);
1487
1539
  }
1540
+ if (requiresReason) requireString2(value.reason, `${path}.reason`);
1488
1541
  }
1489
- function isRecord3(value) {
1490
- return typeof value === "object" && value !== null && !Array.isArray(value);
1491
- }
1492
- function requireString3(value, path) {
1493
- if (typeof value !== "string") {
1494
- throw new Error(`${path} must be a string.`);
1542
+ function validateRealStack(stack, path, minimum) {
1543
+ if (!Array.isArray(stack) || stack.length < minimum || stack.length > 6) {
1544
+ throw new Error(`${path} must contain between ${minimum} and 6 explicit real revisions.`);
1495
1545
  }
1496
- }
1497
- function requireStringArray3(value, path) {
1498
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
1499
- throw new Error(`${path} must be an array of strings.`);
1546
+ const ids = /* @__PURE__ */ new Set();
1547
+ for (const [index, entry] of stack.entries()) {
1548
+ if (!isRecord2(entry)) throw new Error(`${path}[${index}] must be an object.`);
1549
+ requireOnlyKeys2(entry, ["id", "ref", "label"], `${path}[${index}]`);
1550
+ requireString2(entry.id, `${path}[${index}].id`);
1551
+ requireString2(entry.ref, `${path}[${index}].ref`);
1552
+ optionalString(entry.label, `${path}[${index}].label`);
1553
+ if (ids.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
1554
+ ids.add(entry.id);
1500
1555
  }
1501
1556
  }
1502
- function requireNonNegativeInteger(value, path) {
1503
- if (!Number.isInteger(value) || Number(value) < 0) {
1504
- throw new Error(`${path} must be a non-negative integer.`);
1557
+ function validateStackedScene(scene, path, minimum = 3) {
1558
+ requireOnlyKeys2(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "stack", "files", "steps"], path);
1559
+ if (!Array.isArray(scene.stack) || scene.stack.length < minimum || scene.stack.length > 6) {
1560
+ throw new Error(`${path}.stack must contain between ${minimum} and 6 revisions.`);
1505
1561
  }
1506
- }
1507
- function requirePercentage(value, path) {
1508
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
1509
- throw new Error(`${path} must be a number from 0 to 100.`);
1562
+ const stackIds = /* @__PURE__ */ new Set();
1563
+ for (const [index, entry] of scene.stack.entries()) {
1564
+ const entryPath = `${path}.stack[${index}]`;
1565
+ if (!isRecord2(entry)) throw new Error(`${entryPath} must be an object.`);
1566
+ requireOnlyKeys2(entry, ["id", "ref", "label"], entryPath);
1567
+ requireString2(entry.id, `${entryPath}.id`);
1568
+ requireString2(entry.ref, `${entryPath}.ref`);
1569
+ optionalString(entry.label, `${entryPath}.label`);
1570
+ if (stackIds.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
1571
+ stackIds.add(entry.id);
1510
1572
  }
1511
- }
1512
- function requirePositiveInteger(value, path) {
1513
- if (!Number.isInteger(value) || Number(value) < 1) throw new Error(`${path} must be a positive integer.`);
1514
- }
1515
-
1516
- // src/deconstructedChange.ts
1517
- function buildDeconstructedScene(startPath, scene, options = {}) {
1518
- return compileDeconstructedScene(buildChangeInventory(startPath, {
1519
- headRef: options.headRef || scene.target,
1520
- baseRef: options.baseRef || scene.base
1521
- }), scene);
1522
- }
1523
- function compileDeconstructedScene(inventory, scene) {
1524
- const ownership = /* @__PURE__ */ new Map();
1525
- const excludedByFile = /* @__PURE__ */ new Map();
1526
- const cumulativeByFile = /* @__PURE__ */ new Map();
1527
- const wholeFileExclusions = /* @__PURE__ */ new Map();
1528
- const excludedFiles = [];
1529
- for (const exclusion of scene.exclusions || []) {
1530
- const file = requireInventoryFile(inventory, exclusion.file, "Exclusion");
1531
- if (!exclusion.hunks) {
1532
- if (wholeFileExclusions.has(file)) {
1533
- throw new Error(`File is excluded more than once: ${exclusion.file}`);
1534
- }
1535
- wholeFileExclusions.set(file, exclusion.reason);
1536
- excludedFiles.push({ path: file.path, reason: exclusion.reason });
1573
+ if (scene.files !== void 0) requireStringArray2(scene.files, `${path}.files`);
1574
+ if (!Array.isArray(scene.steps) || scene.steps.length === 0) throw new Error(`${path}.steps must be a non-empty array.`);
1575
+ const stepIds = /* @__PURE__ */ new Set();
1576
+ for (const [index, step] of scene.steps.entries()) {
1577
+ const stepPath = `${path}.steps[${index}]`;
1578
+ if (!isRecord2(step)) throw new Error(`${stepPath} must be an object.`);
1579
+ requireOnlyKeys2(step, ["id", "title", "body", "file", "pair", "side", "lines"], stepPath);
1580
+ requireString2(step.id, `${stepPath}.id`);
1581
+ requireString2(step.title, `${stepPath}.title`);
1582
+ requireString2(step.body, `${stepPath}.body`);
1583
+ requireString2(step.file, `${stepPath}.file`);
1584
+ const stepId = step.id;
1585
+ if (stepIds.has(stepId)) throw new Error(`Duplicate step id in scene ${scene.id}: ${stepId}`);
1586
+ stepIds.add(stepId);
1587
+ if (!Array.isArray(step.pair) || step.pair.length !== 2 || step.pair.some((id) => typeof id !== "string")) {
1588
+ throw new Error(`${stepPath}.pair must contain two stack entry ids.`);
1537
1589
  }
1538
- const ids = exclusion.hunks || file.units.map((unit) => unit.id);
1539
- if (file.material !== "text" && exclusion.hunks) {
1540
- throw new Error(`Unsupported file ${exclusion.file} must be excluded as a whole file.`);
1590
+ const pair = step.pair;
1591
+ const leftIndex = scene.stack.findIndex((entry) => isRecord2(entry) && entry.id === pair[0]);
1592
+ const rightIndex = scene.stack.findIndex((entry) => isRecord2(entry) && entry.id === pair[1]);
1593
+ if (leftIndex < 0 || rightIndex !== leftIndex + 1) throw new Error(`${stepPath}.pair must reference adjacent stack entries in order.`);
1594
+ if (step.side !== void 0 && step.side !== "left" && step.side !== "right") throw new Error(`${stepPath}.side must be left or right.`);
1595
+ 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]))) {
1596
+ throw new Error(`${stepPath}.lines must contain an ordered positive line range.`);
1541
1597
  }
1542
- claimHunks(file, ids, `exclusion for ${exclusion.file}`, ownership);
1543
- const excluded = excludedByFile.get(file) || /* @__PURE__ */ new Set();
1544
- ids.forEach((id) => excluded.add(id));
1545
- excludedByFile.set(file, excluded);
1546
1598
  }
1547
- const stageClaims = scene.stages.map((stage) => {
1548
- const claims = [];
1549
- for (const change of stage.changes) {
1550
- const file = requireInventoryFile(inventory, change.file, `Stage ${stage.id}`);
1551
- if (file.material !== "text") {
1552
- throw new Error(`Stage ${stage.id} cannot assign unsupported file: ${change.file}`);
1553
- }
1554
- if (file.changeKind === "renamed") {
1555
- throw new Error(`Stage ${stage.id} cannot assign renamed file ${change.file} until path-transition units are supported.`);
1556
- }
1557
- claimHunks(file, change.hunks, `stage ${stage.id}`, ownership);
1558
- claims.push({ file, ids: [...change.hunks] });
1559
- }
1560
- return { stage, claims };
1561
- });
1562
- for (const file of inventory.files) {
1563
- const wholeFileExcluded = wholeFileExclusions.has(file);
1564
- if (file.material !== "text" || file.changeKind === "renamed" || file.units.length === 0) {
1565
- if (!wholeFileExcluded) {
1566
- const reason = file.changeKind === "renamed" ? "rename path transitions are not supported yet" : file.material !== "text" ? `${file.material} content cannot be materialized` : "the change has no textual hunks";
1567
- throw new Error(`Deconstructed scene ${scene.id} must exclude ${file.path} as a whole file because ${reason}.`);
1568
- }
1569
- continue;
1570
- }
1571
- for (const unit of file.units) {
1572
- if (!ownership.has(unitKey(file, unit.id))) {
1573
- throw new Error(`Unassigned change unit in ${file.path}: ${unit.id}`);
1574
- }
1575
- }
1599
+ }
1600
+ function validateStepRequirement(value, path) {
1601
+ if (value === void 0) return;
1602
+ if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
1603
+ requireOnlyKeys2(value, ["id", "text", "status", "source", "confidence"], path);
1604
+ requireString2(value.id, `${path}.id`);
1605
+ requireString2(value.text, `${path}.text`);
1606
+ if (value.status !== void 0 && !["fulfilled", "gap"].includes(String(value.status))) {
1607
+ throw new Error(`${path}.status must be fulfilled or gap.`);
1576
1608
  }
1577
- for (const file of inventory.files) {
1578
- cumulativeByFile.set(file, new Set(excludedByFile.get(file) || []));
1609
+ optionalString(value.source, `${path}.source`);
1610
+ if (value.confidence !== void 0 && !["high", "medium", "low"].includes(String(value.confidence))) {
1611
+ throw new Error(`${path}.confidence must be high, medium, or low.`);
1579
1612
  }
1580
- const materializableFiles = inventory.files.filter((file) => file.material === "text" && file.changeKind !== "renamed" && file.baseContent !== void 0 && file.headContent !== void 0);
1581
- const baselineFiles = materializableFiles.map((file) => buildFileState(
1582
- file,
1583
- cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
1584
- wholeFileExclusions.has(file)
1585
- ));
1586
- const stages = [];
1587
- for (const { stage, claims } of stageClaims) {
1588
- const introducedHunks = [];
1589
- const introducedFiles = [];
1590
- const introducedChanges = [];
1591
- for (const claim of claims) {
1592
- const cumulative = cumulativeByFile.get(claim.file) || /* @__PURE__ */ new Set();
1593
- claim.ids.forEach((id) => {
1594
- cumulative.add(id);
1595
- introducedHunks.push(id);
1596
- });
1597
- cumulativeByFile.set(claim.file, cumulative);
1598
- if (!introducedFiles.includes(claim.file.path)) introducedFiles.push(claim.file.path);
1599
- const introducedChange = introducedChanges.find((change) => change.file === claim.file.path);
1600
- if (introducedChange) {
1601
- introducedChange.hunks.push(...claim.ids);
1602
- } else {
1603
- introducedChanges.push({ file: claim.file.path, hunks: [...claim.ids] });
1604
- }
1605
- }
1606
- stages.push({
1607
- id: stage.id,
1608
- title: stage.title,
1609
- narration: stage.narration,
1610
- introducedHunks,
1611
- introducedFiles,
1612
- introducedChanges,
1613
- cumulativeHunks: [...cumulativeByFile.values()].flatMap((ids) => [...ids]),
1614
- files: materializableFiles.map((file) => buildFileState(
1615
- file,
1616
- cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
1617
- wholeFileExclusions.has(file)
1618
- ))
1619
- });
1613
+ }
1614
+ function validateSceneOverview(value, path, options = {}) {
1615
+ if (value === void 0) return;
1616
+ if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
1617
+ requireOnlyKeys2(value, ["kind", "path", "comparison", "purpose"], path);
1618
+ if (value.kind !== "directory-diff") {
1619
+ throw new Error(`${path}.kind must be directory-diff.`);
1620
1620
  }
1621
- for (const file of materializableFiles) {
1622
- const finalState = buildFileState(
1623
- file,
1624
- cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
1625
- wholeFileExclusions.has(file)
1626
- );
1627
- const expectedExists = file.changeKind !== "deleted";
1628
- if (finalState.exists !== expectedExists || finalState.content !== file.headContent) {
1629
- throw new Error(`Final deconstructed state does not match the target for ${file.path}.`);
1621
+ if (value.purpose !== void 0) requireString2(value.purpose, `${path}.purpose`);
1622
+ if (value.path !== void 0) validateDirectoryOverviewPath(value.path, `${path}.path`);
1623
+ if (value.comparison === void 0) return;
1624
+ if (options.allowComparison === false) {
1625
+ throw new Error(`${path}.comparison is not supported for walkthrough scenes; use the fixed base-to-head comparison.`);
1626
+ }
1627
+ if (!isRecord2(value.comparison)) throw new Error(`${path}.comparison must be an object.`);
1628
+ requireOnlyKeys2(value.comparison, ["from", "to"], `${path}.comparison`);
1629
+ requireString2(value.comparison.from, `${path}.comparison.from`);
1630
+ requireString2(value.comparison.to, `${path}.comparison.to`);
1631
+ if (value.comparison.from === value.comparison.to) {
1632
+ throw new Error(`${path}.comparison.from and to must identify distinct endpoints.`);
1633
+ }
1634
+ if (options.endpointIds !== void 0) {
1635
+ for (const endpoint of [value.comparison.from, value.comparison.to]) {
1636
+ if (!options.endpointIds.includes(endpoint)) {
1637
+ throw new Error(`${path}.comparison endpoint ${endpoint} is not defined by this scene.`);
1638
+ }
1630
1639
  }
1631
1640
  }
1632
- return {
1633
- id: scene.id,
1634
- title: scene.title,
1635
- baseOid: inventory.range.baseOid,
1636
- targetOid: inventory.range.headOid,
1637
- baselineFiles,
1638
- excludedHunks: [...excludedByFile.values()].flatMap((ids) => [...ids]),
1639
- excludedFiles,
1640
- stages
1641
- };
1642
1641
  }
1643
- function requireInventoryFile(inventory, path, owner) {
1644
- const matches = inventory.files.filter((file) => file.path === path || file.previousPath === path);
1645
- if (matches.length === 0) throw new Error(`${owner} references a file outside the change inventory: ${path}`);
1646
- if (matches.length > 1) throw new Error(`${owner} references an ambiguous changed path: ${path}`);
1647
- return matches[0];
1642
+ function validateDirectoryOverviewPath(value, path) {
1643
+ requireString2(value, path);
1644
+ if (value.includes("\\")) {
1645
+ throw new Error(`${path} must use a relative POSIX directory path without backslashes.`);
1646
+ }
1647
+ if (value.startsWith("/") || /^[A-Za-z]:/.test(value)) {
1648
+ throw new Error(`${path} must be relative to the repository root.`);
1649
+ }
1650
+ if (value.split("/").some((segment) => segment === "..")) {
1651
+ throw new Error(`${path} must not contain parent-directory traversal.`);
1652
+ }
1648
1653
  }
1649
- function claimHunks(file, ids, owner, ownership) {
1650
- const known = new Set(file.units.map((unit) => unit.id));
1651
- for (const id of ids) {
1652
- if (!known.has(id)) throw new Error(`${owner} references an unknown hunk in ${file.path}: ${id}`);
1653
- const key = unitKey(file, id);
1654
- const previousOwner = ownership.get(key);
1655
- if (previousOwner) throw new Error(`Change unit ${id} in ${file.path} is assigned to both ${previousOwner} and ${owner}.`);
1656
- ownership.set(key, owner);
1654
+ function validateNarrative(value, path) {
1655
+ requireString2(value.summary, `${path}.summary`);
1656
+ requireStringArray2(value.bullets, `${path}.bullets`);
1657
+ requireStringArray2(value.tags, `${path}.tags`);
1658
+ requireString2(value.takeaway, `${path}.takeaway`);
1659
+ }
1660
+ function isRecord2(value) {
1661
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1662
+ }
1663
+ function requireString2(value, path) {
1664
+ if (typeof value !== "string" || value.length === 0) {
1665
+ throw new Error(`${path} must be a non-empty string.`);
1657
1666
  }
1658
1667
  }
1659
- function unitKey(file, id) {
1660
- return `${file.previousPath || ""}\0${file.path}\0${id}`;
1668
+ function optionalString(value, path) {
1669
+ if (value !== void 0) {
1670
+ requireString2(value, path);
1671
+ }
1661
1672
  }
1662
- function buildFileState(file, selected, wholeFileExcluded) {
1663
- if (file.baseContent === void 0) throw new Error(`Missing base content for ${file.path}.`);
1664
- const content = materializeChangeUnits(file.baseContent, file.units, selected);
1665
- let exists = true;
1666
- if (file.changeKind === "added") exists = wholeFileExcluded || selected.size > 0;
1667
- if (file.changeKind === "deleted") exists = !(wholeFileExcluded || selected.size === file.units.length);
1668
- return {
1669
- path: file.path,
1670
- previousPath: file.previousPath,
1671
- changeKind: file.changeKind,
1672
- exists,
1673
- content: exists ? content : "",
1674
- appliedHunks: [...selected]
1675
- };
1673
+ function requireProse2(value, path) {
1674
+ if (typeof value !== "string" || !value.trim()) {
1675
+ throw new Error(`${path} must be a non-empty string.`);
1676
+ }
1676
1677
  }
1677
-
1678
- // node_modules/diff/lib/index.mjs
1679
- function Diff() {
1678
+ function requireStringArray2(value, path) {
1679
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
1680
+ throw new Error(`${path} must be an array of strings.`);
1681
+ }
1680
1682
  }
1681
- Diff.prototype = {
1682
- diff: function diff(oldString, newString) {
1683
- var _options$timeout;
1684
- var options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
1685
- var callback = options.callback;
1686
- if (typeof options === "function") {
1687
- callback = options;
1688
- options = {};
1683
+ function requireOnlyKeys2(value, allowed, path) {
1684
+ const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
1685
+ if (unexpected.length > 0) {
1686
+ throw new Error(`${path} contains unknown field${unexpected.length === 1 ? "" : "s"}: ${unexpected.join(", ")}`);
1687
+ }
1688
+ }
1689
+
1690
+ // src/changeTourManifest.ts
1691
+ var CHANGE_TOUR_MANIFEST_VERSION = 6;
1692
+ function parseChangeTourManifest(value, options = {}) {
1693
+ if (isRecord3(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_MANIFEST_VERSION) {
1694
+ throw new Error(
1695
+ `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.`
1696
+ );
1697
+ }
1698
+ if (isRecord3(value) && value.version === 3) {
1699
+ throw new Error("Tour manifest format version 3 is retired. Remove the review block, set source version to 4, and recompile the tour.");
1700
+ }
1701
+ if (!isRecord3(value) || value.version !== 1 && value.version !== 2 && value.version !== 4 && value.version !== 5 && value.version !== CHANGE_TOUR_MANIFEST_VERSION) {
1702
+ throw new Error(`Unsupported or missing change-tour manifest version.`);
1703
+ }
1704
+ if ("review" in value) throw new Error("Change-tour manifests do not support the review field. Put observations in scene and step narrative.");
1705
+ validateTourFraming(value, value.version, "manifest");
1706
+ requireString3(value.title, "title");
1707
+ requireString3(value.generatedAt, "generatedAt");
1708
+ if (value.windowTitle !== void 0) {
1709
+ requireString3(value.windowTitle, "windowTitle");
1710
+ }
1711
+ if (value.sourceUrl !== void 0) {
1712
+ requireString3(value.sourceUrl, "sourceUrl");
1713
+ }
1714
+ if (!isRecord3(value.range)) {
1715
+ throw new Error("Change-tour manifest range must be an object.");
1716
+ }
1717
+ for (const key of ["baseRef", "headRef", "mergeBaseOid", "headOid"]) {
1718
+ requireString3(value.range[key], `range.${key}`);
1719
+ }
1720
+ if (!isRecord3(value.summary)) {
1721
+ throw new Error("Change-tour manifest summary must be an object.");
1722
+ }
1723
+ for (const key of ["changedFiles", "includedScenes", "additions", "deletions", "commitCount"]) {
1724
+ requireNonNegativeInteger(value.summary[key], `summary.${key}`);
1725
+ }
1726
+ requireStringArray3(value.summary.omittedFiles, "summary.omittedFiles");
1727
+ if (!Array.isArray(value.commits) || !Array.isArray(value.chapters) || !Array.isArray(value.scenes)) {
1728
+ throw new Error("Change-tour manifest commits, chapters, and scenes must be arrays.");
1729
+ }
1730
+ if (value.files !== void 0 && !Array.isArray(value.files)) {
1731
+ throw new Error("Change-tour manifest files must be an array.");
1732
+ }
1733
+ const sceneIds = /* @__PURE__ */ new Set();
1734
+ for (const [index, candidate] of value.scenes.entries()) {
1735
+ validateScene(candidate, index, value.version);
1736
+ if (sceneIds.has(candidate.id)) {
1737
+ throw new Error(`Duplicate change-tour scene id: ${candidate.id}`);
1689
1738
  }
1690
- this.options = options;
1691
- var self = this;
1692
- function done(value) {
1693
- if (callback) {
1694
- setTimeout(function() {
1695
- callback(void 0, value);
1696
- }, 0);
1697
- return true;
1698
- } else {
1699
- return value;
1739
+ sceneIds.add(candidate.id);
1740
+ }
1741
+ for (const [index, candidate] of value.chapters.entries()) {
1742
+ if (!isRecord3(candidate)) {
1743
+ throw new Error(`chapters[${index}] must be an object.`);
1744
+ }
1745
+ requireString3(candidate.id, `chapters[${index}].id`);
1746
+ requireString3(candidate.title, `chapters[${index}].title`);
1747
+ validateChapterSummary(candidate.summary, value.version, `chapters[${index}].summary`);
1748
+ requireStringArray3(candidate.sceneIds, `chapters[${index}].sceneIds`);
1749
+ for (const sceneId of candidate.sceneIds) {
1750
+ if (!sceneIds.has(sceneId)) {
1751
+ throw new Error(`Chapter references unknown scene id: ${sceneId}`);
1700
1752
  }
1701
1753
  }
1702
- oldString = this.castInput(oldString);
1703
- newString = this.castInput(newString);
1704
- oldString = this.removeEmpty(this.tokenize(oldString));
1705
- newString = this.removeEmpty(this.tokenize(newString));
1706
- var newLen = newString.length, oldLen = oldString.length;
1707
- var editLength = 1;
1708
- var maxEditLength = newLen + oldLen;
1709
- if (options.maxEditLength) {
1710
- maxEditLength = Math.min(maxEditLength, options.maxEditLength);
1754
+ }
1755
+ if (value.summary.includedScenes !== value.scenes.length) {
1756
+ throw new Error("summary.includedScenes must match the number of scenes.");
1757
+ }
1758
+ if (value.details !== void 0) {
1759
+ if (value.version < 5) throw new Error("details requires manifest version 5.");
1760
+ validateChangeTourDetails(value.details, "details");
1761
+ }
1762
+ const files = value.files === void 0 ? collectLegacyTourFiles(value.scenes) : value.files.map((candidate, index) => {
1763
+ validateTourFile(candidate, index);
1764
+ return candidate;
1765
+ });
1766
+ if (value.files !== void 0 && value.summary.changedFiles !== files.length) {
1767
+ throw new Error("summary.changedFiles must match the number of files.");
1768
+ }
1769
+ if (value.authoringCoverage !== void 0) {
1770
+ validateAuthoringCoverage(value.authoringCoverage, value.version);
1771
+ }
1772
+ if (value.tours !== void 0) {
1773
+ if (value.version !== 4 && value.version !== 5 && value.version !== 6) throw new Error("Independent tours require manifest version 4.");
1774
+ validateModeTours(value.tours, value.version);
1775
+ }
1776
+ if (value.version !== 1) validateZoom(value, value.version, Boolean(options.exported));
1777
+ const symbolScenes = [
1778
+ value.scenes,
1779
+ ...isRecord3(value.zoom) && isRecord3(value.zoom.final) ? [value.zoom.final.scenes] : [],
1780
+ ...isRecord3(value.tours) ? Object.values(value.tours).flatMap((tour) => isRecord3(tour) ? [tour.scenes] : []) : []
1781
+ ];
1782
+ for (const scenes of symbolScenes) for (const scene of scenes) {
1783
+ if (scene.kind !== "walkthrough") continue;
1784
+ for (const step of scene.steps) if (step.symbol) {
1785
+ const symbol = step.symbol;
1786
+ if (symbol.range.baseOid !== value.range.mergeBaseOid || symbol.range.headOid !== value.range.headOid) throw new Error("Symbol tour evidence revisions do not match the tour.");
1787
+ const endpoint = step.focus.revision === "base" ? symbol.before : symbol.after;
1788
+ if (!endpoint || endpoint.path !== step.focus.path || step.focus.startLine < endpoint.startLine || step.focus.endLine > endpoint.endLine) throw new Error("Symbol stop focus must lie within its selected declaration.");
1711
1789
  }
1712
- var maxExecutionTime = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : Infinity;
1713
- var abortAfterTimestamp = Date.now() + maxExecutionTime;
1714
- var bestPath = [{
1715
- oldPos: -1,
1716
- lastComponent: void 0
1717
- }];
1718
- var newPos = this.extractCommon(bestPath[0], newString, oldString, 0);
1719
- if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
1720
- return done([{
1721
- value: this.join(newString),
1722
- count: newString.length
1723
- }]);
1790
+ }
1791
+ return { ...value, files };
1792
+ }
1793
+ function validateAuthoringCoverage(value, version) {
1794
+ if (!isRecord3(value) || !isRecord3(value.walkthrough) || !Array.isArray(value.explanationAssignments)) {
1795
+ throw new Error("authoringCoverage must contain walkthrough and explanationAssignments.");
1796
+ }
1797
+ const expectedScope = version >= 2 ? "final" : "tour";
1798
+ if (value.walkthrough.scope !== expectedScope) {
1799
+ throw new Error(`authoringCoverage.walkthrough.scope must be ${expectedScope}.`);
1800
+ }
1801
+ requireNonNegativeInteger(value.walkthrough.coveredUnits, "authoringCoverage.walkthrough.coveredUnits");
1802
+ requireNonNegativeInteger(value.walkthrough.includedUnits, "authoringCoverage.walkthrough.includedUnits");
1803
+ requirePercentage(value.walkthrough.coveragePercent, "authoringCoverage.walkthrough.coveragePercent");
1804
+ if (value.walkthrough.coveredUnits > value.walkthrough.includedUnits) {
1805
+ throw new Error("authoringCoverage.walkthrough.coveredUnits must not exceed includedUnits.");
1806
+ }
1807
+ for (const [index, assignment] of value.explanationAssignments.entries()) {
1808
+ const path = `authoringCoverage.explanationAssignments[${index}]`;
1809
+ if (!isRecord3(assignment)) throw new Error(`${path} must be an object.`);
1810
+ requireString3(assignment.sceneId, `${path}.sceneId`);
1811
+ requireNonNegativeInteger(assignment.assignedUnits, `${path}.assignedUnits`);
1812
+ requireNonNegativeInteger(assignment.excludedUnits, `${path}.excludedUnits`);
1813
+ requireNonNegativeInteger(assignment.completeUnits, `${path}.completeUnits`);
1814
+ requireNonNegativeInteger(assignment.totalUnits, `${path}.totalUnits`);
1815
+ requirePercentage(assignment.assignmentPercent, `${path}.assignmentPercent`);
1816
+ if (assignment.assignedUnits + assignment.excludedUnits !== assignment.completeUnits || assignment.completeUnits > assignment.totalUnits) {
1817
+ throw new Error(`${path} contains inconsistent unit counts.`);
1818
+ }
1819
+ }
1820
+ }
1821
+ function validateModeTours(value, version) {
1822
+ if (!isRecord3(value)) throw new Error("Change-tour manifest tours must be an object.");
1823
+ if (value.historical === void 0 && value.deconstructed === void 0) {
1824
+ throw new Error("Change-tour manifest tours must contain a historical or deconstructed tour.");
1825
+ }
1826
+ for (const mode of ["historical", "deconstructed"]) {
1827
+ const tour = value[mode];
1828
+ if (tour === void 0) continue;
1829
+ if (!isRecord3(tour) || !Array.isArray(tour.chapters) || !Array.isArray(tour.scenes) || tour.chapters.length === 0 || tour.scenes.length === 0) {
1830
+ throw new Error(`Change-tour manifest tours.${mode} must contain non-empty chapters and scenes arrays.`);
1724
1831
  }
1725
- var minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
1726
- function execEditLength() {
1727
- for (var diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
1728
- var basePath = void 0;
1729
- var removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
1730
- if (removePath) {
1731
- bestPath[diagonalPath - 1] = void 0;
1732
- }
1733
- var canAdd = false;
1734
- if (addPath) {
1735
- var addPathNewPos = addPath.oldPos - diagonalPath;
1736
- canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
1737
- }
1738
- var canRemove = removePath && removePath.oldPos + 1 < oldLen;
1739
- if (!canAdd && !canRemove) {
1740
- bestPath[diagonalPath] = void 0;
1741
- continue;
1742
- }
1743
- if (!canRemove || canAdd && removePath.oldPos + 1 < addPath.oldPos) {
1744
- basePath = self.addToPath(addPath, true, void 0, 0);
1745
- } else {
1746
- basePath = self.addToPath(removePath, void 0, true, 1);
1747
- }
1748
- newPos = self.extractCommon(basePath, newString, oldString, diagonalPath);
1749
- if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
1750
- return done(buildValues(self, basePath.lastComponent, newString, oldString, self.useLongestToken));
1751
- } else {
1752
- bestPath[diagonalPath] = basePath;
1753
- if (basePath.oldPos + 1 >= oldLen) {
1754
- maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
1755
- }
1756
- if (newPos + 1 >= newLen) {
1757
- minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
1758
- }
1759
- }
1832
+ validateTourFraming(tour, version, `tours.${mode}`);
1833
+ const sceneIds = /* @__PURE__ */ new Set();
1834
+ let hasDeconstructed = false;
1835
+ for (const [index, scene] of tour.scenes.entries()) {
1836
+ validateScene(scene, index, version);
1837
+ if (sceneIds.has(scene.id)) {
1838
+ throw new Error(`Duplicate ${mode} tour scene id: ${scene.id}`);
1839
+ }
1840
+ sceneIds.add(scene.id);
1841
+ if (scene.kind === "deconstructed-diff") hasDeconstructed = true;
1842
+ if (mode === "historical" && scene.kind === "deconstructed-diff") {
1843
+ throw new Error("Historical tours cannot contain synthetic deconstructed scenes.");
1760
1844
  }
1761
- editLength++;
1762
1845
  }
1763
- if (callback) {
1764
- (function exec() {
1765
- setTimeout(function() {
1766
- if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
1767
- return callback();
1768
- }
1769
- if (!execEditLength()) {
1770
- exec();
1771
- }
1772
- }, 0);
1773
- })();
1774
- } else {
1775
- while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
1776
- var ret = execEditLength();
1777
- if (ret) {
1778
- return ret;
1846
+ if (tour.details !== void 0) {
1847
+ if (version < 5) throw new Error(`tours.${mode}.details requires manifest version 5.`);
1848
+ validateChangeTourDetails(tour.details, `tours.${mode}.details`);
1849
+ }
1850
+ if (mode === "deconstructed" && !hasDeconstructed) {
1851
+ throw new Error("Deconstructed tours must contain a constructed deconstructed-diff scene.");
1852
+ }
1853
+ for (const [index, chapter] of tour.chapters.entries()) {
1854
+ if (!isRecord3(chapter)) {
1855
+ throw new Error(`tours.${mode}.chapters[${index}] must be an object.`);
1856
+ }
1857
+ requireString3(chapter.id, `tours.${mode}.chapters[${index}].id`);
1858
+ requireString3(chapter.title, `tours.${mode}.chapters[${index}].title`);
1859
+ validateChapterSummary(chapter.summary, version, `tours.${mode}.chapters[${index}].summary`);
1860
+ requireStringArray3(chapter.sceneIds, `tours.${mode}.chapters[${index}].sceneIds`);
1861
+ for (const sceneId of chapter.sceneIds) {
1862
+ if (!sceneIds.has(sceneId)) {
1863
+ throw new Error(`tours.${mode} chapter references unknown scene id: ${sceneId}`);
1779
1864
  }
1780
1865
  }
1781
1866
  }
1782
- },
1783
- addToPath: function addToPath(path, added, removed, oldPosInc) {
1784
- var last = path.lastComponent;
1785
- if (last && last.added === added && last.removed === removed) {
1786
- return {
1787
- oldPos: path.oldPos + oldPosInc,
1788
- lastComponent: {
1789
- count: last.count + 1,
1790
- added,
1791
- removed,
1792
- previousComponent: last.previousComponent
1793
- }
1794
- };
1795
- } else {
1796
- return {
1797
- oldPos: path.oldPos + oldPosInc,
1798
- lastComponent: {
1799
- count: 1,
1800
- added,
1801
- removed,
1802
- previousComponent: last
1803
- }
1804
- };
1867
+ }
1868
+ }
1869
+ function validateZoom(value, version, exported) {
1870
+ if (!exported && (!isRecord3(value.repository) || typeof value.repository.root !== "string" || !value.repository.root)) {
1871
+ throw new Error("A v2 tour requires its originating repository.root.");
1872
+ }
1873
+ if (!isRecord3(value.zoom) || !Array.isArray(value.zoom.revisions) || !isRecord3(value.zoom.final)) {
1874
+ throw new Error("A v2 tour requires zoom metadata and real revision evidence.");
1875
+ }
1876
+ const scenes = value.scenes;
1877
+ const revisions = value.zoom.revisions;
1878
+ const ids = /* @__PURE__ */ new Set();
1879
+ for (const [index, scene] of revisions.entries()) {
1880
+ validateScene(scene, index, version);
1881
+ if (scene.kind !== "stacked-diff") throw new Error("zoom.revisions must contain real stacked-diff scenes.");
1882
+ if (ids.has(scene.id)) throw new Error(`Duplicate zoom revision scene: ${scene.id}`);
1883
+ ids.add(scene.id);
1884
+ const range = value.range;
1885
+ if (scene.stack[0].oid !== range.mergeBaseOid || scene.stack[scene.stack.length - 1].oid !== range.headOid) {
1886
+ throw new Error(`Revision scene ${scene.id} endpoints must match the final diff range.`);
1805
1887
  }
1806
- },
1807
- extractCommon: function extractCommon(basePath, newString, oldString, diagonalPath) {
1808
- var newLen = newString.length, oldLen = oldString.length, oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
1809
- while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newString[newPos + 1], oldString[oldPos + 1])) {
1810
- newPos++;
1811
- oldPos++;
1812
- commonCount++;
1888
+ }
1889
+ for (const scene of scenes) {
1890
+ if (scene.kind !== "deconstructed-diff" && scene.kind !== "stacked-diff") continue;
1891
+ const real = revisions.find((candidate) => candidate.id === scene.id);
1892
+ if (!real) throw new Error(`Scene ${scene.id} is missing its real revision evidence.`);
1893
+ if (scene.kind === "deconstructed-diff" && (real.stack[0].oid !== scene.realRange.baseOid || real.stack[real.stack.length - 1].oid !== scene.realRange.targetOid)) {
1894
+ throw new Error(`Deconstructed scene ${scene.id} real stack endpoints must match its explanation range.`);
1813
1895
  }
1814
- if (commonCount) {
1815
- basePath.lastComponent = {
1816
- count: commonCount,
1817
- previousComponent: basePath.lastComponent
1818
- };
1896
+ }
1897
+ const explanation = scenes.some((scene) => scene.kind === "deconstructed-diff");
1898
+ const distinct = revisions.some((scene) => scene.stack.length > 2);
1899
+ const depth = explanation ? "explanation" : distinct ? "revisions" : "final";
1900
+ const modes = [...explanation ? ["explanation"] : [], ...distinct ? ["revisions"] : [], "final", "history"];
1901
+ if (value.zoom.authoredDepth !== depth || JSON.stringify(value.zoom.modes) !== JSON.stringify(modes)) {
1902
+ throw new Error("zoom depth and modes must match the maximum authored depth.");
1903
+ }
1904
+ if (!Array.isArray(value.files)) throw new Error("A v2 tour requires final endpoint file evidence.");
1905
+ if (!Array.isArray(value.zoom.final.scenes) || !Array.isArray(value.zoom.final.chapters)) {
1906
+ throw new Error("A v2 tour requires a non-stacked final tour.");
1907
+ }
1908
+ const finalSceneIds = /* @__PURE__ */ new Set();
1909
+ for (const [index, scene] of value.zoom.final.scenes.entries()) {
1910
+ validateScene(scene, index, version);
1911
+ if (scene.kind === "stacked-diff" || scene.kind === "deconstructed-diff") {
1912
+ throw new Error("zoom.final must contain only non-stacked scenes.");
1819
1913
  }
1820
- basePath.oldPos = oldPos;
1821
- return newPos;
1822
- },
1823
- equals: function equals(left, right) {
1824
- if (this.options.comparator) {
1825
- return this.options.comparator(left, right);
1826
- } else {
1827
- return left === right || this.options.ignoreCase && left.toLowerCase() === right.toLowerCase();
1914
+ if (finalSceneIds.has(scene.id)) throw new Error(`Duplicate final tour scene: ${scene.id}`);
1915
+ finalSceneIds.add(scene.id);
1916
+ }
1917
+ for (const [index, chapter] of value.zoom.final.chapters.entries()) {
1918
+ if (!isRecord3(chapter) || !Array.isArray(chapter.sceneIds)) {
1919
+ throw new Error(`zoom.final.chapters[${index}] must contain sceneIds.`);
1828
1920
  }
1829
- },
1830
- removeEmpty: function removeEmpty(array) {
1831
- var ret = [];
1832
- for (var i = 0; i < array.length; i++) {
1833
- if (array[i]) {
1834
- ret.push(array[i]);
1921
+ requireString3(chapter.id, `zoom.final.chapters[${index}].id`);
1922
+ requireString3(chapter.title, `zoom.final.chapters[${index}].title`);
1923
+ validateChapterSummary(chapter.summary, version, `zoom.final.chapters[${index}].summary`);
1924
+ for (const sceneId of chapter.sceneIds) {
1925
+ requireString3(sceneId, `zoom.final.chapters[${index}].sceneIds`);
1926
+ if (!finalSceneIds.has(sceneId)) throw new Error(`Final tour chapter references unknown scene: ${sceneId}`);
1927
+ }
1928
+ }
1929
+ }
1930
+ function validateScene(value, index, version) {
1931
+ if (!isRecord3(value) || !["text-diff", "discussion", "walkthrough", "stacked-diff", "deconstructed-diff"].includes(String(value.kind))) {
1932
+ throw new Error(`scenes[${index}] must be a text-diff, discussion, walkthrough, stacked-diff, or deconstructed-diff scene.`);
1933
+ }
1934
+ for (const key of ["id", "title"]) {
1935
+ requireString3(value[key], `scenes[${index}].${key}`);
1936
+ }
1937
+ if (value.overview !== void 0 && version < 4) {
1938
+ throw new Error(`scenes[${index}].overview requires manifest version 4.`);
1939
+ }
1940
+ validateOverviewPurpose(value.overview, version, `scenes[${index}].overview`);
1941
+ validateNarrative2(value, `scenes[${index}]`);
1942
+ if (value.kind === "discussion") {
1943
+ validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
1944
+ return;
1945
+ }
1946
+ if (value.kind === "walkthrough") {
1947
+ validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
1948
+ if (!Array.isArray(value.steps) || value.steps.length === 0) {
1949
+ throw new Error(`scenes[${index}].steps must be a non-empty array.`);
1950
+ }
1951
+ for (const [stepIndex, step] of value.steps.entries()) {
1952
+ const path = `scenes[${index}].steps[${stepIndex}]`;
1953
+ if (!isRecord3(step) || !isRecord3(step.focus) || !isRecord3(step.diff)) {
1954
+ throw new Error(`${path} must contain focus and diff objects.`);
1955
+ }
1956
+ for (const key of ["id", "title", "body"]) requireString3(step[key], `${path}.${key}`);
1957
+ if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
1958
+ throw new Error(`${path}.depth must be mentioned, explained, or contextualized.`);
1959
+ }
1960
+ validateStepRequirement(step.requirement, `${path}.requirement`);
1961
+ validateTourImage(step.image, `${path}.image`);
1962
+ if (step.symbol !== void 0) {
1963
+ if (version < 6) throw new Error(`${path}.symbol requires manifest version 6.`);
1964
+ if (step.image || step.connection) throw new Error(`${path}.symbol cannot combine with image or connection.`);
1965
+ validateTourSymbol(step.symbol);
1966
+ }
1967
+ validateResolvedAnchor(step.focus, `${path}.focus`);
1968
+ validateScene(step.diff, index, version);
1969
+ if (step.diff.kind !== "text-diff") throw new Error(`${path}.diff must be a text-diff scene.`);
1970
+ if (step.connection !== void 0) {
1971
+ if (!isRecord3(step.connection)) throw new Error(`${path}.connection must be an object.`);
1972
+ requireString3(step.connection.id, `${path}.connection.id`);
1973
+ requireString3(step.connection.label, `${path}.connection.label`);
1974
+ validateResolvedAnchor(step.connection.from, `${path}.connection.from`);
1975
+ validateResolvedAnchor(step.connection.to, `${path}.connection.to`);
1835
1976
  }
1836
1977
  }
1837
- return ret;
1838
- },
1839
- castInput: function castInput(value) {
1840
- return value;
1841
- },
1842
- tokenize: function tokenize(value) {
1843
- return value.split("");
1844
- },
1845
- join: function join(chars) {
1846
- return chars.join("");
1978
+ return;
1979
+ }
1980
+ if (value.kind === "stacked-diff") {
1981
+ validateStackedScene2(value, index);
1982
+ validateSceneOverview(value.overview, `scenes[${index}].overview`, {
1983
+ endpointIds: value.stack.map((panel) => panel.id)
1984
+ });
1985
+ return;
1986
+ }
1987
+ if (value.kind === "deconstructed-diff") {
1988
+ validateDeconstructedScene2(value, index);
1989
+ validateSceneOverview(value.overview, `scenes[${index}].overview`, {
1990
+ endpointIds: value.panels.map((panel) => panel.id)
1991
+ });
1992
+ return;
1847
1993
  }
1848
- };
1849
- function buildValues(diff2, lastComponent, newString, oldString, useLongestToken) {
1850
- var components = [];
1851
- var nextComponent;
1852
- while (lastComponent) {
1853
- components.push(lastComponent);
1854
- nextComponent = lastComponent.previousComponent;
1855
- delete lastComponent.previousComponent;
1856
- lastComponent = nextComponent;
1994
+ validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
1995
+ for (const key of ["path", "leftLabel", "rightLabel", "leftContent", "rightContent"]) {
1996
+ requireString3(value[key], `scenes[${index}].${key}`);
1857
1997
  }
1858
- components.reverse();
1859
- var componentPos = 0, componentLen = components.length, newPos = 0, oldPos = 0;
1860
- for (; componentPos < componentLen; componentPos++) {
1861
- var component = components[componentPos];
1862
- if (!component.removed) {
1863
- if (!component.added && useLongestToken) {
1864
- var value = newString.slice(newPos, newPos + component.count);
1865
- value = value.map(function(value2, i) {
1866
- var oldValue = oldString[oldPos + i];
1867
- return oldValue.length > value2.length ? oldValue : value2;
1868
- });
1869
- component.value = diff2.join(value);
1870
- } else {
1871
- component.value = diff2.join(newString.slice(newPos, newPos + component.count));
1998
+ if (value.previousPath !== void 0) {
1999
+ requireString3(value.previousPath, `scenes[${index}].previousPath`);
2000
+ }
2001
+ requireString3(value.changeKind, `scenes[${index}].changeKind`);
2002
+ requireNonNegativeInteger(value.additions, `scenes[${index}].additions`);
2003
+ requireNonNegativeInteger(value.deletions, `scenes[${index}].deletions`);
2004
+ if (value.focusChangeIndex !== void 0) {
2005
+ requireNonNegativeInteger(value.focusChangeIndex, `scenes[${index}].focusChangeIndex`);
2006
+ }
2007
+ }
2008
+ function validateStackedScene2(value, index) {
2009
+ if (!Array.isArray(value.stack) || value.stack.length < 2 || value.stack.length > 6) {
2010
+ throw new Error(`scenes[${index}].stack must contain between 2 and 6 revisions.`);
2011
+ }
2012
+ for (const [panelIndex, panel] of value.stack.entries()) {
2013
+ if (!isRecord3(panel)) throw new Error(`scenes[${index}].stack[${panelIndex}] must be an object.`);
2014
+ for (const key of ["id", "ref", "oid", "label"]) requireString3(panel[key], `scenes[${index}].stack[${panelIndex}].${key}`);
2015
+ }
2016
+ validateMultiPanelFiles(value.files, value.stack.length, index);
2017
+ validateMultiPanelSteps(value.steps, value.stack.length, index, false);
2018
+ }
2019
+ function validateDeconstructedScene2(value, index) {
2020
+ if (value.stageLabel !== "Explanation stages") {
2021
+ throw new Error(`scenes[${index}].stageLabel must be Explanation stages.`);
2022
+ }
2023
+ if (!isRecord3(value.realRange)) throw new Error(`scenes[${index}].realRange must be an object.`);
2024
+ for (const key of ["baseRef", "targetRef", "baseOid", "targetOid"]) {
2025
+ requireString3(value.realRange[key], `scenes[${index}].realRange.${key}`);
2026
+ }
2027
+ if (!Array.isArray(value.panels) || value.panels.length < 2 || value.panels.length > 13) {
2028
+ throw new Error(`scenes[${index}].panels must contain a baseline and between 1 and 12 stages.`);
2029
+ }
2030
+ const panels = value.panels;
2031
+ for (const [panelIndex, panel] of panels.entries()) {
2032
+ if (!isRecord3(panel)) throw new Error(`scenes[${index}].panels[${panelIndex}] must be an object.`);
2033
+ for (const key of ["id", "label", "role"]) requireString3(panel[key], `scenes[${index}].panels[${panelIndex}].${key}`);
2034
+ const expectedRole = panelIndex === 0 ? "baseline" : "stage";
2035
+ if (panel.role !== expectedRole) throw new Error(`scenes[${index}].panels[${panelIndex}].role must be ${expectedRole}.`);
2036
+ if (panelIndex > 0) requireString3(panel.stageId, `scenes[${index}].panels[${panelIndex}].stageId`);
2037
+ }
2038
+ validateMultiPanelFiles(value.files, panels.length, index);
2039
+ validateMultiPanelSteps(value.steps, panels.length, index, true);
2040
+ if (Array.isArray(value.steps)) {
2041
+ let previousPairIndex = -1;
2042
+ value.steps.forEach((step, stepIndex) => {
2043
+ if (!isRecord3(step)) return;
2044
+ const pairIndex = Number(step.pairIndex);
2045
+ if (pairIndex < previousPairIndex) {
2046
+ throw new Error(`scenes[${index}].steps must remain grouped in explanation stage order.`);
1872
2047
  }
1873
- newPos += component.count;
1874
- if (!component.added) {
1875
- oldPos += component.count;
2048
+ previousPairIndex = pairIndex;
2049
+ if (step.stageId !== void 0) requireString3(step.stageId, `scenes[${index}].steps[${stepIndex}].stageId`);
2050
+ if (step.stageIndex !== void 0) requireNonNegativeInteger(step.stageIndex, `scenes[${index}].steps[${stepIndex}].stageIndex`);
2051
+ if (step.focusIndex !== void 0) requireNonNegativeInteger(step.focusIndex, `scenes[${index}].steps[${stepIndex}].focusIndex`);
2052
+ if (step.focusCount !== void 0) requirePositiveInteger(step.focusCount, `scenes[${index}].steps[${stepIndex}].focusCount`);
2053
+ if (step.stageIndex !== void 0 && Number(step.stageIndex) !== pairIndex) {
2054
+ throw new Error(`scenes[${index}].steps[${stepIndex}].stageIndex must match pairIndex.`);
1876
2055
  }
1877
- } else {
1878
- component.value = diff2.join(oldString.slice(oldPos, oldPos + component.count));
1879
- oldPos += component.count;
1880
- if (componentPos && components[componentPos - 1].added) {
1881
- var tmp = components[componentPos - 1];
1882
- components[componentPos - 1] = components[componentPos];
1883
- components[componentPos] = tmp;
2056
+ if (step.focusIndex !== void 0 && step.focusCount !== void 0 && Number(step.focusIndex) >= Number(step.focusCount)) {
2057
+ throw new Error(`scenes[${index}].steps[${stepIndex}].focusIndex must be less than focusCount.`);
2058
+ }
2059
+ });
2060
+ for (let stageIndex = 0; stageIndex < panels.length - 1; stageIndex += 1) {
2061
+ const panel = panels[stageIndex + 1];
2062
+ const stageSteps = value.steps.filter((step) => isRecord3(step) && Number(step.pairIndex) === stageIndex);
2063
+ if (!isRecord3(panel) || stageSteps.length === 0) {
2064
+ throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
2065
+ }
2066
+ const firstStep = stageSteps[0];
2067
+ const fallbackStageId = isRecord3(firstStep) ? firstStep.id : void 0;
2068
+ if (stageSteps.some((step) => {
2069
+ if (!isRecord3(step)) return true;
2070
+ return (step.stageId ?? fallbackStageId) !== panel.stageId;
2071
+ })) {
2072
+ throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
1884
2073
  }
1885
2074
  }
1886
2075
  }
1887
- var finalComponent = components[componentLen - 1];
1888
- if (componentLen > 1 && typeof finalComponent.value === "string" && (finalComponent.added || finalComponent.removed) && diff2.equals("", finalComponent.value)) {
1889
- components[componentLen - 2].value += finalComponent.value;
1890
- components.pop();
1891
- }
1892
- return components;
1893
- }
1894
- var characterDiff = new Diff();
1895
- function diffChars(oldStr, newStr, options) {
1896
- return characterDiff.diff(oldStr, newStr, options);
1897
2076
  }
1898
- var extendedWordChars = /^[A-Za-z\xC0-\u02C6\u02C8-\u02D7\u02DE-\u02FF\u1E00-\u1EFF]+$/;
1899
- var reWhitespace = /\S/;
1900
- var wordDiff = new Diff();
1901
- wordDiff.equals = function(left, right) {
1902
- if (this.options.ignoreCase) {
1903
- left = left.toLowerCase();
1904
- right = right.toLowerCase();
2077
+ function validateMultiPanelFiles(value, panelCount, index) {
2078
+ if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].files must be non-empty.`);
2079
+ for (const [fileIndex, file] of value.entries()) {
2080
+ if (!isRecord3(file)) throw new Error(`scenes[${index}].files[${fileIndex}] must be an object.`);
2081
+ requireString3(file.path, `scenes[${index}].files[${fileIndex}].path`);
2082
+ if (!Array.isArray(file.panels) || file.panels.length !== panelCount) {
2083
+ throw new Error(`scenes[${index}].files[${fileIndex}].panels must match panel count.`);
2084
+ }
2085
+ for (const [panelIndex, panel] of file.panels.entries()) {
2086
+ if (!isRecord3(panel)) throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}] must be an object.`);
2087
+ for (const key of ["id", "label", "content"]) requireString3(panel[key], `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].${key}`);
2088
+ if (panel.path !== void 0) requireString3(panel.path, `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].path`);
2089
+ if (typeof panel.exists !== "boolean") throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}].exists must be boolean.`);
2090
+ }
1905
2091
  }
1906
- return left === right || this.options.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right);
1907
- };
1908
- wordDiff.tokenize = function(value) {
1909
- var tokens = value.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/);
1910
- for (var i = 0; i < tokens.length - 1; i++) {
1911
- if (!tokens[i + 1] && tokens[i + 2] && extendedWordChars.test(tokens[i]) && extendedWordChars.test(tokens[i + 2])) {
1912
- tokens[i] += tokens[i + 2];
1913
- tokens.splice(i + 1, 2);
1914
- i--;
2092
+ }
2093
+ function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
2094
+ if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].steps must be non-empty.`);
2095
+ for (const [stepIndex, step] of value.entries()) {
2096
+ if (!isRecord3(step)) throw new Error(`scenes[${index}].steps[${stepIndex}] must be an object.`);
2097
+ for (const key of ["id", "title", "body", "file", "side"]) requireString3(step[key], `scenes[${index}].steps[${stepIndex}].${key}`);
2098
+ requireNonNegativeInteger(step.pairIndex, `scenes[${index}].steps[${stepIndex}].pairIndex`);
2099
+ if (Number(step.pairIndex) >= panelCount - 1) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex is outside the panels.`);
2100
+ if (deconstructed) {
2101
+ requireStringArray3(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
2102
+ if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
1915
2103
  }
2104
+ if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
2105
+ if (step.startLine !== void 0) requirePositiveInteger(step.startLine, `scenes[${index}].steps[${stepIndex}].startLine`);
2106
+ if (step.endLine !== void 0) requirePositiveInteger(step.endLine, `scenes[${index}].steps[${stepIndex}].endLine`);
1916
2107
  }
1917
- return tokens;
1918
- };
1919
- function diffWordsWithSpace(oldStr, newStr, options) {
1920
- return wordDiff.diff(oldStr, newStr, options);
1921
2108
  }
1922
- var lineDiff = new Diff();
1923
- lineDiff.tokenize = function(value) {
1924
- if (this.options.stripTrailingCr) {
1925
- value = value.replace(/\r\n/g, "\n");
2109
+ function validateTourFile(value, index) {
2110
+ if (!isRecord3(value)) throw new Error(`files[${index}] must be an object.`);
2111
+ if (value.kind === "text-diff") {
2112
+ validateScene(value, index, 1);
2113
+ return;
1926
2114
  }
1927
- var retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
1928
- if (!linesAndNewlines[linesAndNewlines.length - 1]) {
1929
- linesAndNewlines.pop();
2115
+ if (value.kind !== "omitted") {
2116
+ throw new Error(`files[${index}].kind must be text-diff or omitted.`);
1930
2117
  }
1931
- for (var i = 0; i < linesAndNewlines.length; i++) {
1932
- var line = linesAndNewlines[i];
1933
- if (i % 2 && !this.options.newlineIsToken) {
1934
- retLines[retLines.length - 1] += line;
1935
- } else {
1936
- if (this.options.ignoreWhitespace) {
1937
- line = line.trim();
2118
+ for (const key of ["id", "title", "path", "changeKind", "reason"]) {
2119
+ requireString3(value[key], `files[${index}].${key}`);
2120
+ }
2121
+ if (value.previousPath !== void 0) requireString3(value.previousPath, `files[${index}].previousPath`);
2122
+ requireNonNegativeInteger(value.additions, `files[${index}].additions`);
2123
+ requireNonNegativeInteger(value.deletions, `files[${index}].deletions`);
2124
+ }
2125
+ function collectLegacyTourFiles(scenes) {
2126
+ const files = /* @__PURE__ */ new Map();
2127
+ for (const scene of scenes) {
2128
+ if (scene.kind === "text-diff") {
2129
+ if (!files.has(scene.path)) files.set(scene.path, scene);
2130
+ continue;
2131
+ }
2132
+ if (scene.kind === "walkthrough") {
2133
+ for (const step of scene.steps) {
2134
+ if (!files.has(step.diff.path)) files.set(step.diff.path, step.diff);
1938
2135
  }
1939
- retLines.push(line);
1940
2136
  }
1941
2137
  }
1942
- return retLines;
1943
- };
1944
- var sentenceDiff = new Diff();
1945
- sentenceDiff.tokenize = function(value) {
1946
- return value.split(/(\S.+?[.!?])(?=\s+|$)/);
1947
- };
1948
- var cssDiff = new Diff();
1949
- cssDiff.tokenize = function(value) {
1950
- return value.split(/([{}:;,]|\s+)/);
1951
- };
1952
- function _typeof(obj) {
1953
- "@babel/helpers - typeof";
1954
- if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
1955
- _typeof = function(obj2) {
1956
- return typeof obj2;
1957
- };
1958
- } else {
1959
- _typeof = function(obj2) {
1960
- return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
1961
- };
2138
+ return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
2139
+ }
2140
+ function validateResolvedAnchor(value, path) {
2141
+ if (!isRecord3(value)) throw new Error(`${path} must be an object.`);
2142
+ for (const key of ["id", "path", "revision", "excerpt"]) requireString3(value[key], `${path}.${key}`);
2143
+ if (value.revision !== "base" && value.revision !== "head") throw new Error(`${path}.revision must be base or head.`);
2144
+ requirePositiveInteger(value.startLine, `${path}.startLine`);
2145
+ requirePositiveInteger(value.endLine, `${path}.endLine`);
2146
+ if (Number(value.endLine) < Number(value.startLine)) throw new Error(`${path}.endLine must not precede startLine.`);
2147
+ }
2148
+ function validateNarrative2(value, path) {
2149
+ requireString3(value.summary, `${path}.summary`);
2150
+ requireStringArray3(value.bullets, `${path}.bullets`);
2151
+ requireStringArray3(value.tags, `${path}.tags`);
2152
+ requireString3(value.takeaway, `${path}.takeaway`);
2153
+ }
2154
+ function validateChapterSummary(value, version, path) {
2155
+ if (value === void 0) return;
2156
+ if (version < 5) throw new Error(`${path} requires manifest version 5.`);
2157
+ if (typeof value !== "string" || !value.trim()) {
2158
+ throw new Error(`${path} must be a non-empty string.`);
2159
+ }
2160
+ }
2161
+ function isRecord3(value) {
2162
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2163
+ }
2164
+ function requireString3(value, path) {
2165
+ if (typeof value !== "string") {
2166
+ throw new Error(`${path} must be a string.`);
2167
+ }
2168
+ }
2169
+ function requireStringArray3(value, path) {
2170
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
2171
+ throw new Error(`${path} must be an array of strings.`);
1962
2172
  }
1963
- return _typeof(obj);
1964
2173
  }
1965
- var objectPrototypeToString = Object.prototype.toString;
1966
- var jsonDiff = new Diff();
1967
- jsonDiff.useLongestToken = true;
1968
- jsonDiff.tokenize = lineDiff.tokenize;
1969
- jsonDiff.castInput = function(value) {
1970
- var _this$options = this.options, undefinedReplacement = _this$options.undefinedReplacement, _this$options$stringi = _this$options.stringifyReplacer, stringifyReplacer = _this$options$stringi === void 0 ? function(k, v) {
1971
- return typeof v === "undefined" ? undefinedReplacement : v;
1972
- } : _this$options$stringi;
1973
- return typeof value === "string" ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, " ");
1974
- };
1975
- jsonDiff.equals = function(left, right) {
1976
- return Diff.prototype.equals.call(jsonDiff, left.replace(/,([\r\n])/g, "$1"), right.replace(/,([\r\n])/g, "$1"));
1977
- };
1978
- function canonicalize(obj, stack, replacementStack, replacer, key) {
1979
- stack = stack || [];
1980
- replacementStack = replacementStack || [];
1981
- if (replacer) {
1982
- obj = replacer(key, obj);
2174
+ function requireNonNegativeInteger(value, path) {
2175
+ if (!Number.isInteger(value) || Number(value) < 0) {
2176
+ throw new Error(`${path} must be a non-negative integer.`);
1983
2177
  }
1984
- var i;
1985
- for (i = 0; i < stack.length; i += 1) {
1986
- if (stack[i] === obj) {
1987
- return replacementStack[i];
2178
+ }
2179
+ function requirePercentage(value, path) {
2180
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
2181
+ throw new Error(`${path} must be a number from 0 to 100.`);
2182
+ }
2183
+ }
2184
+ function requirePositiveInteger(value, path) {
2185
+ if (!Number.isInteger(value) || Number(value) < 1) throw new Error(`${path} must be a positive integer.`);
2186
+ }
2187
+
2188
+ // src/deconstructedChange.ts
2189
+ function buildDeconstructedScene(startPath, scene, options = {}) {
2190
+ return compileDeconstructedScene(buildChangeInventory(startPath, {
2191
+ headRef: options.headRef || scene.target,
2192
+ baseRef: options.baseRef || scene.base
2193
+ }), scene);
2194
+ }
2195
+ function compileDeconstructedScene(inventory, scene) {
2196
+ const ownership = /* @__PURE__ */ new Map();
2197
+ const excludedByFile = /* @__PURE__ */ new Map();
2198
+ const cumulativeByFile = /* @__PURE__ */ new Map();
2199
+ const wholeFileExclusions = /* @__PURE__ */ new Map();
2200
+ const excludedFiles = [];
2201
+ for (const exclusion of scene.exclusions || []) {
2202
+ const file = requireInventoryFile(inventory, exclusion.file, "Exclusion");
2203
+ if (!exclusion.hunks) {
2204
+ if (wholeFileExclusions.has(file)) {
2205
+ throw new Error(`File is excluded more than once: ${exclusion.file}`);
2206
+ }
2207
+ wholeFileExclusions.set(file, exclusion.reason);
2208
+ excludedFiles.push({ path: file.path, reason: exclusion.reason });
2209
+ }
2210
+ const ids = exclusion.hunks || file.units.map((unit) => unit.id);
2211
+ if (file.material !== "text" && exclusion.hunks) {
2212
+ throw new Error(`Unsupported file ${exclusion.file} must be excluded as a whole file.`);
1988
2213
  }
2214
+ claimHunks(file, ids, `exclusion for ${exclusion.file}`, ownership);
2215
+ const excluded = excludedByFile.get(file) || /* @__PURE__ */ new Set();
2216
+ ids.forEach((id) => excluded.add(id));
2217
+ excludedByFile.set(file, excluded);
1989
2218
  }
1990
- var canonicalizedObj;
1991
- if ("[object Array]" === objectPrototypeToString.call(obj)) {
1992
- stack.push(obj);
1993
- canonicalizedObj = new Array(obj.length);
1994
- replacementStack.push(canonicalizedObj);
1995
- for (i = 0; i < obj.length; i += 1) {
1996
- canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key);
2219
+ const stageClaims = scene.stages.map((stage) => {
2220
+ const claims = [];
2221
+ for (const change of stage.changes) {
2222
+ const file = requireInventoryFile(inventory, change.file, `Stage ${stage.id}`);
2223
+ if (file.material !== "text") {
2224
+ throw new Error(`Stage ${stage.id} cannot assign unsupported file: ${change.file}`);
2225
+ }
2226
+ if (file.changeKind === "renamed") {
2227
+ throw new Error(`Stage ${stage.id} cannot assign renamed file ${change.file} until path-transition units are supported.`);
2228
+ }
2229
+ claimHunks(file, change.hunks, `stage ${stage.id}`, ownership);
2230
+ claims.push({ file, ids: [...change.hunks] });
2231
+ }
2232
+ return { stage, claims };
2233
+ });
2234
+ for (const file of inventory.files) {
2235
+ const wholeFileExcluded = wholeFileExclusions.has(file);
2236
+ if (file.material !== "text" || file.changeKind === "renamed" || file.units.length === 0) {
2237
+ if (!wholeFileExcluded) {
2238
+ const reason = file.changeKind === "renamed" ? "rename path transitions are not supported yet" : file.material !== "text" ? `${file.material} content cannot be materialized` : "the change has no textual hunks";
2239
+ throw new Error(`Deconstructed scene ${scene.id} must exclude ${file.path} as a whole file because ${reason}.`);
2240
+ }
2241
+ continue;
2242
+ }
2243
+ for (const unit of file.units) {
2244
+ if (!ownership.has(unitKey(file, unit.id))) {
2245
+ throw new Error(`Unassigned change unit in ${file.path}: ${unit.id}`);
2246
+ }
1997
2247
  }
1998
- stack.pop();
1999
- replacementStack.pop();
2000
- return canonicalizedObj;
2001
2248
  }
2002
- if (obj && obj.toJSON) {
2003
- obj = obj.toJSON();
2249
+ for (const file of inventory.files) {
2250
+ cumulativeByFile.set(file, new Set(excludedByFile.get(file) || []));
2004
2251
  }
2005
- if (_typeof(obj) === "object" && obj !== null) {
2006
- stack.push(obj);
2007
- canonicalizedObj = {};
2008
- replacementStack.push(canonicalizedObj);
2009
- var sortedKeys = [], _key;
2010
- for (_key in obj) {
2011
- if (obj.hasOwnProperty(_key)) {
2012
- sortedKeys.push(_key);
2252
+ const materializableFiles = inventory.files.filter((file) => file.material === "text" && file.changeKind !== "renamed" && file.baseContent !== void 0 && file.headContent !== void 0);
2253
+ const baselineFiles = materializableFiles.map((file) => buildFileState(
2254
+ file,
2255
+ cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
2256
+ wholeFileExclusions.has(file)
2257
+ ));
2258
+ const stages = [];
2259
+ for (const { stage, claims } of stageClaims) {
2260
+ const introducedHunks = [];
2261
+ const introducedFiles = [];
2262
+ const introducedChanges = [];
2263
+ for (const claim of claims) {
2264
+ const cumulative = cumulativeByFile.get(claim.file) || /* @__PURE__ */ new Set();
2265
+ claim.ids.forEach((id) => {
2266
+ cumulative.add(id);
2267
+ introducedHunks.push(id);
2268
+ });
2269
+ cumulativeByFile.set(claim.file, cumulative);
2270
+ if (!introducedFiles.includes(claim.file.path)) introducedFiles.push(claim.file.path);
2271
+ const introducedChange = introducedChanges.find((change) => change.file === claim.file.path);
2272
+ if (introducedChange) {
2273
+ introducedChange.hunks.push(...claim.ids);
2274
+ } else {
2275
+ introducedChanges.push({ file: claim.file.path, hunks: [...claim.ids] });
2013
2276
  }
2014
2277
  }
2015
- sortedKeys.sort();
2016
- for (i = 0; i < sortedKeys.length; i += 1) {
2017
- _key = sortedKeys[i];
2018
- canonicalizedObj[_key] = canonicalize(obj[_key], stack, replacementStack, replacer, _key);
2278
+ stages.push({
2279
+ id: stage.id,
2280
+ title: stage.title,
2281
+ narration: stage.narration,
2282
+ introducedHunks,
2283
+ introducedFiles,
2284
+ introducedChanges,
2285
+ cumulativeHunks: [...cumulativeByFile.values()].flatMap((ids) => [...ids]),
2286
+ files: materializableFiles.map((file) => buildFileState(
2287
+ file,
2288
+ cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
2289
+ wholeFileExclusions.has(file)
2290
+ ))
2291
+ });
2292
+ }
2293
+ for (const file of materializableFiles) {
2294
+ const finalState = buildFileState(
2295
+ file,
2296
+ cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
2297
+ wholeFileExclusions.has(file)
2298
+ );
2299
+ const expectedExists = file.changeKind !== "deleted";
2300
+ if (finalState.exists !== expectedExists || finalState.content !== file.headContent) {
2301
+ throw new Error(`Final deconstructed state does not match the target for ${file.path}.`);
2019
2302
  }
2020
- stack.pop();
2021
- replacementStack.pop();
2022
- } else {
2023
- canonicalizedObj = obj;
2024
2303
  }
2025
- return canonicalizedObj;
2304
+ return {
2305
+ id: scene.id,
2306
+ title: scene.title,
2307
+ baseOid: inventory.range.baseOid,
2308
+ targetOid: inventory.range.headOid,
2309
+ baselineFiles,
2310
+ excludedHunks: [...excludedByFile.values()].flatMap((ids) => [...ids]),
2311
+ excludedFiles,
2312
+ stages
2313
+ };
2026
2314
  }
2027
- var arrayDiff = new Diff();
2028
- arrayDiff.tokenize = function(value) {
2029
- return value.slice();
2030
- };
2031
- arrayDiff.join = arrayDiff.removeEmpty = function(value) {
2032
- return value;
2033
- };
2034
- function diffArrays(oldArr, newArr, callback) {
2035
- return arrayDiff.diff(oldArr, newArr, callback);
2315
+ function requireInventoryFile(inventory, path, owner) {
2316
+ const matches = inventory.files.filter((file) => file.path === path || file.previousPath === path);
2317
+ if (matches.length === 0) throw new Error(`${owner} references a file outside the change inventory: ${path}`);
2318
+ if (matches.length > 1) throw new Error(`${owner} references an ambiguous changed path: ${path}`);
2319
+ return matches[0];
2320
+ }
2321
+ function claimHunks(file, ids, owner, ownership) {
2322
+ const known = new Set(file.units.map((unit) => unit.id));
2323
+ for (const id of ids) {
2324
+ if (!known.has(id)) throw new Error(`${owner} references an unknown hunk in ${file.path}: ${id}`);
2325
+ const key = unitKey(file, id);
2326
+ const previousOwner = ownership.get(key);
2327
+ if (previousOwner) throw new Error(`Change unit ${id} in ${file.path} is assigned to both ${previousOwner} and ${owner}.`);
2328
+ ownership.set(key, owner);
2329
+ }
2330
+ }
2331
+ function unitKey(file, id) {
2332
+ return `${file.previousPath || ""}\0${file.path}\0${id}`;
2333
+ }
2334
+ function buildFileState(file, selected, wholeFileExcluded) {
2335
+ if (file.baseContent === void 0) throw new Error(`Missing base content for ${file.path}.`);
2336
+ const content = materializeChangeUnits(file.baseContent, file.units, selected);
2337
+ let exists = true;
2338
+ if (file.changeKind === "added") exists = wholeFileExcluded || selected.size > 0;
2339
+ if (file.changeKind === "deleted") exists = !(wholeFileExcluded || selected.size === file.units.length);
2340
+ return {
2341
+ path: file.path,
2342
+ previousPath: file.previousPath,
2343
+ changeKind: file.changeKind,
2344
+ exists,
2345
+ content: exists ? content : "",
2346
+ appliedHunks: [...selected]
2347
+ };
2036
2348
  }
2037
2349
 
2038
2350
  // src/diffEngine.ts
@@ -2065,17 +2377,10 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
2065
2377
  model.formattingOnly = true;
2066
2378
  return model;
2067
2379
  }
2068
- const interned = internDiffLines(leftLines, rightLines);
2069
- const internedChanges = diffArrays(interned.left, interned.right, {
2070
- timeout: options.timeoutMs ?? DIFF_TIMEOUT_MS
2071
- });
2072
- if (!internedChanges) {
2380
+ const changes = diffAnchoredLines(leftLines, rightLines, options.timeoutMs ?? DIFF_TIMEOUT_MS);
2381
+ if (!changes) {
2073
2382
  return buildConservativeTwoWayDiffModel(leftLines, rightLines);
2074
2383
  }
2075
- const changes = internedChanges.map((change) => ({
2076
- ...change,
2077
- value: change.value.map((lineId) => interned.lines[lineId])
2078
- }));
2079
2384
  const rows = [];
2080
2385
  const renderedLeftLines = [];
2081
2386
  const renderedRightLines = [];
@@ -2190,6 +2495,54 @@ function mergeTouchingBlocks(blocks) {
2190
2495
  }
2191
2496
  return merged;
2192
2497
  }
2498
+ function diffAnchoredLines(leftLines, rightLines, timeoutMs) {
2499
+ const deadline = Date.now() + timeoutMs;
2500
+ const namedLines = (lines) => lines.flatMap((line, index) => {
2501
+ const key = normalizeMatchingContent(line);
2502
+ return isInformativeLine(key) ? [{ key, index }] : [];
2503
+ });
2504
+ const leftNames = namedLines(leftLines);
2505
+ const rightNames = namedLines(rightLines);
2506
+ const namedChanges = diffArrays(leftNames.map((line) => line.key), rightNames.map((line) => line.key), {
2507
+ timeout: deadline - Date.now()
2508
+ });
2509
+ if (!namedChanges) return void 0;
2510
+ const changes = [];
2511
+ let leftStart = 0;
2512
+ let rightStart = 0;
2513
+ const appendGap = (leftEnd, rightEnd) => {
2514
+ if (leftStart === leftEnd && rightStart === rightEnd) return true;
2515
+ const interned = internDiffLines(leftLines.slice(leftStart, leftEnd), rightLines.slice(rightStart, rightEnd));
2516
+ const gap = diffArrays(interned.left, interned.right, { timeout: deadline - Date.now() });
2517
+ if (!gap) return false;
2518
+ changes.push(...gap.map((change) => ({ ...change, value: change.value.map((id) => interned.lines[id]) })));
2519
+ return true;
2520
+ };
2521
+ let leftName = 0;
2522
+ let rightName = 0;
2523
+ for (const change of namedChanges) {
2524
+ if (change.removed) {
2525
+ leftName += change.value.length;
2526
+ continue;
2527
+ }
2528
+ if (change.added) {
2529
+ rightName += change.value.length;
2530
+ continue;
2531
+ }
2532
+ for (let index = 0; index < change.value.length; index++) {
2533
+ const leftIndex = leftNames[leftName++].index;
2534
+ const rightIndex = rightNames[rightName++].index;
2535
+ if (!appendGap(leftIndex, rightIndex)) return void 0;
2536
+ const left = leftLines[leftIndex];
2537
+ const right = rightLines[rightIndex];
2538
+ if (left === right) changes.push({ value: [left] });
2539
+ else changes.push({ removed: true, value: [left] }, { added: true, value: [right] });
2540
+ leftStart = leftIndex + 1;
2541
+ rightStart = rightIndex + 1;
2542
+ }
2543
+ }
2544
+ return appendGap(leftLines.length, rightLines.length) ? changes : void 0;
2545
+ }
2193
2546
  function internDiffLines(leftLines, rightLines) {
2194
2547
  const lineIds = /* @__PURE__ */ new Map();
2195
2548
  const lines = [];
@@ -3170,8 +3523,8 @@ function buildInlineSegments(leftContent, rightContent) {
3170
3523
  }
3171
3524
  }
3172
3525
  return {
3173
- leftSegments,
3174
- rightSegments,
3526
+ leftSegments: normalizeIndentationHighlights(leftSegments),
3527
+ rightSegments: normalizeIndentationHighlights(rightSegments),
3175
3528
  hasInlineChanges
3176
3529
  };
3177
3530
  }
@@ -3205,7 +3558,40 @@ function buildWhitespaceOnlySegments(left, right) {
3205
3558
  append(leftSegments, "context", part.slice(part.length - suffix));
3206
3559
  append(rightSegments, "context", other.slice(other.length - suffix));
3207
3560
  });
3208
- return { leftSegments, rightSegments };
3561
+ return {
3562
+ leftSegments: normalizeIndentationHighlights(leftSegments),
3563
+ rightSegments: normalizeIndentationHighlights(rightSegments)
3564
+ };
3565
+ }
3566
+ function normalizeIndentationHighlights(segments) {
3567
+ const result = [];
3568
+ splitSegmentsIntoLines(segments).forEach((line, index) => {
3569
+ if (index > 0) result.push({ kind: "context", text: "\n", emphasis: false });
3570
+ const content = line.map((segment) => segment.text).join("");
3571
+ const indentLength = content.match(/^[\t ]*/u)[0].length;
3572
+ let offset = 0;
3573
+ let emphasized = 0;
3574
+ let kind = "context";
3575
+ for (const segment of line) {
3576
+ if (offset >= indentLength) break;
3577
+ if (segment.emphasis) {
3578
+ emphasized += Math.min(segment.text.length, indentLength - offset);
3579
+ kind = segment.kind;
3580
+ }
3581
+ offset += segment.text.length;
3582
+ }
3583
+ if (emphasized) result.push({ kind, text: content.slice(0, emphasized), emphasis: true });
3584
+ if (indentLength > emphasized) {
3585
+ result.push({ kind: "context", text: content.slice(emphasized, indentLength), emphasis: false });
3586
+ }
3587
+ offset = 0;
3588
+ for (const segment of line) {
3589
+ const start = Math.max(0, indentLength - offset);
3590
+ if (start < segment.text.length) result.push({ ...segment, text: segment.text.slice(start) });
3591
+ offset += segment.text.length;
3592
+ }
3593
+ });
3594
+ return result;
3209
3595
  }
3210
3596
  function splitSegmentsIntoLines(segments) {
3211
3597
  const lines = [[]];
@@ -3341,8 +3727,8 @@ var LOCKFILES = /* @__PURE__ */ new Set([
3341
3727
  ]);
3342
3728
  var MAX_FAN_IN_FILES = 250;
3343
3729
  var MAX_FAN_IN_TEXT_BYTES = 64 * 1024;
3344
- function classifyChangeFileRole(filePath) {
3345
- const lower = filePath.toLowerCase();
3730
+ function classifyChangeFileRole(filePath2) {
3731
+ const lower = filePath2.toLowerCase();
3346
3732
  const fileName = lower.split("/").pop() || lower;
3347
3733
  if (/(^|\/)(dist|build|generated|vendor)(\/|$)/.test(lower) || /(?:\.min\.js|\.map)$/.test(lower)) return "generated";
3348
3734
  if (/(^|\/)(tests?|__tests__)(\/|$)/.test(lower) || /(^|\/)test_[^/]+$/.test(lower) || /\.(test|spec)\.[^/]+$/.test(lower)) return "test";
@@ -3659,10 +4045,21 @@ function buildChangeTourManifest(startPath, options = {}) {
3659
4045
  }
3660
4046
  function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot, imageRevisions) {
3661
4047
  var _a, _b;
4048
+ const correspondence = source.correspondence ? (() => {
4049
+ const file = (0, import_fs2.realpathSync)((0, import_path.resolve)(repoRoot, source.correspondence));
4050
+ const name = (0, import_path.relative)((0, import_fs2.realpathSync)(repoRoot), file);
4051
+ if ((0, import_path.isAbsolute)(name) || name === ".." || name.startsWith(`..${import_path.sep}`)) throw new Error("Correspondence artifact must be inside the repository.");
4052
+ return loadChangeCorrespondence(repoRoot, file, { baseOid: imageRevisions.base, headOid: imageRevisions.head });
4053
+ })() : void 0;
4054
+ const compileSymbol = (step, focus) => {
4055
+ if (!step.symbol) return void 0;
4056
+ if (!correspondence) throw new Error(`Step ${step.id} requires a correspondence artifact.`);
4057
+ return compileTourSymbol(correspondence, step.symbol, focus);
4058
+ };
3662
4059
  const available = new Map(defaultScenes.map((scene) => [scene.path, scene]));
3663
4060
  const resolvedAnchors = /* @__PURE__ */ new Map();
3664
4061
  for (const [id, anchor] of Object.entries(source.anchors)) {
3665
- const file = available.get(anchor.file);
4062
+ const file = available.get(anchor.file) || (anchor.revision === "base" ? defaultScenes.find((scene) => scene.previousPath === anchor.file) : void 0);
3666
4063
  if (!file) {
3667
4064
  throw new Error(`Anchor ${id} references a file outside the text change range: ${anchor.file}`);
3668
4065
  }
@@ -3699,7 +4096,8 @@ function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot,
3699
4096
  available,
3700
4097
  resolvedAnchors,
3701
4098
  connections,
3702
- (image) => compileTourImage(repoRoot, image, imageRevisions[image.revision])
4099
+ (image) => compileTourImage(repoRoot, image, imageRevisions[image.revision]),
4100
+ compileSymbol
3703
4101
  );
3704
4102
  finalScenes.push(finalScene);
3705
4103
  finalSceneIds.push(finalScene.id);
@@ -3728,7 +4126,8 @@ function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot,
3728
4126
  available,
3729
4127
  resolvedAnchors,
3730
4128
  connections,
3731
- (image) => compileTourImage(repoRoot, image, imageRevisions[image.revision])
4129
+ (image) => compileTourImage(repoRoot, image, imageRevisions[image.revision]),
4130
+ compileSymbol
3732
4131
  );
3733
4132
  scenes.push(scene);
3734
4133
  sceneIds.push(scene.id);
@@ -3833,7 +4232,7 @@ function compileSceneOverview(overview, includeComparison = true) {
3833
4232
  ...includeComparison && overview.comparison ? { comparison: { ...overview.comparison } } : {}
3834
4233
  };
3835
4234
  }
3836
- function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections, resolveImage) {
4235
+ function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections, resolveImage, compileSymbol) {
3837
4236
  const overview = compileSceneOverview(authoredScene.overview, false);
3838
4237
  return {
3839
4238
  id: authoredScene.id,
@@ -3846,7 +4245,7 @@ function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors
3846
4245
  ...overview ? { overview } : {},
3847
4246
  steps: authoredScene.steps.map((step) => {
3848
4247
  const focus = requireResolvedAnchor(resolvedAnchors, step.focus);
3849
- const diff2 = available.get(focus.path);
4248
+ const diff2 = available.get(focus.path) || (focus.revision === "base" ? [...available.values()].find((scene) => scene.previousPath === focus.path) : void 0);
3850
4249
  if (!diff2) throw new Error(`Step ${step.id} has no text diff for ${focus.path}.`);
3851
4250
  return {
3852
4251
  id: step.id,
@@ -3856,6 +4255,7 @@ function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors
3856
4255
  requirement: step.requirement,
3857
4256
  ...step.image ? { image: resolveImage(step.image) } : {},
3858
4257
  focus,
4258
+ ...step.symbol ? { symbol: compileSymbol(step, focus) } : {},
3859
4259
  connection: step.connection ? connections.get(step.connection) : void 0,
3860
4260
  diff: { ...diff2, id: `${authoredScene.id}-${step.id}` }
3861
4261
  };
@@ -3875,20 +4275,20 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, a
3875
4275
  stageId: stage.id
3876
4276
  }))];
3877
4277
  const materializablePaths = new Set(compiled.baselineFiles.map((file) => file.path));
3878
- const includedPaths = availableFilePaths.length > 0 ? availableFilePaths.filter((filePath) => materializablePaths.has(filePath)).sort() : [...materializablePaths].sort();
3879
- const missingIntroducedPath = compiled.stages.flatMap((stage) => stage.introducedFiles).find((filePath) => !includedPaths.includes(filePath));
4278
+ const includedPaths = availableFilePaths.length > 0 ? availableFilePaths.filter((filePath2) => materializablePaths.has(filePath2)).sort() : [...materializablePaths].sort();
4279
+ const missingIntroducedPath = compiled.stages.flatMap((stage) => stage.introducedFiles).find((filePath2) => !includedPaths.includes(filePath2));
3880
4280
  if (missingIntroducedPath) {
3881
4281
  throw new Error(`Deconstructed tour file is not renderable: ${missingIntroducedPath}`);
3882
4282
  }
3883
4283
  const introducedPaths = new Set(compiled.stages.flatMap((stage) => stage.introducedFiles));
3884
4284
  let totalContentBytes = 0;
3885
- const files = includedPaths.flatMap((filePath) => {
4285
+ const files = includedPaths.flatMap((filePath2) => {
3886
4286
  const states = [
3887
- compiled.baselineFiles.find((file) => file.path === filePath),
3888
- ...compiled.stages.map((stage) => stage.files.find((file) => file.path === filePath))
4287
+ compiled.baselineFiles.find((file) => file.path === filePath2),
4288
+ ...compiled.stages.map((stage) => stage.files.find((file) => file.path === filePath2))
3889
4289
  ];
3890
4290
  if (states.some((state) => !state)) {
3891
- throw new Error(`Deconstructed scene ${source.id} is missing virtual state for ${filePath}.`);
4291
+ throw new Error(`Deconstructed scene ${source.id} is missing virtual state for ${filePath2}.`);
3892
4292
  }
3893
4293
  const fileContentBytes = states.reduce((total, state) => total + Buffer.byteLength((state == null ? void 0 : state.content) || "", "utf8"), 0);
3894
4294
  const exceedsFileLimit = states.some((state) => {
@@ -3897,20 +4297,20 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, a
3897
4297
  });
3898
4298
  const exceedsSceneLimit = totalContentBytes + fileContentBytes > DEFAULT_MAX_STACK_CONTENT_BYTES;
3899
4299
  if (exceedsFileLimit || exceedsSceneLimit) {
3900
- if (introducedPaths.has(filePath)) {
3901
- throw new Error(exceedsFileLimit ? `Deconstructed tour file is too large to present: ${filePath}` : `Deconstructed tour content exceeds ${DEFAULT_MAX_STACK_CONTENT_BYTES} bytes.`);
4300
+ if (introducedPaths.has(filePath2)) {
4301
+ throw new Error(exceedsFileLimit ? `Deconstructed tour file is too large to present: ${filePath2}` : `Deconstructed tour content exceeds ${DEFAULT_MAX_STACK_CONTENT_BYTES} bytes.`);
3902
4302
  }
3903
4303
  return [];
3904
4304
  }
3905
4305
  totalContentBytes += fileContentBytes;
3906
4306
  return [{
3907
- path: filePath,
4307
+ path: filePath2,
3908
4308
  panels: states.map((state, index) => {
3909
- if (!state) throw new Error(`Missing virtual state for ${filePath}.`);
4309
+ if (!state) throw new Error(`Missing virtual state for ${filePath2}.`);
3910
4310
  return {
3911
4311
  id: panels[index].id,
3912
- label: `${panels[index].label} / ${filePath}${state.exists ? "" : " (absent)"}`,
3913
- path: state.exists ? filePath : void 0,
4312
+ label: `${panels[index].label} / ${filePath2}${state.exists ? "" : " (absent)"}`,
4313
+ path: state.exists ? filePath2 : void 0,
3914
4314
  content: state.content,
3915
4315
  exists: state.exists
3916
4316
  };
@@ -4004,7 +4404,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
4004
4404
  oid: runGitText(repoRoot, ["rev-parse", "--verify", `${entry.ref}^{commit}`]),
4005
4405
  label: entry.label || entry.ref
4006
4406
  }));
4007
- const adjacentChanges = stack.slice(0, -1).map((entry, index) => parseNameStatusZ((0, import_child_process3.execFileSync)("git", [
4407
+ const adjacentChanges = stack.slice(0, -1).map((entry, index) => parseNameStatusZ((0, import_child_process4.execFileSync)("git", [
4008
4408
  "diff",
4009
4409
  "--name-status",
4010
4410
  "-z",
@@ -4028,8 +4428,8 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
4028
4428
  ...source.steps.map((step) => step.file)
4029
4429
  ]);
4030
4430
  let totalContentBytes = 0;
4031
- const files = filePaths.flatMap((filePath) => {
4032
- const aliases = /* @__PURE__ */ new Set([filePath]);
4431
+ const files = filePaths.flatMap((filePath2) => {
4432
+ const aliases = /* @__PURE__ */ new Set([filePath2]);
4033
4433
  for (const changes of [...adjacentChanges].reverse()) {
4034
4434
  for (const change of changes) {
4035
4435
  if (aliases.has(change.path) || change.previousPath && aliases.has(change.previousPath)) {
@@ -4042,15 +4442,15 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
4042
4442
  const panels = [];
4043
4443
  for (const entry of stack) {
4044
4444
  const candidates = [...aliases].filter((candidate) => gitBlobExists(repoRoot, entry.oid, candidate));
4045
- if (candidates.length > 1) throw new Error(`Ambiguous revision paths for ${filePath} at ${entry.ref}: ${candidates.join(", ")}`);
4445
+ if (candidates.length > 1) throw new Error(`Ambiguous revision paths for ${filePath2} at ${entry.ref}: ${candidates.join(", ")}`);
4046
4446
  const resolvedPath = candidates[0];
4047
4447
  if (!resolvedPath) {
4048
- panels.push({ id: entry.id, label: `${entry.label} / ${filePath} (missing)`, content: "", exists: false });
4448
+ panels.push({ id: entry.id, label: `${entry.label} / ${filePath2} (missing)`, content: "", exists: false });
4049
4449
  continue;
4050
4450
  }
4051
4451
  const content = readGitText(repoRoot, entry.oid, resolvedPath, DEFAULT_MAX_TOUR_FILE_BYTES, DEFAULT_MAX_TOUR_LINE_BYTES);
4052
4452
  if (content.kind !== "text") {
4053
- if (requiredFilePaths.has(filePath)) {
4453
+ if (requiredFilePaths.has(filePath2)) {
4054
4454
  throw new Error(`Stacked tour file is binary or too large: ${resolvedPath}`);
4055
4455
  }
4056
4456
  return [];
@@ -4068,7 +4468,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
4068
4468
  if (totalContentBytes > DEFAULT_MAX_STACK_CONTENT_BYTES) {
4069
4469
  throw new Error(`Stacked tour content exceeds ${DEFAULT_MAX_STACK_CONTENT_BYTES} bytes.`);
4070
4470
  }
4071
- return [{ path: filePath, panels }];
4471
+ return [{ path: filePath2, panels }];
4072
4472
  });
4073
4473
  if (files.length === 0) throw new Error(`Stacked scene ${source.id} has no materializable text files.`);
4074
4474
  const stackIds = stack.map((entry) => entry.id);
@@ -4108,7 +4508,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
4108
4508
  }
4109
4509
  function gitBlobExists(repoRoot, oid, relativePath) {
4110
4510
  try {
4111
- (0, import_child_process3.execFileSync)("git", ["cat-file", "-e", `${oid}:${relativePath}`], {
4511
+ (0, import_child_process4.execFileSync)("git", ["cat-file", "-e", `${oid}:${relativePath}`], {
4112
4512
  cwd: repoRoot,
4113
4513
  stdio: ["ignore", "ignore", "ignore"]
4114
4514
  });
@@ -4118,7 +4518,7 @@ function gitBlobExists(repoRoot, oid, relativePath) {
4118
4518
  }
4119
4519
  }
4120
4520
  function runGitText(repoRoot, args) {
4121
- return (0, import_child_process3.execFileSync)("git", args, {
4521
+ return (0, import_child_process4.execFileSync)("git", args, {
4122
4522
  cwd: repoRoot,
4123
4523
  encoding: "utf8",
4124
4524
  stdio: ["ignore", "pipe", "pipe"]
@@ -4201,9 +4601,9 @@ function buildChapters(records, sceneIdByPath) {
4201
4601
  sceneIds: records.filter(({ chapter }) => chapter.id === definition.id).map(({ scene }) => sceneIdByPath.get(scene.path)).filter((sceneId) => Boolean(sceneId))
4202
4602
  }));
4203
4603
  }
4204
- function chapterForPath(filePath) {
4205
- const lower = filePath.toLowerCase();
4206
- const role = classifyChangeFileRole(filePath);
4604
+ function chapterForPath(filePath2) {
4605
+ const lower = filePath2.toLowerCase();
4606
+ const role = classifyChangeFileRole(filePath2);
4207
4607
  if (role === "documentation" || lower.includes("architecture")) {
4208
4608
  return CHAPTERS.context;
4209
4609
  }
@@ -4273,9 +4673,9 @@ function readGitText(repoRoot, oid, relativePath, maxBytes, maxLineBytes) {
4273
4673
  if (!relativePath) {
4274
4674
  return { kind: "text", content: "" };
4275
4675
  }
4276
- const size = Number((0, import_child_process3.execFileSync)("git", ["cat-file", "-s", `${oid}:${relativePath}`], { cwd: repoRoot, encoding: "utf8" }).trim());
4676
+ const size = Number((0, import_child_process4.execFileSync)("git", ["cat-file", "-s", `${oid}:${relativePath}`], { cwd: repoRoot, encoding: "utf8" }).trim());
4277
4677
  if (size > maxBytes) return { kind: "binary-or-large", content: "" };
4278
- const content = (0, import_child_process3.execFileSync)("git", ["show", `${oid}:${relativePath}`], {
4678
+ const content = (0, import_child_process4.execFileSync)("git", ["show", `${oid}:${relativePath}`], {
4279
4679
  cwd: repoRoot,
4280
4680
  encoding: "buffer",
4281
4681
  maxBuffer: Math.max(maxBytes + 1, DEFAULT_MAX_TOUR_FILE_BYTES + 1),
@@ -4295,15 +4695,15 @@ function hasLineLongerThan(content, maxLineBytes) {
4295
4695
  }
4296
4696
  return content.length - lineStart > maxLineBytes;
4297
4697
  }
4298
- function readGitLineStats(repoRoot, baseOid, headOid, filePath, previousPath) {
4299
- const output = (0, import_child_process3.execFileSync)("git", [
4698
+ function readGitLineStats(repoRoot, baseOid, headOid, filePath2, previousPath) {
4699
+ const output = (0, import_child_process4.execFileSync)("git", [
4300
4700
  "diff",
4301
4701
  "--numstat",
4302
4702
  "--find-renames",
4303
4703
  baseOid,
4304
4704
  headOid,
4305
4705
  "--",
4306
- ...[previousPath, filePath].filter((candidate) => Boolean(candidate))
4706
+ ...[previousPath, filePath2].filter((candidate) => Boolean(candidate))
4307
4707
  ], {
4308
4708
  cwd: repoRoot,
4309
4709
  encoding: "utf8",
@@ -4341,10 +4741,10 @@ function pinTourSource(source, repoRoot, resolved, baseOid, headOid) {
4341
4741
  return copy;
4342
4742
  }
4343
4743
  function compileTourImage(repoRoot, source, revision) {
4344
- const object = `${revision}:${source.file}`;
4744
+ const object2 = `${revision}:${source.file}`;
4345
4745
  let size;
4346
4746
  try {
4347
- size = Number((0, import_child_process3.execFileSync)("git", ["cat-file", "-s", object], {
4747
+ size = Number((0, import_child_process4.execFileSync)("git", ["cat-file", "-s", object2], {
4348
4748
  cwd: repoRoot,
4349
4749
  encoding: "utf8",
4350
4750
  stdio: ["ignore", "pipe", "pipe"]
@@ -4355,7 +4755,7 @@ function compileTourImage(repoRoot, source, revision) {
4355
4755
  if (!Number.isSafeInteger(size) || size < 33 || size > MAX_TOUR_IMAGE_BYTES) {
4356
4756
  throw new Error(`Image ${source.file} must be a PNG of at most 8 MiB.`);
4357
4757
  }
4358
- const bytes = (0, import_child_process3.execFileSync)("git", ["cat-file", "blob", object], { cwd: repoRoot, maxBuffer: MAX_TOUR_IMAGE_BYTES });
4758
+ const bytes = (0, import_child_process4.execFileSync)("git", ["cat-file", "blob", object2], { cwd: repoRoot, maxBuffer: MAX_TOUR_IMAGE_BYTES });
4359
4759
  validatePngHeader(bytes, source.file);
4360
4760
  return { path: source.file, revision, alt: source.alt, dataUrl: `data:image/png;base64,${bytes.toString("base64")}` };
4361
4761
  }
@@ -4402,7 +4802,11 @@ function buildTourCoverageReport(startPath, sourceValue) {
4402
4802
  for (const scene of walkthroughScenes) {
4403
4803
  if (scene.kind !== "walkthrough") continue;
4404
4804
  for (const step of scene.steps) {
4405
- const matches = includedUnits.filter((unit) => evidenceIntersectsUnit(step.focus, unit));
4805
+ const endpoints = step.symbol ? [[step.symbol.before, "base"], [step.symbol.after, "head"]].flatMap(([entry, revision]) => {
4806
+ if (!entry) return [];
4807
+ return [{ path: entry.path, revision, startLine: entry.startLine, endLine: entry.endLine }];
4808
+ }) : [step.focus];
4809
+ const matches = includedUnits.filter((unit) => endpoints.some((endpoint) => evidenceIntersectsUnit(endpoint, unit)));
4406
4810
  if (matches.length === 0) {
4407
4811
  contextualOnlyEvidence.push({ sceneId: scene.id, stepId: step.id, path: step.focus.path });
4408
4812
  continue;