@davmash/bygone 0.8.14 → 0.9.2
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 +40 -0
- package/README.md +2 -2
- package/cli/commandSpec.js +12 -2
- package/cli/completions.js +1 -1
- package/cli/present.js +89 -7
- package/cli/tour.js +13 -2
- package/cli/tourFile.js +9 -0
- package/cli/tourHistory.js +432 -0
- package/completions/_bygone +2 -1
- package/completions/bygone +1 -1
- package/completions/bygone.fish +1 -0
- package/docs/change-tour-format.md +199 -7
- package/docs/explore.md +21 -1
- package/docs/generating-change-tours.md +70 -17
- package/docs/present.md +134 -5
- package/docs/product-surface.md +64 -8
- package/docs/releasing.md +25 -29
- package/examples/bygone-history.bygone +52 -10
- package/examples/navigation-lab.bygone.yaml +420 -0
- package/media/diff.worker.js +3 -3
- package/media/webview.css +1 -1
- package/media/webview.js +546 -178
- package/out/changeTour.js +1391 -469
- package/out/gitComparison.js +354 -0
- package/out/standalone-main.js +13989 -4749
- package/out/tourCoverage.js +1434 -461
- package/package.json +2 -2
- package/schemas/change-tour-source.schema.json +222 -3
- package/skills/pr-tour-guide/SKILL.md +111 -11
- package/standalone/index.html +6 -1
- package/web/index.html +114 -73
- package/web/presenter.css +658 -230
- package/web/web-host.js +374 -5
package/out/tourCoverage.js
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var tourCoverage_exports = {};
|
|
32
32
|
__export(tourCoverage_exports, {
|
|
33
33
|
TOUR_COVERAGE_REPORT_VERSION: () => TOUR_COVERAGE_REPORT_VERSION,
|
|
34
|
+
buildTourAuthoringCoverage: () => buildTourAuthoringCoverage,
|
|
34
35
|
buildTourCoverageReport: () => buildTourCoverageReport
|
|
35
36
|
});
|
|
36
37
|
module.exports = __toCommonJS(tourCoverage_exports);
|
|
@@ -473,33 +474,544 @@ function runGit(repoRoot, args) {
|
|
|
473
474
|
// src/changeTour.ts
|
|
474
475
|
var import_child_process3 = require("child_process");
|
|
475
476
|
|
|
477
|
+
// src/changeTourSource.ts
|
|
478
|
+
var CHANGE_TOUR_SOURCE_VERSION = 3;
|
|
479
|
+
function parseChangeTourSource(value) {
|
|
480
|
+
if (isRecord(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_SOURCE_VERSION) {
|
|
481
|
+
throw new Error(
|
|
482
|
+
`This tour uses source format version ${value.version}, but this version of Bygone supports up to version ${CHANGE_TOUR_SOURCE_VERSION}. Upgrade Bygone to open it.`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
if (!isRecord(value) || value.version !== 1 && value.version !== 2 && value.version !== CHANGE_TOUR_SOURCE_VERSION) {
|
|
486
|
+
throw new Error("Unsupported or missing change-tour source version.");
|
|
487
|
+
}
|
|
488
|
+
requireOnlyKeys(value, [
|
|
489
|
+
"version",
|
|
490
|
+
"title",
|
|
491
|
+
"windowTitle",
|
|
492
|
+
"sourceUrl",
|
|
493
|
+
"range",
|
|
494
|
+
"anchors",
|
|
495
|
+
"connections",
|
|
496
|
+
"chapters",
|
|
497
|
+
"tours",
|
|
498
|
+
"coverage",
|
|
499
|
+
"review"
|
|
500
|
+
], "source");
|
|
501
|
+
optionalString(value.title, "title");
|
|
502
|
+
optionalString(value.windowTitle, "windowTitle");
|
|
503
|
+
optionalString(value.sourceUrl, "sourceUrl");
|
|
504
|
+
if (value.range !== void 0) {
|
|
505
|
+
if (!isRecord(value.range)) throw new Error("range must be an object.");
|
|
506
|
+
requireOnlyKeys(value.range, ["base", "head"], "range");
|
|
507
|
+
requireString(value.range.base, "range.base");
|
|
508
|
+
requireString(value.range.head, "range.head");
|
|
509
|
+
}
|
|
510
|
+
if (value.coverage !== void 0) {
|
|
511
|
+
if (!isRecord(value.coverage) || !Array.isArray(value.coverage.exclusions)) {
|
|
512
|
+
throw new Error("coverage must contain an exclusions array.");
|
|
513
|
+
}
|
|
514
|
+
requireOnlyKeys(value.coverage, ["exclusions"], "coverage");
|
|
515
|
+
for (const [index, exclusion] of value.coverage.exclusions.entries()) {
|
|
516
|
+
const exclusionPath = `coverage.exclusions[${index}]`;
|
|
517
|
+
if (!isRecord(exclusion)) throw new Error(`${exclusionPath} must be an object.`);
|
|
518
|
+
requireOnlyKeys(exclusion, ["path", "hunks", "reason"], exclusionPath);
|
|
519
|
+
requireString(exclusion.path, `${exclusionPath}.path`);
|
|
520
|
+
requireString(exclusion.reason, `${exclusionPath}.reason`);
|
|
521
|
+
if (exclusion.hunks !== void 0) requireStringArray(exclusion.hunks, `${exclusionPath}.hunks`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (!isRecord(value.anchors)) {
|
|
525
|
+
throw new Error("anchors must be an object.");
|
|
526
|
+
}
|
|
527
|
+
for (const [id, anchor] of Object.entries(value.anchors)) {
|
|
528
|
+
if (!isRecord(anchor)) {
|
|
529
|
+
throw new Error(`anchors.${id} must be an object.`);
|
|
530
|
+
}
|
|
531
|
+
requireOnlyKeys(anchor, ["file", "revision", "contains", "occurrence"], `anchors.${id}`);
|
|
532
|
+
requireString(anchor.file, `anchors.${id}.file`);
|
|
533
|
+
if (anchor.revision !== "base" && anchor.revision !== "head") {
|
|
534
|
+
throw new Error(`anchors.${id}.revision must be base or head.`);
|
|
535
|
+
}
|
|
536
|
+
requireString(anchor.contains, `anchors.${id}.contains`);
|
|
537
|
+
if (anchor.occurrence !== void 0 && (!Number.isInteger(anchor.occurrence) || Number(anchor.occurrence) < 1)) {
|
|
538
|
+
throw new Error(`anchors.${id}.occurrence must be a positive integer.`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const rawConnections = Array.isArray(value.connections) ? value.connections : isRecord(value.connections) ? Object.entries(value.connections).map(([id, connection]) => isRecord(connection) ? { id, ...connection } : connection) : null;
|
|
542
|
+
if (!rawConnections || !Array.isArray(value.chapters) || value.chapters.length === 0) {
|
|
543
|
+
throw new Error("connections must be an array or object, and chapters must be a non-empty array.");
|
|
544
|
+
}
|
|
545
|
+
const connectionIds = /* @__PURE__ */ new Set();
|
|
546
|
+
for (const [index, connection] of rawConnections.entries()) {
|
|
547
|
+
if (!isRecord(connection)) {
|
|
548
|
+
throw new Error(`connections[${index}] must be an object.`);
|
|
549
|
+
}
|
|
550
|
+
requireOnlyKeys(connection, ["id", "from", "to", "label"], `connections[${index}]`);
|
|
551
|
+
requireString(connection.id, `connections[${index}].id`);
|
|
552
|
+
requireString(connection.from, `connections[${index}].from`);
|
|
553
|
+
requireString(connection.to, `connections[${index}].to`);
|
|
554
|
+
requireString(connection.label, `connections[${index}].label`);
|
|
555
|
+
const { id, from, to } = connection;
|
|
556
|
+
if (!value.anchors[from] || !value.anchors[to]) {
|
|
557
|
+
throw new Error(`Connection ${id} references an unknown anchor.`);
|
|
558
|
+
}
|
|
559
|
+
if (connectionIds.has(id)) {
|
|
560
|
+
throw new Error(`Duplicate connection id: ${id}`);
|
|
561
|
+
}
|
|
562
|
+
connectionIds.add(id);
|
|
563
|
+
}
|
|
564
|
+
const authoredWalkthroughSteps = validateSourceChapters(
|
|
565
|
+
value.chapters,
|
|
566
|
+
"chapters",
|
|
567
|
+
value.version,
|
|
568
|
+
value.anchors,
|
|
569
|
+
connectionIds,
|
|
570
|
+
"root"
|
|
571
|
+
);
|
|
572
|
+
if (value.tours !== void 0) {
|
|
573
|
+
if (value.version !== 3) throw new Error("Independent tours require version 3.");
|
|
574
|
+
if (!isRecord(value.tours)) throw new Error("tours must be an object.");
|
|
575
|
+
requireOnlyKeys(value.tours, ["historical", "deconstructed"], "tours");
|
|
576
|
+
if (value.tours.historical === void 0 && value.tours.deconstructed === void 0) {
|
|
577
|
+
throw new Error("tours must contain a historical or deconstructed tour.");
|
|
578
|
+
}
|
|
579
|
+
for (const mode of ["historical", "deconstructed"]) {
|
|
580
|
+
const tour = value.tours[mode];
|
|
581
|
+
if (tour === void 0) continue;
|
|
582
|
+
const path = `tours.${mode}`;
|
|
583
|
+
if (!isRecord(tour)) throw new Error(`${path} must be an object.`);
|
|
584
|
+
requireOnlyKeys(tour, ["chapters"], path);
|
|
585
|
+
validateSourceChapters(tour.chapters, `${path}.chapters`, value.version, value.anchors, connectionIds, mode);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (value.review !== void 0) {
|
|
589
|
+
if (value.version !== 3) throw new Error("Review notes require version 3.");
|
|
590
|
+
validateChangeTourReview(value.review, { authoredWalkthroughSteps });
|
|
591
|
+
}
|
|
592
|
+
return { ...value, connections: rawConnections };
|
|
593
|
+
}
|
|
594
|
+
function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, connectionIds, mode) {
|
|
595
|
+
if (!Array.isArray(chaptersValue) || chaptersValue.length === 0) {
|
|
596
|
+
throw new Error(`${chaptersPath} must be a non-empty array.`);
|
|
597
|
+
}
|
|
598
|
+
const chapterIds = /* @__PURE__ */ new Set();
|
|
599
|
+
const sceneIds = /* @__PURE__ */ new Set();
|
|
600
|
+
const authoredWalkthroughSteps = /* @__PURE__ */ new Map();
|
|
601
|
+
for (const [chapterIndex, chapter] of chaptersValue.entries()) {
|
|
602
|
+
const chapterPath = `${chaptersPath}[${chapterIndex}]`;
|
|
603
|
+
if (!isRecord(chapter) || !Array.isArray(chapter.scenes) || chapter.scenes.length === 0) {
|
|
604
|
+
throw new Error(`${chapterPath} must contain a non-empty scenes array.`);
|
|
605
|
+
}
|
|
606
|
+
requireString(chapter.id, `${chapterPath}.id`);
|
|
607
|
+
requireString(chapter.title, `${chapterPath}.title`);
|
|
608
|
+
requireOnlyKeys(chapter, ["id", "title", "scenes"], chapterPath);
|
|
609
|
+
if (chapterIds.has(chapter.id)) throw new Error(`Duplicate chapter id: ${chapter.id}`);
|
|
610
|
+
chapterIds.add(chapter.id);
|
|
611
|
+
for (const [sceneIndex, scene] of chapter.scenes.entries()) {
|
|
612
|
+
const path = `${chapterPath}.scenes[${sceneIndex}]`;
|
|
613
|
+
if (!isRecord(scene)) throw new Error(`${path} must be an object.`);
|
|
614
|
+
requireString(scene.id, `${path}.id`);
|
|
615
|
+
requireString(scene.title, `${path}.title`);
|
|
616
|
+
if (sceneIds.has(scene.id)) throw new Error(`Duplicate scene id: ${scene.id}`);
|
|
617
|
+
sceneIds.add(scene.id);
|
|
618
|
+
if (scene.overview !== void 0 && version !== 3) {
|
|
619
|
+
throw new Error(`${path}.overview requires version 3.`);
|
|
620
|
+
}
|
|
621
|
+
validateNarrative(scene, path);
|
|
622
|
+
if (scene.kind === "deconstructed-diff") {
|
|
623
|
+
if (mode === "historical") {
|
|
624
|
+
throw new Error(`${path} cannot contain a deconstructed-diff scene in the historical tour.`);
|
|
625
|
+
}
|
|
626
|
+
validateDeconstructedScene(scene, path);
|
|
627
|
+
validateSceneOverview(scene.overview, `${path}.overview`, {
|
|
628
|
+
endpointIds: deconstructedOverviewEndpointIds(scene, path)
|
|
629
|
+
});
|
|
630
|
+
const requireRealEvidence = mode === "root" && version >= 2;
|
|
631
|
+
if (requireRealEvidence || scene.stack !== void 0) {
|
|
632
|
+
validateRealStack(scene.stack, `${path}.stack`, 2);
|
|
633
|
+
}
|
|
634
|
+
if (requireRealEvidence || scene.steps !== void 0) {
|
|
635
|
+
validateWalkthroughSteps(scene, path, anchors, connectionIds);
|
|
636
|
+
if (scene.steps !== void 0) {
|
|
637
|
+
authoredWalkthroughSteps.set(scene.id, collectStepIds(scene.steps));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
|
|
643
|
+
throw new Error(`${path} must contain a non-empty steps array.`);
|
|
644
|
+
}
|
|
645
|
+
if (scene.kind === "stacked-diff") {
|
|
646
|
+
validateStackedScene(scene, path, mode === "root" ? version >= 2 ? 2 : 3 : 2);
|
|
647
|
+
validateSceneOverview(scene.overview, `${path}.overview`, {
|
|
648
|
+
endpointIds: stackedOverviewEndpointIds(scene, path)
|
|
649
|
+
});
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (scene.kind !== void 0 && scene.kind !== "walkthrough") {
|
|
653
|
+
throw new Error(`${path}.kind must be walkthrough, stacked-diff, or deconstructed-diff.`);
|
|
654
|
+
}
|
|
655
|
+
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "steps"], path);
|
|
656
|
+
validateSceneOverview(scene.overview, `${path}.overview`, { allowComparison: false });
|
|
657
|
+
validateWalkthroughSteps(scene, path, anchors, connectionIds);
|
|
658
|
+
authoredWalkthroughSteps.set(scene.id, collectStepIds(scene.steps));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (mode === "deconstructed" && !chaptersValue.some((chapter) => isRecord(chapter) && Array.isArray(chapter.scenes) && chapter.scenes.some((scene) => isRecord(scene) && scene.kind === "deconstructed-diff"))) {
|
|
662
|
+
throw new Error(`${chaptersPath} must contain at least one deconstructed-diff scene.`);
|
|
663
|
+
}
|
|
664
|
+
return authoredWalkthroughSteps;
|
|
665
|
+
}
|
|
666
|
+
function stackedOverviewEndpointIds(scene, path) {
|
|
667
|
+
if (!Array.isArray(scene.stack)) {
|
|
668
|
+
throw new Error(`${path}.stack must be an array.`);
|
|
669
|
+
}
|
|
670
|
+
return scene.stack.filter(isRecord).map((entry) => typeof entry.id === "string" ? entry.id : "").filter((id) => id.length > 0);
|
|
671
|
+
}
|
|
672
|
+
function deconstructedOverviewEndpointIds(scene, path) {
|
|
673
|
+
if (!Array.isArray(scene.stages)) {
|
|
674
|
+
throw new Error(`${path}.stages must be an array.`);
|
|
675
|
+
}
|
|
676
|
+
return [
|
|
677
|
+
"explanation-baseline",
|
|
678
|
+
...scene.stages.filter(isRecord).map((stage) => typeof stage.id === "string" ? `explanation-stage-${stage.id}` : "").filter((id) => id.length > 0)
|
|
679
|
+
];
|
|
680
|
+
}
|
|
681
|
+
function validateChangeTourReview(value, context = {}) {
|
|
682
|
+
if (!isRecord(value)) {
|
|
683
|
+
throw new Error("review must be an object.");
|
|
684
|
+
}
|
|
685
|
+
requireOnlyKeys(value, ["baseOid", "headOid", "items"], "review");
|
|
686
|
+
requireFullGitOid(value.baseOid, "review.baseOid");
|
|
687
|
+
requireFullGitOid(value.headOid, "review.headOid");
|
|
688
|
+
if (!Array.isArray(value.items) || value.items.length === 0) {
|
|
689
|
+
throw new Error("review.items must be a non-empty array.");
|
|
690
|
+
}
|
|
691
|
+
if (context.expectedRange) {
|
|
692
|
+
if (normalizeGitOid(value.baseOid) !== normalizeGitOid(context.expectedRange.baseOid)) {
|
|
693
|
+
throw new Error(
|
|
694
|
+
`Stale review: review.baseOid ${value.baseOid} does not match the resolved range.mergeBaseOid ${context.expectedRange.baseOid}.`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
if (normalizeGitOid(value.headOid) !== normalizeGitOid(context.expectedRange.headOid)) {
|
|
698
|
+
throw new Error(
|
|
699
|
+
`Stale review: review.headOid ${value.headOid} does not match the resolved range.headOid ${context.expectedRange.headOid}.`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const itemIds = /* @__PURE__ */ new Set();
|
|
704
|
+
for (const [index, item] of value.items.entries()) {
|
|
705
|
+
const itemPath = `review.items[${index}]`;
|
|
706
|
+
if (!isRecord(item)) throw new Error(`${itemPath} must be an object.`);
|
|
707
|
+
requireOnlyKeys(item, ["id", "kind", "title", "body", "evidence", "nextCheck"], itemPath);
|
|
708
|
+
requireReviewString(item.id, `${itemPath}.id`);
|
|
709
|
+
if (itemIds.has(item.id)) throw new Error(`Duplicate review item id: ${item.id}`);
|
|
710
|
+
itemIds.add(item.id);
|
|
711
|
+
if (typeof item.kind !== "string" || !["concept", "boundary", "tradeoff", "question"].includes(item.kind)) {
|
|
712
|
+
throw new Error(`${itemPath}.kind must be concept, boundary, tradeoff, or question.`);
|
|
713
|
+
}
|
|
714
|
+
requireReviewString(item.title, `${itemPath}.title`);
|
|
715
|
+
requireReviewString(item.body, `${itemPath}.body`);
|
|
716
|
+
if (!Array.isArray(item.evidence)) {
|
|
717
|
+
throw new Error(`${itemPath}.evidence must be an array.`);
|
|
718
|
+
}
|
|
719
|
+
if (item.kind !== "question" && item.evidence.length === 0) {
|
|
720
|
+
throw new Error(`${itemPath}.evidence must contain at least one reference for claims.`);
|
|
721
|
+
}
|
|
722
|
+
if (item.kind === "question" && item.nextCheck === void 0) {
|
|
723
|
+
throw new Error(`${itemPath}.nextCheck must be a non-empty string for question items.`);
|
|
724
|
+
}
|
|
725
|
+
if (item.nextCheck !== void 0) requireReviewString(item.nextCheck, `${itemPath}.nextCheck`);
|
|
726
|
+
for (const [evidenceIndex, evidence] of item.evidence.entries()) {
|
|
727
|
+
const evidencePath = `${itemPath}.evidence[${evidenceIndex}]`;
|
|
728
|
+
if (!isRecord(evidence)) throw new Error(`${evidencePath} must be an object.`);
|
|
729
|
+
requireOnlyKeys(evidence, ["sceneId", "stepId"], evidencePath);
|
|
730
|
+
requireReviewString(evidence.sceneId, `${evidencePath}.sceneId`);
|
|
731
|
+
requireReviewString(evidence.stepId, `${evidencePath}.stepId`);
|
|
732
|
+
if (context.authoredWalkthroughSteps) {
|
|
733
|
+
const stepIds = context.authoredWalkthroughSteps.get(evidence.sceneId);
|
|
734
|
+
if (!stepIds) {
|
|
735
|
+
throw new Error(
|
|
736
|
+
`${evidencePath} references unknown authored walkthrough scene ${evidence.sceneId}.`
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
if (!stepIds.has(evidence.stepId)) {
|
|
740
|
+
throw new Error(
|
|
741
|
+
`${evidencePath} references unknown authored walkthrough step ${evidence.sceneId}/${evidence.stepId}.`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function validateDeconstructedScene(scene, path) {
|
|
749
|
+
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "base", "target", "stack", "steps", "stages", "exclusions"], path);
|
|
750
|
+
optionalString(scene.base, `${path}.base`);
|
|
751
|
+
optionalString(scene.target, `${path}.target`);
|
|
752
|
+
if (!Array.isArray(scene.stages) || scene.stages.length === 0 || scene.stages.length > 12) {
|
|
753
|
+
throw new Error(`${path}.stages must contain between 1 and 12 stages.`);
|
|
754
|
+
}
|
|
755
|
+
const stageIds = /* @__PURE__ */ new Set();
|
|
756
|
+
for (const [stageIndex, stage] of scene.stages.entries()) {
|
|
757
|
+
const stagePath = `${path}.stages[${stageIndex}]`;
|
|
758
|
+
if (!isRecord(stage)) throw new Error(`${stagePath} must be an object.`);
|
|
759
|
+
requireOnlyKeys(stage, ["id", "title", "narration", "changes"], stagePath);
|
|
760
|
+
requireString(stage.id, `${stagePath}.id`);
|
|
761
|
+
requireString(stage.title, `${stagePath}.title`);
|
|
762
|
+
requireString(stage.narration, `${stagePath}.narration`);
|
|
763
|
+
if (!Array.isArray(stage.changes) || stage.changes.length === 0) {
|
|
764
|
+
throw new Error(`${stagePath}.changes must be a non-empty array.`);
|
|
765
|
+
}
|
|
766
|
+
if (stageIds.has(stage.id)) throw new Error(`Duplicate deconstructed stage id: ${stage.id}`);
|
|
767
|
+
stageIds.add(stage.id);
|
|
768
|
+
stage.changes.forEach((change, changeIndex) => validateDeconstructedSelection(
|
|
769
|
+
change,
|
|
770
|
+
`${stagePath}.changes[${changeIndex}]`,
|
|
771
|
+
false
|
|
772
|
+
));
|
|
773
|
+
}
|
|
774
|
+
if (scene.exclusions !== void 0) {
|
|
775
|
+
if (!Array.isArray(scene.exclusions)) throw new Error(`${path}.exclusions must be an array.`);
|
|
776
|
+
scene.exclusions.forEach((exclusion, exclusionIndex) => validateDeconstructedSelection(
|
|
777
|
+
exclusion,
|
|
778
|
+
`${path}.exclusions[${exclusionIndex}]`,
|
|
779
|
+
true
|
|
780
|
+
));
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function validateWalkthroughSteps(scene, path, anchors, connectionIds) {
|
|
784
|
+
if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
|
|
785
|
+
throw new Error(`${path} must contain a non-empty steps array.`);
|
|
786
|
+
}
|
|
787
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
788
|
+
for (const [stepIndex, step] of scene.steps.entries()) {
|
|
789
|
+
const stepPath = `${path}.steps[${stepIndex}]`;
|
|
790
|
+
if (!isRecord(step)) throw new Error(`${stepPath} must be an object.`);
|
|
791
|
+
requireString(step.id, `${stepPath}.id`);
|
|
792
|
+
requireString(step.title, `${stepPath}.title`);
|
|
793
|
+
requireString(step.body, `${stepPath}.body`);
|
|
794
|
+
requireString(step.focus, `${stepPath}.focus`);
|
|
795
|
+
requireOnlyKeys(step, ["id", "title", "body", "focus", "connection", "depth", "requirement"], stepPath);
|
|
796
|
+
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
797
|
+
throw new Error(`${stepPath}.depth must be mentioned, explained, or contextualized.`);
|
|
798
|
+
}
|
|
799
|
+
validateStepRequirement(step.requirement, `${stepPath}.requirement`);
|
|
800
|
+
if (stepIds.has(step.id)) throw new Error(`Duplicate step id in scene ${scene.id}: ${step.id}`);
|
|
801
|
+
stepIds.add(step.id);
|
|
802
|
+
if (!anchors[step.focus]) throw new Error(`${stepPath} references unknown anchor ${step.focus}.`);
|
|
803
|
+
optionalString(step.connection, `${stepPath}.connection`);
|
|
804
|
+
if (typeof step.connection === "string" && !connectionIds.has(step.connection)) {
|
|
805
|
+
throw new Error(`${stepPath} references unknown connection ${step.connection}.`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function validateDeconstructedSelection(value, path, requiresReason) {
|
|
810
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
811
|
+
requireOnlyKeys(value, requiresReason ? ["file", "hunks", "reason"] : ["file", "hunks"], path);
|
|
812
|
+
requireString(value.file, `${path}.file`);
|
|
813
|
+
if (!requiresReason || value.hunks !== void 0) {
|
|
814
|
+
requireStringArray(value.hunks, `${path}.hunks`);
|
|
815
|
+
if (value.hunks.length === 0) throw new Error(`${path}.hunks must not be empty.`);
|
|
816
|
+
}
|
|
817
|
+
if (requiresReason) requireString(value.reason, `${path}.reason`);
|
|
818
|
+
}
|
|
819
|
+
function validateRealStack(stack, path, minimum) {
|
|
820
|
+
if (!Array.isArray(stack) || stack.length < minimum || stack.length > 6) {
|
|
821
|
+
throw new Error(`${path} must contain between ${minimum} and 6 explicit real revisions.`);
|
|
822
|
+
}
|
|
823
|
+
const ids = /* @__PURE__ */ new Set();
|
|
824
|
+
for (const [index, entry] of stack.entries()) {
|
|
825
|
+
if (!isRecord(entry)) throw new Error(`${path}[${index}] must be an object.`);
|
|
826
|
+
requireOnlyKeys(entry, ["id", "ref", "label"], `${path}[${index}]`);
|
|
827
|
+
requireString(entry.id, `${path}[${index}].id`);
|
|
828
|
+
requireString(entry.ref, `${path}[${index}].ref`);
|
|
829
|
+
optionalString(entry.label, `${path}[${index}].label`);
|
|
830
|
+
if (ids.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
831
|
+
ids.add(entry.id);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function validateStackedScene(scene, path, minimum = 3) {
|
|
835
|
+
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "stack", "files", "steps"], path);
|
|
836
|
+
if (!Array.isArray(scene.stack) || scene.stack.length < minimum || scene.stack.length > 6) {
|
|
837
|
+
throw new Error(`${path}.stack must contain between ${minimum} and 6 revisions.`);
|
|
838
|
+
}
|
|
839
|
+
const stackIds = /* @__PURE__ */ new Set();
|
|
840
|
+
for (const [index, entry] of scene.stack.entries()) {
|
|
841
|
+
const entryPath = `${path}.stack[${index}]`;
|
|
842
|
+
if (!isRecord(entry)) throw new Error(`${entryPath} must be an object.`);
|
|
843
|
+
requireOnlyKeys(entry, ["id", "ref", "label"], entryPath);
|
|
844
|
+
requireString(entry.id, `${entryPath}.id`);
|
|
845
|
+
requireString(entry.ref, `${entryPath}.ref`);
|
|
846
|
+
optionalString(entry.label, `${entryPath}.label`);
|
|
847
|
+
if (stackIds.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
848
|
+
stackIds.add(entry.id);
|
|
849
|
+
}
|
|
850
|
+
if (scene.files !== void 0) requireStringArray(scene.files, `${path}.files`);
|
|
851
|
+
if (!Array.isArray(scene.steps) || scene.steps.length === 0) throw new Error(`${path}.steps must be a non-empty array.`);
|
|
852
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
853
|
+
for (const [index, step] of scene.steps.entries()) {
|
|
854
|
+
const stepPath = `${path}.steps[${index}]`;
|
|
855
|
+
if (!isRecord(step)) throw new Error(`${stepPath} must be an object.`);
|
|
856
|
+
requireOnlyKeys(step, ["id", "title", "body", "file", "pair", "side", "lines"], stepPath);
|
|
857
|
+
requireString(step.id, `${stepPath}.id`);
|
|
858
|
+
requireString(step.title, `${stepPath}.title`);
|
|
859
|
+
requireString(step.body, `${stepPath}.body`);
|
|
860
|
+
requireString(step.file, `${stepPath}.file`);
|
|
861
|
+
const stepId = step.id;
|
|
862
|
+
if (stepIds.has(stepId)) throw new Error(`Duplicate step id in scene ${scene.id}: ${stepId}`);
|
|
863
|
+
stepIds.add(stepId);
|
|
864
|
+
if (!Array.isArray(step.pair) || step.pair.length !== 2 || step.pair.some((id) => typeof id !== "string")) {
|
|
865
|
+
throw new Error(`${stepPath}.pair must contain two stack entry ids.`);
|
|
866
|
+
}
|
|
867
|
+
const pair = step.pair;
|
|
868
|
+
const leftIndex = scene.stack.findIndex((entry) => isRecord(entry) && entry.id === pair[0]);
|
|
869
|
+
const rightIndex = scene.stack.findIndex((entry) => isRecord(entry) && entry.id === pair[1]);
|
|
870
|
+
if (leftIndex < 0 || rightIndex !== leftIndex + 1) throw new Error(`${stepPath}.pair must reference adjacent stack entries in order.`);
|
|
871
|
+
if (step.side !== void 0 && step.side !== "left" && step.side !== "right") throw new Error(`${stepPath}.side must be left or right.`);
|
|
872
|
+
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]))) {
|
|
873
|
+
throw new Error(`${stepPath}.lines must contain an ordered positive line range.`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function validateStepRequirement(value, path) {
|
|
878
|
+
if (value === void 0) return;
|
|
879
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
880
|
+
requireOnlyKeys(value, ["id", "text", "status", "source", "confidence"], path);
|
|
881
|
+
requireString(value.id, `${path}.id`);
|
|
882
|
+
requireString(value.text, `${path}.text`);
|
|
883
|
+
if (value.status !== void 0 && !["fulfilled", "gap"].includes(String(value.status))) {
|
|
884
|
+
throw new Error(`${path}.status must be fulfilled or gap.`);
|
|
885
|
+
}
|
|
886
|
+
optionalString(value.source, `${path}.source`);
|
|
887
|
+
if (value.confidence !== void 0 && !["high", "medium", "low"].includes(String(value.confidence))) {
|
|
888
|
+
throw new Error(`${path}.confidence must be high, medium, or low.`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function validateSceneOverview(value, path, options = {}) {
|
|
892
|
+
if (value === void 0) return;
|
|
893
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
894
|
+
requireOnlyKeys(value, ["kind", "path", "comparison"], path);
|
|
895
|
+
if (value.kind !== "directory-diff") {
|
|
896
|
+
throw new Error(`${path}.kind must be directory-diff.`);
|
|
897
|
+
}
|
|
898
|
+
if (value.path !== void 0) validateDirectoryOverviewPath(value.path, `${path}.path`);
|
|
899
|
+
if (value.comparison === void 0) return;
|
|
900
|
+
if (options.allowComparison === false) {
|
|
901
|
+
throw new Error(`${path}.comparison is not supported for walkthrough scenes; use the fixed base-to-head comparison.`);
|
|
902
|
+
}
|
|
903
|
+
if (!isRecord(value.comparison)) throw new Error(`${path}.comparison must be an object.`);
|
|
904
|
+
requireOnlyKeys(value.comparison, ["from", "to"], `${path}.comparison`);
|
|
905
|
+
requireString(value.comparison.from, `${path}.comparison.from`);
|
|
906
|
+
requireString(value.comparison.to, `${path}.comparison.to`);
|
|
907
|
+
if (value.comparison.from === value.comparison.to) {
|
|
908
|
+
throw new Error(`${path}.comparison.from and to must identify distinct endpoints.`);
|
|
909
|
+
}
|
|
910
|
+
if (options.endpointIds !== void 0) {
|
|
911
|
+
for (const endpoint of [value.comparison.from, value.comparison.to]) {
|
|
912
|
+
if (!options.endpointIds.includes(endpoint)) {
|
|
913
|
+
throw new Error(`${path}.comparison endpoint ${endpoint} is not defined by this scene.`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function validateDirectoryOverviewPath(value, path) {
|
|
919
|
+
requireString(value, path);
|
|
920
|
+
if (value.includes("\\")) {
|
|
921
|
+
throw new Error(`${path} must use a relative POSIX directory path without backslashes.`);
|
|
922
|
+
}
|
|
923
|
+
if (value.startsWith("/") || /^[A-Za-z]:/.test(value)) {
|
|
924
|
+
throw new Error(`${path} must be relative to the repository root.`);
|
|
925
|
+
}
|
|
926
|
+
if (value.split("/").some((segment) => segment === "..")) {
|
|
927
|
+
throw new Error(`${path} must not contain parent-directory traversal.`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
function collectStepIds(value) {
|
|
931
|
+
const ids = /* @__PURE__ */ new Set();
|
|
932
|
+
if (!Array.isArray(value)) return ids;
|
|
933
|
+
for (const step of value) {
|
|
934
|
+
if (isRecord(step) && typeof step.id === "string") ids.add(step.id);
|
|
935
|
+
}
|
|
936
|
+
return ids;
|
|
937
|
+
}
|
|
938
|
+
function validateNarrative(value, path) {
|
|
939
|
+
requireString(value.summary, `${path}.summary`);
|
|
940
|
+
requireStringArray(value.bullets, `${path}.bullets`);
|
|
941
|
+
requireStringArray(value.tags, `${path}.tags`);
|
|
942
|
+
requireString(value.takeaway, `${path}.takeaway`);
|
|
943
|
+
}
|
|
944
|
+
function isRecord(value) {
|
|
945
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
946
|
+
}
|
|
947
|
+
function requireString(value, path) {
|
|
948
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
949
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function requireFullGitOid(value, path) {
|
|
953
|
+
requireString(value, path);
|
|
954
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(value)) {
|
|
955
|
+
throw new Error(`${path} must be a full 40- or 64-character hexadecimal Git OID.`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
function requireReviewString(value, path) {
|
|
959
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
960
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function normalizeGitOid(value) {
|
|
964
|
+
return value.toLowerCase();
|
|
965
|
+
}
|
|
966
|
+
function optionalString(value, path) {
|
|
967
|
+
if (value !== void 0) {
|
|
968
|
+
requireString(value, path);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function requireStringArray(value, path) {
|
|
972
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
973
|
+
throw new Error(`${path} must be an array of strings.`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function requireOnlyKeys(value, allowed, path) {
|
|
977
|
+
const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
978
|
+
if (unexpected.length > 0) {
|
|
979
|
+
throw new Error(`${path} contains unknown field${unexpected.length === 1 ? "" : "s"}: ${unexpected.join(", ")}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
476
983
|
// src/changeTourManifest.ts
|
|
477
|
-
var CHANGE_TOUR_MANIFEST_VERSION =
|
|
984
|
+
var CHANGE_TOUR_MANIFEST_VERSION = 3;
|
|
478
985
|
function parseChangeTourManifest(value) {
|
|
479
|
-
if (
|
|
986
|
+
if (isRecord2(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_MANIFEST_VERSION) {
|
|
987
|
+
throw new Error(
|
|
988
|
+
`This tour uses manifest format version ${value.version}, but this version of Bygone supports up to version ${CHANGE_TOUR_MANIFEST_VERSION}. Upgrade Bygone to open it.`
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
if (!isRecord2(value) || value.version !== 1 && value.version !== 2 && value.version !== CHANGE_TOUR_MANIFEST_VERSION) {
|
|
480
992
|
throw new Error(`Unsupported or missing change-tour manifest version.`);
|
|
481
993
|
}
|
|
482
|
-
|
|
483
|
-
|
|
994
|
+
requireString2(value.title, "title");
|
|
995
|
+
requireString2(value.generatedAt, "generatedAt");
|
|
484
996
|
if (value.windowTitle !== void 0) {
|
|
485
|
-
|
|
997
|
+
requireString2(value.windowTitle, "windowTitle");
|
|
486
998
|
}
|
|
487
999
|
if (value.sourceUrl !== void 0) {
|
|
488
|
-
|
|
1000
|
+
requireString2(value.sourceUrl, "sourceUrl");
|
|
489
1001
|
}
|
|
490
|
-
if (!
|
|
1002
|
+
if (!isRecord2(value.range)) {
|
|
491
1003
|
throw new Error("Change-tour manifest range must be an object.");
|
|
492
1004
|
}
|
|
493
1005
|
for (const key of ["baseRef", "headRef", "mergeBaseOid", "headOid"]) {
|
|
494
|
-
|
|
1006
|
+
requireString2(value.range[key], `range.${key}`);
|
|
495
1007
|
}
|
|
496
|
-
if (!
|
|
1008
|
+
if (!isRecord2(value.summary)) {
|
|
497
1009
|
throw new Error("Change-tour manifest summary must be an object.");
|
|
498
1010
|
}
|
|
499
1011
|
for (const key of ["changedFiles", "includedScenes", "additions", "deletions", "commitCount"]) {
|
|
500
1012
|
requireNonNegativeInteger(value.summary[key], `summary.${key}`);
|
|
501
1013
|
}
|
|
502
|
-
|
|
1014
|
+
requireStringArray2(value.summary.omittedFiles, "summary.omittedFiles");
|
|
503
1015
|
if (!Array.isArray(value.commits) || !Array.isArray(value.chapters) || !Array.isArray(value.scenes)) {
|
|
504
1016
|
throw new Error("Change-tour manifest commits, chapters, and scenes must be arrays.");
|
|
505
1017
|
}
|
|
@@ -508,19 +1020,19 @@ function parseChangeTourManifest(value) {
|
|
|
508
1020
|
}
|
|
509
1021
|
const sceneIds = /* @__PURE__ */ new Set();
|
|
510
1022
|
for (const [index, candidate] of value.scenes.entries()) {
|
|
511
|
-
validateScene(candidate, index);
|
|
1023
|
+
validateScene(candidate, index, value.version);
|
|
512
1024
|
if (sceneIds.has(candidate.id)) {
|
|
513
1025
|
throw new Error(`Duplicate change-tour scene id: ${candidate.id}`);
|
|
514
1026
|
}
|
|
515
1027
|
sceneIds.add(candidate.id);
|
|
516
1028
|
}
|
|
517
1029
|
for (const [index, candidate] of value.chapters.entries()) {
|
|
518
|
-
if (!
|
|
1030
|
+
if (!isRecord2(candidate)) {
|
|
519
1031
|
throw new Error(`chapters[${index}] must be an object.`);
|
|
520
1032
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
1033
|
+
requireString2(candidate.id, `chapters[${index}].id`);
|
|
1034
|
+
requireString2(candidate.title, `chapters[${index}].title`);
|
|
1035
|
+
requireStringArray2(candidate.sceneIds, `chapters[${index}].sceneIds`);
|
|
524
1036
|
for (const sceneId of candidate.sceneIds) {
|
|
525
1037
|
if (!sceneIds.has(sceneId)) {
|
|
526
1038
|
throw new Error(`Chapter references unknown scene id: ${sceneId}`);
|
|
@@ -537,39 +1049,209 @@ function parseChangeTourManifest(value) {
|
|
|
537
1049
|
if (value.files !== void 0 && value.summary.changedFiles !== files.length) {
|
|
538
1050
|
throw new Error("summary.changedFiles must match the number of files.");
|
|
539
1051
|
}
|
|
1052
|
+
if (value.authoringCoverage !== void 0) {
|
|
1053
|
+
validateAuthoringCoverage(value.authoringCoverage, value.version);
|
|
1054
|
+
}
|
|
1055
|
+
if (value.tours !== void 0) {
|
|
1056
|
+
if (value.version !== 3) throw new Error("Independent tours require manifest version 3.");
|
|
1057
|
+
validateModeTours(value.tours, value.version);
|
|
1058
|
+
}
|
|
1059
|
+
if (value.version !== 1) validateZoom(value, value.version);
|
|
1060
|
+
if (value.review !== void 0) {
|
|
1061
|
+
if (value.version !== 3) throw new Error("Review notes require manifest version 3.");
|
|
1062
|
+
const evidenceScenes = value.version >= 2 && isRecord2(value.zoom) && isRecord2(value.zoom.final) ? value.zoom.final.scenes : value.scenes;
|
|
1063
|
+
validateChangeTourReview(value.review, {
|
|
1064
|
+
expectedRange: {
|
|
1065
|
+
baseOid: String(value.range.mergeBaseOid),
|
|
1066
|
+
headOid: String(value.range.headOid)
|
|
1067
|
+
},
|
|
1068
|
+
authoredWalkthroughSteps: collectAuthoredWalkthroughStepIds(evidenceScenes)
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
540
1071
|
return { ...value, files };
|
|
541
1072
|
}
|
|
542
|
-
function
|
|
543
|
-
|
|
1073
|
+
function collectAuthoredWalkthroughStepIds(value) {
|
|
1074
|
+
const scenes = /* @__PURE__ */ new Map();
|
|
1075
|
+
if (!Array.isArray(value)) return scenes;
|
|
1076
|
+
for (const scene of value) {
|
|
1077
|
+
if (!isRecord2(scene) || scene.kind !== "walkthrough" || !Array.isArray(scene.steps) || typeof scene.id !== "string") {
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
1081
|
+
for (const step of scene.steps) {
|
|
1082
|
+
if (isRecord2(step) && typeof step.id === "string") stepIds.add(step.id);
|
|
1083
|
+
}
|
|
1084
|
+
scenes.set(scene.id, stepIds);
|
|
1085
|
+
}
|
|
1086
|
+
return scenes;
|
|
1087
|
+
}
|
|
1088
|
+
function validateAuthoringCoverage(value, version) {
|
|
1089
|
+
if (!isRecord2(value) || !isRecord2(value.walkthrough) || !Array.isArray(value.explanationAssignments)) {
|
|
1090
|
+
throw new Error("authoringCoverage must contain walkthrough and explanationAssignments.");
|
|
1091
|
+
}
|
|
1092
|
+
const expectedScope = version >= 2 ? "final" : "tour";
|
|
1093
|
+
if (value.walkthrough.scope !== expectedScope) {
|
|
1094
|
+
throw new Error(`authoringCoverage.walkthrough.scope must be ${expectedScope}.`);
|
|
1095
|
+
}
|
|
1096
|
+
requireNonNegativeInteger(value.walkthrough.coveredUnits, "authoringCoverage.walkthrough.coveredUnits");
|
|
1097
|
+
requireNonNegativeInteger(value.walkthrough.includedUnits, "authoringCoverage.walkthrough.includedUnits");
|
|
1098
|
+
requirePercentage(value.walkthrough.coveragePercent, "authoringCoverage.walkthrough.coveragePercent");
|
|
1099
|
+
if (value.walkthrough.coveredUnits > value.walkthrough.includedUnits) {
|
|
1100
|
+
throw new Error("authoringCoverage.walkthrough.coveredUnits must not exceed includedUnits.");
|
|
1101
|
+
}
|
|
1102
|
+
for (const [index, assignment] of value.explanationAssignments.entries()) {
|
|
1103
|
+
const path = `authoringCoverage.explanationAssignments[${index}]`;
|
|
1104
|
+
if (!isRecord2(assignment)) throw new Error(`${path} must be an object.`);
|
|
1105
|
+
requireString2(assignment.sceneId, `${path}.sceneId`);
|
|
1106
|
+
requireNonNegativeInteger(assignment.assignedUnits, `${path}.assignedUnits`);
|
|
1107
|
+
requireNonNegativeInteger(assignment.excludedUnits, `${path}.excludedUnits`);
|
|
1108
|
+
requireNonNegativeInteger(assignment.completeUnits, `${path}.completeUnits`);
|
|
1109
|
+
requireNonNegativeInteger(assignment.totalUnits, `${path}.totalUnits`);
|
|
1110
|
+
requirePercentage(assignment.assignmentPercent, `${path}.assignmentPercent`);
|
|
1111
|
+
if (assignment.assignedUnits + assignment.excludedUnits !== assignment.completeUnits || assignment.completeUnits > assignment.totalUnits) {
|
|
1112
|
+
throw new Error(`${path} contains inconsistent unit counts.`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function validateModeTours(value, version) {
|
|
1117
|
+
if (!isRecord2(value)) throw new Error("Change-tour manifest tours must be an object.");
|
|
1118
|
+
if (value.historical === void 0 && value.deconstructed === void 0) {
|
|
1119
|
+
throw new Error("Change-tour manifest tours must contain a historical or deconstructed tour.");
|
|
1120
|
+
}
|
|
1121
|
+
for (const mode of ["historical", "deconstructed"]) {
|
|
1122
|
+
const tour = value[mode];
|
|
1123
|
+
if (tour === void 0) continue;
|
|
1124
|
+
if (!isRecord2(tour) || !Array.isArray(tour.chapters) || !Array.isArray(tour.scenes) || tour.chapters.length === 0 || tour.scenes.length === 0) {
|
|
1125
|
+
throw new Error(`Change-tour manifest tours.${mode} must contain non-empty chapters and scenes arrays.`);
|
|
1126
|
+
}
|
|
1127
|
+
const sceneIds = /* @__PURE__ */ new Set();
|
|
1128
|
+
let hasDeconstructed = false;
|
|
1129
|
+
for (const [index, scene] of tour.scenes.entries()) {
|
|
1130
|
+
validateScene(scene, index, version);
|
|
1131
|
+
if (sceneIds.has(scene.id)) {
|
|
1132
|
+
throw new Error(`Duplicate ${mode} tour scene id: ${scene.id}`);
|
|
1133
|
+
}
|
|
1134
|
+
sceneIds.add(scene.id);
|
|
1135
|
+
if (scene.kind === "deconstructed-diff") hasDeconstructed = true;
|
|
1136
|
+
if (mode === "historical" && scene.kind === "deconstructed-diff") {
|
|
1137
|
+
throw new Error("Historical tours cannot contain synthetic deconstructed scenes.");
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (mode === "deconstructed" && !hasDeconstructed) {
|
|
1141
|
+
throw new Error("Deconstructed tours must contain a constructed deconstructed-diff scene.");
|
|
1142
|
+
}
|
|
1143
|
+
for (const [index, chapter] of tour.chapters.entries()) {
|
|
1144
|
+
if (!isRecord2(chapter)) {
|
|
1145
|
+
throw new Error(`tours.${mode}.chapters[${index}] must be an object.`);
|
|
1146
|
+
}
|
|
1147
|
+
requireString2(chapter.id, `tours.${mode}.chapters[${index}].id`);
|
|
1148
|
+
requireString2(chapter.title, `tours.${mode}.chapters[${index}].title`);
|
|
1149
|
+
requireStringArray2(chapter.sceneIds, `tours.${mode}.chapters[${index}].sceneIds`);
|
|
1150
|
+
for (const sceneId of chapter.sceneIds) {
|
|
1151
|
+
if (!sceneIds.has(sceneId)) {
|
|
1152
|
+
throw new Error(`tours.${mode} chapter references unknown scene id: ${sceneId}`);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
function validateZoom(value, version) {
|
|
1159
|
+
if (!isRecord2(value.repository) || typeof value.repository.root !== "string" || !value.repository.root) {
|
|
1160
|
+
throw new Error("A v2 tour requires its originating repository.root.");
|
|
1161
|
+
}
|
|
1162
|
+
if (!isRecord2(value.zoom) || !Array.isArray(value.zoom.revisions) || !isRecord2(value.zoom.final)) {
|
|
1163
|
+
throw new Error("A v2 tour requires zoom metadata and real revision evidence.");
|
|
1164
|
+
}
|
|
1165
|
+
const scenes = value.scenes;
|
|
1166
|
+
const revisions = value.zoom.revisions;
|
|
1167
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1168
|
+
for (const [index, scene] of revisions.entries()) {
|
|
1169
|
+
validateScene(scene, index, version);
|
|
1170
|
+
if (scene.kind !== "stacked-diff") throw new Error("zoom.revisions must contain real stacked-diff scenes.");
|
|
1171
|
+
if (ids.has(scene.id)) throw new Error(`Duplicate zoom revision scene: ${scene.id}`);
|
|
1172
|
+
ids.add(scene.id);
|
|
1173
|
+
const range = value.range;
|
|
1174
|
+
if (scene.stack[0].oid !== range.mergeBaseOid || scene.stack[scene.stack.length - 1].oid !== range.headOid) {
|
|
1175
|
+
throw new Error(`Revision scene ${scene.id} endpoints must match the final diff range.`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
for (const scene of scenes) {
|
|
1179
|
+
if (scene.kind !== "deconstructed-diff" && scene.kind !== "stacked-diff") continue;
|
|
1180
|
+
const real = revisions.find((candidate) => candidate.id === scene.id);
|
|
1181
|
+
if (!real) throw new Error(`Scene ${scene.id} is missing its real revision evidence.`);
|
|
1182
|
+
if (scene.kind === "deconstructed-diff" && (real.stack[0].oid !== scene.realRange.baseOid || real.stack[real.stack.length - 1].oid !== scene.realRange.targetOid)) {
|
|
1183
|
+
throw new Error(`Deconstructed scene ${scene.id} real stack endpoints must match its explanation range.`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
const explanation = scenes.some((scene) => scene.kind === "deconstructed-diff");
|
|
1187
|
+
const distinct = revisions.some((scene) => scene.stack.length > 2);
|
|
1188
|
+
const depth = explanation ? "explanation" : distinct ? "revisions" : "final";
|
|
1189
|
+
const modes = [...explanation ? ["explanation"] : [], ...distinct ? ["revisions"] : [], "final", "history"];
|
|
1190
|
+
if (value.zoom.authoredDepth !== depth || JSON.stringify(value.zoom.modes) !== JSON.stringify(modes)) {
|
|
1191
|
+
throw new Error("zoom depth and modes must match the maximum authored depth.");
|
|
1192
|
+
}
|
|
1193
|
+
if (!Array.isArray(value.files)) throw new Error("A v2 tour requires final endpoint file evidence.");
|
|
1194
|
+
if (!Array.isArray(value.zoom.final.scenes) || !Array.isArray(value.zoom.final.chapters)) {
|
|
1195
|
+
throw new Error("A v2 tour requires a non-stacked final tour.");
|
|
1196
|
+
}
|
|
1197
|
+
const finalSceneIds = /* @__PURE__ */ new Set();
|
|
1198
|
+
for (const [index, scene] of value.zoom.final.scenes.entries()) {
|
|
1199
|
+
validateScene(scene, index, version);
|
|
1200
|
+
if (scene.kind === "stacked-diff" || scene.kind === "deconstructed-diff") {
|
|
1201
|
+
throw new Error("zoom.final must contain only non-stacked scenes.");
|
|
1202
|
+
}
|
|
1203
|
+
if (finalSceneIds.has(scene.id)) throw new Error(`Duplicate final tour scene: ${scene.id}`);
|
|
1204
|
+
finalSceneIds.add(scene.id);
|
|
1205
|
+
}
|
|
1206
|
+
for (const [index, chapter] of value.zoom.final.chapters.entries()) {
|
|
1207
|
+
if (!isRecord2(chapter) || !Array.isArray(chapter.sceneIds)) {
|
|
1208
|
+
throw new Error(`zoom.final.chapters[${index}] must contain sceneIds.`);
|
|
1209
|
+
}
|
|
1210
|
+
requireString2(chapter.id, `zoom.final.chapters[${index}].id`);
|
|
1211
|
+
requireString2(chapter.title, `zoom.final.chapters[${index}].title`);
|
|
1212
|
+
for (const sceneId of chapter.sceneIds) {
|
|
1213
|
+
requireString2(sceneId, `zoom.final.chapters[${index}].sceneIds`);
|
|
1214
|
+
if (!finalSceneIds.has(sceneId)) throw new Error(`Final tour chapter references unknown scene: ${sceneId}`);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
function validateScene(value, index, version) {
|
|
1219
|
+
if (!isRecord2(value) || !["text-diff", "discussion", "walkthrough", "stacked-diff", "deconstructed-diff"].includes(String(value.kind))) {
|
|
544
1220
|
throw new Error(`scenes[${index}] must be a text-diff, discussion, walkthrough, stacked-diff, or deconstructed-diff scene.`);
|
|
545
1221
|
}
|
|
546
1222
|
for (const key of ["id", "title"]) {
|
|
547
|
-
|
|
1223
|
+
requireString2(value[key], `scenes[${index}].${key}`);
|
|
1224
|
+
}
|
|
1225
|
+
if (value.overview !== void 0 && version !== 3) {
|
|
1226
|
+
throw new Error(`scenes[${index}].overview requires manifest version 3.`);
|
|
548
1227
|
}
|
|
549
|
-
|
|
1228
|
+
validateNarrative2(value, `scenes[${index}]`);
|
|
550
1229
|
if (value.kind === "discussion") {
|
|
1230
|
+
validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
|
|
551
1231
|
return;
|
|
552
1232
|
}
|
|
553
1233
|
if (value.kind === "walkthrough") {
|
|
1234
|
+
validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
|
|
554
1235
|
if (!Array.isArray(value.steps) || value.steps.length === 0) {
|
|
555
1236
|
throw new Error(`scenes[${index}].steps must be a non-empty array.`);
|
|
556
1237
|
}
|
|
557
1238
|
for (const [stepIndex, step] of value.steps.entries()) {
|
|
558
1239
|
const path = `scenes[${index}].steps[${stepIndex}]`;
|
|
559
|
-
if (!
|
|
1240
|
+
if (!isRecord2(step) || !isRecord2(step.focus) || !isRecord2(step.diff)) {
|
|
560
1241
|
throw new Error(`${path} must contain focus and diff objects.`);
|
|
561
1242
|
}
|
|
562
|
-
for (const key of ["id", "title", "body"])
|
|
1243
|
+
for (const key of ["id", "title", "body"]) requireString2(step[key], `${path}.${key}`);
|
|
563
1244
|
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
564
1245
|
throw new Error(`${path}.depth must be mentioned, explained, or contextualized.`);
|
|
565
1246
|
}
|
|
1247
|
+
validateStepRequirement(step.requirement, `${path}.requirement`);
|
|
566
1248
|
validateResolvedAnchor(step.focus, `${path}.focus`);
|
|
567
|
-
validateScene(step.diff, index);
|
|
1249
|
+
validateScene(step.diff, index, version);
|
|
568
1250
|
if (step.diff.kind !== "text-diff") throw new Error(`${path}.diff must be a text-diff scene.`);
|
|
569
1251
|
if (step.connection !== void 0) {
|
|
570
|
-
if (!
|
|
571
|
-
|
|
572
|
-
|
|
1252
|
+
if (!isRecord2(step.connection)) throw new Error(`${path}.connection must be an object.`);
|
|
1253
|
+
requireString2(step.connection.id, `${path}.connection.id`);
|
|
1254
|
+
requireString2(step.connection.label, `${path}.connection.label`);
|
|
573
1255
|
validateResolvedAnchor(step.connection.from, `${path}.connection.from`);
|
|
574
1256
|
validateResolvedAnchor(step.connection.to, `${path}.connection.to`);
|
|
575
1257
|
}
|
|
@@ -577,68 +1259,75 @@ function validateScene(value, index) {
|
|
|
577
1259
|
return;
|
|
578
1260
|
}
|
|
579
1261
|
if (value.kind === "stacked-diff") {
|
|
580
|
-
|
|
1262
|
+
validateStackedScene2(value, index);
|
|
1263
|
+
validateSceneOverview(value.overview, `scenes[${index}].overview`, {
|
|
1264
|
+
endpointIds: value.stack.map((panel) => panel.id)
|
|
1265
|
+
});
|
|
581
1266
|
return;
|
|
582
1267
|
}
|
|
583
1268
|
if (value.kind === "deconstructed-diff") {
|
|
584
|
-
|
|
1269
|
+
validateDeconstructedScene2(value, index);
|
|
1270
|
+
validateSceneOverview(value.overview, `scenes[${index}].overview`, {
|
|
1271
|
+
endpointIds: value.panels.map((panel) => panel.id)
|
|
1272
|
+
});
|
|
585
1273
|
return;
|
|
586
1274
|
}
|
|
1275
|
+
validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
|
|
587
1276
|
for (const key of ["path", "leftLabel", "rightLabel", "leftContent", "rightContent"]) {
|
|
588
|
-
|
|
1277
|
+
requireString2(value[key], `scenes[${index}].${key}`);
|
|
589
1278
|
}
|
|
590
1279
|
if (value.previousPath !== void 0) {
|
|
591
|
-
|
|
1280
|
+
requireString2(value.previousPath, `scenes[${index}].previousPath`);
|
|
592
1281
|
}
|
|
593
|
-
|
|
1282
|
+
requireString2(value.changeKind, `scenes[${index}].changeKind`);
|
|
594
1283
|
requireNonNegativeInteger(value.additions, `scenes[${index}].additions`);
|
|
595
1284
|
requireNonNegativeInteger(value.deletions, `scenes[${index}].deletions`);
|
|
596
1285
|
if (value.focusChangeIndex !== void 0) {
|
|
597
1286
|
requireNonNegativeInteger(value.focusChangeIndex, `scenes[${index}].focusChangeIndex`);
|
|
598
1287
|
}
|
|
599
1288
|
}
|
|
600
|
-
function
|
|
601
|
-
if (!Array.isArray(value.stack) || value.stack.length <
|
|
602
|
-
throw new Error(`scenes[${index}].stack must contain between
|
|
1289
|
+
function validateStackedScene2(value, index) {
|
|
1290
|
+
if (!Array.isArray(value.stack) || value.stack.length < 2 || value.stack.length > 6) {
|
|
1291
|
+
throw new Error(`scenes[${index}].stack must contain between 2 and 6 revisions.`);
|
|
603
1292
|
}
|
|
604
1293
|
for (const [panelIndex, panel] of value.stack.entries()) {
|
|
605
|
-
if (!
|
|
606
|
-
for (const key of ["id", "ref", "oid", "label"])
|
|
1294
|
+
if (!isRecord2(panel)) throw new Error(`scenes[${index}].stack[${panelIndex}] must be an object.`);
|
|
1295
|
+
for (const key of ["id", "ref", "oid", "label"]) requireString2(panel[key], `scenes[${index}].stack[${panelIndex}].${key}`);
|
|
607
1296
|
}
|
|
608
1297
|
validateMultiPanelFiles(value.files, value.stack.length, index);
|
|
609
1298
|
validateMultiPanelSteps(value.steps, value.stack.length, index, false);
|
|
610
1299
|
}
|
|
611
|
-
function
|
|
1300
|
+
function validateDeconstructedScene2(value, index) {
|
|
612
1301
|
if (value.stageLabel !== "Explanation stages") {
|
|
613
1302
|
throw new Error(`scenes[${index}].stageLabel must be Explanation stages.`);
|
|
614
1303
|
}
|
|
615
|
-
if (!
|
|
1304
|
+
if (!isRecord2(value.realRange)) throw new Error(`scenes[${index}].realRange must be an object.`);
|
|
616
1305
|
for (const key of ["baseRef", "targetRef", "baseOid", "targetOid"]) {
|
|
617
|
-
|
|
1306
|
+
requireString2(value.realRange[key], `scenes[${index}].realRange.${key}`);
|
|
618
1307
|
}
|
|
619
1308
|
if (!Array.isArray(value.panels) || value.panels.length < 2 || value.panels.length > 13) {
|
|
620
1309
|
throw new Error(`scenes[${index}].panels must contain a baseline and between 1 and 12 stages.`);
|
|
621
1310
|
}
|
|
622
1311
|
const panels = value.panels;
|
|
623
1312
|
for (const [panelIndex, panel] of panels.entries()) {
|
|
624
|
-
if (!
|
|
625
|
-
for (const key of ["id", "label", "role"])
|
|
1313
|
+
if (!isRecord2(panel)) throw new Error(`scenes[${index}].panels[${panelIndex}] must be an object.`);
|
|
1314
|
+
for (const key of ["id", "label", "role"]) requireString2(panel[key], `scenes[${index}].panels[${panelIndex}].${key}`);
|
|
626
1315
|
const expectedRole = panelIndex === 0 ? "baseline" : "stage";
|
|
627
1316
|
if (panel.role !== expectedRole) throw new Error(`scenes[${index}].panels[${panelIndex}].role must be ${expectedRole}.`);
|
|
628
|
-
if (panelIndex > 0)
|
|
1317
|
+
if (panelIndex > 0) requireString2(panel.stageId, `scenes[${index}].panels[${panelIndex}].stageId`);
|
|
629
1318
|
}
|
|
630
1319
|
validateMultiPanelFiles(value.files, panels.length, index);
|
|
631
1320
|
validateMultiPanelSteps(value.steps, panels.length, index, true);
|
|
632
1321
|
if (Array.isArray(value.steps)) {
|
|
633
1322
|
let previousPairIndex = -1;
|
|
634
1323
|
value.steps.forEach((step, stepIndex) => {
|
|
635
|
-
if (!
|
|
1324
|
+
if (!isRecord2(step)) return;
|
|
636
1325
|
const pairIndex = Number(step.pairIndex);
|
|
637
1326
|
if (pairIndex < previousPairIndex) {
|
|
638
1327
|
throw new Error(`scenes[${index}].steps must remain grouped in explanation stage order.`);
|
|
639
1328
|
}
|
|
640
1329
|
previousPairIndex = pairIndex;
|
|
641
|
-
if (step.stageId !== void 0)
|
|
1330
|
+
if (step.stageId !== void 0) requireString2(step.stageId, `scenes[${index}].steps[${stepIndex}].stageId`);
|
|
642
1331
|
if (step.stageIndex !== void 0) requireNonNegativeInteger(step.stageIndex, `scenes[${index}].steps[${stepIndex}].stageIndex`);
|
|
643
1332
|
if (step.focusIndex !== void 0) requireNonNegativeInteger(step.focusIndex, `scenes[${index}].steps[${stepIndex}].focusIndex`);
|
|
644
1333
|
if (step.focusCount !== void 0) requirePositiveInteger(step.focusCount, `scenes[${index}].steps[${stepIndex}].focusCount`);
|
|
@@ -651,341 +1340,91 @@ function validateDeconstructedScene(value, index) {
|
|
|
651
1340
|
});
|
|
652
1341
|
for (let stageIndex = 0; stageIndex < panels.length - 1; stageIndex += 1) {
|
|
653
1342
|
const panel = panels[stageIndex + 1];
|
|
654
|
-
const stageSteps = value.steps.filter((step) =>
|
|
655
|
-
if (!
|
|
656
|
-
throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
|
|
657
|
-
}
|
|
658
|
-
const firstStep = stageSteps[0];
|
|
659
|
-
const fallbackStageId = isRecord(firstStep) ? firstStep.id : void 0;
|
|
660
|
-
if (stageSteps.some((step) => {
|
|
661
|
-
if (!isRecord(step)) return true;
|
|
662
|
-
return (step.stageId ?? fallbackStageId) !== panel.stageId;
|
|
663
|
-
})) {
|
|
664
|
-
throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
function validateMultiPanelFiles(value, panelCount, index) {
|
|
670
|
-
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].files must be non-empty.`);
|
|
671
|
-
for (const [fileIndex, file] of value.entries()) {
|
|
672
|
-
if (!isRecord(file)) throw new Error(`scenes[${index}].files[${fileIndex}] must be an object.`);
|
|
673
|
-
requireString(file.path, `scenes[${index}].files[${fileIndex}].path`);
|
|
674
|
-
if (!Array.isArray(file.panels) || file.panels.length !== panelCount) {
|
|
675
|
-
throw new Error(`scenes[${index}].files[${fileIndex}].panels must match panel count.`);
|
|
676
|
-
}
|
|
677
|
-
for (const [panelIndex, panel] of file.panels.entries()) {
|
|
678
|
-
if (!isRecord(panel)) throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}] must be an object.`);
|
|
679
|
-
for (const key of ["id", "label", "content"]) requireString(panel[key], `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].${key}`);
|
|
680
|
-
if (panel.path !== void 0) requireString(panel.path, `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].path`);
|
|
681
|
-
if (typeof panel.exists !== "boolean") throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}].exists must be boolean.`);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
686
|
-
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].steps must be non-empty.`);
|
|
687
|
-
for (const [stepIndex, step] of value.entries()) {
|
|
688
|
-
if (!isRecord(step)) throw new Error(`scenes[${index}].steps[${stepIndex}] must be an object.`);
|
|
689
|
-
for (const key of ["id", "title", "body", "file", "side"]) requireString(step[key], `scenes[${index}].steps[${stepIndex}].${key}`);
|
|
690
|
-
requireNonNegativeInteger(step.pairIndex, `scenes[${index}].steps[${stepIndex}].pairIndex`);
|
|
691
|
-
if (Number(step.pairIndex) >= panelCount - 1) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex is outside the panels.`);
|
|
692
|
-
if (deconstructed) {
|
|
693
|
-
requireStringArray(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
|
|
694
|
-
if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
|
|
695
|
-
}
|
|
696
|
-
if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
|
|
697
|
-
if (step.startLine !== void 0) requirePositiveInteger(step.startLine, `scenes[${index}].steps[${stepIndex}].startLine`);
|
|
698
|
-
if (step.endLine !== void 0) requirePositiveInteger(step.endLine, `scenes[${index}].steps[${stepIndex}].endLine`);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
function validateTourFile(value, index) {
|
|
702
|
-
if (!isRecord(value)) throw new Error(`files[${index}] must be an object.`);
|
|
703
|
-
if (value.kind === "text-diff") {
|
|
704
|
-
validateScene(value, index);
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
if (value.kind !== "omitted") {
|
|
708
|
-
throw new Error(`files[${index}].kind must be text-diff or omitted.`);
|
|
709
|
-
}
|
|
710
|
-
for (const key of ["id", "title", "path", "changeKind", "reason"]) {
|
|
711
|
-
requireString(value[key], `files[${index}].${key}`);
|
|
712
|
-
}
|
|
713
|
-
if (value.previousPath !== void 0) requireString(value.previousPath, `files[${index}].previousPath`);
|
|
714
|
-
requireNonNegativeInteger(value.additions, `files[${index}].additions`);
|
|
715
|
-
requireNonNegativeInteger(value.deletions, `files[${index}].deletions`);
|
|
716
|
-
}
|
|
717
|
-
function collectLegacyTourFiles(scenes) {
|
|
718
|
-
const files = /* @__PURE__ */ new Map();
|
|
719
|
-
for (const scene of scenes) {
|
|
720
|
-
if (scene.kind === "text-diff") {
|
|
721
|
-
if (!files.has(scene.path)) files.set(scene.path, scene);
|
|
722
|
-
continue;
|
|
723
|
-
}
|
|
724
|
-
if (scene.kind === "walkthrough") {
|
|
725
|
-
for (const step of scene.steps) {
|
|
726
|
-
if (!files.has(step.diff.path)) files.set(step.diff.path, step.diff);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
731
|
-
}
|
|
732
|
-
function validateResolvedAnchor(value, path) {
|
|
733
|
-
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
734
|
-
for (const key of ["id", "path", "revision", "excerpt"]) requireString(value[key], `${path}.${key}`);
|
|
735
|
-
if (value.revision !== "base" && value.revision !== "head") throw new Error(`${path}.revision must be base or head.`);
|
|
736
|
-
requirePositiveInteger(value.startLine, `${path}.startLine`);
|
|
737
|
-
requirePositiveInteger(value.endLine, `${path}.endLine`);
|
|
738
|
-
if (Number(value.endLine) < Number(value.startLine)) throw new Error(`${path}.endLine must not precede startLine.`);
|
|
739
|
-
}
|
|
740
|
-
function validateNarrative(value, path) {
|
|
741
|
-
requireString(value.summary, `${path}.summary`);
|
|
742
|
-
requireStringArray(value.bullets, `${path}.bullets`);
|
|
743
|
-
requireStringArray(value.tags, `${path}.tags`);
|
|
744
|
-
requireString(value.takeaway, `${path}.takeaway`);
|
|
745
|
-
}
|
|
746
|
-
function isRecord(value) {
|
|
747
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
748
|
-
}
|
|
749
|
-
function requireString(value, path) {
|
|
750
|
-
if (typeof value !== "string") {
|
|
751
|
-
throw new Error(`${path} must be a string.`);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
function requireStringArray(value, path) {
|
|
755
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
756
|
-
throw new Error(`${path} must be an array of strings.`);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
function requireNonNegativeInteger(value, path) {
|
|
760
|
-
if (!Number.isInteger(value) || Number(value) < 0) {
|
|
761
|
-
throw new Error(`${path} must be a non-negative integer.`);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
function requirePositiveInteger(value, path) {
|
|
765
|
-
if (!Number.isInteger(value) || Number(value) < 1) throw new Error(`${path} must be a positive integer.`);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// src/changeTourSource.ts
|
|
769
|
-
var CHANGE_TOUR_SOURCE_VERSION = 1;
|
|
770
|
-
function parseChangeTourSource(value) {
|
|
771
|
-
if (!isRecord2(value) || value.version !== CHANGE_TOUR_SOURCE_VERSION) {
|
|
772
|
-
throw new Error("Unsupported or missing change-tour source version.");
|
|
773
|
-
}
|
|
774
|
-
requireOnlyKeys(value, ["version", "title", "windowTitle", "sourceUrl", "range", "anchors", "connections", "chapters", "coverage"], "source");
|
|
775
|
-
optionalString(value.title, "title");
|
|
776
|
-
optionalString(value.windowTitle, "windowTitle");
|
|
777
|
-
optionalString(value.sourceUrl, "sourceUrl");
|
|
778
|
-
if (value.range !== void 0) {
|
|
779
|
-
if (!isRecord2(value.range)) throw new Error("range must be an object.");
|
|
780
|
-
requireOnlyKeys(value.range, ["base", "head"], "range");
|
|
781
|
-
requireString2(value.range.base, "range.base");
|
|
782
|
-
requireString2(value.range.head, "range.head");
|
|
783
|
-
}
|
|
784
|
-
if (value.coverage !== void 0) {
|
|
785
|
-
if (!isRecord2(value.coverage) || !Array.isArray(value.coverage.exclusions)) {
|
|
786
|
-
throw new Error("coverage must contain an exclusions array.");
|
|
787
|
-
}
|
|
788
|
-
requireOnlyKeys(value.coverage, ["exclusions"], "coverage");
|
|
789
|
-
for (const [index, exclusion] of value.coverage.exclusions.entries()) {
|
|
790
|
-
const exclusionPath = `coverage.exclusions[${index}]`;
|
|
791
|
-
if (!isRecord2(exclusion)) throw new Error(`${exclusionPath} must be an object.`);
|
|
792
|
-
requireOnlyKeys(exclusion, ["path", "hunks", "reason"], exclusionPath);
|
|
793
|
-
requireString2(exclusion.path, `${exclusionPath}.path`);
|
|
794
|
-
requireString2(exclusion.reason, `${exclusionPath}.reason`);
|
|
795
|
-
if (exclusion.hunks !== void 0) requireStringArray2(exclusion.hunks, `${exclusionPath}.hunks`);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
if (!isRecord2(value.anchors)) {
|
|
799
|
-
throw new Error("anchors must be an object.");
|
|
800
|
-
}
|
|
801
|
-
for (const [id, anchor] of Object.entries(value.anchors)) {
|
|
802
|
-
if (!isRecord2(anchor)) {
|
|
803
|
-
throw new Error(`anchors.${id} must be an object.`);
|
|
804
|
-
}
|
|
805
|
-
requireOnlyKeys(anchor, ["file", "revision", "contains", "occurrence"], `anchors.${id}`);
|
|
806
|
-
requireString2(anchor.file, `anchors.${id}.file`);
|
|
807
|
-
if (anchor.revision !== "base" && anchor.revision !== "head") {
|
|
808
|
-
throw new Error(`anchors.${id}.revision must be base or head.`);
|
|
809
|
-
}
|
|
810
|
-
requireString2(anchor.contains, `anchors.${id}.contains`);
|
|
811
|
-
if (anchor.occurrence !== void 0 && (!Number.isInteger(anchor.occurrence) || Number(anchor.occurrence) < 1)) {
|
|
812
|
-
throw new Error(`anchors.${id}.occurrence must be a positive integer.`);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
const rawConnections = Array.isArray(value.connections) ? value.connections : isRecord2(value.connections) ? Object.entries(value.connections).map(([id, connection]) => isRecord2(connection) ? { id, ...connection } : connection) : null;
|
|
816
|
-
if (!rawConnections || !Array.isArray(value.chapters) || value.chapters.length === 0) {
|
|
817
|
-
throw new Error("connections must be an array or object, and chapters must be a non-empty array.");
|
|
818
|
-
}
|
|
819
|
-
const connectionIds = /* @__PURE__ */ new Set();
|
|
820
|
-
for (const [index, connection] of rawConnections.entries()) {
|
|
821
|
-
if (!isRecord2(connection)) {
|
|
822
|
-
throw new Error(`connections[${index}] must be an object.`);
|
|
823
|
-
}
|
|
824
|
-
requireOnlyKeys(connection, ["id", "from", "to", "label"], `connections[${index}]`);
|
|
825
|
-
requireString2(connection.id, `connections[${index}].id`);
|
|
826
|
-
requireString2(connection.from, `connections[${index}].from`);
|
|
827
|
-
requireString2(connection.to, `connections[${index}].to`);
|
|
828
|
-
requireString2(connection.label, `connections[${index}].label`);
|
|
829
|
-
const { id, from, to } = connection;
|
|
830
|
-
if (!value.anchors[from] || !value.anchors[to]) {
|
|
831
|
-
throw new Error(`Connection ${id} references an unknown anchor.`);
|
|
832
|
-
}
|
|
833
|
-
if (connectionIds.has(id)) {
|
|
834
|
-
throw new Error(`Duplicate connection id: ${id}`);
|
|
835
|
-
}
|
|
836
|
-
connectionIds.add(id);
|
|
837
|
-
}
|
|
838
|
-
const chapterIds = /* @__PURE__ */ new Set();
|
|
839
|
-
const sceneIds = /* @__PURE__ */ new Set();
|
|
840
|
-
for (const [chapterIndex, chapter] of value.chapters.entries()) {
|
|
841
|
-
if (!isRecord2(chapter) || !Array.isArray(chapter.scenes) || chapter.scenes.length === 0) {
|
|
842
|
-
throw new Error(`chapters[${chapterIndex}] must contain a non-empty scenes array.`);
|
|
843
|
-
}
|
|
844
|
-
requireString2(chapter.id, `chapters[${chapterIndex}].id`);
|
|
845
|
-
requireString2(chapter.title, `chapters[${chapterIndex}].title`);
|
|
846
|
-
requireOnlyKeys(chapter, ["id", "title", "scenes"], `chapters[${chapterIndex}]`);
|
|
847
|
-
if (chapterIds.has(chapter.id)) throw new Error(`Duplicate chapter id: ${chapter.id}`);
|
|
848
|
-
chapterIds.add(chapter.id);
|
|
849
|
-
for (const [sceneIndex, scene] of chapter.scenes.entries()) {
|
|
850
|
-
const path = `chapters[${chapterIndex}].scenes[${sceneIndex}]`;
|
|
851
|
-
if (!isRecord2(scene)) throw new Error(`${path} must be an object.`);
|
|
852
|
-
requireString2(scene.id, `${path}.id`);
|
|
853
|
-
requireString2(scene.title, `${path}.title`);
|
|
854
|
-
if (sceneIds.has(scene.id)) throw new Error(`Duplicate scene id: ${scene.id}`);
|
|
855
|
-
sceneIds.add(scene.id);
|
|
856
|
-
validateNarrative2(scene, path);
|
|
857
|
-
if (scene.kind === "deconstructed-diff") {
|
|
858
|
-
validateDeconstructedScene2(scene, path);
|
|
859
|
-
continue;
|
|
860
|
-
}
|
|
861
|
-
if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
|
|
862
|
-
throw new Error(`${path} must contain a non-empty steps array.`);
|
|
863
|
-
}
|
|
864
|
-
if (scene.kind === "stacked-diff") {
|
|
865
|
-
validateStackedScene2(scene, path);
|
|
866
|
-
continue;
|
|
867
|
-
}
|
|
868
|
-
if (scene.kind !== void 0 && scene.kind !== "walkthrough") {
|
|
869
|
-
throw new Error(`${path}.kind must be walkthrough, stacked-diff, or deconstructed-diff.`);
|
|
1343
|
+
const stageSteps = value.steps.filter((step) => isRecord2(step) && Number(step.pairIndex) === stageIndex);
|
|
1344
|
+
if (!isRecord2(panel) || stageSteps.length === 0) {
|
|
1345
|
+
throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
|
|
870
1346
|
}
|
|
871
|
-
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
requireString2(step.id, `${stepPath}.id`);
|
|
879
|
-
requireString2(step.title, `${stepPath}.title`);
|
|
880
|
-
requireString2(step.body, `${stepPath}.body`);
|
|
881
|
-
requireString2(step.focus, `${stepPath}.focus`);
|
|
882
|
-
requireOnlyKeys(step, ["id", "title", "body", "focus", "connection", "depth"], stepPath);
|
|
883
|
-
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
884
|
-
throw new Error(`${stepPath}.depth must be mentioned, explained, or contextualized.`);
|
|
885
|
-
}
|
|
886
|
-
if (stepIds.has(step.id)) throw new Error(`Duplicate step id in scene ${scene.id}: ${step.id}`);
|
|
887
|
-
stepIds.add(step.id);
|
|
888
|
-
const focus = step.focus;
|
|
889
|
-
if (!value.anchors[focus]) {
|
|
890
|
-
throw new Error(`${stepPath} references unknown anchor ${focus}.`);
|
|
891
|
-
}
|
|
892
|
-
optionalString(step.connection, `${stepPath}.connection`);
|
|
893
|
-
const connection = step.connection;
|
|
894
|
-
if (typeof connection === "string" && !connectionIds.has(connection)) {
|
|
895
|
-
throw new Error(`${stepPath} references unknown connection ${connection}.`);
|
|
896
|
-
}
|
|
1347
|
+
const firstStep = stageSteps[0];
|
|
1348
|
+
const fallbackStageId = isRecord2(firstStep) ? firstStep.id : void 0;
|
|
1349
|
+
if (stageSteps.some((step) => {
|
|
1350
|
+
if (!isRecord2(step)) return true;
|
|
1351
|
+
return (step.stageId ?? fallbackStageId) !== panel.stageId;
|
|
1352
|
+
})) {
|
|
1353
|
+
throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
|
|
897
1354
|
}
|
|
898
1355
|
}
|
|
899
1356
|
}
|
|
900
|
-
return { ...value, connections: rawConnections };
|
|
901
1357
|
}
|
|
902
|
-
function
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
requireString2(stage.title, `${stagePath}.title`);
|
|
916
|
-
requireString2(stage.narration, `${stagePath}.narration`);
|
|
917
|
-
if (!Array.isArray(stage.changes) || stage.changes.length === 0) {
|
|
918
|
-
throw new Error(`${stagePath}.changes must be a non-empty array.`);
|
|
1358
|
+
function validateMultiPanelFiles(value, panelCount, index) {
|
|
1359
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].files must be non-empty.`);
|
|
1360
|
+
for (const [fileIndex, file] of value.entries()) {
|
|
1361
|
+
if (!isRecord2(file)) throw new Error(`scenes[${index}].files[${fileIndex}] must be an object.`);
|
|
1362
|
+
requireString2(file.path, `scenes[${index}].files[${fileIndex}].path`);
|
|
1363
|
+
if (!Array.isArray(file.panels) || file.panels.length !== panelCount) {
|
|
1364
|
+
throw new Error(`scenes[${index}].files[${fileIndex}].panels must match panel count.`);
|
|
1365
|
+
}
|
|
1366
|
+
for (const [panelIndex, panel] of file.panels.entries()) {
|
|
1367
|
+
if (!isRecord2(panel)) throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}] must be an object.`);
|
|
1368
|
+
for (const key of ["id", "label", "content"]) requireString2(panel[key], `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].${key}`);
|
|
1369
|
+
if (panel.path !== void 0) requireString2(panel.path, `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].path`);
|
|
1370
|
+
if (typeof panel.exists !== "boolean") throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}].exists must be boolean.`);
|
|
919
1371
|
}
|
|
920
|
-
if (stageIds.has(stage.id)) throw new Error(`Duplicate deconstructed stage id: ${stage.id}`);
|
|
921
|
-
stageIds.add(stage.id);
|
|
922
|
-
stage.changes.forEach((change, changeIndex) => validateDeconstructedSelection(
|
|
923
|
-
change,
|
|
924
|
-
`${stagePath}.changes[${changeIndex}]`,
|
|
925
|
-
false
|
|
926
|
-
));
|
|
927
|
-
}
|
|
928
|
-
if (scene.exclusions !== void 0) {
|
|
929
|
-
if (!Array.isArray(scene.exclusions)) throw new Error(`${path}.exclusions must be an array.`);
|
|
930
|
-
scene.exclusions.forEach((exclusion, exclusionIndex) => validateDeconstructedSelection(
|
|
931
|
-
exclusion,
|
|
932
|
-
`${path}.exclusions[${exclusionIndex}]`,
|
|
933
|
-
true
|
|
934
|
-
));
|
|
935
1372
|
}
|
|
936
1373
|
}
|
|
937
|
-
function
|
|
938
|
-
if (!
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
if (
|
|
1374
|
+
function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
1375
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].steps must be non-empty.`);
|
|
1376
|
+
for (const [stepIndex, step] of value.entries()) {
|
|
1377
|
+
if (!isRecord2(step)) throw new Error(`scenes[${index}].steps[${stepIndex}] must be an object.`);
|
|
1378
|
+
for (const key of ["id", "title", "body", "file", "side"]) requireString2(step[key], `scenes[${index}].steps[${stepIndex}].${key}`);
|
|
1379
|
+
requireNonNegativeInteger(step.pairIndex, `scenes[${index}].steps[${stepIndex}].pairIndex`);
|
|
1380
|
+
if (Number(step.pairIndex) >= panelCount - 1) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex is outside the panels.`);
|
|
1381
|
+
if (deconstructed) {
|
|
1382
|
+
requireStringArray2(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
|
|
1383
|
+
if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
|
|
1384
|
+
}
|
|
1385
|
+
if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
|
|
1386
|
+
if (step.startLine !== void 0) requirePositiveInteger(step.startLine, `scenes[${index}].steps[${stepIndex}].startLine`);
|
|
1387
|
+
if (step.endLine !== void 0) requirePositiveInteger(step.endLine, `scenes[${index}].steps[${stepIndex}].endLine`);
|
|
944
1388
|
}
|
|
945
|
-
if (requiresReason) requireString2(value.reason, `${path}.reason`);
|
|
946
1389
|
}
|
|
947
|
-
function
|
|
948
|
-
|
|
949
|
-
if (
|
|
950
|
-
|
|
1390
|
+
function validateTourFile(value, index) {
|
|
1391
|
+
if (!isRecord2(value)) throw new Error(`files[${index}] must be an object.`);
|
|
1392
|
+
if (value.kind === "text-diff") {
|
|
1393
|
+
validateScene(value, index, 1);
|
|
1394
|
+
return;
|
|
951
1395
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
const entryPath = `${path}.stack[${index}]`;
|
|
955
|
-
if (!isRecord2(entry)) throw new Error(`${entryPath} must be an object.`);
|
|
956
|
-
requireOnlyKeys(entry, ["id", "ref", "label"], entryPath);
|
|
957
|
-
requireString2(entry.id, `${entryPath}.id`);
|
|
958
|
-
requireString2(entry.ref, `${entryPath}.ref`);
|
|
959
|
-
optionalString(entry.label, `${entryPath}.label`);
|
|
960
|
-
if (stackIds.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
961
|
-
stackIds.add(entry.id);
|
|
1396
|
+
if (value.kind !== "omitted") {
|
|
1397
|
+
throw new Error(`files[${index}].kind must be text-diff or omitted.`);
|
|
962
1398
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
stepIds.add(stepId);
|
|
977
|
-
if (!Array.isArray(step.pair) || step.pair.length !== 2 || step.pair.some((id) => typeof id !== "string")) {
|
|
978
|
-
throw new Error(`${stepPath}.pair must contain two stack entry ids.`);
|
|
1399
|
+
for (const key of ["id", "title", "path", "changeKind", "reason"]) {
|
|
1400
|
+
requireString2(value[key], `files[${index}].${key}`);
|
|
1401
|
+
}
|
|
1402
|
+
if (value.previousPath !== void 0) requireString2(value.previousPath, `files[${index}].previousPath`);
|
|
1403
|
+
requireNonNegativeInteger(value.additions, `files[${index}].additions`);
|
|
1404
|
+
requireNonNegativeInteger(value.deletions, `files[${index}].deletions`);
|
|
1405
|
+
}
|
|
1406
|
+
function collectLegacyTourFiles(scenes) {
|
|
1407
|
+
const files = /* @__PURE__ */ new Map();
|
|
1408
|
+
for (const scene of scenes) {
|
|
1409
|
+
if (scene.kind === "text-diff") {
|
|
1410
|
+
if (!files.has(scene.path)) files.set(scene.path, scene);
|
|
1411
|
+
continue;
|
|
979
1412
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
if (step.side !== void 0 && step.side !== "left" && step.side !== "right") throw new Error(`${stepPath}.side must be left or right.`);
|
|
985
|
-
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]))) {
|
|
986
|
-
throw new Error(`${stepPath}.lines must contain an ordered positive line range.`);
|
|
1413
|
+
if (scene.kind === "walkthrough") {
|
|
1414
|
+
for (const step of scene.steps) {
|
|
1415
|
+
if (!files.has(step.diff.path)) files.set(step.diff.path, step.diff);
|
|
1416
|
+
}
|
|
987
1417
|
}
|
|
988
1418
|
}
|
|
1419
|
+
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
1420
|
+
}
|
|
1421
|
+
function validateResolvedAnchor(value, path) {
|
|
1422
|
+
if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
|
|
1423
|
+
for (const key of ["id", "path", "revision", "excerpt"]) requireString2(value[key], `${path}.${key}`);
|
|
1424
|
+
if (value.revision !== "base" && value.revision !== "head") throw new Error(`${path}.revision must be base or head.`);
|
|
1425
|
+
requirePositiveInteger(value.startLine, `${path}.startLine`);
|
|
1426
|
+
requirePositiveInteger(value.endLine, `${path}.endLine`);
|
|
1427
|
+
if (Number(value.endLine) < Number(value.startLine)) throw new Error(`${path}.endLine must not precede startLine.`);
|
|
989
1428
|
}
|
|
990
1429
|
function validateNarrative2(value, path) {
|
|
991
1430
|
requireString2(value.summary, `${path}.summary`);
|
|
@@ -997,13 +1436,8 @@ function isRecord2(value) {
|
|
|
997
1436
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
998
1437
|
}
|
|
999
1438
|
function requireString2(value, path) {
|
|
1000
|
-
if (typeof value !== "string"
|
|
1001
|
-
throw new Error(`${path} must be a
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
function optionalString(value, path) {
|
|
1005
|
-
if (value !== void 0) {
|
|
1006
|
-
requireString2(value, path);
|
|
1439
|
+
if (typeof value !== "string") {
|
|
1440
|
+
throw new Error(`${path} must be a string.`);
|
|
1007
1441
|
}
|
|
1008
1442
|
}
|
|
1009
1443
|
function requireStringArray2(value, path) {
|
|
@@ -1011,12 +1445,19 @@ function requireStringArray2(value, path) {
|
|
|
1011
1445
|
throw new Error(`${path} must be an array of strings.`);
|
|
1012
1446
|
}
|
|
1013
1447
|
}
|
|
1014
|
-
function
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1448
|
+
function requireNonNegativeInteger(value, path) {
|
|
1449
|
+
if (!Number.isInteger(value) || Number(value) < 0) {
|
|
1450
|
+
throw new Error(`${path} must be a non-negative integer.`);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
function requirePercentage(value, path) {
|
|
1454
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
|
|
1455
|
+
throw new Error(`${path} must be a number from 0 to 100.`);
|
|
1018
1456
|
}
|
|
1019
1457
|
}
|
|
1458
|
+
function requirePositiveInteger(value, path) {
|
|
1459
|
+
if (!Number.isInteger(value) || Number(value) < 1) throw new Error(`${path} must be a positive integer.`);
|
|
1460
|
+
}
|
|
1020
1461
|
|
|
1021
1462
|
// src/deconstructedChange.ts
|
|
1022
1463
|
function buildDeconstructedScene(startPath, scene, options = {}) {
|
|
@@ -1551,12 +1992,20 @@ var DIFF_TIMEOUT_MS = readPositiveIntegerEnv("BYGONE_DIFF_TIMEOUT_MS", DEFAULT_D
|
|
|
1551
1992
|
function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
1552
1993
|
const leftLines = normalizeLines(leftContent);
|
|
1553
1994
|
const rightLines = normalizeLines(rightContent);
|
|
1554
|
-
|
|
1995
|
+
if (leftContent === rightContent) {
|
|
1996
|
+
return buildIdenticalTwoWayDiffModel(leftLines);
|
|
1997
|
+
}
|
|
1998
|
+
const interned = internDiffLines(leftLines, rightLines);
|
|
1999
|
+
const internedChanges = diffArrays(interned.left, interned.right, {
|
|
1555
2000
|
timeout: options.timeoutMs ?? DIFF_TIMEOUT_MS
|
|
1556
2001
|
});
|
|
1557
|
-
if (!
|
|
2002
|
+
if (!internedChanges) {
|
|
1558
2003
|
return buildConservativeTwoWayDiffModel(leftLines, rightLines);
|
|
1559
2004
|
}
|
|
2005
|
+
const changes = internedChanges.map((change) => ({
|
|
2006
|
+
...change,
|
|
2007
|
+
value: change.value.map((lineId) => interned.lines[lineId])
|
|
2008
|
+
}));
|
|
1560
2009
|
const rows = [];
|
|
1561
2010
|
const renderedLeftLines = [];
|
|
1562
2011
|
const renderedRightLines = [];
|
|
@@ -1580,29 +2029,18 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
|
1580
2029
|
}
|
|
1581
2030
|
if (change.removed && index + 1 < changes.length && changes[index + 1].added) {
|
|
1582
2031
|
const nextChange = changes[index + 1];
|
|
1583
|
-
const
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
renderedRightLines.push(renderedRightLine);
|
|
1596
|
-
}
|
|
1597
|
-
if (renderedLeftLine && renderedRightLine) {
|
|
1598
|
-
applyInlineHighlightPair(renderedLeftLine, renderedRightLine);
|
|
1599
|
-
}
|
|
1600
|
-
rows.push(makeDiffRow(
|
|
1601
|
-
removedLine === void 0 ? makePlaceholder() : makeDiffCell("removed", removedLine, leftLineNumber++),
|
|
1602
|
-
addedLine === void 0 ? makePlaceholder() : makeDiffCell("added", addedLine, rightLineNumber++)
|
|
1603
|
-
));
|
|
1604
|
-
}
|
|
1605
|
-
blocks.push(makeDiffBlock("replace", leftStart, renderedLeftLines.length, rightStart, renderedRightLines.length));
|
|
2032
|
+
const nextLineNumbers = appendReplacementCandidate(
|
|
2033
|
+
removedLines,
|
|
2034
|
+
nextChange.value,
|
|
2035
|
+
leftLineNumber,
|
|
2036
|
+
rightLineNumber,
|
|
2037
|
+
rows,
|
|
2038
|
+
renderedLeftLines,
|
|
2039
|
+
renderedRightLines,
|
|
2040
|
+
blocks
|
|
2041
|
+
);
|
|
2042
|
+
leftLineNumber = nextLineNumbers.left;
|
|
2043
|
+
rightLineNumber = nextLineNumbers.right;
|
|
1606
2044
|
index++;
|
|
1607
2045
|
continue;
|
|
1608
2046
|
}
|
|
@@ -1638,7 +2076,40 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
|
1638
2076
|
leftLines: renderedLeftLines,
|
|
1639
2077
|
rightLines: renderedRightLines,
|
|
1640
2078
|
blocks,
|
|
1641
|
-
hasChanges
|
|
2079
|
+
hasChanges,
|
|
2080
|
+
quality: "exact"
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
function internDiffLines(leftLines, rightLines) {
|
|
2084
|
+
const lineIds = /* @__PURE__ */ new Map();
|
|
2085
|
+
const lines = [];
|
|
2086
|
+
const intern = (line) => {
|
|
2087
|
+
const existingId = lineIds.get(line);
|
|
2088
|
+
if (existingId !== void 0) {
|
|
2089
|
+
return existingId;
|
|
2090
|
+
}
|
|
2091
|
+
const lineId = lines.length;
|
|
2092
|
+
lineIds.set(line, lineId);
|
|
2093
|
+
lines.push(line);
|
|
2094
|
+
return lineId;
|
|
2095
|
+
};
|
|
2096
|
+
return {
|
|
2097
|
+
left: leftLines.map(intern),
|
|
2098
|
+
right: rightLines.map(intern),
|
|
2099
|
+
lines
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
function buildIdenticalTwoWayDiffModel(lines) {
|
|
2103
|
+
return {
|
|
2104
|
+
rows: lines.map((line, index) => makeDiffRow(
|
|
2105
|
+
makeDiffCell("context", line, index + 1),
|
|
2106
|
+
makeDiffCell("context", line, index + 1)
|
|
2107
|
+
)),
|
|
2108
|
+
leftLines: lines.map((line, index) => makeDiffLine("context", line, index + 1)),
|
|
2109
|
+
rightLines: lines.map((line, index) => makeDiffLine("context", line, index + 1)),
|
|
2110
|
+
blocks: [],
|
|
2111
|
+
hasChanges: false,
|
|
2112
|
+
quality: "exact"
|
|
1642
2113
|
};
|
|
1643
2114
|
}
|
|
1644
2115
|
function buildConservativeTwoWayDiffModel(leftLines, rightLines) {
|
|
@@ -1665,7 +2136,8 @@ function buildConservativeTwoWayDiffModel(leftLines, rightLines) {
|
|
|
1665
2136
|
leftLines: renderedLeftLines,
|
|
1666
2137
|
rightLines: renderedRightLines,
|
|
1667
2138
|
blocks: rows.length === 0 ? [] : [makeDiffBlock(blockKind, 0, leftLines.length, 0, rightLines.length)],
|
|
1668
|
-
hasChanges: rows.length > 0
|
|
2139
|
+
hasChanges: rows.length > 0,
|
|
2140
|
+
quality: "fallback"
|
|
1669
2141
|
};
|
|
1670
2142
|
}
|
|
1671
2143
|
function normalizeLines(content) {
|
|
@@ -1678,6 +2150,100 @@ function normalizeLines(content) {
|
|
|
1678
2150
|
}
|
|
1679
2151
|
return lines;
|
|
1680
2152
|
}
|
|
2153
|
+
function appendReplacementCandidate(leftLines, rightLines, initialLeftLineNumber, initialRightLineNumber, rows, renderedLeftLines, renderedRightLines, blocks) {
|
|
2154
|
+
const aligned = alignReplacementLines(leftLines, rightLines);
|
|
2155
|
+
const crediblePairs = aligned.filter((row) => row.left !== void 0 && row.right !== void 0 && hasCredibleLineCorrespondence(row.left, row.right)).length;
|
|
2156
|
+
const coherentBlock = hasCoherentBlockCorrespondence(leftLines, rightLines, crediblePairs);
|
|
2157
|
+
const normalized = aligned.flatMap((row) => {
|
|
2158
|
+
if (row.left === void 0 || row.right === void 0 || hasCredibleLineCorrespondence(row.left, row.right)) {
|
|
2159
|
+
return [row];
|
|
2160
|
+
}
|
|
2161
|
+
return [{ left: row.left }, { right: row.right }];
|
|
2162
|
+
});
|
|
2163
|
+
let leftLineNumber = initialLeftLineNumber;
|
|
2164
|
+
let rightLineNumber = initialRightLineNumber;
|
|
2165
|
+
let blockKind;
|
|
2166
|
+
let blockLeftStart = renderedLeftLines.length;
|
|
2167
|
+
let blockRightStart = renderedRightLines.length;
|
|
2168
|
+
const flushBlock = () => {
|
|
2169
|
+
if (!blockKind) return;
|
|
2170
|
+
blocks.push(makeDiffBlock(
|
|
2171
|
+
blockKind,
|
|
2172
|
+
blockLeftStart,
|
|
2173
|
+
renderedLeftLines.length,
|
|
2174
|
+
blockRightStart,
|
|
2175
|
+
renderedRightLines.length
|
|
2176
|
+
));
|
|
2177
|
+
};
|
|
2178
|
+
for (const { left, right } of normalized) {
|
|
2179
|
+
const nextKind = coherentBlock ? "replace" : left !== void 0 && right !== void 0 ? "replace" : left !== void 0 ? "delete" : "insert";
|
|
2180
|
+
if (blockKind !== nextKind) {
|
|
2181
|
+
flushBlock();
|
|
2182
|
+
blockKind = nextKind;
|
|
2183
|
+
blockLeftStart = renderedLeftLines.length;
|
|
2184
|
+
blockRightStart = renderedRightLines.length;
|
|
2185
|
+
}
|
|
2186
|
+
let renderedLeftLine;
|
|
2187
|
+
let renderedRightLine;
|
|
2188
|
+
if (left !== void 0) {
|
|
2189
|
+
renderedLeftLine = makeDiffLine("removed", left, leftLineNumber);
|
|
2190
|
+
renderedLeftLines.push(renderedLeftLine);
|
|
2191
|
+
}
|
|
2192
|
+
if (right !== void 0) {
|
|
2193
|
+
renderedRightLine = makeDiffLine("added", right, rightLineNumber);
|
|
2194
|
+
renderedRightLines.push(renderedRightLine);
|
|
2195
|
+
}
|
|
2196
|
+
if (renderedLeftLine && renderedRightLine) {
|
|
2197
|
+
applyInlineHighlightPair(renderedLeftLine, renderedRightLine);
|
|
2198
|
+
} else if (coherentBlock) {
|
|
2199
|
+
if (renderedLeftLine) applyFullLineHighlight(renderedLeftLine);
|
|
2200
|
+
if (renderedRightLine) applyFullLineHighlight(renderedRightLine);
|
|
2201
|
+
}
|
|
2202
|
+
rows.push(makeDiffRow(
|
|
2203
|
+
left === void 0 ? makePlaceholder() : makeDiffCell("removed", left, leftLineNumber++),
|
|
2204
|
+
right === void 0 ? makePlaceholder() : makeDiffCell("added", right, rightLineNumber++)
|
|
2205
|
+
));
|
|
2206
|
+
}
|
|
2207
|
+
flushBlock();
|
|
2208
|
+
return { left: leftLineNumber, right: rightLineNumber };
|
|
2209
|
+
}
|
|
2210
|
+
function hasCredibleLineCorrespondence(left, right) {
|
|
2211
|
+
const score = scoreReplacementLinePair(left, right);
|
|
2212
|
+
if (score.eligible) return true;
|
|
2213
|
+
const leftAnchor = declarationAnchorKey(left);
|
|
2214
|
+
if (leftAnchor && leftAnchor === declarationAnchorKey(right)) return true;
|
|
2215
|
+
const leftTokens = new Set(tokenizeMatchingContent(normalizeMatchingContent(left)).filter(isSharedContentToken));
|
|
2216
|
+
return tokenizeMatchingContent(normalizeMatchingContent(right)).some((token) => leftTokens.has(token) && isSharedContentToken(token));
|
|
2217
|
+
}
|
|
2218
|
+
var NON_SUBSTANTIVE_SHARED_TOKENS = /* @__PURE__ */ new Set([
|
|
2219
|
+
"and",
|
|
2220
|
+
"are",
|
|
2221
|
+
"for",
|
|
2222
|
+
"from",
|
|
2223
|
+
"into",
|
|
2224
|
+
"that",
|
|
2225
|
+
"the",
|
|
2226
|
+
"this",
|
|
2227
|
+
"to",
|
|
2228
|
+
"with"
|
|
2229
|
+
]);
|
|
2230
|
+
function isSharedContentToken(token) {
|
|
2231
|
+
return token.length >= 3 && /^[\p{L}\p{N}_$]+$/u.test(token) && !NON_SUBSTANTIVE_SHARED_TOKENS.has(token);
|
|
2232
|
+
}
|
|
2233
|
+
function hasCoherentBlockCorrespondence(leftLines, rightLines, crediblePairs) {
|
|
2234
|
+
if (crediblePairs === 0) return false;
|
|
2235
|
+
const longestSide = Math.max(leftLines.length, rightLines.length);
|
|
2236
|
+
if (longestSide <= 2 || crediblePairs * 2 >= longestSide) return true;
|
|
2237
|
+
const roles = [...leftLines, ...rightLines].map(collectionLineRole);
|
|
2238
|
+
return roles[0] !== null && roles.every((role) => role === roles[0]);
|
|
2239
|
+
}
|
|
2240
|
+
function collectionLineRole(line) {
|
|
2241
|
+
const normalized = line.trimStart();
|
|
2242
|
+
if (/^[-*+]\s+/.test(normalized)) return "bullet";
|
|
2243
|
+
if (/^\d+[.)]\s+/.test(normalized)) return "ordered";
|
|
2244
|
+
if (/^>\s+/.test(normalized)) return "quote";
|
|
2245
|
+
return null;
|
|
2246
|
+
}
|
|
1681
2247
|
var MAX_ALIGNMENT_CELLS = 1e4;
|
|
1682
2248
|
var MAX_SCORING_LINE_LENGTH = 2e3;
|
|
1683
2249
|
var MINIMUM_MATCH_SCORE = 0.52;
|
|
@@ -1690,6 +2256,7 @@ var CONTEXTUAL_AMBIGUITY_MARGIN = 0.05;
|
|
|
1690
2256
|
var GAP_PENALTY = -0.12;
|
|
1691
2257
|
var MAX_BOUNDED_CANDIDATES = 5e3;
|
|
1692
2258
|
var MAX_LARGE_HUNK_ANCHORS = 2e3;
|
|
2259
|
+
var MAX_ALIGNMENT_CHARACTER_CELLS = 5e7;
|
|
1693
2260
|
var MAX_RARE_TOKEN_OCCURRENCES = 4;
|
|
1694
2261
|
var MAX_CONTEXTUAL_HUNK_LINES = 8;
|
|
1695
2262
|
var MAX_CONTEXTUAL_LENGTH_DELTA = 4;
|
|
@@ -1700,9 +2267,17 @@ function alignReplacementLines(leftLines, rightLines) {
|
|
|
1700
2267
|
if (rightLines.length === 0) {
|
|
1701
2268
|
return leftLines.map((left) => ({ left }));
|
|
1702
2269
|
}
|
|
2270
|
+
const leftCharacterCount = leftLines.reduce((total, line) => total + line.length, 0);
|
|
2271
|
+
const rightCharacterCount = rightLines.reduce((total, line) => total + line.length, 0);
|
|
2272
|
+
if (leftCharacterCount * rightCharacterCount > MAX_ALIGNMENT_CHARACTER_CELLS) {
|
|
2273
|
+
return [
|
|
2274
|
+
...leftLines.map((left) => ({ left })),
|
|
2275
|
+
...rightLines.map((right) => ({ right }))
|
|
2276
|
+
];
|
|
2277
|
+
}
|
|
1703
2278
|
if (leftLines.length === 1 && rightLines.length === 1) {
|
|
1704
2279
|
const singletonScore = scoreReplacementLinePair(leftLines[0], rightLines[0]);
|
|
1705
|
-
if ((singletonScore.eligible || singletonScore.score >= MINIMUM_SINGLE_PAIR_SCORE) && haveCompatibleLineRoles(leftLines[0], rightLines[0]) && isInformativeLine(normalizeMatchingContent(leftLines[0])) && isInformativeLine(normalizeMatchingContent(rightLines[0]))) {
|
|
2280
|
+
if ((singletonScore.eligible || singletonScore.score >= MINIMUM_SINGLE_PAIR_SCORE) && hasCredibleLineCorrespondence(leftLines[0], rightLines[0]) && haveCompatibleLineRoles(leftLines[0], rightLines[0]) && isInformativeLine(normalizeMatchingContent(leftLines[0])) && isInformativeLine(normalizeMatchingContent(rightLines[0]))) {
|
|
1706
2281
|
return [{ left: leftLines[0], right: rightLines[0] }];
|
|
1707
2282
|
}
|
|
1708
2283
|
return [{ left: leftLines[0] }, { right: rightLines[0] }];
|
|
@@ -2224,6 +2799,11 @@ function applyInlineHighlightPair(leftLine, rightLine) {
|
|
|
2224
2799
|
rightLine.segments = rightSegments;
|
|
2225
2800
|
}
|
|
2226
2801
|
}
|
|
2802
|
+
function applyFullLineHighlight(line) {
|
|
2803
|
+
if (/[^\s]/.test(line.content)) {
|
|
2804
|
+
line.segments = [{ kind: line.kind, text: line.content, emphasis: true }];
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2227
2807
|
function buildInlineSegments(leftContent, rightContent) {
|
|
2228
2808
|
const changes = diffWordsWithSpace(leftContent, rightContent);
|
|
2229
2809
|
const leftSegments = [];
|
|
@@ -2375,6 +2955,125 @@ function findBlankLineBoundary(lines, target, minimum, maximum) {
|
|
|
2375
2955
|
return Math.min(Math.max(target, minimum), maximum - 1);
|
|
2376
2956
|
}
|
|
2377
2957
|
|
|
2958
|
+
// src/changeAttention.ts
|
|
2959
|
+
var CHANGE_ATTENTION_VERSION = 1;
|
|
2960
|
+
var LOCKFILES = /* @__PURE__ */ new Set([
|
|
2961
|
+
"cargo.lock",
|
|
2962
|
+
"package-lock.json",
|
|
2963
|
+
"pnpm-lock.yaml",
|
|
2964
|
+
"yarn.lock",
|
|
2965
|
+
"poetry.lock",
|
|
2966
|
+
"gemfile.lock",
|
|
2967
|
+
"composer.lock"
|
|
2968
|
+
]);
|
|
2969
|
+
var MAX_FAN_IN_FILES = 250;
|
|
2970
|
+
var MAX_FAN_IN_TEXT_BYTES = 64 * 1024;
|
|
2971
|
+
function classifyChangeFileRole(filePath) {
|
|
2972
|
+
const lower = filePath.toLowerCase();
|
|
2973
|
+
const fileName = lower.split("/").pop() || lower;
|
|
2974
|
+
if (/(^|\/)(dist|build|generated|vendor)(\/|$)/.test(lower) || /(?:\.min\.js|\.map)$/.test(lower)) return "generated";
|
|
2975
|
+
if (/(^|\/)(tests?|__tests__)(\/|$)/.test(lower) || /(^|\/)test_[^/]+$/.test(lower) || /\.(test|spec)\.[^/]+$/.test(lower)) return "test";
|
|
2976
|
+
if (lower.startsWith("docs/") || fileName === "readme.md" || /\.(md|mdx|rst)$/.test(lower)) return "documentation";
|
|
2977
|
+
if (LOCKFILES.has(fileName) || /(^|\/)(pyproject\.toml|package\.json|go\.mod|go\.sum|cargo\.toml)$/.test(lower)) return "dependency";
|
|
2978
|
+
return "production";
|
|
2979
|
+
}
|
|
2980
|
+
function buildChangeAttention(inputs) {
|
|
2981
|
+
const prepared = inputs.map((input) => prepareFile(input, inputs));
|
|
2982
|
+
const production = prepared.filter((file) => file.role === "production" && !file.mechanical);
|
|
2983
|
+
const documentation = prepared.filter((file) => file.role === "documentation" && !file.mechanical);
|
|
2984
|
+
const eligible = production.length > 0 ? production : documentation;
|
|
2985
|
+
const entry = [...eligible].sort(compareRanked)[0];
|
|
2986
|
+
const fallback = [...prepared].filter((file) => !file.binary).sort((left, right) => right.changedLines - left.changedLines || left.path.localeCompare(right.path))[0];
|
|
2987
|
+
const focusCandidates = prepared.filter((file) => file.path !== (entry == null ? void 0 : entry.path) && file.role === "production" && !file.mechanical).sort(compareRanked).slice(0, Math.max(0, 7 - (entry ? 1 : 0)));
|
|
2988
|
+
const pairedTest = entry || focusCandidates.length > 0 ? prepared.filter((file) => file.role === "test" && !file.mechanical && file.changedLines >= 5 && isTestForFocus(file.path, [entry, ...focusCandidates].filter(Boolean).map((candidate) => candidate.path))).sort(compareRanked)[0] : void 0;
|
|
2989
|
+
if (pairedTest && focusCandidates.length < Math.max(0, 7 - (entry ? 1 : 0))) focusCandidates.push(pairedTest);
|
|
2990
|
+
const focusPaths = focusCandidates.map((file) => file.path);
|
|
2991
|
+
const files = prepared.map((file) => {
|
|
2992
|
+
const attention = file.path === (entry == null ? void 0 : entry.path) ? "entry" : focusPaths.includes(file.path) ? "focus" : file.mechanical ? "mechanical" : "background";
|
|
2993
|
+
return {
|
|
2994
|
+
path: file.path,
|
|
2995
|
+
previousPath: file.previousPath,
|
|
2996
|
+
changeKind: file.changeKind,
|
|
2997
|
+
role: file.role,
|
|
2998
|
+
attention,
|
|
2999
|
+
additions: file.additions,
|
|
3000
|
+
deletions: file.deletions,
|
|
3001
|
+
reason: attention === "entry" ? entryReason(file, production.length) : attentionReason(file),
|
|
3002
|
+
score: file.score
|
|
3003
|
+
};
|
|
3004
|
+
}).sort((left, right) => left.path.localeCompare(right.path));
|
|
3005
|
+
return {
|
|
3006
|
+
version: CHANGE_ATTENTION_VERSION,
|
|
3007
|
+
entryPath: (entry == null ? void 0 : entry.path) || null,
|
|
3008
|
+
entryReason: entry ? entryReason(entry, production.length) : noEntryReason(prepared),
|
|
3009
|
+
fallbackPath: (fallback == null ? void 0 : fallback.path) || null,
|
|
3010
|
+
focusPaths,
|
|
3011
|
+
backgroundPaths: files.filter((file) => file.attention === "background").map((file) => file.path),
|
|
3012
|
+
mechanicalPaths: files.filter((file) => file.attention === "mechanical").map((file) => file.path),
|
|
3013
|
+
files
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
function prepareFile(input, all) {
|
|
3017
|
+
const role = classifyChangeFileRole(input.path);
|
|
3018
|
+
const fileName = input.path.toLowerCase().split("/").pop() || input.path.toLowerCase();
|
|
3019
|
+
const mechanical = Boolean(input.binary) || role === "generated" || LOCKFILES.has(fileName);
|
|
3020
|
+
const changedLines = (input.additions ?? 0) + (input.deletions ?? 0);
|
|
3021
|
+
const contract = /(^|[\/._-])(types?|schema|models?|contracts?|interfaces?)([\/._-]|$)/i.test(input.path);
|
|
3022
|
+
const referenceHints = new Set([
|
|
3023
|
+
fileName.replace(/\.[^.]+$/, ""),
|
|
3024
|
+
...(input.symbolHints || []).map((hint) => hint.toLowerCase())
|
|
3025
|
+
].filter((hint) => hint.length >= 4));
|
|
3026
|
+
const fanIn = [...all].filter((candidate) => candidate.path !== input.path).sort((left, right) => left.path.localeCompare(right.path)).slice(0, MAX_FAN_IN_FILES).filter((candidate) => {
|
|
3027
|
+
var _a;
|
|
3028
|
+
const boundedText = (_a = candidate.headText) == null ? void 0 : _a.slice(0, MAX_FAN_IN_TEXT_BYTES).toLowerCase();
|
|
3029
|
+
return Boolean(boundedText && [...referenceHints].some((hint) => boundedText.includes(hint)));
|
|
3030
|
+
}).length;
|
|
3031
|
+
const roleScore = role === "production" ? 300 : role === "documentation" ? 150 : role === "dependency" ? 80 : role === "test" ? 50 : 0;
|
|
3032
|
+
const sizePenalty = changedLines > 2e3 ? 80 : changedLines > 500 ? 30 : 0;
|
|
3033
|
+
const readmePenalty = fileName === "readme.md" && all.filter((candidate) => !candidate.binary).length > 1 ? 100 : 0;
|
|
3034
|
+
return {
|
|
3035
|
+
...input,
|
|
3036
|
+
role,
|
|
3037
|
+
mechanical,
|
|
3038
|
+
changedLines,
|
|
3039
|
+
contract,
|
|
3040
|
+
fanIn,
|
|
3041
|
+
binary: Boolean(input.binary),
|
|
3042
|
+
score: mechanical ? -1e3 : roleScore + (contract ? 100 : 0) + fanIn * 40 + (input.commitCount || 0) * 5 - sizePenalty - readmePenalty
|
|
3043
|
+
};
|
|
3044
|
+
}
|
|
3045
|
+
function compareRanked(left, right) {
|
|
3046
|
+
return right.score - left.score || right.fanIn - left.fanIn || left.changedLines - right.changedLines || left.path.localeCompare(right.path);
|
|
3047
|
+
}
|
|
3048
|
+
function entryReason(file, productionCount) {
|
|
3049
|
+
if (file.fanIn > 0) return `Referenced by ${file.fanIn} other changed file${file.fanIn === 1 ? "" : "s"}.`;
|
|
3050
|
+
if (file.contract) return "Contract-shaped path central to the change.";
|
|
3051
|
+
if (productionCount === 1) return "Only production file in this range.";
|
|
3052
|
+
return "Highest-ranked production file for a first pass.";
|
|
3053
|
+
}
|
|
3054
|
+
function attentionReason(file) {
|
|
3055
|
+
if (file.binary) return "Binary file.";
|
|
3056
|
+
if (file.mechanical) return file.role === "generated" ? "Generated artifact." : "Dependency lockfile.";
|
|
3057
|
+
if (file.role === "test") return "Test or proof file.";
|
|
3058
|
+
if (file.role === "documentation") return "Documentation change.";
|
|
3059
|
+
if (file.role === "dependency") return "Dependency or packaging manifest.";
|
|
3060
|
+
return "Additional production file.";
|
|
3061
|
+
}
|
|
3062
|
+
function noEntryReason(files) {
|
|
3063
|
+
if (files.every((file) => file.mechanical)) return "This change is entirely mechanical or generated.";
|
|
3064
|
+
if (files.every((file) => file.mechanical || file.role === "test")) return "This change contains only tests and mechanical files.";
|
|
3065
|
+
return "No single entry-point is reliable for this change.";
|
|
3066
|
+
}
|
|
3067
|
+
function isTestForFocus(testPath, focusPaths) {
|
|
3068
|
+
const normalizedTest = testPath.toLowerCase().replace(/\.(test|spec)(?=\.)/, "").replace(/^test_/, "");
|
|
3069
|
+
return focusPaths.some((candidate) => {
|
|
3070
|
+
var _a;
|
|
3071
|
+
const normalizedCandidate = candidate.toLowerCase();
|
|
3072
|
+
const stem = ((_a = normalizedCandidate.split("/").pop()) == null ? void 0 : _a.replace(/\.[^.]+$/, "")) || normalizedCandidate;
|
|
3073
|
+
return stem.length >= 3 && normalizedTest.includes(stem);
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
|
|
2378
3077
|
// src/changeTourContext.ts
|
|
2379
3078
|
var DEFAULT_MAX_PATCH_BYTES = 128 * 1024;
|
|
2380
3079
|
var DEFAULT_MAX_TOTAL_PATCH_BYTES = 2 * 1024 * 1024;
|
|
@@ -2393,8 +3092,21 @@ var CHAPTERS = {
|
|
|
2393
3092
|
packaging: { id: "packaging", title: "Packaging and dependencies", priority: 4 }
|
|
2394
3093
|
};
|
|
2395
3094
|
function buildChangeTourManifest(startPath, options = {}) {
|
|
2396
|
-
var _a, _b, _c, _d
|
|
2397
|
-
const
|
|
3095
|
+
var _a, _b, _c, _d;
|
|
3096
|
+
const source = options.source ? parseChangeTourSource(options.source) : void 0;
|
|
3097
|
+
const range = resolveBranchReviewRange(
|
|
3098
|
+
startPath,
|
|
3099
|
+
options.headRef ?? ((_a = source == null ? void 0 : source.range) == null ? void 0 : _a.head),
|
|
3100
|
+
options.baseRef ?? ((_b = source == null ? void 0 : source.range) == null ? void 0 : _b.base)
|
|
3101
|
+
);
|
|
3102
|
+
if (source == null ? void 0 : source.review) {
|
|
3103
|
+
validateChangeTourReview(source.review, {
|
|
3104
|
+
expectedRange: {
|
|
3105
|
+
baseOid: range.mergeBaseOid,
|
|
3106
|
+
headOid: range.headOid
|
|
3107
|
+
}
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
2398
3110
|
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_TOUR_FILE_BYTES;
|
|
2399
3111
|
const maxLineBytes = options.maxLineBytes ?? DEFAULT_MAX_TOUR_LINE_BYTES;
|
|
2400
3112
|
const omittedFiles = [];
|
|
@@ -2402,8 +3114,9 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2402
3114
|
const sceneRecords = [];
|
|
2403
3115
|
let totalAdditions = 0;
|
|
2404
3116
|
let totalDeletions = 0;
|
|
3117
|
+
let totalBinaryFiles = 0;
|
|
2405
3118
|
for (const changedPath of range.changedPaths) {
|
|
2406
|
-
const { additions, deletions } = readGitLineStats(
|
|
3119
|
+
const { additions, deletions, binary } = readGitLineStats(
|
|
2407
3120
|
range.repoRoot,
|
|
2408
3121
|
range.mergeBaseOid,
|
|
2409
3122
|
range.headOid,
|
|
@@ -2412,6 +3125,7 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2412
3125
|
);
|
|
2413
3126
|
totalAdditions += additions;
|
|
2414
3127
|
totalDeletions += deletions;
|
|
3128
|
+
if (binary) totalBinaryFiles += 1;
|
|
2415
3129
|
const pair = resolveReviewPathPair(range.changedPaths, changedPath.path);
|
|
2416
3130
|
if (!pair) {
|
|
2417
3131
|
omittedFiles.push(changedPath.path);
|
|
@@ -2452,21 +3166,50 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2452
3166
|
}
|
|
2453
3167
|
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
2454
3168
|
sceneRecords.sort((left, right) => left.chapter.priority - right.chapter.priority || left.scene.path.localeCompare(right.scene.path));
|
|
2455
|
-
|
|
2456
|
-
if (options.story &&
|
|
3169
|
+
let defaultScenes = sceneRecords.map(({ scene }, index) => ({ ...scene, id: `file-${index + 1}` }));
|
|
3170
|
+
if (options.story && source) {
|
|
2457
3171
|
throw new Error("A change tour can use either a legacy story or a source file, not both.");
|
|
2458
3172
|
}
|
|
2459
|
-
const
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
3173
|
+
const attention = buildChangeAttention(files.map((file) => ({
|
|
3174
|
+
path: file.path,
|
|
3175
|
+
previousPath: file.previousPath,
|
|
3176
|
+
changeKind: file.changeKind,
|
|
3177
|
+
additions: file.additions,
|
|
3178
|
+
deletions: file.deletions,
|
|
3179
|
+
binary: file.kind === "omitted",
|
|
3180
|
+
headText: file.kind === "text-diff" ? file.rightContent : void 0,
|
|
3181
|
+
symbolHints: file.kind === "text-diff" ? findDeclarationNames(file.rightContent) : []
|
|
3182
|
+
})));
|
|
3183
|
+
if (!options.story && !options.source && attention.entryPath) {
|
|
3184
|
+
const focusRank = (scenePath) => {
|
|
3185
|
+
const index = attention.focusPaths.indexOf(scenePath);
|
|
3186
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
3187
|
+
};
|
|
3188
|
+
defaultScenes = [...defaultScenes].sort((left, right) => Number(right.path === attention.entryPath) - Number(left.path === attention.entryPath) || focusRank(left.path) - focusRank(right.path) || left.id.localeCompare(right.id));
|
|
3189
|
+
}
|
|
3190
|
+
const authored = source ? applySource(source, defaultScenes, range.repoRoot) : options.story ? applyStory(options.story, defaultScenes) : buildGeneratedTourLanding(
|
|
3191
|
+
defaultScenes,
|
|
3192
|
+
buildChapters(sceneRecords, new Map(defaultScenes.map((scene) => [scene.path, scene.id]))),
|
|
3193
|
+
attention,
|
|
3194
|
+
{
|
|
3195
|
+
changedFiles: range.changedPaths.length,
|
|
3196
|
+
binaryFiles: totalBinaryFiles,
|
|
3197
|
+
additions: totalAdditions,
|
|
3198
|
+
deletions: totalDeletions,
|
|
3199
|
+
commitCount: range.commits.length
|
|
3200
|
+
}
|
|
3201
|
+
);
|
|
2463
3202
|
const scenes = authored.scenes;
|
|
2464
3203
|
const chapters = authored.chapters;
|
|
3204
|
+
const finalScenes = "finalScenes" in authored ? authored.finalScenes : scenes;
|
|
3205
|
+
const finalChapters = "finalChapters" in authored ? authored.finalChapters : chapters;
|
|
2465
3206
|
const manifest = {
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
3207
|
+
// Only authored v2+ files opt into repository-bound zoom modes. Generated
|
|
3208
|
+
// and legacy/story tours retain the portable v1 manifest contract.
|
|
3209
|
+
version: (source == null ? void 0 : source.version) ?? 1,
|
|
3210
|
+
title: (source == null ? void 0 : source.title) || ((_c = options.story) == null ? void 0 : _c.title) || options.title || `${range.headRef} against ${range.baseRef}`,
|
|
3211
|
+
windowTitle: source == null ? void 0 : source.windowTitle,
|
|
3212
|
+
sourceUrl: (source == null ? void 0 : source.sourceUrl) || ((_d = options.story) == null ? void 0 : _d.sourceUrl) || options.sourceUrl,
|
|
2470
3213
|
generatedAt: options.generatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2471
3214
|
range: {
|
|
2472
3215
|
baseRef: range.baseRef,
|
|
@@ -2485,11 +3228,68 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2485
3228
|
commits: range.commits,
|
|
2486
3229
|
files,
|
|
2487
3230
|
chapters,
|
|
2488
|
-
scenes
|
|
3231
|
+
scenes,
|
|
3232
|
+
...(source == null ? void 0 : source.review) ? { review: source.review } : {}
|
|
2489
3233
|
};
|
|
3234
|
+
const authoredModeTours = (source == null ? void 0 : source.version) === 3 ? buildAuthoredModeTours(
|
|
3235
|
+
source,
|
|
3236
|
+
authored,
|
|
3237
|
+
defaultScenes,
|
|
3238
|
+
range.repoRoot,
|
|
3239
|
+
range.mergeBaseOid,
|
|
3240
|
+
range.headOid
|
|
3241
|
+
) : void 0;
|
|
3242
|
+
if (authoredModeTours) manifest.tours = authoredModeTours;
|
|
3243
|
+
if (manifest.version >= 2) {
|
|
3244
|
+
manifest.repository = { root: range.repoRoot };
|
|
3245
|
+
const revisions = [];
|
|
3246
|
+
for (const scene of scenes) {
|
|
3247
|
+
let realStack;
|
|
3248
|
+
if (scene.kind === "stacked-diff") realStack = scene;
|
|
3249
|
+
if (scene.kind === "deconstructed-diff") {
|
|
3250
|
+
const authoredScene = source == null ? void 0 : source.chapters.flatMap((chapter) => chapter.scenes).find((candidate) => candidate.id === scene.id);
|
|
3251
|
+
if ((authoredScene == null ? void 0 : authoredScene.kind) !== "deconstructed-diff" || !authoredScene.stack) {
|
|
3252
|
+
throw new Error(`Deconstructed scene ${scene.id} requires an explicit real revision stack in v2+.`);
|
|
3253
|
+
}
|
|
3254
|
+
realStack = buildStackedScene(range.repoRoot, {
|
|
3255
|
+
...authoredScene,
|
|
3256
|
+
kind: "stacked-diff",
|
|
3257
|
+
overview: compileSceneOverview(authoredScene.overview, false),
|
|
3258
|
+
stack: authoredScene.stack,
|
|
3259
|
+
files: scene.files.map((file) => file.path),
|
|
3260
|
+
steps: []
|
|
3261
|
+
}, files.map((file) => file.path));
|
|
3262
|
+
if (realStack.stack[0].oid !== scene.realRange.baseOid || realStack.stack[realStack.stack.length - 1].oid !== scene.realRange.targetOid) {
|
|
3263
|
+
throw new Error(`Deconstructed scene ${scene.id} real stack endpoints must match its explanation range.`);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
if (realStack) {
|
|
3267
|
+
if (realStack.stack[0].oid !== range.mergeBaseOid || realStack.stack[realStack.stack.length - 1].oid !== range.headOid) {
|
|
3268
|
+
throw new Error(`Scene ${scene.id} real stack endpoints must match the tour's final diff range.`);
|
|
3269
|
+
}
|
|
3270
|
+
revisions.push(realStack);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
const explanation = scenes.some((scene) => scene.kind === "deconstructed-diff");
|
|
3274
|
+
const distinctRevisions = revisions.some((scene) => scene.stack.length > 2);
|
|
3275
|
+
manifest.zoom = {
|
|
3276
|
+
authoredDepth: explanation ? "explanation" : distinctRevisions ? "revisions" : "final",
|
|
3277
|
+
modes: [
|
|
3278
|
+
...explanation ? ["explanation"] : [],
|
|
3279
|
+
...distinctRevisions ? ["revisions"] : [],
|
|
3280
|
+
"final",
|
|
3281
|
+
"history"
|
|
3282
|
+
],
|
|
3283
|
+
revisions,
|
|
3284
|
+
final: { scenes: finalScenes, chapters: finalChapters }
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
2490
3287
|
return parseChangeTourManifest(manifest);
|
|
2491
3288
|
}
|
|
2492
3289
|
function applySource(source, defaultScenes, repoRoot) {
|
|
3290
|
+
return applySourceChapters(source, source.chapters, defaultScenes, repoRoot);
|
|
3291
|
+
}
|
|
3292
|
+
function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot) {
|
|
2493
3293
|
var _a, _b;
|
|
2494
3294
|
const available = new Map(defaultScenes.map((scene) => [scene.path, scene]));
|
|
2495
3295
|
const resolvedAnchors = /* @__PURE__ */ new Map();
|
|
@@ -2509,8 +3309,11 @@ function applySource(source, defaultScenes, repoRoot) {
|
|
|
2509
3309
|
}]));
|
|
2510
3310
|
const scenes = [];
|
|
2511
3311
|
const chapters = [];
|
|
2512
|
-
|
|
3312
|
+
const finalScenes = [];
|
|
3313
|
+
const finalChapters = [];
|
|
3314
|
+
for (const chapter of chaptersToCompile) {
|
|
2513
3315
|
const sceneIds = [];
|
|
3316
|
+
const finalSceneIds = [];
|
|
2514
3317
|
for (const authoredScene of chapter.scenes) {
|
|
2515
3318
|
if (authoredScene.kind === "deconstructed-diff") {
|
|
2516
3319
|
const deconstructedScene = buildDeconstructedManifestScene(
|
|
@@ -2522,6 +3325,16 @@ function applySource(source, defaultScenes, repoRoot) {
|
|
|
2522
3325
|
);
|
|
2523
3326
|
scenes.push(deconstructedScene);
|
|
2524
3327
|
sceneIds.push(deconstructedScene.id);
|
|
3328
|
+
if (authoredScene.steps) {
|
|
3329
|
+
const finalScene = buildWalkthroughManifestScene(
|
|
3330
|
+
{ ...authoredScene, steps: authoredScene.steps },
|
|
3331
|
+
available,
|
|
3332
|
+
resolvedAnchors,
|
|
3333
|
+
connections
|
|
3334
|
+
);
|
|
3335
|
+
finalScenes.push(finalScene);
|
|
3336
|
+
finalSceneIds.push(finalScene.id);
|
|
3337
|
+
}
|
|
2525
3338
|
continue;
|
|
2526
3339
|
}
|
|
2527
3340
|
if (authoredScene.kind === "stacked-diff") {
|
|
@@ -2532,37 +3345,105 @@ function applySource(source, defaultScenes, repoRoot) {
|
|
|
2532
3345
|
);
|
|
2533
3346
|
scenes.push(stackedScene);
|
|
2534
3347
|
sceneIds.push(stackedScene.id);
|
|
3348
|
+
for (const [index, file] of stackedScene.files.entries()) {
|
|
3349
|
+
const endpoint = available.get(file.path);
|
|
3350
|
+
if (!endpoint) continue;
|
|
3351
|
+
const finalScene = { ...endpoint, id: `${stackedScene.id}-final-${index + 1}`, title: file.path };
|
|
3352
|
+
finalScenes.push(finalScene);
|
|
3353
|
+
finalSceneIds.push(finalScene.id);
|
|
3354
|
+
}
|
|
2535
3355
|
continue;
|
|
2536
3356
|
}
|
|
2537
|
-
const scene =
|
|
2538
|
-
id: authoredScene.id,
|
|
2539
|
-
kind: "walkthrough",
|
|
2540
|
-
title: authoredScene.title,
|
|
2541
|
-
summary: authoredScene.summary,
|
|
2542
|
-
bullets: authoredScene.bullets,
|
|
2543
|
-
tags: authoredScene.tags,
|
|
2544
|
-
takeaway: authoredScene.takeaway,
|
|
2545
|
-
steps: authoredScene.steps.map((step) => {
|
|
2546
|
-
const focus = requireResolvedAnchor(resolvedAnchors, step.focus);
|
|
2547
|
-
const diff2 = available.get(focus.path);
|
|
2548
|
-
if (!diff2) throw new Error(`Step ${step.id} has no text diff for ${focus.path}.`);
|
|
2549
|
-
return {
|
|
2550
|
-
id: step.id,
|
|
2551
|
-
title: step.title,
|
|
2552
|
-
body: step.body,
|
|
2553
|
-
depth: step.depth,
|
|
2554
|
-
focus,
|
|
2555
|
-
connection: step.connection ? connections.get(step.connection) : void 0,
|
|
2556
|
-
diff: { ...diff2, id: `${authoredScene.id}-${step.id}` }
|
|
2557
|
-
};
|
|
2558
|
-
})
|
|
2559
|
-
};
|
|
3357
|
+
const scene = buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections);
|
|
2560
3358
|
scenes.push(scene);
|
|
2561
3359
|
sceneIds.push(scene.id);
|
|
3360
|
+
finalScenes.push(scene);
|
|
3361
|
+
finalSceneIds.push(scene.id);
|
|
2562
3362
|
}
|
|
2563
3363
|
chapters.push({ id: chapter.id, title: chapter.title, sceneIds });
|
|
3364
|
+
finalChapters.push({ id: chapter.id, title: chapter.title, sceneIds: finalSceneIds });
|
|
2564
3365
|
}
|
|
2565
|
-
return { scenes, chapters };
|
|
3366
|
+
return { scenes, chapters, finalScenes, finalChapters };
|
|
3367
|
+
}
|
|
3368
|
+
function buildAuthoredModeTours(source, root, defaultScenes, repoRoot, mergeBaseOid, headOid) {
|
|
3369
|
+
var _a, _b;
|
|
3370
|
+
const tours = {};
|
|
3371
|
+
if ((_a = source.tours) == null ? void 0 : _a.historical) {
|
|
3372
|
+
const compiled = applySourceChapters(source, source.tours.historical.chapters, defaultScenes, repoRoot);
|
|
3373
|
+
validateHistoricalStackEndpoints(compiled.scenes, mergeBaseOid, headOid);
|
|
3374
|
+
tours.historical = { chapters: compiled.chapters, scenes: compiled.scenes };
|
|
3375
|
+
} else if (source.version === 3) {
|
|
3376
|
+
const fallback = buildHistoricalFallback(source.chapters, root);
|
|
3377
|
+
if (fallback) tours.historical = fallback;
|
|
3378
|
+
}
|
|
3379
|
+
if ((_b = source.tours) == null ? void 0 : _b.deconstructed) {
|
|
3380
|
+
const compiled = applySourceChapters(source, source.tours.deconstructed.chapters, defaultScenes, repoRoot);
|
|
3381
|
+
tours.deconstructed = { chapters: compiled.chapters, scenes: compiled.scenes };
|
|
3382
|
+
} else if (source.version === 3 && source.chapters.some((chapter) => chapter.scenes.some((scene) => scene.kind === "deconstructed-diff"))) {
|
|
3383
|
+
tours.deconstructed = { chapters: root.chapters, scenes: root.scenes };
|
|
3384
|
+
}
|
|
3385
|
+
return tours.historical || tours.deconstructed ? tours : void 0;
|
|
3386
|
+
}
|
|
3387
|
+
function validateHistoricalStackEndpoints(scenes, mergeBaseOid, headOid) {
|
|
3388
|
+
for (const scene of scenes) {
|
|
3389
|
+
if (scene.kind !== "stacked-diff") continue;
|
|
3390
|
+
if (scene.stack[0].oid !== mergeBaseOid || scene.stack[scene.stack.length - 1].oid !== headOid) {
|
|
3391
|
+
throw new Error(`Historical scene ${scene.id} real stack endpoints must match the tour's final diff range.`);
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
function buildHistoricalFallback(sourceChapters, root) {
|
|
3396
|
+
const regularScenes = new Map(root.scenes.map((scene) => [scene.id, scene]));
|
|
3397
|
+
const finalScenes = new Map(root.finalScenes.map((scene) => [scene.id, scene]));
|
|
3398
|
+
const scenes = [];
|
|
3399
|
+
const chapters = [];
|
|
3400
|
+
for (const chapter of sourceChapters) {
|
|
3401
|
+
const sceneIds = [];
|
|
3402
|
+
for (const authoredScene of chapter.scenes) {
|
|
3403
|
+
const scene = authoredScene.kind === "deconstructed-diff" ? authoredScene.steps ? finalScenes.get(authoredScene.id) : void 0 : regularScenes.get(authoredScene.id);
|
|
3404
|
+
if (!scene) continue;
|
|
3405
|
+
scenes.push(scene);
|
|
3406
|
+
sceneIds.push(scene.id);
|
|
3407
|
+
}
|
|
3408
|
+
if (sceneIds.length > 0) chapters.push({ id: chapter.id, title: chapter.title, sceneIds });
|
|
3409
|
+
}
|
|
3410
|
+
return scenes.length > 0 ? { chapters, scenes } : void 0;
|
|
3411
|
+
}
|
|
3412
|
+
function compileSceneOverview(overview, includeComparison = true) {
|
|
3413
|
+
if (!overview) return void 0;
|
|
3414
|
+
return {
|
|
3415
|
+
kind: overview.kind,
|
|
3416
|
+
...overview.path === void 0 ? {} : { path: overview.path },
|
|
3417
|
+
...includeComparison && overview.comparison ? { comparison: { ...overview.comparison } } : {}
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3420
|
+
function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections) {
|
|
3421
|
+
const overview = compileSceneOverview(authoredScene.overview, false);
|
|
3422
|
+
return {
|
|
3423
|
+
id: authoredScene.id,
|
|
3424
|
+
kind: "walkthrough",
|
|
3425
|
+
title: authoredScene.title,
|
|
3426
|
+
summary: authoredScene.summary,
|
|
3427
|
+
bullets: authoredScene.bullets,
|
|
3428
|
+
tags: authoredScene.tags,
|
|
3429
|
+
takeaway: authoredScene.takeaway,
|
|
3430
|
+
...overview ? { overview } : {},
|
|
3431
|
+
steps: authoredScene.steps.map((step) => {
|
|
3432
|
+
const focus = requireResolvedAnchor(resolvedAnchors, step.focus);
|
|
3433
|
+
const diff2 = available.get(focus.path);
|
|
3434
|
+
if (!diff2) throw new Error(`Step ${step.id} has no text diff for ${focus.path}.`);
|
|
3435
|
+
return {
|
|
3436
|
+
id: step.id,
|
|
3437
|
+
title: step.title,
|
|
3438
|
+
body: step.body,
|
|
3439
|
+
depth: step.depth,
|
|
3440
|
+
requirement: step.requirement,
|
|
3441
|
+
focus,
|
|
3442
|
+
connection: step.connection ? connections.get(step.connection) : void 0,
|
|
3443
|
+
diff: { ...diff2, id: `${authoredScene.id}-${step.id}` }
|
|
3444
|
+
};
|
|
3445
|
+
})
|
|
3446
|
+
};
|
|
2566
3447
|
}
|
|
2567
3448
|
function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, availableFilePaths = []) {
|
|
2568
3449
|
const compiled = buildDeconstructedScene(repoRoot, source, { baseRef, headRef: targetRef });
|
|
@@ -2621,6 +3502,7 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, a
|
|
|
2621
3502
|
});
|
|
2622
3503
|
if (files.length === 0) throw new Error(`Deconstructed scene ${source.id} has no staged files.`);
|
|
2623
3504
|
const steps = buildDeconstructedFocusSteps(source.id, compiled.stages, files);
|
|
3505
|
+
const overview = compileSceneOverview(source.overview);
|
|
2624
3506
|
return {
|
|
2625
3507
|
id: source.id,
|
|
2626
3508
|
kind: "deconstructed-diff",
|
|
@@ -2629,6 +3511,7 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, a
|
|
|
2629
3511
|
bullets: source.bullets,
|
|
2630
3512
|
tags: source.tags,
|
|
2631
3513
|
takeaway: source.takeaway,
|
|
3514
|
+
...overview ? { overview } : {},
|
|
2632
3515
|
stageLabel: "Explanation stages",
|
|
2633
3516
|
realRange: {
|
|
2634
3517
|
baseRef: baseRef || compiled.baseOid,
|
|
@@ -2728,7 +3611,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2728
3611
|
let totalContentBytes = 0;
|
|
2729
3612
|
const files = filePaths.flatMap((filePath) => {
|
|
2730
3613
|
const aliases = /* @__PURE__ */ new Set([filePath]);
|
|
2731
|
-
for (const changes of adjacentChanges) {
|
|
3614
|
+
for (const changes of [...adjacentChanges].reverse()) {
|
|
2732
3615
|
for (const change of changes) {
|
|
2733
3616
|
if (aliases.has(change.path) || change.previousPath && aliases.has(change.previousPath)) {
|
|
2734
3617
|
aliases.add(change.path);
|
|
@@ -2739,7 +3622,9 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2739
3622
|
let fileContentBytes = 0;
|
|
2740
3623
|
const panels = [];
|
|
2741
3624
|
for (const entry of stack) {
|
|
2742
|
-
const
|
|
3625
|
+
const candidates = [...aliases].filter((candidate) => gitBlobExists(repoRoot, entry.oid, candidate));
|
|
3626
|
+
if (candidates.length > 1) throw new Error(`Ambiguous revision paths for ${filePath} at ${entry.ref}: ${candidates.join(", ")}`);
|
|
3627
|
+
const resolvedPath = candidates[0];
|
|
2743
3628
|
if (!resolvedPath) {
|
|
2744
3629
|
panels.push({ id: entry.id, label: `${entry.label} / ${filePath} (missing)`, content: "", exists: false });
|
|
2745
3630
|
continue;
|
|
@@ -2766,7 +3651,9 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2766
3651
|
}
|
|
2767
3652
|
return [{ path: filePath, panels }];
|
|
2768
3653
|
});
|
|
3654
|
+
if (files.length === 0) throw new Error(`Stacked scene ${source.id} has no materializable text files.`);
|
|
2769
3655
|
const stackIds = stack.map((entry) => entry.id);
|
|
3656
|
+
const overview = compileSceneOverview(source.overview);
|
|
2770
3657
|
return {
|
|
2771
3658
|
id: source.id,
|
|
2772
3659
|
kind: "stacked-diff",
|
|
@@ -2775,9 +3662,17 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2775
3662
|
bullets: source.bullets,
|
|
2776
3663
|
tags: source.tags,
|
|
2777
3664
|
takeaway: source.takeaway,
|
|
3665
|
+
...overview ? { overview } : {},
|
|
2778
3666
|
stack,
|
|
2779
3667
|
files,
|
|
2780
|
-
steps: source.steps.map((
|
|
3668
|
+
steps: source.steps.length === 0 ? stack.slice(0, -1).map((_, pairIndex) => ({
|
|
3669
|
+
id: `${source.id}-revision-${pairIndex}`,
|
|
3670
|
+
title: "",
|
|
3671
|
+
body: "",
|
|
3672
|
+
file: files[0].path,
|
|
3673
|
+
pairIndex,
|
|
3674
|
+
side: "right"
|
|
3675
|
+
})) : source.steps.map((step) => {
|
|
2781
3676
|
var _a, _b;
|
|
2782
3677
|
return {
|
|
2783
3678
|
id: step.id,
|
|
@@ -2889,21 +3784,51 @@ function buildChapters(records, sceneIdByPath) {
|
|
|
2889
3784
|
}
|
|
2890
3785
|
function chapterForPath(filePath) {
|
|
2891
3786
|
const lower = filePath.toLowerCase();
|
|
2892
|
-
const
|
|
2893
|
-
if (
|
|
3787
|
+
const role = classifyChangeFileRole(filePath);
|
|
3788
|
+
if (role === "documentation" || lower.includes("architecture")) {
|
|
2894
3789
|
return CHAPTERS.context;
|
|
2895
3790
|
}
|
|
2896
|
-
if (
|
|
3791
|
+
if (role === "test") {
|
|
2897
3792
|
return CHAPTERS.proof;
|
|
2898
3793
|
}
|
|
2899
3794
|
if (/(^|\/)(models?|schema|types?|contracts?)\.[^/]+$/.test(lower)) {
|
|
2900
3795
|
return CHAPTERS.contracts;
|
|
2901
3796
|
}
|
|
2902
|
-
if (
|
|
3797
|
+
if (role === "dependency" || role === "generated") {
|
|
2903
3798
|
return CHAPTERS.packaging;
|
|
2904
3799
|
}
|
|
2905
3800
|
return CHAPTERS.behavior;
|
|
2906
3801
|
}
|
|
3802
|
+
function buildGeneratedTourLanding(scenes, chapters, attention, summary) {
|
|
3803
|
+
const entry = attention.entryPath ? `Start here: ${attention.entryPath} \u2014 ${attention.entryReason}` : `No single entry-point \u2014 ${attention.entryReason}`;
|
|
3804
|
+
const focus = attention.focusPaths.length > 0 ? `Pay attention: ${attention.focusPaths.join(", ")}` : "Pay attention: no additional production files were identified.";
|
|
3805
|
+
const skipped = attention.backgroundPaths.length + attention.mechanicalPaths.length;
|
|
3806
|
+
const overview = {
|
|
3807
|
+
id: "change-overview",
|
|
3808
|
+
kind: "discussion",
|
|
3809
|
+
title: "Change overview",
|
|
3810
|
+
summary: `${summary.changedFiles} files (${summary.changedFiles - summary.binaryFiles} text, ${summary.binaryFiles} binary) \xB7 +${summary.additions} \u2212${summary.deletions} \xB7 ${summary.commitCount} commits`,
|
|
3811
|
+
bullets: [entry, focus, `Usually skip on a first pass: ${skipped} file${skipped === 1 ? "" : "s"}. All files remain available.`],
|
|
3812
|
+
tags: ["start here", "first pass"],
|
|
3813
|
+
takeaway: attention.entryPath ? `Next: open ${attention.entryPath}.` : "Next: open the first available textual change."
|
|
3814
|
+
};
|
|
3815
|
+
return {
|
|
3816
|
+
scenes: [overview, ...scenes],
|
|
3817
|
+
chapters: [
|
|
3818
|
+
{ id: "start-here", title: "Start here", sceneIds: [overview.id] },
|
|
3819
|
+
...chapters
|
|
3820
|
+
]
|
|
3821
|
+
};
|
|
3822
|
+
}
|
|
3823
|
+
function findDeclarationNames(content) {
|
|
3824
|
+
const names = /* @__PURE__ */ new Set();
|
|
3825
|
+
for (const line of content.split("\n")) {
|
|
3826
|
+
const match = line.match(/^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|interface|type|enum|const|let|var)\s+([A-Za-z_$][\w$]*)/) || line.match(/^\s*(?:async\s+)?(?:class|def)\s+([A-Za-z_]\w*)/);
|
|
3827
|
+
if (match) names.add(match[1]);
|
|
3828
|
+
if (names.size >= 25) break;
|
|
3829
|
+
}
|
|
3830
|
+
return [...names];
|
|
3831
|
+
}
|
|
2907
3832
|
function buildSceneNote(kind, additions, deletions, previousPath) {
|
|
2908
3833
|
const label = formatChangeKind(kind);
|
|
2909
3834
|
const rename = previousPath ? ` from ${previousPath}` : "";
|
|
@@ -2966,19 +3891,31 @@ function readGitLineStats(repoRoot, baseOid, headOid, filePath, previousPath) {
|
|
|
2966
3891
|
const [additionsText = "0", deletionsText = "0"] = output.trim().split(/\s+/, 3);
|
|
2967
3892
|
return {
|
|
2968
3893
|
additions: Number.parseInt(additionsText, 10) || 0,
|
|
2969
|
-
deletions: Number.parseInt(deletionsText, 10) || 0
|
|
3894
|
+
deletions: Number.parseInt(deletionsText, 10) || 0,
|
|
3895
|
+
binary: additionsText === "-" || deletionsText === "-"
|
|
2970
3896
|
};
|
|
2971
3897
|
}
|
|
2972
3898
|
|
|
2973
3899
|
// src/tourCoverage.ts
|
|
2974
|
-
var TOUR_COVERAGE_REPORT_VERSION =
|
|
3900
|
+
var TOUR_COVERAGE_REPORT_VERSION = 2;
|
|
3901
|
+
function buildTourAuthoringCoverage(report) {
|
|
3902
|
+
return {
|
|
3903
|
+
walkthrough: {
|
|
3904
|
+
scope: report.version === 2 ? "final" : "tour",
|
|
3905
|
+
coveredUnits: report.totals.coveredUnits,
|
|
3906
|
+
includedUnits: report.totals.includedUnits,
|
|
3907
|
+
coveragePercent: report.totals.coveragePercent
|
|
3908
|
+
},
|
|
3909
|
+
explanationAssignments: report.version === 2 ? report.explanationAssignments : []
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
2975
3912
|
var DEPTH_ORDER = {
|
|
2976
3913
|
mentioned: 0,
|
|
2977
3914
|
explained: 1,
|
|
2978
3915
|
contextualized: 2
|
|
2979
3916
|
};
|
|
2980
3917
|
function buildTourCoverageReport(startPath, sourceValue) {
|
|
2981
|
-
var _a, _b, _c, _d, _e;
|
|
3918
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
2982
3919
|
const source = parseChangeTourSource(sourceValue);
|
|
2983
3920
|
const inventory = buildChangeInventory(startPath, {
|
|
2984
3921
|
headRef: (_a = source.range) == null ? void 0 : _a.head,
|
|
@@ -2994,7 +3931,11 @@ function buildTourCoverageReport(startPath, sourceValue) {
|
|
|
2994
3931
|
const includedUnits = textualUnits.filter((unit) => !excludedIds.has(unit.id));
|
|
2995
3932
|
const coverage = /* @__PURE__ */ new Map();
|
|
2996
3933
|
const contextualOnlyEvidence = [];
|
|
2997
|
-
|
|
3934
|
+
const percentageDecimalPlaces = manifest.version >= 2 ? 2 : 0;
|
|
3935
|
+
const authoredHistoricalScenes = (_f = (_e = manifest.tours) == null ? void 0 : _e.historical) == null ? void 0 : _f.scenes;
|
|
3936
|
+
const historicalHasWalkthrough = (authoredHistoricalScenes == null ? void 0 : authoredHistoricalScenes.some((scene) => scene.kind === "walkthrough")) === true;
|
|
3937
|
+
const walkthroughScenes = manifest.version >= 2 ? requireFinalScenes(historicalHasWalkthrough ? authoredHistoricalScenes : (_g = manifest.zoom) == null ? void 0 : _g.final.scenes) : historicalHasWalkthrough ? authoredHistoricalScenes : manifest.scenes;
|
|
3938
|
+
for (const scene of walkthroughScenes) {
|
|
2998
3939
|
if (scene.kind !== "walkthrough") continue;
|
|
2999
3940
|
for (const step of scene.steps) {
|
|
3000
3941
|
const matches = includedUnits.filter((unit) => evidenceIntersectsUnit(step.focus, unit));
|
|
@@ -3015,11 +3956,10 @@ function buildTourCoverageReport(startPath, sourceValue) {
|
|
|
3015
3956
|
const highest = covered.depths.reduce((left, right) => DEPTH_ORDER[right] > DEPTH_ORDER[left] ? right : left);
|
|
3016
3957
|
depth[highest] += 1;
|
|
3017
3958
|
}
|
|
3018
|
-
const files = inventory.files.filter((file) => file.material === "text").map((file) => buildFileResult(file, excludedIds, coverage));
|
|
3959
|
+
const files = inventory.files.filter((file) => file.material === "text").map((file) => buildFileResult(file, excludedIds, coverage, percentageDecimalPlaces));
|
|
3019
3960
|
const includedChangedLines = includedUnits.reduce((total, unit) => total + unit.additions + unit.deletions, 0);
|
|
3020
3961
|
const coveredChangedLines = [...coverage.values()].reduce((total, entry) => total + entry.unit.additions + entry.unit.deletions, 0);
|
|
3021
|
-
|
|
3022
|
-
version: TOUR_COVERAGE_REPORT_VERSION,
|
|
3962
|
+
const report = {
|
|
3023
3963
|
range: {
|
|
3024
3964
|
baseRef: inventory.range.baseRef,
|
|
3025
3965
|
headRef: inventory.range.headRef,
|
|
@@ -3032,13 +3972,13 @@ function buildTourCoverageReport(startPath, sourceValue) {
|
|
|
3032
3972
|
includedUnits: includedUnits.length,
|
|
3033
3973
|
coveredUnits: coverage.size,
|
|
3034
3974
|
uncoveredUnits: includedUnits.length - coverage.size,
|
|
3035
|
-
coveragePercent: percentage(coverage.size, includedUnits.length),
|
|
3975
|
+
coveragePercent: percentage(coverage.size, includedUnits.length, percentageDecimalPlaces),
|
|
3036
3976
|
coveredChangedLines,
|
|
3037
3977
|
includedChangedLines
|
|
3038
3978
|
},
|
|
3039
3979
|
depth,
|
|
3040
3980
|
files,
|
|
3041
|
-
exclusions: (((
|
|
3981
|
+
exclusions: (((_h = source.coverage) == null ? void 0 : _h.exclusions) || []).map((exclusion) => {
|
|
3042
3982
|
var _a2;
|
|
3043
3983
|
return {
|
|
3044
3984
|
path: exclusion.path,
|
|
@@ -3049,6 +3989,36 @@ function buildTourCoverageReport(startPath, sourceValue) {
|
|
|
3049
3989
|
unsupported: inventory.files.filter((file) => file.material !== "text").map((file) => ({ path: file.path, material: file.material })),
|
|
3050
3990
|
contextualOnlyEvidence
|
|
3051
3991
|
};
|
|
3992
|
+
if (manifest.version === 1) return { version: 1, ...report };
|
|
3993
|
+
return {
|
|
3994
|
+
version: TOUR_COVERAGE_REPORT_VERSION,
|
|
3995
|
+
...report,
|
|
3996
|
+
walkthroughScope: "final",
|
|
3997
|
+
explanationAssignments: buildExplanationAssignments(source, inventory.files)
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
function requireFinalScenes(scenes) {
|
|
4001
|
+
if (!scenes) throw new Error("A v2 tour requires Final-mode scenes for coverage reporting.");
|
|
4002
|
+
return scenes;
|
|
4003
|
+
}
|
|
4004
|
+
function buildExplanationAssignments(source, files) {
|
|
4005
|
+
var _a, _b;
|
|
4006
|
+
const materializableFiles = files.filter((file) => file.material === "text" && file.changeKind !== "renamed" && file.baseContent !== void 0 && file.headContent !== void 0);
|
|
4007
|
+
const totalUnits = materializableFiles.reduce((total, file) => total + file.units.length, 0);
|
|
4008
|
+
const deconstructedChapters = ((_b = (_a = source.tours) == null ? void 0 : _a.deconstructed) == null ? void 0 : _b.chapters) || source.chapters;
|
|
4009
|
+
return deconstructedChapters.flatMap((chapter) => chapter.scenes).filter((scene) => scene.kind === "deconstructed-diff").map((scene) => {
|
|
4010
|
+
const assignedUnits = scene.stages.reduce((stageTotal, stage) => stageTotal + stage.changes.reduce((changeTotal, change) => changeTotal + change.hunks.length, 0), 0);
|
|
4011
|
+
const excludedUnits = totalUnits - assignedUnits;
|
|
4012
|
+
const completeUnits = assignedUnits + excludedUnits;
|
|
4013
|
+
return {
|
|
4014
|
+
sceneId: scene.id,
|
|
4015
|
+
assignedUnits,
|
|
4016
|
+
excludedUnits,
|
|
4017
|
+
completeUnits,
|
|
4018
|
+
totalUnits,
|
|
4019
|
+
assignmentPercent: percentage(completeUnits, totalUnits)
|
|
4020
|
+
};
|
|
4021
|
+
});
|
|
3052
4022
|
}
|
|
3053
4023
|
function resolveExclusions(source, files) {
|
|
3054
4024
|
var _a, _b, _c;
|
|
@@ -3075,7 +4045,7 @@ function evidenceIntersectsUnit(focus, unit) {
|
|
|
3075
4045
|
const end = start + count - 1;
|
|
3076
4046
|
return focus.startLine <= end && focus.endLine >= start;
|
|
3077
4047
|
}
|
|
3078
|
-
function buildFileResult(file, excludedIds, coverage) {
|
|
4048
|
+
function buildFileResult(file, excludedIds, coverage, percentageDecimalPlaces) {
|
|
3079
4049
|
const included = file.units.filter((unit) => !excludedIds.has(unit.id));
|
|
3080
4050
|
const covered = included.filter((unit) => coverage.has(unit.id));
|
|
3081
4051
|
return {
|
|
@@ -3083,16 +4053,19 @@ function buildFileResult(file, excludedIds, coverage) {
|
|
|
3083
4053
|
originalUnits: file.units.length,
|
|
3084
4054
|
includedUnits: included.length,
|
|
3085
4055
|
coveredUnits: covered.length,
|
|
3086
|
-
coveragePercent: percentage(covered.length, included.length),
|
|
4056
|
+
coveragePercent: percentage(covered.length, included.length, percentageDecimalPlaces),
|
|
3087
4057
|
uncoveredHunks: included.filter((unit) => !coverage.has(unit.id)).map((unit) => unit.id)
|
|
3088
4058
|
};
|
|
3089
4059
|
}
|
|
3090
|
-
function percentage(numerator, denominator) {
|
|
3091
|
-
|
|
4060
|
+
function percentage(numerator, denominator, decimalPlaces = 0) {
|
|
4061
|
+
if (denominator === 0) return 100;
|
|
4062
|
+
const precision = 10 ** decimalPlaces;
|
|
4063
|
+
return Math.round(numerator / denominator * 100 * precision) / precision;
|
|
3092
4064
|
}
|
|
3093
4065
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3094
4066
|
0 && (module.exports = {
|
|
3095
4067
|
TOUR_COVERAGE_REPORT_VERSION,
|
|
4068
|
+
buildTourAuthoringCoverage,
|
|
3096
4069
|
buildTourCoverageReport
|
|
3097
4070
|
});
|
|
3098
4071
|
//# sourceMappingURL=tourCoverage.js.map
|