@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.
- package/CHANGELOG.md +24 -0
- package/README.md +32 -0
- package/bin/bygone.js +8 -1
- package/cli/commandSpec.js +11 -2
- package/cli/completions.js +9 -8
- package/cli/correspondence.js +71 -0
- package/cli/present.js +74 -20
- package/cli/tour.js +8 -1
- package/cli/tourExport.js +1 -1
- package/completions/_bygone +5 -3
- package/completions/bygone +5 -5
- package/completions/bygone.fish +2 -0
- package/docs/change-tour-format.md +60 -6
- package/docs/correspondence.md +259 -0
- package/docs/generating-change-tours.md +3 -0
- package/docs/present.md +3 -0
- package/docs/releasing.md +11 -0
- package/media/diff.worker.js +10 -9
- package/media/webview.css +1 -1
- package/media/webview.js +178 -178
- package/out/changeCorrespondence.js +739 -0
- package/out/changeTour.js +1359 -960
- package/out/gitComparison.js +4 -0
- package/out/standalone-main.js +3279 -1341
- package/out/tourCoverage.js +1852 -1448
- package/package.json +2 -2
- package/schemas/change-tour-source.schema.json +48 -3
- package/web/export-runtime.json +6 -6
- package/web/index.html +12 -0
- package/web/presenter.css +81 -0
- package/web/web-host.js +23 -20
package/out/tourCoverage.js
CHANGED
|
@@ -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
|
|
119
|
-
if (!
|
|
120
|
+
const status2 = fields[index++];
|
|
121
|
+
if (!status2) {
|
|
120
122
|
continue;
|
|
121
123
|
}
|
|
122
|
-
const code =
|
|
123
|
-
const similarityText =
|
|
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
|
|
477
|
+
var import_fs2 = require("fs");
|
|
478
|
+
var import_path = require("path");
|
|
513
479
|
|
|
514
|
-
// src/
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
//
|
|
547
|
-
function
|
|
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
|
-
|
|
604
|
-
var
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
776
|
-
|
|
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
|
-
|
|
791
|
-
|
|
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
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
-
|
|
699
|
+
return components;
|
|
888
700
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
if (
|
|
907
|
-
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
if (
|
|
958
|
-
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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 (
|
|
995
|
-
|
|
812
|
+
if (obj && obj.toJSON) {
|
|
813
|
+
obj = obj.toJSON();
|
|
996
814
|
}
|
|
997
|
-
if (
|
|
998
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
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
|
|
1011
|
-
if (
|
|
1012
|
-
|
|
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
|
|
1016
|
-
if (value !==
|
|
1017
|
-
|
|
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
|
|
1021
|
-
if (
|
|
1022
|
-
|
|
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
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
|
1031
|
-
const
|
|
1032
|
-
if (
|
|
1033
|
-
|
|
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
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
if (
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
-
|
|
1117
|
-
|
|
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
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
if (
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
for (const
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
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
|
-
|
|
1207
|
-
|
|
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
|
-
|
|
1210
|
-
|
|
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
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
1238
|
-
if (
|
|
1239
|
-
throw new Error(
|
|
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
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
1268
|
-
|
|
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
|
|
1271
|
-
throw new Error(
|
|
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
|
-
|
|
1274
|
-
|
|
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
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
-
|
|
1330
|
-
|
|
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
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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
|
-
|
|
1341
|
-
if (
|
|
1342
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
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
|
-
|
|
1352
|
-
for (const
|
|
1353
|
-
|
|
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
|
-
|
|
1356
|
-
|
|
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
|
-
|
|
1359
|
-
|
|
1360
|
-
if (!
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
|
|
1377
|
-
|
|
1378
|
-
if (
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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 (
|
|
1385
|
-
throw new Error(
|
|
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
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
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
|
-
|
|
1395
|
-
|
|
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
|
|
1406
|
-
if (!Array.isArray(
|
|
1407
|
-
|
|
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
|
|
1422
|
-
if (!Array.isArray(
|
|
1423
|
-
|
|
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
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
-
|
|
1444
|
-
|
|
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
|
-
|
|
1447
|
-
|
|
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
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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 (
|
|
1461
|
-
|
|
1462
|
-
|
|
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
|
|
1483
|
-
if (value
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
|
1490
|
-
|
|
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
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
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
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
-
|
|
1508
|
-
|
|
1509
|
-
throw new Error(`${
|
|
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
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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
|
-
|
|
1578
|
-
|
|
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
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
)
|
|
1586
|
-
|
|
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
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
|
1644
|
-
|
|
1645
|
-
if (
|
|
1646
|
-
|
|
1647
|
-
|
|
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
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
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
|
|
1660
|
-
|
|
1668
|
+
function optionalString(value, path) {
|
|
1669
|
+
if (value !== void 0) {
|
|
1670
|
+
requireString2(value, path);
|
|
1671
|
+
}
|
|
1661
1672
|
}
|
|
1662
|
-
function
|
|
1663
|
-
if (
|
|
1664
|
-
|
|
1665
|
-
|
|
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
|
-
|
|
1679
|
-
|
|
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
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
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
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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 (
|
|
1764
|
-
(
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
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
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
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
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
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
|
-
|
|
1821
|
-
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
if (
|
|
1825
|
-
|
|
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
|
-
|
|
1831
|
-
|
|
1832
|
-
for (
|
|
1833
|
-
|
|
1834
|
-
|
|
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
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
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
|
-
|
|
1850
|
-
|
|
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
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
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
|
-
|
|
1874
|
-
if (
|
|
1875
|
-
|
|
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
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
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
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
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
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
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
|
-
|
|
1923
|
-
|
|
1924
|
-
if (
|
|
1925
|
-
value
|
|
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
|
-
|
|
1928
|
-
|
|
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 (
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
};
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
}
|
|
1952
|
-
function
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
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
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
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
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
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
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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
|
-
|
|
2003
|
-
|
|
2249
|
+
for (const file of inventory.files) {
|
|
2250
|
+
cumulativeByFile.set(file, new Set(excludedByFile.get(file) || []));
|
|
2004
2251
|
}
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
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
|
|
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
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
};
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
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
|
|
2069
|
-
|
|
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 {
|
|
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(
|
|
3345
|
-
const lower =
|
|
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((
|
|
3879
|
-
const missingIntroducedPath = compiled.stages.flatMap((stage) => stage.introducedFiles).find((
|
|
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((
|
|
4285
|
+
const files = includedPaths.flatMap((filePath2) => {
|
|
3886
4286
|
const states = [
|
|
3887
|
-
compiled.baselineFiles.find((file) => file.path ===
|
|
3888
|
-
...compiled.stages.map((stage) => stage.files.find((file) => file.path ===
|
|
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 ${
|
|
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(
|
|
3901
|
-
throw new Error(exceedsFileLimit ? `Deconstructed tour file is too large to present: ${
|
|
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:
|
|
4307
|
+
path: filePath2,
|
|
3908
4308
|
panels: states.map((state, index) => {
|
|
3909
|
-
if (!state) throw new Error(`Missing virtual state for ${
|
|
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} / ${
|
|
3913
|
-
path: state.exists ?
|
|
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,
|
|
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((
|
|
4032
|
-
const aliases = /* @__PURE__ */ new Set([
|
|
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 ${
|
|
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} / ${
|
|
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(
|
|
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:
|
|
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,
|
|
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,
|
|
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(
|
|
4205
|
-
const lower =
|
|
4206
|
-
const role = classifyChangeFileRole(
|
|
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,
|
|
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,
|
|
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,
|
|
4299
|
-
const output = (0,
|
|
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,
|
|
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
|
|
4744
|
+
const object2 = `${revision}:${source.file}`;
|
|
4345
4745
|
let size;
|
|
4346
4746
|
try {
|
|
4347
|
-
size = Number((0,
|
|
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,
|
|
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
|
|
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;
|