@davmash/bygone 0.8.13 → 0.8.16
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 +16 -0
- package/README.md +2 -2
- package/cli/commandSpec.js +12 -2
- package/cli/completions.js +1 -1
- package/cli/present.js +42 -1
- package/cli/tour.js +13 -2
- package/cli/tourHistory.js +90 -0
- package/completions/_bygone +2 -1
- package/completions/bygone +1 -1
- package/completions/bygone.fish +1 -0
- package/docs/change-tour-format.md +46 -6
- package/docs/explore.md +36 -10
- package/docs/present.md +9 -1
- package/docs/product-surface.md +18 -14
- package/docs/releasing.md +25 -29
- package/media/diff.worker.js +3 -3
- package/media/webview.css +1 -1
- package/media/webview.js +173 -173
- package/out/changeTour.js +953 -410
- package/out/gitComparison.js +354 -0
- package/out/standalone-main.js +7678 -2124
- package/out/tourCoverage.js +992 -402
- package/package.json +1 -1
- package/schemas/change-tour-source.schema.json +58 -3
- package/skills/pr-tour-guide/SKILL.md +16 -7
- package/standalone/index.html +5 -0
- package/web/index.html +14 -0
- package/web/presenter.css +101 -0
- package/web/web-host.js +5 -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,330 @@ 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 = 2;
|
|
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 !== CHANGE_TOUR_SOURCE_VERSION) {
|
|
486
|
+
throw new Error("Unsupported or missing change-tour source version.");
|
|
487
|
+
}
|
|
488
|
+
requireOnlyKeys(value, ["version", "title", "windowTitle", "sourceUrl", "range", "anchors", "connections", "chapters", "coverage"], "source");
|
|
489
|
+
optionalString(value.title, "title");
|
|
490
|
+
optionalString(value.windowTitle, "windowTitle");
|
|
491
|
+
optionalString(value.sourceUrl, "sourceUrl");
|
|
492
|
+
if (value.range !== void 0) {
|
|
493
|
+
if (!isRecord(value.range)) throw new Error("range must be an object.");
|
|
494
|
+
requireOnlyKeys(value.range, ["base", "head"], "range");
|
|
495
|
+
requireString(value.range.base, "range.base");
|
|
496
|
+
requireString(value.range.head, "range.head");
|
|
497
|
+
}
|
|
498
|
+
if (value.coverage !== void 0) {
|
|
499
|
+
if (!isRecord(value.coverage) || !Array.isArray(value.coverage.exclusions)) {
|
|
500
|
+
throw new Error("coverage must contain an exclusions array.");
|
|
501
|
+
}
|
|
502
|
+
requireOnlyKeys(value.coverage, ["exclusions"], "coverage");
|
|
503
|
+
for (const [index, exclusion] of value.coverage.exclusions.entries()) {
|
|
504
|
+
const exclusionPath = `coverage.exclusions[${index}]`;
|
|
505
|
+
if (!isRecord(exclusion)) throw new Error(`${exclusionPath} must be an object.`);
|
|
506
|
+
requireOnlyKeys(exclusion, ["path", "hunks", "reason"], exclusionPath);
|
|
507
|
+
requireString(exclusion.path, `${exclusionPath}.path`);
|
|
508
|
+
requireString(exclusion.reason, `${exclusionPath}.reason`);
|
|
509
|
+
if (exclusion.hunks !== void 0) requireStringArray(exclusion.hunks, `${exclusionPath}.hunks`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (!isRecord(value.anchors)) {
|
|
513
|
+
throw new Error("anchors must be an object.");
|
|
514
|
+
}
|
|
515
|
+
for (const [id, anchor] of Object.entries(value.anchors)) {
|
|
516
|
+
if (!isRecord(anchor)) {
|
|
517
|
+
throw new Error(`anchors.${id} must be an object.`);
|
|
518
|
+
}
|
|
519
|
+
requireOnlyKeys(anchor, ["file", "revision", "contains", "occurrence"], `anchors.${id}`);
|
|
520
|
+
requireString(anchor.file, `anchors.${id}.file`);
|
|
521
|
+
if (anchor.revision !== "base" && anchor.revision !== "head") {
|
|
522
|
+
throw new Error(`anchors.${id}.revision must be base or head.`);
|
|
523
|
+
}
|
|
524
|
+
requireString(anchor.contains, `anchors.${id}.contains`);
|
|
525
|
+
if (anchor.occurrence !== void 0 && (!Number.isInteger(anchor.occurrence) || Number(anchor.occurrence) < 1)) {
|
|
526
|
+
throw new Error(`anchors.${id}.occurrence must be a positive integer.`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const rawConnections = Array.isArray(value.connections) ? value.connections : isRecord(value.connections) ? Object.entries(value.connections).map(([id, connection]) => isRecord(connection) ? { id, ...connection } : connection) : null;
|
|
530
|
+
if (!rawConnections || !Array.isArray(value.chapters) || value.chapters.length === 0) {
|
|
531
|
+
throw new Error("connections must be an array or object, and chapters must be a non-empty array.");
|
|
532
|
+
}
|
|
533
|
+
const connectionIds = /* @__PURE__ */ new Set();
|
|
534
|
+
for (const [index, connection] of rawConnections.entries()) {
|
|
535
|
+
if (!isRecord(connection)) {
|
|
536
|
+
throw new Error(`connections[${index}] must be an object.`);
|
|
537
|
+
}
|
|
538
|
+
requireOnlyKeys(connection, ["id", "from", "to", "label"], `connections[${index}]`);
|
|
539
|
+
requireString(connection.id, `connections[${index}].id`);
|
|
540
|
+
requireString(connection.from, `connections[${index}].from`);
|
|
541
|
+
requireString(connection.to, `connections[${index}].to`);
|
|
542
|
+
requireString(connection.label, `connections[${index}].label`);
|
|
543
|
+
const { id, from, to } = connection;
|
|
544
|
+
if (!value.anchors[from] || !value.anchors[to]) {
|
|
545
|
+
throw new Error(`Connection ${id} references an unknown anchor.`);
|
|
546
|
+
}
|
|
547
|
+
if (connectionIds.has(id)) {
|
|
548
|
+
throw new Error(`Duplicate connection id: ${id}`);
|
|
549
|
+
}
|
|
550
|
+
connectionIds.add(id);
|
|
551
|
+
}
|
|
552
|
+
const chapterIds = /* @__PURE__ */ new Set();
|
|
553
|
+
const sceneIds = /* @__PURE__ */ new Set();
|
|
554
|
+
for (const [chapterIndex, chapter] of value.chapters.entries()) {
|
|
555
|
+
if (!isRecord(chapter) || !Array.isArray(chapter.scenes) || chapter.scenes.length === 0) {
|
|
556
|
+
throw new Error(`chapters[${chapterIndex}] must contain a non-empty scenes array.`);
|
|
557
|
+
}
|
|
558
|
+
requireString(chapter.id, `chapters[${chapterIndex}].id`);
|
|
559
|
+
requireString(chapter.title, `chapters[${chapterIndex}].title`);
|
|
560
|
+
requireOnlyKeys(chapter, ["id", "title", "scenes"], `chapters[${chapterIndex}]`);
|
|
561
|
+
if (chapterIds.has(chapter.id)) throw new Error(`Duplicate chapter id: ${chapter.id}`);
|
|
562
|
+
chapterIds.add(chapter.id);
|
|
563
|
+
for (const [sceneIndex, scene] of chapter.scenes.entries()) {
|
|
564
|
+
const path = `chapters[${chapterIndex}].scenes[${sceneIndex}]`;
|
|
565
|
+
if (!isRecord(scene)) throw new Error(`${path} must be an object.`);
|
|
566
|
+
requireString(scene.id, `${path}.id`);
|
|
567
|
+
requireString(scene.title, `${path}.title`);
|
|
568
|
+
if (sceneIds.has(scene.id)) throw new Error(`Duplicate scene id: ${scene.id}`);
|
|
569
|
+
sceneIds.add(scene.id);
|
|
570
|
+
validateNarrative(scene, path);
|
|
571
|
+
if (scene.kind === "deconstructed-diff") {
|
|
572
|
+
validateDeconstructedScene(scene, path);
|
|
573
|
+
if (value.version === 2 || scene.stack !== void 0) validateRealStack(scene.stack, `${path}.stack`, 2);
|
|
574
|
+
if (value.version === 2 || scene.steps !== void 0) {
|
|
575
|
+
validateWalkthroughSteps(scene, path, value.anchors, connectionIds);
|
|
576
|
+
}
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
|
|
580
|
+
throw new Error(`${path} must contain a non-empty steps array.`);
|
|
581
|
+
}
|
|
582
|
+
if (scene.kind === "stacked-diff") {
|
|
583
|
+
validateStackedScene(scene, path, value.version === 2 ? 2 : 3);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (scene.kind !== void 0 && scene.kind !== "walkthrough") {
|
|
587
|
+
throw new Error(`${path}.kind must be walkthrough, stacked-diff, or deconstructed-diff.`);
|
|
588
|
+
}
|
|
589
|
+
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "steps"], path);
|
|
590
|
+
validateWalkthroughSteps(scene, path, value.anchors, connectionIds);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return { ...value, connections: rawConnections };
|
|
594
|
+
}
|
|
595
|
+
function validateDeconstructedScene(scene, path) {
|
|
596
|
+
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "base", "target", "stack", "steps", "stages", "exclusions"], path);
|
|
597
|
+
optionalString(scene.base, `${path}.base`);
|
|
598
|
+
optionalString(scene.target, `${path}.target`);
|
|
599
|
+
if (!Array.isArray(scene.stages) || scene.stages.length === 0 || scene.stages.length > 12) {
|
|
600
|
+
throw new Error(`${path}.stages must contain between 1 and 12 stages.`);
|
|
601
|
+
}
|
|
602
|
+
const stageIds = /* @__PURE__ */ new Set();
|
|
603
|
+
for (const [stageIndex, stage] of scene.stages.entries()) {
|
|
604
|
+
const stagePath = `${path}.stages[${stageIndex}]`;
|
|
605
|
+
if (!isRecord(stage)) throw new Error(`${stagePath} must be an object.`);
|
|
606
|
+
requireOnlyKeys(stage, ["id", "title", "narration", "changes"], stagePath);
|
|
607
|
+
requireString(stage.id, `${stagePath}.id`);
|
|
608
|
+
requireString(stage.title, `${stagePath}.title`);
|
|
609
|
+
requireString(stage.narration, `${stagePath}.narration`);
|
|
610
|
+
if (!Array.isArray(stage.changes) || stage.changes.length === 0) {
|
|
611
|
+
throw new Error(`${stagePath}.changes must be a non-empty array.`);
|
|
612
|
+
}
|
|
613
|
+
if (stageIds.has(stage.id)) throw new Error(`Duplicate deconstructed stage id: ${stage.id}`);
|
|
614
|
+
stageIds.add(stage.id);
|
|
615
|
+
stage.changes.forEach((change, changeIndex) => validateDeconstructedSelection(
|
|
616
|
+
change,
|
|
617
|
+
`${stagePath}.changes[${changeIndex}]`,
|
|
618
|
+
false
|
|
619
|
+
));
|
|
620
|
+
}
|
|
621
|
+
if (scene.exclusions !== void 0) {
|
|
622
|
+
if (!Array.isArray(scene.exclusions)) throw new Error(`${path}.exclusions must be an array.`);
|
|
623
|
+
scene.exclusions.forEach((exclusion, exclusionIndex) => validateDeconstructedSelection(
|
|
624
|
+
exclusion,
|
|
625
|
+
`${path}.exclusions[${exclusionIndex}]`,
|
|
626
|
+
true
|
|
627
|
+
));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function validateWalkthroughSteps(scene, path, anchors, connectionIds) {
|
|
631
|
+
if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
|
|
632
|
+
throw new Error(`${path} must contain a non-empty steps array.`);
|
|
633
|
+
}
|
|
634
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
635
|
+
for (const [stepIndex, step] of scene.steps.entries()) {
|
|
636
|
+
const stepPath = `${path}.steps[${stepIndex}]`;
|
|
637
|
+
if (!isRecord(step)) throw new Error(`${stepPath} must be an object.`);
|
|
638
|
+
requireString(step.id, `${stepPath}.id`);
|
|
639
|
+
requireString(step.title, `${stepPath}.title`);
|
|
640
|
+
requireString(step.body, `${stepPath}.body`);
|
|
641
|
+
requireString(step.focus, `${stepPath}.focus`);
|
|
642
|
+
requireOnlyKeys(step, ["id", "title", "body", "focus", "connection", "depth", "requirement"], stepPath);
|
|
643
|
+
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
644
|
+
throw new Error(`${stepPath}.depth must be mentioned, explained, or contextualized.`);
|
|
645
|
+
}
|
|
646
|
+
validateStepRequirement(step.requirement, `${stepPath}.requirement`);
|
|
647
|
+
if (stepIds.has(step.id)) throw new Error(`Duplicate step id in scene ${scene.id}: ${step.id}`);
|
|
648
|
+
stepIds.add(step.id);
|
|
649
|
+
if (!anchors[step.focus]) throw new Error(`${stepPath} references unknown anchor ${step.focus}.`);
|
|
650
|
+
optionalString(step.connection, `${stepPath}.connection`);
|
|
651
|
+
if (typeof step.connection === "string" && !connectionIds.has(step.connection)) {
|
|
652
|
+
throw new Error(`${stepPath} references unknown connection ${step.connection}.`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function validateDeconstructedSelection(value, path, requiresReason) {
|
|
657
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
658
|
+
requireOnlyKeys(value, requiresReason ? ["file", "hunks", "reason"] : ["file", "hunks"], path);
|
|
659
|
+
requireString(value.file, `${path}.file`);
|
|
660
|
+
if (!requiresReason || value.hunks !== void 0) {
|
|
661
|
+
requireStringArray(value.hunks, `${path}.hunks`);
|
|
662
|
+
if (value.hunks.length === 0) throw new Error(`${path}.hunks must not be empty.`);
|
|
663
|
+
}
|
|
664
|
+
if (requiresReason) requireString(value.reason, `${path}.reason`);
|
|
665
|
+
}
|
|
666
|
+
function validateRealStack(stack, path, minimum) {
|
|
667
|
+
if (!Array.isArray(stack) || stack.length < minimum || stack.length > 6) {
|
|
668
|
+
throw new Error(`${path} must contain between ${minimum} and 6 explicit real revisions.`);
|
|
669
|
+
}
|
|
670
|
+
const ids = /* @__PURE__ */ new Set();
|
|
671
|
+
for (const [index, entry] of stack.entries()) {
|
|
672
|
+
if (!isRecord(entry)) throw new Error(`${path}[${index}] must be an object.`);
|
|
673
|
+
requireOnlyKeys(entry, ["id", "ref", "label"], `${path}[${index}]`);
|
|
674
|
+
requireString(entry.id, `${path}[${index}].id`);
|
|
675
|
+
requireString(entry.ref, `${path}[${index}].ref`);
|
|
676
|
+
optionalString(entry.label, `${path}[${index}].label`);
|
|
677
|
+
if (ids.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
678
|
+
ids.add(entry.id);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function validateStackedScene(scene, path, minimum = 3) {
|
|
682
|
+
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "stack", "files", "steps"], path);
|
|
683
|
+
if (!Array.isArray(scene.stack) || scene.stack.length < minimum || scene.stack.length > 6) {
|
|
684
|
+
throw new Error(`${path}.stack must contain between ${minimum} and 6 revisions.`);
|
|
685
|
+
}
|
|
686
|
+
const stackIds = /* @__PURE__ */ new Set();
|
|
687
|
+
for (const [index, entry] of scene.stack.entries()) {
|
|
688
|
+
const entryPath = `${path}.stack[${index}]`;
|
|
689
|
+
if (!isRecord(entry)) throw new Error(`${entryPath} must be an object.`);
|
|
690
|
+
requireOnlyKeys(entry, ["id", "ref", "label"], entryPath);
|
|
691
|
+
requireString(entry.id, `${entryPath}.id`);
|
|
692
|
+
requireString(entry.ref, `${entryPath}.ref`);
|
|
693
|
+
optionalString(entry.label, `${entryPath}.label`);
|
|
694
|
+
if (stackIds.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
695
|
+
stackIds.add(entry.id);
|
|
696
|
+
}
|
|
697
|
+
if (scene.files !== void 0) requireStringArray(scene.files, `${path}.files`);
|
|
698
|
+
if (!Array.isArray(scene.steps) || scene.steps.length === 0) throw new Error(`${path}.steps must be a non-empty array.`);
|
|
699
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
700
|
+
for (const [index, step] of scene.steps.entries()) {
|
|
701
|
+
const stepPath = `${path}.steps[${index}]`;
|
|
702
|
+
if (!isRecord(step)) throw new Error(`${stepPath} must be an object.`);
|
|
703
|
+
requireOnlyKeys(step, ["id", "title", "body", "file", "pair", "side", "lines"], stepPath);
|
|
704
|
+
requireString(step.id, `${stepPath}.id`);
|
|
705
|
+
requireString(step.title, `${stepPath}.title`);
|
|
706
|
+
requireString(step.body, `${stepPath}.body`);
|
|
707
|
+
requireString(step.file, `${stepPath}.file`);
|
|
708
|
+
const stepId = step.id;
|
|
709
|
+
if (stepIds.has(stepId)) throw new Error(`Duplicate step id in scene ${scene.id}: ${stepId}`);
|
|
710
|
+
stepIds.add(stepId);
|
|
711
|
+
if (!Array.isArray(step.pair) || step.pair.length !== 2 || step.pair.some((id) => typeof id !== "string")) {
|
|
712
|
+
throw new Error(`${stepPath}.pair must contain two stack entry ids.`);
|
|
713
|
+
}
|
|
714
|
+
const pair = step.pair;
|
|
715
|
+
const leftIndex = scene.stack.findIndex((entry) => isRecord(entry) && entry.id === pair[0]);
|
|
716
|
+
const rightIndex = scene.stack.findIndex((entry) => isRecord(entry) && entry.id === pair[1]);
|
|
717
|
+
if (leftIndex < 0 || rightIndex !== leftIndex + 1) throw new Error(`${stepPath}.pair must reference adjacent stack entries in order.`);
|
|
718
|
+
if (step.side !== void 0 && step.side !== "left" && step.side !== "right") throw new Error(`${stepPath}.side must be left or right.`);
|
|
719
|
+
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]))) {
|
|
720
|
+
throw new Error(`${stepPath}.lines must contain an ordered positive line range.`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function validateStepRequirement(value, path) {
|
|
725
|
+
if (value === void 0) return;
|
|
726
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
727
|
+
requireOnlyKeys(value, ["id", "text", "status", "source", "confidence"], path);
|
|
728
|
+
requireString(value.id, `${path}.id`);
|
|
729
|
+
requireString(value.text, `${path}.text`);
|
|
730
|
+
if (value.status !== void 0 && !["fulfilled", "gap"].includes(String(value.status))) {
|
|
731
|
+
throw new Error(`${path}.status must be fulfilled or gap.`);
|
|
732
|
+
}
|
|
733
|
+
optionalString(value.source, `${path}.source`);
|
|
734
|
+
if (value.confidence !== void 0 && !["high", "medium", "low"].includes(String(value.confidence))) {
|
|
735
|
+
throw new Error(`${path}.confidence must be high, medium, or low.`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function validateNarrative(value, path) {
|
|
739
|
+
requireString(value.summary, `${path}.summary`);
|
|
740
|
+
requireStringArray(value.bullets, `${path}.bullets`);
|
|
741
|
+
requireStringArray(value.tags, `${path}.tags`);
|
|
742
|
+
requireString(value.takeaway, `${path}.takeaway`);
|
|
743
|
+
}
|
|
744
|
+
function isRecord(value) {
|
|
745
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
746
|
+
}
|
|
747
|
+
function requireString(value, path) {
|
|
748
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
749
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function optionalString(value, path) {
|
|
753
|
+
if (value !== void 0) {
|
|
754
|
+
requireString(value, path);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function requireStringArray(value, path) {
|
|
758
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
759
|
+
throw new Error(`${path} must be an array of strings.`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function requireOnlyKeys(value, allowed, path) {
|
|
763
|
+
const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
764
|
+
if (unexpected.length > 0) {
|
|
765
|
+
throw new Error(`${path} contains unknown field${unexpected.length === 1 ? "" : "s"}: ${unexpected.join(", ")}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
476
769
|
// src/changeTourManifest.ts
|
|
477
|
-
var CHANGE_TOUR_MANIFEST_VERSION =
|
|
770
|
+
var CHANGE_TOUR_MANIFEST_VERSION = 2;
|
|
478
771
|
function parseChangeTourManifest(value) {
|
|
479
|
-
if (
|
|
772
|
+
if (isRecord2(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_MANIFEST_VERSION) {
|
|
773
|
+
throw new Error(
|
|
774
|
+
`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.`
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
if (!isRecord2(value) || value.version !== 1 && value.version !== CHANGE_TOUR_MANIFEST_VERSION) {
|
|
480
778
|
throw new Error(`Unsupported or missing change-tour manifest version.`);
|
|
481
779
|
}
|
|
482
|
-
|
|
483
|
-
|
|
780
|
+
requireString2(value.title, "title");
|
|
781
|
+
requireString2(value.generatedAt, "generatedAt");
|
|
484
782
|
if (value.windowTitle !== void 0) {
|
|
485
|
-
|
|
783
|
+
requireString2(value.windowTitle, "windowTitle");
|
|
486
784
|
}
|
|
487
785
|
if (value.sourceUrl !== void 0) {
|
|
488
|
-
|
|
786
|
+
requireString2(value.sourceUrl, "sourceUrl");
|
|
489
787
|
}
|
|
490
|
-
if (!
|
|
788
|
+
if (!isRecord2(value.range)) {
|
|
491
789
|
throw new Error("Change-tour manifest range must be an object.");
|
|
492
790
|
}
|
|
493
791
|
for (const key of ["baseRef", "headRef", "mergeBaseOid", "headOid"]) {
|
|
494
|
-
|
|
792
|
+
requireString2(value.range[key], `range.${key}`);
|
|
495
793
|
}
|
|
496
|
-
if (!
|
|
794
|
+
if (!isRecord2(value.summary)) {
|
|
497
795
|
throw new Error("Change-tour manifest summary must be an object.");
|
|
498
796
|
}
|
|
499
797
|
for (const key of ["changedFiles", "includedScenes", "additions", "deletions", "commitCount"]) {
|
|
500
798
|
requireNonNegativeInteger(value.summary[key], `summary.${key}`);
|
|
501
799
|
}
|
|
502
|
-
|
|
800
|
+
requireStringArray2(value.summary.omittedFiles, "summary.omittedFiles");
|
|
503
801
|
if (!Array.isArray(value.commits) || !Array.isArray(value.chapters) || !Array.isArray(value.scenes)) {
|
|
504
802
|
throw new Error("Change-tour manifest commits, chapters, and scenes must be arrays.");
|
|
505
803
|
}
|
|
@@ -515,12 +813,12 @@ function parseChangeTourManifest(value) {
|
|
|
515
813
|
sceneIds.add(candidate.id);
|
|
516
814
|
}
|
|
517
815
|
for (const [index, candidate] of value.chapters.entries()) {
|
|
518
|
-
if (!
|
|
816
|
+
if (!isRecord2(candidate)) {
|
|
519
817
|
throw new Error(`chapters[${index}] must be an object.`);
|
|
520
818
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
819
|
+
requireString2(candidate.id, `chapters[${index}].id`);
|
|
820
|
+
requireString2(candidate.title, `chapters[${index}].title`);
|
|
821
|
+
requireStringArray2(candidate.sceneIds, `chapters[${index}].sceneIds`);
|
|
524
822
|
for (const sceneId of candidate.sceneIds) {
|
|
525
823
|
if (!sceneIds.has(sceneId)) {
|
|
526
824
|
throw new Error(`Chapter references unknown scene id: ${sceneId}`);
|
|
@@ -537,16 +835,108 @@ function parseChangeTourManifest(value) {
|
|
|
537
835
|
if (value.files !== void 0 && value.summary.changedFiles !== files.length) {
|
|
538
836
|
throw new Error("summary.changedFiles must match the number of files.");
|
|
539
837
|
}
|
|
838
|
+
if (value.authoringCoverage !== void 0) {
|
|
839
|
+
validateAuthoringCoverage(value.authoringCoverage, value.version);
|
|
840
|
+
}
|
|
841
|
+
if (value.version === 2) validateZoom(value);
|
|
540
842
|
return { ...value, files };
|
|
541
843
|
}
|
|
844
|
+
function validateAuthoringCoverage(value, version) {
|
|
845
|
+
if (!isRecord2(value) || !isRecord2(value.walkthrough) || !Array.isArray(value.explanationAssignments)) {
|
|
846
|
+
throw new Error("authoringCoverage must contain walkthrough and explanationAssignments.");
|
|
847
|
+
}
|
|
848
|
+
const expectedScope = version === 2 ? "final" : "tour";
|
|
849
|
+
if (value.walkthrough.scope !== expectedScope) {
|
|
850
|
+
throw new Error(`authoringCoverage.walkthrough.scope must be ${expectedScope}.`);
|
|
851
|
+
}
|
|
852
|
+
requireNonNegativeInteger(value.walkthrough.coveredUnits, "authoringCoverage.walkthrough.coveredUnits");
|
|
853
|
+
requireNonNegativeInteger(value.walkthrough.includedUnits, "authoringCoverage.walkthrough.includedUnits");
|
|
854
|
+
requirePercentage(value.walkthrough.coveragePercent, "authoringCoverage.walkthrough.coveragePercent");
|
|
855
|
+
if (value.walkthrough.coveredUnits > value.walkthrough.includedUnits) {
|
|
856
|
+
throw new Error("authoringCoverage.walkthrough.coveredUnits must not exceed includedUnits.");
|
|
857
|
+
}
|
|
858
|
+
for (const [index, assignment] of value.explanationAssignments.entries()) {
|
|
859
|
+
const path = `authoringCoverage.explanationAssignments[${index}]`;
|
|
860
|
+
if (!isRecord2(assignment)) throw new Error(`${path} must be an object.`);
|
|
861
|
+
requireString2(assignment.sceneId, `${path}.sceneId`);
|
|
862
|
+
requireNonNegativeInteger(assignment.assignedUnits, `${path}.assignedUnits`);
|
|
863
|
+
requireNonNegativeInteger(assignment.excludedUnits, `${path}.excludedUnits`);
|
|
864
|
+
requireNonNegativeInteger(assignment.completeUnits, `${path}.completeUnits`);
|
|
865
|
+
requireNonNegativeInteger(assignment.totalUnits, `${path}.totalUnits`);
|
|
866
|
+
requirePercentage(assignment.assignmentPercent, `${path}.assignmentPercent`);
|
|
867
|
+
if (assignment.assignedUnits + assignment.excludedUnits !== assignment.completeUnits || assignment.completeUnits > assignment.totalUnits) {
|
|
868
|
+
throw new Error(`${path} contains inconsistent unit counts.`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function validateZoom(value) {
|
|
873
|
+
if (!isRecord2(value.repository) || typeof value.repository.root !== "string" || !value.repository.root) {
|
|
874
|
+
throw new Error("A v2 tour requires its originating repository.root.");
|
|
875
|
+
}
|
|
876
|
+
if (!isRecord2(value.zoom) || !Array.isArray(value.zoom.revisions) || !isRecord2(value.zoom.final)) {
|
|
877
|
+
throw new Error("A v2 tour requires zoom metadata and real revision evidence.");
|
|
878
|
+
}
|
|
879
|
+
const scenes = value.scenes;
|
|
880
|
+
const revisions = value.zoom.revisions;
|
|
881
|
+
const ids = /* @__PURE__ */ new Set();
|
|
882
|
+
for (const [index, scene] of revisions.entries()) {
|
|
883
|
+
validateScene(scene, index);
|
|
884
|
+
if (scene.kind !== "stacked-diff") throw new Error("zoom.revisions must contain real stacked-diff scenes.");
|
|
885
|
+
if (ids.has(scene.id)) throw new Error(`Duplicate zoom revision scene: ${scene.id}`);
|
|
886
|
+
ids.add(scene.id);
|
|
887
|
+
const range = value.range;
|
|
888
|
+
if (scene.stack[0].oid !== range.mergeBaseOid || scene.stack[scene.stack.length - 1].oid !== range.headOid) {
|
|
889
|
+
throw new Error(`Revision scene ${scene.id} endpoints must match the final diff range.`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
for (const scene of scenes) {
|
|
893
|
+
if (scene.kind !== "deconstructed-diff" && scene.kind !== "stacked-diff") continue;
|
|
894
|
+
const real = revisions.find((candidate) => candidate.id === scene.id);
|
|
895
|
+
if (!real) throw new Error(`Scene ${scene.id} is missing its real revision evidence.`);
|
|
896
|
+
if (scene.kind === "deconstructed-diff" && (real.stack[0].oid !== scene.realRange.baseOid || real.stack[real.stack.length - 1].oid !== scene.realRange.targetOid)) {
|
|
897
|
+
throw new Error(`Deconstructed scene ${scene.id} real stack endpoints must match its explanation range.`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const explanation = scenes.some((scene) => scene.kind === "deconstructed-diff");
|
|
901
|
+
const distinct = revisions.some((scene) => scene.stack.length > 2);
|
|
902
|
+
const depth = explanation ? "explanation" : distinct ? "revisions" : "final";
|
|
903
|
+
const modes = [...explanation ? ["explanation"] : [], ...distinct ? ["revisions"] : [], "final", "history"];
|
|
904
|
+
if (value.zoom.authoredDepth !== depth || JSON.stringify(value.zoom.modes) !== JSON.stringify(modes)) {
|
|
905
|
+
throw new Error("zoom depth and modes must match the maximum authored depth.");
|
|
906
|
+
}
|
|
907
|
+
if (!Array.isArray(value.files)) throw new Error("A v2 tour requires final endpoint file evidence.");
|
|
908
|
+
if (!Array.isArray(value.zoom.final.scenes) || !Array.isArray(value.zoom.final.chapters)) {
|
|
909
|
+
throw new Error("A v2 tour requires a non-stacked final tour.");
|
|
910
|
+
}
|
|
911
|
+
const finalSceneIds = /* @__PURE__ */ new Set();
|
|
912
|
+
for (const [index, scene] of value.zoom.final.scenes.entries()) {
|
|
913
|
+
validateScene(scene, index);
|
|
914
|
+
if (scene.kind === "stacked-diff" || scene.kind === "deconstructed-diff") {
|
|
915
|
+
throw new Error("zoom.final must contain only non-stacked scenes.");
|
|
916
|
+
}
|
|
917
|
+
if (finalSceneIds.has(scene.id)) throw new Error(`Duplicate final tour scene: ${scene.id}`);
|
|
918
|
+
finalSceneIds.add(scene.id);
|
|
919
|
+
}
|
|
920
|
+
for (const [index, chapter] of value.zoom.final.chapters.entries()) {
|
|
921
|
+
if (!isRecord2(chapter) || !Array.isArray(chapter.sceneIds)) {
|
|
922
|
+
throw new Error(`zoom.final.chapters[${index}] must contain sceneIds.`);
|
|
923
|
+
}
|
|
924
|
+
requireString2(chapter.id, `zoom.final.chapters[${index}].id`);
|
|
925
|
+
requireString2(chapter.title, `zoom.final.chapters[${index}].title`);
|
|
926
|
+
for (const sceneId of chapter.sceneIds) {
|
|
927
|
+
requireString2(sceneId, `zoom.final.chapters[${index}].sceneIds`);
|
|
928
|
+
if (!finalSceneIds.has(sceneId)) throw new Error(`Final tour chapter references unknown scene: ${sceneId}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
542
932
|
function validateScene(value, index) {
|
|
543
|
-
if (!
|
|
933
|
+
if (!isRecord2(value) || !["text-diff", "discussion", "walkthrough", "stacked-diff", "deconstructed-diff"].includes(String(value.kind))) {
|
|
544
934
|
throw new Error(`scenes[${index}] must be a text-diff, discussion, walkthrough, stacked-diff, or deconstructed-diff scene.`);
|
|
545
935
|
}
|
|
546
936
|
for (const key of ["id", "title"]) {
|
|
547
|
-
|
|
937
|
+
requireString2(value[key], `scenes[${index}].${key}`);
|
|
548
938
|
}
|
|
549
|
-
|
|
939
|
+
validateNarrative2(value, `scenes[${index}]`);
|
|
550
940
|
if (value.kind === "discussion") {
|
|
551
941
|
return;
|
|
552
942
|
}
|
|
@@ -556,20 +946,21 @@ function validateScene(value, index) {
|
|
|
556
946
|
}
|
|
557
947
|
for (const [stepIndex, step] of value.steps.entries()) {
|
|
558
948
|
const path = `scenes[${index}].steps[${stepIndex}]`;
|
|
559
|
-
if (!
|
|
949
|
+
if (!isRecord2(step) || !isRecord2(step.focus) || !isRecord2(step.diff)) {
|
|
560
950
|
throw new Error(`${path} must contain focus and diff objects.`);
|
|
561
951
|
}
|
|
562
|
-
for (const key of ["id", "title", "body"])
|
|
952
|
+
for (const key of ["id", "title", "body"]) requireString2(step[key], `${path}.${key}`);
|
|
563
953
|
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
564
954
|
throw new Error(`${path}.depth must be mentioned, explained, or contextualized.`);
|
|
565
955
|
}
|
|
956
|
+
validateStepRequirement(step.requirement, `${path}.requirement`);
|
|
566
957
|
validateResolvedAnchor(step.focus, `${path}.focus`);
|
|
567
958
|
validateScene(step.diff, index);
|
|
568
959
|
if (step.diff.kind !== "text-diff") throw new Error(`${path}.diff must be a text-diff scene.`);
|
|
569
960
|
if (step.connection !== void 0) {
|
|
570
|
-
if (!
|
|
571
|
-
|
|
572
|
-
|
|
961
|
+
if (!isRecord2(step.connection)) throw new Error(`${path}.connection must be an object.`);
|
|
962
|
+
requireString2(step.connection.id, `${path}.connection.id`);
|
|
963
|
+
requireString2(step.connection.label, `${path}.connection.label`);
|
|
573
964
|
validateResolvedAnchor(step.connection.from, `${path}.connection.from`);
|
|
574
965
|
validateResolvedAnchor(step.connection.to, `${path}.connection.to`);
|
|
575
966
|
}
|
|
@@ -577,68 +968,68 @@ function validateScene(value, index) {
|
|
|
577
968
|
return;
|
|
578
969
|
}
|
|
579
970
|
if (value.kind === "stacked-diff") {
|
|
580
|
-
|
|
971
|
+
validateStackedScene2(value, index);
|
|
581
972
|
return;
|
|
582
973
|
}
|
|
583
974
|
if (value.kind === "deconstructed-diff") {
|
|
584
|
-
|
|
975
|
+
validateDeconstructedScene2(value, index);
|
|
585
976
|
return;
|
|
586
977
|
}
|
|
587
978
|
for (const key of ["path", "leftLabel", "rightLabel", "leftContent", "rightContent"]) {
|
|
588
|
-
|
|
979
|
+
requireString2(value[key], `scenes[${index}].${key}`);
|
|
589
980
|
}
|
|
590
981
|
if (value.previousPath !== void 0) {
|
|
591
|
-
|
|
982
|
+
requireString2(value.previousPath, `scenes[${index}].previousPath`);
|
|
592
983
|
}
|
|
593
|
-
|
|
984
|
+
requireString2(value.changeKind, `scenes[${index}].changeKind`);
|
|
594
985
|
requireNonNegativeInteger(value.additions, `scenes[${index}].additions`);
|
|
595
986
|
requireNonNegativeInteger(value.deletions, `scenes[${index}].deletions`);
|
|
596
987
|
if (value.focusChangeIndex !== void 0) {
|
|
597
988
|
requireNonNegativeInteger(value.focusChangeIndex, `scenes[${index}].focusChangeIndex`);
|
|
598
989
|
}
|
|
599
990
|
}
|
|
600
|
-
function
|
|
601
|
-
if (!Array.isArray(value.stack) || value.stack.length <
|
|
602
|
-
throw new Error(`scenes[${index}].stack must contain between
|
|
991
|
+
function validateStackedScene2(value, index) {
|
|
992
|
+
if (!Array.isArray(value.stack) || value.stack.length < 2 || value.stack.length > 6) {
|
|
993
|
+
throw new Error(`scenes[${index}].stack must contain between 2 and 6 revisions.`);
|
|
603
994
|
}
|
|
604
995
|
for (const [panelIndex, panel] of value.stack.entries()) {
|
|
605
|
-
if (!
|
|
606
|
-
for (const key of ["id", "ref", "oid", "label"])
|
|
996
|
+
if (!isRecord2(panel)) throw new Error(`scenes[${index}].stack[${panelIndex}] must be an object.`);
|
|
997
|
+
for (const key of ["id", "ref", "oid", "label"]) requireString2(panel[key], `scenes[${index}].stack[${panelIndex}].${key}`);
|
|
607
998
|
}
|
|
608
999
|
validateMultiPanelFiles(value.files, value.stack.length, index);
|
|
609
1000
|
validateMultiPanelSteps(value.steps, value.stack.length, index, false);
|
|
610
1001
|
}
|
|
611
|
-
function
|
|
1002
|
+
function validateDeconstructedScene2(value, index) {
|
|
612
1003
|
if (value.stageLabel !== "Explanation stages") {
|
|
613
1004
|
throw new Error(`scenes[${index}].stageLabel must be Explanation stages.`);
|
|
614
1005
|
}
|
|
615
|
-
if (!
|
|
1006
|
+
if (!isRecord2(value.realRange)) throw new Error(`scenes[${index}].realRange must be an object.`);
|
|
616
1007
|
for (const key of ["baseRef", "targetRef", "baseOid", "targetOid"]) {
|
|
617
|
-
|
|
1008
|
+
requireString2(value.realRange[key], `scenes[${index}].realRange.${key}`);
|
|
618
1009
|
}
|
|
619
1010
|
if (!Array.isArray(value.panels) || value.panels.length < 2 || value.panels.length > 13) {
|
|
620
1011
|
throw new Error(`scenes[${index}].panels must contain a baseline and between 1 and 12 stages.`);
|
|
621
1012
|
}
|
|
622
1013
|
const panels = value.panels;
|
|
623
1014
|
for (const [panelIndex, panel] of panels.entries()) {
|
|
624
|
-
if (!
|
|
625
|
-
for (const key of ["id", "label", "role"])
|
|
1015
|
+
if (!isRecord2(panel)) throw new Error(`scenes[${index}].panels[${panelIndex}] must be an object.`);
|
|
1016
|
+
for (const key of ["id", "label", "role"]) requireString2(panel[key], `scenes[${index}].panels[${panelIndex}].${key}`);
|
|
626
1017
|
const expectedRole = panelIndex === 0 ? "baseline" : "stage";
|
|
627
1018
|
if (panel.role !== expectedRole) throw new Error(`scenes[${index}].panels[${panelIndex}].role must be ${expectedRole}.`);
|
|
628
|
-
if (panelIndex > 0)
|
|
1019
|
+
if (panelIndex > 0) requireString2(panel.stageId, `scenes[${index}].panels[${panelIndex}].stageId`);
|
|
629
1020
|
}
|
|
630
1021
|
validateMultiPanelFiles(value.files, panels.length, index);
|
|
631
1022
|
validateMultiPanelSteps(value.steps, panels.length, index, true);
|
|
632
1023
|
if (Array.isArray(value.steps)) {
|
|
633
1024
|
let previousPairIndex = -1;
|
|
634
1025
|
value.steps.forEach((step, stepIndex) => {
|
|
635
|
-
if (!
|
|
1026
|
+
if (!isRecord2(step)) return;
|
|
636
1027
|
const pairIndex = Number(step.pairIndex);
|
|
637
1028
|
if (pairIndex < previousPairIndex) {
|
|
638
1029
|
throw new Error(`scenes[${index}].steps must remain grouped in explanation stage order.`);
|
|
639
1030
|
}
|
|
640
1031
|
previousPairIndex = pairIndex;
|
|
641
|
-
if (step.stageId !== void 0)
|
|
1032
|
+
if (step.stageId !== void 0) requireString2(step.stageId, `scenes[${index}].steps[${stepIndex}].stageId`);
|
|
642
1033
|
if (step.stageIndex !== void 0) requireNonNegativeInteger(step.stageIndex, `scenes[${index}].steps[${stepIndex}].stageIndex`);
|
|
643
1034
|
if (step.focusIndex !== void 0) requireNonNegativeInteger(step.focusIndex, `scenes[${index}].steps[${stepIndex}].focusIndex`);
|
|
644
1035
|
if (step.focusCount !== void 0) requirePositiveInteger(step.focusCount, `scenes[${index}].steps[${stepIndex}].focusCount`);
|
|
@@ -651,14 +1042,14 @@ function validateDeconstructedScene(value, index) {
|
|
|
651
1042
|
});
|
|
652
1043
|
for (let stageIndex = 0; stageIndex < panels.length - 1; stageIndex += 1) {
|
|
653
1044
|
const panel = panels[stageIndex + 1];
|
|
654
|
-
const stageSteps = value.steps.filter((step) =>
|
|
655
|
-
if (!
|
|
1045
|
+
const stageSteps = value.steps.filter((step) => isRecord2(step) && Number(step.pairIndex) === stageIndex);
|
|
1046
|
+
if (!isRecord2(panel) || stageSteps.length === 0) {
|
|
656
1047
|
throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
|
|
657
1048
|
}
|
|
658
1049
|
const firstStep = stageSteps[0];
|
|
659
|
-
const fallbackStageId =
|
|
1050
|
+
const fallbackStageId = isRecord2(firstStep) ? firstStep.id : void 0;
|
|
660
1051
|
if (stageSteps.some((step) => {
|
|
661
|
-
if (!
|
|
1052
|
+
if (!isRecord2(step)) return true;
|
|
662
1053
|
return (step.stageId ?? fallbackStageId) !== panel.stageId;
|
|
663
1054
|
})) {
|
|
664
1055
|
throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
|
|
@@ -669,15 +1060,15 @@ function validateDeconstructedScene(value, index) {
|
|
|
669
1060
|
function validateMultiPanelFiles(value, panelCount, index) {
|
|
670
1061
|
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].files must be non-empty.`);
|
|
671
1062
|
for (const [fileIndex, file] of value.entries()) {
|
|
672
|
-
if (!
|
|
673
|
-
|
|
1063
|
+
if (!isRecord2(file)) throw new Error(`scenes[${index}].files[${fileIndex}] must be an object.`);
|
|
1064
|
+
requireString2(file.path, `scenes[${index}].files[${fileIndex}].path`);
|
|
674
1065
|
if (!Array.isArray(file.panels) || file.panels.length !== panelCount) {
|
|
675
1066
|
throw new Error(`scenes[${index}].files[${fileIndex}].panels must match panel count.`);
|
|
676
1067
|
}
|
|
677
1068
|
for (const [panelIndex, panel] of file.panels.entries()) {
|
|
678
|
-
if (!
|
|
679
|
-
for (const key of ["id", "label", "content"])
|
|
680
|
-
if (panel.path !== void 0)
|
|
1069
|
+
if (!isRecord2(panel)) throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}] must be an object.`);
|
|
1070
|
+
for (const key of ["id", "label", "content"]) requireString2(panel[key], `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].${key}`);
|
|
1071
|
+
if (panel.path !== void 0) requireString2(panel.path, `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].path`);
|
|
681
1072
|
if (typeof panel.exists !== "boolean") throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}].exists must be boolean.`);
|
|
682
1073
|
}
|
|
683
1074
|
}
|
|
@@ -685,12 +1076,12 @@ function validateMultiPanelFiles(value, panelCount, index) {
|
|
|
685
1076
|
function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
686
1077
|
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].steps must be non-empty.`);
|
|
687
1078
|
for (const [stepIndex, step] of value.entries()) {
|
|
688
|
-
if (!
|
|
689
|
-
for (const key of ["id", "title", "body", "file", "side"])
|
|
1079
|
+
if (!isRecord2(step)) throw new Error(`scenes[${index}].steps[${stepIndex}] must be an object.`);
|
|
1080
|
+
for (const key of ["id", "title", "body", "file", "side"]) requireString2(step[key], `scenes[${index}].steps[${stepIndex}].${key}`);
|
|
690
1081
|
requireNonNegativeInteger(step.pairIndex, `scenes[${index}].steps[${stepIndex}].pairIndex`);
|
|
691
1082
|
if (Number(step.pairIndex) >= panelCount - 1) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex is outside the panels.`);
|
|
692
1083
|
if (deconstructed) {
|
|
693
|
-
|
|
1084
|
+
requireStringArray2(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
|
|
694
1085
|
if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
|
|
695
1086
|
}
|
|
696
1087
|
if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
|
|
@@ -699,7 +1090,7 @@ function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
|
699
1090
|
}
|
|
700
1091
|
}
|
|
701
1092
|
function validateTourFile(value, index) {
|
|
702
|
-
if (!
|
|
1093
|
+
if (!isRecord2(value)) throw new Error(`files[${index}] must be an object.`);
|
|
703
1094
|
if (value.kind === "text-diff") {
|
|
704
1095
|
validateScene(value, index);
|
|
705
1096
|
return;
|
|
@@ -708,9 +1099,9 @@ function validateTourFile(value, index) {
|
|
|
708
1099
|
throw new Error(`files[${index}].kind must be text-diff or omitted.`);
|
|
709
1100
|
}
|
|
710
1101
|
for (const key of ["id", "title", "path", "changeKind", "reason"]) {
|
|
711
|
-
|
|
1102
|
+
requireString2(value[key], `files[${index}].${key}`);
|
|
712
1103
|
}
|
|
713
|
-
if (value.previousPath !== void 0)
|
|
1104
|
+
if (value.previousPath !== void 0) requireString2(value.previousPath, `files[${index}].previousPath`);
|
|
714
1105
|
requireNonNegativeInteger(value.additions, `files[${index}].additions`);
|
|
715
1106
|
requireNonNegativeInteger(value.deletions, `files[${index}].deletions`);
|
|
716
1107
|
}
|
|
@@ -730,263 +1121,13 @@ function collectLegacyTourFiles(scenes) {
|
|
|
730
1121
|
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
731
1122
|
}
|
|
732
1123
|
function validateResolvedAnchor(value, path) {
|
|
733
|
-
if (!
|
|
734
|
-
for (const key of ["id", "path", "revision", "excerpt"])
|
|
1124
|
+
if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
|
|
1125
|
+
for (const key of ["id", "path", "revision", "excerpt"]) requireString2(value[key], `${path}.${key}`);
|
|
735
1126
|
if (value.revision !== "base" && value.revision !== "head") throw new Error(`${path}.revision must be base or head.`);
|
|
736
1127
|
requirePositiveInteger(value.startLine, `${path}.startLine`);
|
|
737
1128
|
requirePositiveInteger(value.endLine, `${path}.endLine`);
|
|
738
1129
|
if (Number(value.endLine) < Number(value.startLine)) throw new Error(`${path}.endLine must not precede startLine.`);
|
|
739
1130
|
}
|
|
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.`);
|
|
870
|
-
}
|
|
871
|
-
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "steps"], path);
|
|
872
|
-
const stepIds = /* @__PURE__ */ new Set();
|
|
873
|
-
for (const [stepIndex, step] of scene.steps.entries()) {
|
|
874
|
-
const stepPath = `${path}.steps[${stepIndex}]`;
|
|
875
|
-
if (!isRecord2(step)) {
|
|
876
|
-
throw new Error(`${stepPath} must be an object.`);
|
|
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
|
-
}
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
return { ...value, connections: rawConnections };
|
|
901
|
-
}
|
|
902
|
-
function validateDeconstructedScene2(scene, path) {
|
|
903
|
-
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "base", "target", "stages", "exclusions"], path);
|
|
904
|
-
optionalString(scene.base, `${path}.base`);
|
|
905
|
-
optionalString(scene.target, `${path}.target`);
|
|
906
|
-
if (!Array.isArray(scene.stages) || scene.stages.length === 0 || scene.stages.length > 12) {
|
|
907
|
-
throw new Error(`${path}.stages must contain between 1 and 12 stages.`);
|
|
908
|
-
}
|
|
909
|
-
const stageIds = /* @__PURE__ */ new Set();
|
|
910
|
-
for (const [stageIndex, stage] of scene.stages.entries()) {
|
|
911
|
-
const stagePath = `${path}.stages[${stageIndex}]`;
|
|
912
|
-
if (!isRecord2(stage)) throw new Error(`${stagePath} must be an object.`);
|
|
913
|
-
requireOnlyKeys(stage, ["id", "title", "narration", "changes"], stagePath);
|
|
914
|
-
requireString2(stage.id, `${stagePath}.id`);
|
|
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.`);
|
|
919
|
-
}
|
|
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
|
-
}
|
|
936
|
-
}
|
|
937
|
-
function validateDeconstructedSelection(value, path, requiresReason) {
|
|
938
|
-
if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
|
|
939
|
-
requireOnlyKeys(value, requiresReason ? ["file", "hunks", "reason"] : ["file", "hunks"], path);
|
|
940
|
-
requireString2(value.file, `${path}.file`);
|
|
941
|
-
if (!requiresReason || value.hunks !== void 0) {
|
|
942
|
-
requireStringArray2(value.hunks, `${path}.hunks`);
|
|
943
|
-
if (value.hunks.length === 0) throw new Error(`${path}.hunks must not be empty.`);
|
|
944
|
-
}
|
|
945
|
-
if (requiresReason) requireString2(value.reason, `${path}.reason`);
|
|
946
|
-
}
|
|
947
|
-
function validateStackedScene2(scene, path) {
|
|
948
|
-
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "stack", "files", "steps"], path);
|
|
949
|
-
if (!Array.isArray(scene.stack) || scene.stack.length < 3 || scene.stack.length > 6) {
|
|
950
|
-
throw new Error(`${path}.stack must contain between 3 and 6 revisions.`);
|
|
951
|
-
}
|
|
952
|
-
const stackIds = /* @__PURE__ */ new Set();
|
|
953
|
-
for (const [index, entry] of scene.stack.entries()) {
|
|
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);
|
|
962
|
-
}
|
|
963
|
-
if (scene.files !== void 0) requireStringArray2(scene.files, `${path}.files`);
|
|
964
|
-
if (!Array.isArray(scene.steps) || scene.steps.length === 0) throw new Error(`${path}.steps must be a non-empty array.`);
|
|
965
|
-
const stepIds = /* @__PURE__ */ new Set();
|
|
966
|
-
for (const [index, step] of scene.steps.entries()) {
|
|
967
|
-
const stepPath = `${path}.steps[${index}]`;
|
|
968
|
-
if (!isRecord2(step)) throw new Error(`${stepPath} must be an object.`);
|
|
969
|
-
requireOnlyKeys(step, ["id", "title", "body", "file", "pair", "side", "lines"], stepPath);
|
|
970
|
-
requireString2(step.id, `${stepPath}.id`);
|
|
971
|
-
requireString2(step.title, `${stepPath}.title`);
|
|
972
|
-
requireString2(step.body, `${stepPath}.body`);
|
|
973
|
-
requireString2(step.file, `${stepPath}.file`);
|
|
974
|
-
const stepId = step.id;
|
|
975
|
-
if (stepIds.has(stepId)) throw new Error(`Duplicate step id in scene ${scene.id}: ${stepId}`);
|
|
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.`);
|
|
979
|
-
}
|
|
980
|
-
const pair = step.pair;
|
|
981
|
-
const leftIndex = scene.stack.findIndex((entry) => isRecord2(entry) && entry.id === pair[0]);
|
|
982
|
-
const rightIndex = scene.stack.findIndex((entry) => isRecord2(entry) && entry.id === pair[1]);
|
|
983
|
-
if (leftIndex < 0 || rightIndex !== leftIndex + 1) throw new Error(`${stepPath}.pair must reference adjacent stack entries in order.`);
|
|
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.`);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
1131
|
function validateNarrative2(value, path) {
|
|
991
1132
|
requireString2(value.summary, `${path}.summary`);
|
|
992
1133
|
requireStringArray2(value.bullets, `${path}.bullets`);
|
|
@@ -997,13 +1138,8 @@ function isRecord2(value) {
|
|
|
997
1138
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
998
1139
|
}
|
|
999
1140
|
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);
|
|
1141
|
+
if (typeof value !== "string") {
|
|
1142
|
+
throw new Error(`${path} must be a string.`);
|
|
1007
1143
|
}
|
|
1008
1144
|
}
|
|
1009
1145
|
function requireStringArray2(value, path) {
|
|
@@ -1011,12 +1147,19 @@ function requireStringArray2(value, path) {
|
|
|
1011
1147
|
throw new Error(`${path} must be an array of strings.`);
|
|
1012
1148
|
}
|
|
1013
1149
|
}
|
|
1014
|
-
function
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1150
|
+
function requireNonNegativeInteger(value, path) {
|
|
1151
|
+
if (!Number.isInteger(value) || Number(value) < 0) {
|
|
1152
|
+
throw new Error(`${path} must be a non-negative integer.`);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function requirePercentage(value, path) {
|
|
1156
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
|
|
1157
|
+
throw new Error(`${path} must be a number from 0 to 100.`);
|
|
1018
1158
|
}
|
|
1019
1159
|
}
|
|
1160
|
+
function requirePositiveInteger(value, path) {
|
|
1161
|
+
if (!Number.isInteger(value) || Number(value) < 1) throw new Error(`${path} must be a positive integer.`);
|
|
1162
|
+
}
|
|
1020
1163
|
|
|
1021
1164
|
// src/deconstructedChange.ts
|
|
1022
1165
|
function buildDeconstructedScene(startPath, scene, options = {}) {
|
|
@@ -1551,12 +1694,20 @@ var DIFF_TIMEOUT_MS = readPositiveIntegerEnv("BYGONE_DIFF_TIMEOUT_MS", DEFAULT_D
|
|
|
1551
1694
|
function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
1552
1695
|
const leftLines = normalizeLines(leftContent);
|
|
1553
1696
|
const rightLines = normalizeLines(rightContent);
|
|
1554
|
-
|
|
1697
|
+
if (leftContent === rightContent) {
|
|
1698
|
+
return buildIdenticalTwoWayDiffModel(leftLines);
|
|
1699
|
+
}
|
|
1700
|
+
const interned = internDiffLines(leftLines, rightLines);
|
|
1701
|
+
const internedChanges = diffArrays(interned.left, interned.right, {
|
|
1555
1702
|
timeout: options.timeoutMs ?? DIFF_TIMEOUT_MS
|
|
1556
1703
|
});
|
|
1557
|
-
if (!
|
|
1704
|
+
if (!internedChanges) {
|
|
1558
1705
|
return buildConservativeTwoWayDiffModel(leftLines, rightLines);
|
|
1559
1706
|
}
|
|
1707
|
+
const changes = internedChanges.map((change) => ({
|
|
1708
|
+
...change,
|
|
1709
|
+
value: change.value.map((lineId) => interned.lines[lineId])
|
|
1710
|
+
}));
|
|
1560
1711
|
const rows = [];
|
|
1561
1712
|
const renderedLeftLines = [];
|
|
1562
1713
|
const renderedRightLines = [];
|
|
@@ -1580,29 +1731,18 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
|
1580
1731
|
}
|
|
1581
1732
|
if (change.removed && index + 1 < changes.length && changes[index + 1].added) {
|
|
1582
1733
|
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));
|
|
1734
|
+
const nextLineNumbers = appendReplacementCandidate(
|
|
1735
|
+
removedLines,
|
|
1736
|
+
nextChange.value,
|
|
1737
|
+
leftLineNumber,
|
|
1738
|
+
rightLineNumber,
|
|
1739
|
+
rows,
|
|
1740
|
+
renderedLeftLines,
|
|
1741
|
+
renderedRightLines,
|
|
1742
|
+
blocks
|
|
1743
|
+
);
|
|
1744
|
+
leftLineNumber = nextLineNumbers.left;
|
|
1745
|
+
rightLineNumber = nextLineNumbers.right;
|
|
1606
1746
|
index++;
|
|
1607
1747
|
continue;
|
|
1608
1748
|
}
|
|
@@ -1638,7 +1778,40 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
|
1638
1778
|
leftLines: renderedLeftLines,
|
|
1639
1779
|
rightLines: renderedRightLines,
|
|
1640
1780
|
blocks,
|
|
1641
|
-
hasChanges
|
|
1781
|
+
hasChanges,
|
|
1782
|
+
quality: "exact"
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
function internDiffLines(leftLines, rightLines) {
|
|
1786
|
+
const lineIds = /* @__PURE__ */ new Map();
|
|
1787
|
+
const lines = [];
|
|
1788
|
+
const intern = (line) => {
|
|
1789
|
+
const existingId = lineIds.get(line);
|
|
1790
|
+
if (existingId !== void 0) {
|
|
1791
|
+
return existingId;
|
|
1792
|
+
}
|
|
1793
|
+
const lineId = lines.length;
|
|
1794
|
+
lineIds.set(line, lineId);
|
|
1795
|
+
lines.push(line);
|
|
1796
|
+
return lineId;
|
|
1797
|
+
};
|
|
1798
|
+
return {
|
|
1799
|
+
left: leftLines.map(intern),
|
|
1800
|
+
right: rightLines.map(intern),
|
|
1801
|
+
lines
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
function buildIdenticalTwoWayDiffModel(lines) {
|
|
1805
|
+
return {
|
|
1806
|
+
rows: lines.map((line, index) => makeDiffRow(
|
|
1807
|
+
makeDiffCell("context", line, index + 1),
|
|
1808
|
+
makeDiffCell("context", line, index + 1)
|
|
1809
|
+
)),
|
|
1810
|
+
leftLines: lines.map((line, index) => makeDiffLine("context", line, index + 1)),
|
|
1811
|
+
rightLines: lines.map((line, index) => makeDiffLine("context", line, index + 1)),
|
|
1812
|
+
blocks: [],
|
|
1813
|
+
hasChanges: false,
|
|
1814
|
+
quality: "exact"
|
|
1642
1815
|
};
|
|
1643
1816
|
}
|
|
1644
1817
|
function buildConservativeTwoWayDiffModel(leftLines, rightLines) {
|
|
@@ -1665,7 +1838,8 @@ function buildConservativeTwoWayDiffModel(leftLines, rightLines) {
|
|
|
1665
1838
|
leftLines: renderedLeftLines,
|
|
1666
1839
|
rightLines: renderedRightLines,
|
|
1667
1840
|
blocks: rows.length === 0 ? [] : [makeDiffBlock(blockKind, 0, leftLines.length, 0, rightLines.length)],
|
|
1668
|
-
hasChanges: rows.length > 0
|
|
1841
|
+
hasChanges: rows.length > 0,
|
|
1842
|
+
quality: "fallback"
|
|
1669
1843
|
};
|
|
1670
1844
|
}
|
|
1671
1845
|
function normalizeLines(content) {
|
|
@@ -1678,6 +1852,100 @@ function normalizeLines(content) {
|
|
|
1678
1852
|
}
|
|
1679
1853
|
return lines;
|
|
1680
1854
|
}
|
|
1855
|
+
function appendReplacementCandidate(leftLines, rightLines, initialLeftLineNumber, initialRightLineNumber, rows, renderedLeftLines, renderedRightLines, blocks) {
|
|
1856
|
+
const aligned = alignReplacementLines(leftLines, rightLines);
|
|
1857
|
+
const crediblePairs = aligned.filter((row) => row.left !== void 0 && row.right !== void 0 && hasCredibleLineCorrespondence(row.left, row.right)).length;
|
|
1858
|
+
const coherentBlock = hasCoherentBlockCorrespondence(leftLines, rightLines, crediblePairs);
|
|
1859
|
+
const normalized = aligned.flatMap((row) => {
|
|
1860
|
+
if (row.left === void 0 || row.right === void 0 || hasCredibleLineCorrespondence(row.left, row.right)) {
|
|
1861
|
+
return [row];
|
|
1862
|
+
}
|
|
1863
|
+
return [{ left: row.left }, { right: row.right }];
|
|
1864
|
+
});
|
|
1865
|
+
let leftLineNumber = initialLeftLineNumber;
|
|
1866
|
+
let rightLineNumber = initialRightLineNumber;
|
|
1867
|
+
let blockKind;
|
|
1868
|
+
let blockLeftStart = renderedLeftLines.length;
|
|
1869
|
+
let blockRightStart = renderedRightLines.length;
|
|
1870
|
+
const flushBlock = () => {
|
|
1871
|
+
if (!blockKind) return;
|
|
1872
|
+
blocks.push(makeDiffBlock(
|
|
1873
|
+
blockKind,
|
|
1874
|
+
blockLeftStart,
|
|
1875
|
+
renderedLeftLines.length,
|
|
1876
|
+
blockRightStart,
|
|
1877
|
+
renderedRightLines.length
|
|
1878
|
+
));
|
|
1879
|
+
};
|
|
1880
|
+
for (const { left, right } of normalized) {
|
|
1881
|
+
const nextKind = coherentBlock ? "replace" : left !== void 0 && right !== void 0 ? "replace" : left !== void 0 ? "delete" : "insert";
|
|
1882
|
+
if (blockKind !== nextKind) {
|
|
1883
|
+
flushBlock();
|
|
1884
|
+
blockKind = nextKind;
|
|
1885
|
+
blockLeftStart = renderedLeftLines.length;
|
|
1886
|
+
blockRightStart = renderedRightLines.length;
|
|
1887
|
+
}
|
|
1888
|
+
let renderedLeftLine;
|
|
1889
|
+
let renderedRightLine;
|
|
1890
|
+
if (left !== void 0) {
|
|
1891
|
+
renderedLeftLine = makeDiffLine("removed", left, leftLineNumber);
|
|
1892
|
+
renderedLeftLines.push(renderedLeftLine);
|
|
1893
|
+
}
|
|
1894
|
+
if (right !== void 0) {
|
|
1895
|
+
renderedRightLine = makeDiffLine("added", right, rightLineNumber);
|
|
1896
|
+
renderedRightLines.push(renderedRightLine);
|
|
1897
|
+
}
|
|
1898
|
+
if (renderedLeftLine && renderedRightLine) {
|
|
1899
|
+
applyInlineHighlightPair(renderedLeftLine, renderedRightLine);
|
|
1900
|
+
} else if (coherentBlock) {
|
|
1901
|
+
if (renderedLeftLine) applyFullLineHighlight(renderedLeftLine);
|
|
1902
|
+
if (renderedRightLine) applyFullLineHighlight(renderedRightLine);
|
|
1903
|
+
}
|
|
1904
|
+
rows.push(makeDiffRow(
|
|
1905
|
+
left === void 0 ? makePlaceholder() : makeDiffCell("removed", left, leftLineNumber++),
|
|
1906
|
+
right === void 0 ? makePlaceholder() : makeDiffCell("added", right, rightLineNumber++)
|
|
1907
|
+
));
|
|
1908
|
+
}
|
|
1909
|
+
flushBlock();
|
|
1910
|
+
return { left: leftLineNumber, right: rightLineNumber };
|
|
1911
|
+
}
|
|
1912
|
+
function hasCredibleLineCorrespondence(left, right) {
|
|
1913
|
+
const score = scoreReplacementLinePair(left, right);
|
|
1914
|
+
if (score.eligible) return true;
|
|
1915
|
+
const leftAnchor = declarationAnchorKey(left);
|
|
1916
|
+
if (leftAnchor && leftAnchor === declarationAnchorKey(right)) return true;
|
|
1917
|
+
const leftTokens = new Set(tokenizeMatchingContent(normalizeMatchingContent(left)).filter(isSharedContentToken));
|
|
1918
|
+
return tokenizeMatchingContent(normalizeMatchingContent(right)).some((token) => leftTokens.has(token) && isSharedContentToken(token));
|
|
1919
|
+
}
|
|
1920
|
+
var NON_SUBSTANTIVE_SHARED_TOKENS = /* @__PURE__ */ new Set([
|
|
1921
|
+
"and",
|
|
1922
|
+
"are",
|
|
1923
|
+
"for",
|
|
1924
|
+
"from",
|
|
1925
|
+
"into",
|
|
1926
|
+
"that",
|
|
1927
|
+
"the",
|
|
1928
|
+
"this",
|
|
1929
|
+
"to",
|
|
1930
|
+
"with"
|
|
1931
|
+
]);
|
|
1932
|
+
function isSharedContentToken(token) {
|
|
1933
|
+
return token.length >= 3 && /^[\p{L}\p{N}_$]+$/u.test(token) && !NON_SUBSTANTIVE_SHARED_TOKENS.has(token);
|
|
1934
|
+
}
|
|
1935
|
+
function hasCoherentBlockCorrespondence(leftLines, rightLines, crediblePairs) {
|
|
1936
|
+
if (crediblePairs === 0) return false;
|
|
1937
|
+
const longestSide = Math.max(leftLines.length, rightLines.length);
|
|
1938
|
+
if (longestSide <= 2 || crediblePairs * 2 >= longestSide) return true;
|
|
1939
|
+
const roles = [...leftLines, ...rightLines].map(collectionLineRole);
|
|
1940
|
+
return roles[0] !== null && roles.every((role) => role === roles[0]);
|
|
1941
|
+
}
|
|
1942
|
+
function collectionLineRole(line) {
|
|
1943
|
+
const normalized = line.trimStart();
|
|
1944
|
+
if (/^[-*+]\s+/.test(normalized)) return "bullet";
|
|
1945
|
+
if (/^\d+[.)]\s+/.test(normalized)) return "ordered";
|
|
1946
|
+
if (/^>\s+/.test(normalized)) return "quote";
|
|
1947
|
+
return null;
|
|
1948
|
+
}
|
|
1681
1949
|
var MAX_ALIGNMENT_CELLS = 1e4;
|
|
1682
1950
|
var MAX_SCORING_LINE_LENGTH = 2e3;
|
|
1683
1951
|
var MINIMUM_MATCH_SCORE = 0.52;
|
|
@@ -1690,6 +1958,7 @@ var CONTEXTUAL_AMBIGUITY_MARGIN = 0.05;
|
|
|
1690
1958
|
var GAP_PENALTY = -0.12;
|
|
1691
1959
|
var MAX_BOUNDED_CANDIDATES = 5e3;
|
|
1692
1960
|
var MAX_LARGE_HUNK_ANCHORS = 2e3;
|
|
1961
|
+
var MAX_ALIGNMENT_CHARACTER_CELLS = 5e7;
|
|
1693
1962
|
var MAX_RARE_TOKEN_OCCURRENCES = 4;
|
|
1694
1963
|
var MAX_CONTEXTUAL_HUNK_LINES = 8;
|
|
1695
1964
|
var MAX_CONTEXTUAL_LENGTH_DELTA = 4;
|
|
@@ -1700,9 +1969,17 @@ function alignReplacementLines(leftLines, rightLines) {
|
|
|
1700
1969
|
if (rightLines.length === 0) {
|
|
1701
1970
|
return leftLines.map((left) => ({ left }));
|
|
1702
1971
|
}
|
|
1972
|
+
const leftCharacterCount = leftLines.reduce((total, line) => total + line.length, 0);
|
|
1973
|
+
const rightCharacterCount = rightLines.reduce((total, line) => total + line.length, 0);
|
|
1974
|
+
if (leftCharacterCount * rightCharacterCount > MAX_ALIGNMENT_CHARACTER_CELLS) {
|
|
1975
|
+
return [
|
|
1976
|
+
...leftLines.map((left) => ({ left })),
|
|
1977
|
+
...rightLines.map((right) => ({ right }))
|
|
1978
|
+
];
|
|
1979
|
+
}
|
|
1703
1980
|
if (leftLines.length === 1 && rightLines.length === 1) {
|
|
1704
1981
|
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]))) {
|
|
1982
|
+
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
1983
|
return [{ left: leftLines[0], right: rightLines[0] }];
|
|
1707
1984
|
}
|
|
1708
1985
|
return [{ left: leftLines[0] }, { right: rightLines[0] }];
|
|
@@ -2224,6 +2501,11 @@ function applyInlineHighlightPair(leftLine, rightLine) {
|
|
|
2224
2501
|
rightLine.segments = rightSegments;
|
|
2225
2502
|
}
|
|
2226
2503
|
}
|
|
2504
|
+
function applyFullLineHighlight(line) {
|
|
2505
|
+
if (/[^\s]/.test(line.content)) {
|
|
2506
|
+
line.segments = [{ kind: line.kind, text: line.content, emphasis: true }];
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2227
2509
|
function buildInlineSegments(leftContent, rightContent) {
|
|
2228
2510
|
const changes = diffWordsWithSpace(leftContent, rightContent);
|
|
2229
2511
|
const leftSegments = [];
|
|
@@ -2375,6 +2657,125 @@ function findBlankLineBoundary(lines, target, minimum, maximum) {
|
|
|
2375
2657
|
return Math.min(Math.max(target, minimum), maximum - 1);
|
|
2376
2658
|
}
|
|
2377
2659
|
|
|
2660
|
+
// src/changeAttention.ts
|
|
2661
|
+
var CHANGE_ATTENTION_VERSION = 1;
|
|
2662
|
+
var LOCKFILES = /* @__PURE__ */ new Set([
|
|
2663
|
+
"cargo.lock",
|
|
2664
|
+
"package-lock.json",
|
|
2665
|
+
"pnpm-lock.yaml",
|
|
2666
|
+
"yarn.lock",
|
|
2667
|
+
"poetry.lock",
|
|
2668
|
+
"gemfile.lock",
|
|
2669
|
+
"composer.lock"
|
|
2670
|
+
]);
|
|
2671
|
+
var MAX_FAN_IN_FILES = 250;
|
|
2672
|
+
var MAX_FAN_IN_TEXT_BYTES = 64 * 1024;
|
|
2673
|
+
function classifyChangeFileRole(filePath) {
|
|
2674
|
+
const lower = filePath.toLowerCase();
|
|
2675
|
+
const fileName = lower.split("/").pop() || lower;
|
|
2676
|
+
if (/(^|\/)(dist|build|generated|vendor)(\/|$)/.test(lower) || /(?:\.min\.js|\.map)$/.test(lower)) return "generated";
|
|
2677
|
+
if (/(^|\/)(tests?|__tests__)(\/|$)/.test(lower) || /(^|\/)test_[^/]+$/.test(lower) || /\.(test|spec)\.[^/]+$/.test(lower)) return "test";
|
|
2678
|
+
if (lower.startsWith("docs/") || fileName === "readme.md" || /\.(md|mdx|rst)$/.test(lower)) return "documentation";
|
|
2679
|
+
if (LOCKFILES.has(fileName) || /(^|\/)(pyproject\.toml|package\.json|go\.mod|go\.sum|cargo\.toml)$/.test(lower)) return "dependency";
|
|
2680
|
+
return "production";
|
|
2681
|
+
}
|
|
2682
|
+
function buildChangeAttention(inputs) {
|
|
2683
|
+
const prepared = inputs.map((input) => prepareFile(input, inputs));
|
|
2684
|
+
const production = prepared.filter((file) => file.role === "production" && !file.mechanical);
|
|
2685
|
+
const documentation = prepared.filter((file) => file.role === "documentation" && !file.mechanical);
|
|
2686
|
+
const eligible = production.length > 0 ? production : documentation;
|
|
2687
|
+
const entry = [...eligible].sort(compareRanked)[0];
|
|
2688
|
+
const fallback = [...prepared].filter((file) => !file.binary).sort((left, right) => right.changedLines - left.changedLines || left.path.localeCompare(right.path))[0];
|
|
2689
|
+
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)));
|
|
2690
|
+
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;
|
|
2691
|
+
if (pairedTest && focusCandidates.length < Math.max(0, 7 - (entry ? 1 : 0))) focusCandidates.push(pairedTest);
|
|
2692
|
+
const focusPaths = focusCandidates.map((file) => file.path);
|
|
2693
|
+
const files = prepared.map((file) => {
|
|
2694
|
+
const attention = file.path === (entry == null ? void 0 : entry.path) ? "entry" : focusPaths.includes(file.path) ? "focus" : file.mechanical ? "mechanical" : "background";
|
|
2695
|
+
return {
|
|
2696
|
+
path: file.path,
|
|
2697
|
+
previousPath: file.previousPath,
|
|
2698
|
+
changeKind: file.changeKind,
|
|
2699
|
+
role: file.role,
|
|
2700
|
+
attention,
|
|
2701
|
+
additions: file.additions,
|
|
2702
|
+
deletions: file.deletions,
|
|
2703
|
+
reason: attention === "entry" ? entryReason(file, production.length) : attentionReason(file),
|
|
2704
|
+
score: file.score
|
|
2705
|
+
};
|
|
2706
|
+
}).sort((left, right) => left.path.localeCompare(right.path));
|
|
2707
|
+
return {
|
|
2708
|
+
version: CHANGE_ATTENTION_VERSION,
|
|
2709
|
+
entryPath: (entry == null ? void 0 : entry.path) || null,
|
|
2710
|
+
entryReason: entry ? entryReason(entry, production.length) : noEntryReason(prepared),
|
|
2711
|
+
fallbackPath: (fallback == null ? void 0 : fallback.path) || null,
|
|
2712
|
+
focusPaths,
|
|
2713
|
+
backgroundPaths: files.filter((file) => file.attention === "background").map((file) => file.path),
|
|
2714
|
+
mechanicalPaths: files.filter((file) => file.attention === "mechanical").map((file) => file.path),
|
|
2715
|
+
files
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
function prepareFile(input, all) {
|
|
2719
|
+
const role = classifyChangeFileRole(input.path);
|
|
2720
|
+
const fileName = input.path.toLowerCase().split("/").pop() || input.path.toLowerCase();
|
|
2721
|
+
const mechanical = Boolean(input.binary) || role === "generated" || LOCKFILES.has(fileName);
|
|
2722
|
+
const changedLines = (input.additions ?? 0) + (input.deletions ?? 0);
|
|
2723
|
+
const contract = /(^|[\/._-])(types?|schema|models?|contracts?|interfaces?)([\/._-]|$)/i.test(input.path);
|
|
2724
|
+
const referenceHints = new Set([
|
|
2725
|
+
fileName.replace(/\.[^.]+$/, ""),
|
|
2726
|
+
...(input.symbolHints || []).map((hint) => hint.toLowerCase())
|
|
2727
|
+
].filter((hint) => hint.length >= 4));
|
|
2728
|
+
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) => {
|
|
2729
|
+
var _a;
|
|
2730
|
+
const boundedText = (_a = candidate.headText) == null ? void 0 : _a.slice(0, MAX_FAN_IN_TEXT_BYTES).toLowerCase();
|
|
2731
|
+
return Boolean(boundedText && [...referenceHints].some((hint) => boundedText.includes(hint)));
|
|
2732
|
+
}).length;
|
|
2733
|
+
const roleScore = role === "production" ? 300 : role === "documentation" ? 150 : role === "dependency" ? 80 : role === "test" ? 50 : 0;
|
|
2734
|
+
const sizePenalty = changedLines > 2e3 ? 80 : changedLines > 500 ? 30 : 0;
|
|
2735
|
+
const readmePenalty = fileName === "readme.md" && all.filter((candidate) => !candidate.binary).length > 1 ? 100 : 0;
|
|
2736
|
+
return {
|
|
2737
|
+
...input,
|
|
2738
|
+
role,
|
|
2739
|
+
mechanical,
|
|
2740
|
+
changedLines,
|
|
2741
|
+
contract,
|
|
2742
|
+
fanIn,
|
|
2743
|
+
binary: Boolean(input.binary),
|
|
2744
|
+
score: mechanical ? -1e3 : roleScore + (contract ? 100 : 0) + fanIn * 40 + (input.commitCount || 0) * 5 - sizePenalty - readmePenalty
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
function compareRanked(left, right) {
|
|
2748
|
+
return right.score - left.score || right.fanIn - left.fanIn || left.changedLines - right.changedLines || left.path.localeCompare(right.path);
|
|
2749
|
+
}
|
|
2750
|
+
function entryReason(file, productionCount) {
|
|
2751
|
+
if (file.fanIn > 0) return `Referenced by ${file.fanIn} other changed file${file.fanIn === 1 ? "" : "s"}.`;
|
|
2752
|
+
if (file.contract) return "Contract-shaped path central to the change.";
|
|
2753
|
+
if (productionCount === 1) return "Only production file in this range.";
|
|
2754
|
+
return "Highest-ranked production file for a first pass.";
|
|
2755
|
+
}
|
|
2756
|
+
function attentionReason(file) {
|
|
2757
|
+
if (file.binary) return "Binary file.";
|
|
2758
|
+
if (file.mechanical) return file.role === "generated" ? "Generated artifact." : "Dependency lockfile.";
|
|
2759
|
+
if (file.role === "test") return "Test or proof file.";
|
|
2760
|
+
if (file.role === "documentation") return "Documentation change.";
|
|
2761
|
+
if (file.role === "dependency") return "Dependency or packaging manifest.";
|
|
2762
|
+
return "Additional production file.";
|
|
2763
|
+
}
|
|
2764
|
+
function noEntryReason(files) {
|
|
2765
|
+
if (files.every((file) => file.mechanical)) return "This change is entirely mechanical or generated.";
|
|
2766
|
+
if (files.every((file) => file.mechanical || file.role === "test")) return "This change contains only tests and mechanical files.";
|
|
2767
|
+
return "No single entry-point is reliable for this change.";
|
|
2768
|
+
}
|
|
2769
|
+
function isTestForFocus(testPath, focusPaths) {
|
|
2770
|
+
const normalizedTest = testPath.toLowerCase().replace(/\.(test|spec)(?=\.)/, "").replace(/^test_/, "");
|
|
2771
|
+
return focusPaths.some((candidate) => {
|
|
2772
|
+
var _a;
|
|
2773
|
+
const normalizedCandidate = candidate.toLowerCase();
|
|
2774
|
+
const stem = ((_a = normalizedCandidate.split("/").pop()) == null ? void 0 : _a.replace(/\.[^.]+$/, "")) || normalizedCandidate;
|
|
2775
|
+
return stem.length >= 3 && normalizedTest.includes(stem);
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2378
2779
|
// src/changeTourContext.ts
|
|
2379
2780
|
var DEFAULT_MAX_PATCH_BYTES = 128 * 1024;
|
|
2380
2781
|
var DEFAULT_MAX_TOTAL_PATCH_BYTES = 2 * 1024 * 1024;
|
|
@@ -2393,8 +2794,12 @@ var CHAPTERS = {
|
|
|
2393
2794
|
packaging: { id: "packaging", title: "Packaging and dependencies", priority: 4 }
|
|
2394
2795
|
};
|
|
2395
2796
|
function buildChangeTourManifest(startPath, options = {}) {
|
|
2396
|
-
var _a, _b, _c, _d, _e;
|
|
2397
|
-
const range = resolveBranchReviewRange(
|
|
2797
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
2798
|
+
const range = resolveBranchReviewRange(
|
|
2799
|
+
startPath,
|
|
2800
|
+
options.headRef ?? ((_b = (_a = options.source) == null ? void 0 : _a.range) == null ? void 0 : _b.head),
|
|
2801
|
+
options.baseRef ?? ((_d = (_c = options.source) == null ? void 0 : _c.range) == null ? void 0 : _d.base)
|
|
2802
|
+
);
|
|
2398
2803
|
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_TOUR_FILE_BYTES;
|
|
2399
2804
|
const maxLineBytes = options.maxLineBytes ?? DEFAULT_MAX_TOUR_LINE_BYTES;
|
|
2400
2805
|
const omittedFiles = [];
|
|
@@ -2402,8 +2807,9 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2402
2807
|
const sceneRecords = [];
|
|
2403
2808
|
let totalAdditions = 0;
|
|
2404
2809
|
let totalDeletions = 0;
|
|
2810
|
+
let totalBinaryFiles = 0;
|
|
2405
2811
|
for (const changedPath of range.changedPaths) {
|
|
2406
|
-
const { additions, deletions } = readGitLineStats(
|
|
2812
|
+
const { additions, deletions, binary } = readGitLineStats(
|
|
2407
2813
|
range.repoRoot,
|
|
2408
2814
|
range.mergeBaseOid,
|
|
2409
2815
|
range.headOid,
|
|
@@ -2412,6 +2818,7 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2412
2818
|
);
|
|
2413
2819
|
totalAdditions += additions;
|
|
2414
2820
|
totalDeletions += deletions;
|
|
2821
|
+
if (binary) totalBinaryFiles += 1;
|
|
2415
2822
|
const pair = resolveReviewPathPair(range.changedPaths, changedPath.path);
|
|
2416
2823
|
if (!pair) {
|
|
2417
2824
|
omittedFiles.push(changedPath.path);
|
|
@@ -2452,21 +2859,50 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2452
2859
|
}
|
|
2453
2860
|
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
2454
2861
|
sceneRecords.sort((left, right) => left.chapter.priority - right.chapter.priority || left.scene.path.localeCompare(right.scene.path));
|
|
2455
|
-
|
|
2862
|
+
let defaultScenes = sceneRecords.map(({ scene }, index) => ({ ...scene, id: `file-${index + 1}` }));
|
|
2456
2863
|
if (options.story && options.source) {
|
|
2457
2864
|
throw new Error("A change tour can use either a legacy story or a source file, not both.");
|
|
2458
2865
|
}
|
|
2459
|
-
const
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2866
|
+
const attention = buildChangeAttention(files.map((file) => ({
|
|
2867
|
+
path: file.path,
|
|
2868
|
+
previousPath: file.previousPath,
|
|
2869
|
+
changeKind: file.changeKind,
|
|
2870
|
+
additions: file.additions,
|
|
2871
|
+
deletions: file.deletions,
|
|
2872
|
+
binary: file.kind === "omitted",
|
|
2873
|
+
headText: file.kind === "text-diff" ? file.rightContent : void 0,
|
|
2874
|
+
symbolHints: file.kind === "text-diff" ? findDeclarationNames(file.rightContent) : []
|
|
2875
|
+
})));
|
|
2876
|
+
if (!options.story && !options.source && attention.entryPath) {
|
|
2877
|
+
const focusRank = (scenePath) => {
|
|
2878
|
+
const index = attention.focusPaths.indexOf(scenePath);
|
|
2879
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
2880
|
+
};
|
|
2881
|
+
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));
|
|
2882
|
+
}
|
|
2883
|
+
const authored = options.source ? applySource(parseChangeTourSource(options.source), defaultScenes, range.repoRoot) : options.story ? applyStory(options.story, defaultScenes) : buildGeneratedTourLanding(
|
|
2884
|
+
defaultScenes,
|
|
2885
|
+
buildChapters(sceneRecords, new Map(defaultScenes.map((scene) => [scene.path, scene.id]))),
|
|
2886
|
+
attention,
|
|
2887
|
+
{
|
|
2888
|
+
changedFiles: range.changedPaths.length,
|
|
2889
|
+
binaryFiles: totalBinaryFiles,
|
|
2890
|
+
additions: totalAdditions,
|
|
2891
|
+
deletions: totalDeletions,
|
|
2892
|
+
commitCount: range.commits.length
|
|
2893
|
+
}
|
|
2894
|
+
);
|
|
2463
2895
|
const scenes = authored.scenes;
|
|
2464
2896
|
const chapters = authored.chapters;
|
|
2897
|
+
const finalScenes = "finalScenes" in authored ? authored.finalScenes : scenes;
|
|
2898
|
+
const finalChapters = "finalChapters" in authored ? authored.finalChapters : chapters;
|
|
2465
2899
|
const manifest = {
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2900
|
+
// Only authored v2 files opt into repository-bound zoom modes. Generated
|
|
2901
|
+
// and legacy/story tours retain the portable v1 manifest contract.
|
|
2902
|
+
version: ((_e = options.source) == null ? void 0 : _e.version) ?? 1,
|
|
2903
|
+
title: ((_f = options.source) == null ? void 0 : _f.title) || ((_g = options.story) == null ? void 0 : _g.title) || options.title || `${range.headRef} against ${range.baseRef}`,
|
|
2904
|
+
windowTitle: (_h = options.source) == null ? void 0 : _h.windowTitle,
|
|
2905
|
+
sourceUrl: ((_i = options.source) == null ? void 0 : _i.sourceUrl) || ((_j = options.story) == null ? void 0 : _j.sourceUrl) || options.sourceUrl,
|
|
2470
2906
|
generatedAt: options.generatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2471
2907
|
range: {
|
|
2472
2908
|
baseRef: range.baseRef,
|
|
@@ -2487,6 +2923,49 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2487
2923
|
chapters,
|
|
2488
2924
|
scenes
|
|
2489
2925
|
};
|
|
2926
|
+
if (manifest.version === 2) {
|
|
2927
|
+
manifest.repository = { root: range.repoRoot };
|
|
2928
|
+
const revisions = [];
|
|
2929
|
+
for (const scene of scenes) {
|
|
2930
|
+
let realStack;
|
|
2931
|
+
if (scene.kind === "stacked-diff") realStack = scene;
|
|
2932
|
+
if (scene.kind === "deconstructed-diff") {
|
|
2933
|
+
const authoredScene = (_k = options.source) == null ? void 0 : _k.chapters.flatMap((chapter) => chapter.scenes).find((candidate) => candidate.id === scene.id);
|
|
2934
|
+
if ((authoredScene == null ? void 0 : authoredScene.kind) !== "deconstructed-diff" || !authoredScene.stack) {
|
|
2935
|
+
throw new Error(`Deconstructed scene ${scene.id} requires an explicit real revision stack in v2.`);
|
|
2936
|
+
}
|
|
2937
|
+
realStack = buildStackedScene(range.repoRoot, {
|
|
2938
|
+
...authoredScene,
|
|
2939
|
+
kind: "stacked-diff",
|
|
2940
|
+
stack: authoredScene.stack,
|
|
2941
|
+
files: scene.files.map((file) => file.path),
|
|
2942
|
+
steps: []
|
|
2943
|
+
}, files.map((file) => file.path));
|
|
2944
|
+
if (realStack.stack[0].oid !== scene.realRange.baseOid || realStack.stack[realStack.stack.length - 1].oid !== scene.realRange.targetOid) {
|
|
2945
|
+
throw new Error(`Deconstructed scene ${scene.id} real stack endpoints must match its explanation range.`);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
if (realStack) {
|
|
2949
|
+
if (realStack.stack[0].oid !== range.mergeBaseOid || realStack.stack[realStack.stack.length - 1].oid !== range.headOid) {
|
|
2950
|
+
throw new Error(`Scene ${scene.id} real stack endpoints must match the tour's final diff range.`);
|
|
2951
|
+
}
|
|
2952
|
+
revisions.push(realStack);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
const explanation = scenes.some((scene) => scene.kind === "deconstructed-diff");
|
|
2956
|
+
const distinctRevisions = revisions.some((scene) => scene.stack.length > 2);
|
|
2957
|
+
manifest.zoom = {
|
|
2958
|
+
authoredDepth: explanation ? "explanation" : distinctRevisions ? "revisions" : "final",
|
|
2959
|
+
modes: [
|
|
2960
|
+
...explanation ? ["explanation"] : [],
|
|
2961
|
+
...distinctRevisions ? ["revisions"] : [],
|
|
2962
|
+
"final",
|
|
2963
|
+
"history"
|
|
2964
|
+
],
|
|
2965
|
+
revisions,
|
|
2966
|
+
final: { scenes: finalScenes, chapters: finalChapters }
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2490
2969
|
return parseChangeTourManifest(manifest);
|
|
2491
2970
|
}
|
|
2492
2971
|
function applySource(source, defaultScenes, repoRoot) {
|
|
@@ -2509,8 +2988,11 @@ function applySource(source, defaultScenes, repoRoot) {
|
|
|
2509
2988
|
}]));
|
|
2510
2989
|
const scenes = [];
|
|
2511
2990
|
const chapters = [];
|
|
2991
|
+
const finalScenes = [];
|
|
2992
|
+
const finalChapters = [];
|
|
2512
2993
|
for (const chapter of source.chapters) {
|
|
2513
2994
|
const sceneIds = [];
|
|
2995
|
+
const finalSceneIds = [];
|
|
2514
2996
|
for (const authoredScene of chapter.scenes) {
|
|
2515
2997
|
if (authoredScene.kind === "deconstructed-diff") {
|
|
2516
2998
|
const deconstructedScene = buildDeconstructedManifestScene(
|
|
@@ -2522,6 +3004,16 @@ function applySource(source, defaultScenes, repoRoot) {
|
|
|
2522
3004
|
);
|
|
2523
3005
|
scenes.push(deconstructedScene);
|
|
2524
3006
|
sceneIds.push(deconstructedScene.id);
|
|
3007
|
+
if (authoredScene.steps) {
|
|
3008
|
+
const finalScene = buildWalkthroughManifestScene(
|
|
3009
|
+
{ ...authoredScene, steps: authoredScene.steps },
|
|
3010
|
+
available,
|
|
3011
|
+
resolvedAnchors,
|
|
3012
|
+
connections
|
|
3013
|
+
);
|
|
3014
|
+
finalScenes.push(finalScene);
|
|
3015
|
+
finalSceneIds.push(finalScene.id);
|
|
3016
|
+
}
|
|
2525
3017
|
continue;
|
|
2526
3018
|
}
|
|
2527
3019
|
if (authoredScene.kind === "stacked-diff") {
|
|
@@ -2532,37 +3024,51 @@ function applySource(source, defaultScenes, repoRoot) {
|
|
|
2532
3024
|
);
|
|
2533
3025
|
scenes.push(stackedScene);
|
|
2534
3026
|
sceneIds.push(stackedScene.id);
|
|
3027
|
+
for (const [index, file] of stackedScene.files.entries()) {
|
|
3028
|
+
const endpoint = available.get(file.path);
|
|
3029
|
+
if (!endpoint) continue;
|
|
3030
|
+
const finalScene = { ...endpoint, id: `${stackedScene.id}-final-${index + 1}`, title: file.path };
|
|
3031
|
+
finalScenes.push(finalScene);
|
|
3032
|
+
finalSceneIds.push(finalScene.id);
|
|
3033
|
+
}
|
|
2535
3034
|
continue;
|
|
2536
3035
|
}
|
|
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
|
-
};
|
|
3036
|
+
const scene = buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections);
|
|
2560
3037
|
scenes.push(scene);
|
|
2561
3038
|
sceneIds.push(scene.id);
|
|
3039
|
+
finalScenes.push(scene);
|
|
3040
|
+
finalSceneIds.push(scene.id);
|
|
2562
3041
|
}
|
|
2563
3042
|
chapters.push({ id: chapter.id, title: chapter.title, sceneIds });
|
|
3043
|
+
finalChapters.push({ id: chapter.id, title: chapter.title, sceneIds: finalSceneIds });
|
|
2564
3044
|
}
|
|
2565
|
-
return { scenes, chapters };
|
|
3045
|
+
return { scenes, chapters, finalScenes, finalChapters };
|
|
3046
|
+
}
|
|
3047
|
+
function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections) {
|
|
3048
|
+
return {
|
|
3049
|
+
id: authoredScene.id,
|
|
3050
|
+
kind: "walkthrough",
|
|
3051
|
+
title: authoredScene.title,
|
|
3052
|
+
summary: authoredScene.summary,
|
|
3053
|
+
bullets: authoredScene.bullets,
|
|
3054
|
+
tags: authoredScene.tags,
|
|
3055
|
+
takeaway: authoredScene.takeaway,
|
|
3056
|
+
steps: authoredScene.steps.map((step) => {
|
|
3057
|
+
const focus = requireResolvedAnchor(resolvedAnchors, step.focus);
|
|
3058
|
+
const diff2 = available.get(focus.path);
|
|
3059
|
+
if (!diff2) throw new Error(`Step ${step.id} has no text diff for ${focus.path}.`);
|
|
3060
|
+
return {
|
|
3061
|
+
id: step.id,
|
|
3062
|
+
title: step.title,
|
|
3063
|
+
body: step.body,
|
|
3064
|
+
depth: step.depth,
|
|
3065
|
+
requirement: step.requirement,
|
|
3066
|
+
focus,
|
|
3067
|
+
connection: step.connection ? connections.get(step.connection) : void 0,
|
|
3068
|
+
diff: { ...diff2, id: `${authoredScene.id}-${step.id}` }
|
|
3069
|
+
};
|
|
3070
|
+
})
|
|
3071
|
+
};
|
|
2566
3072
|
}
|
|
2567
3073
|
function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, availableFilePaths = []) {
|
|
2568
3074
|
const compiled = buildDeconstructedScene(repoRoot, source, { baseRef, headRef: targetRef });
|
|
@@ -2728,7 +3234,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2728
3234
|
let totalContentBytes = 0;
|
|
2729
3235
|
const files = filePaths.flatMap((filePath) => {
|
|
2730
3236
|
const aliases = /* @__PURE__ */ new Set([filePath]);
|
|
2731
|
-
for (const changes of adjacentChanges) {
|
|
3237
|
+
for (const changes of [...adjacentChanges].reverse()) {
|
|
2732
3238
|
for (const change of changes) {
|
|
2733
3239
|
if (aliases.has(change.path) || change.previousPath && aliases.has(change.previousPath)) {
|
|
2734
3240
|
aliases.add(change.path);
|
|
@@ -2739,7 +3245,9 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2739
3245
|
let fileContentBytes = 0;
|
|
2740
3246
|
const panels = [];
|
|
2741
3247
|
for (const entry of stack) {
|
|
2742
|
-
const
|
|
3248
|
+
const candidates = [...aliases].filter((candidate) => gitBlobExists(repoRoot, entry.oid, candidate));
|
|
3249
|
+
if (candidates.length > 1) throw new Error(`Ambiguous revision paths for ${filePath} at ${entry.ref}: ${candidates.join(", ")}`);
|
|
3250
|
+
const resolvedPath = candidates[0];
|
|
2743
3251
|
if (!resolvedPath) {
|
|
2744
3252
|
panels.push({ id: entry.id, label: `${entry.label} / ${filePath} (missing)`, content: "", exists: false });
|
|
2745
3253
|
continue;
|
|
@@ -2766,6 +3274,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2766
3274
|
}
|
|
2767
3275
|
return [{ path: filePath, panels }];
|
|
2768
3276
|
});
|
|
3277
|
+
if (files.length === 0) throw new Error(`Stacked scene ${source.id} has no materializable text files.`);
|
|
2769
3278
|
const stackIds = stack.map((entry) => entry.id);
|
|
2770
3279
|
return {
|
|
2771
3280
|
id: source.id,
|
|
@@ -2777,7 +3286,14 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2777
3286
|
takeaway: source.takeaway,
|
|
2778
3287
|
stack,
|
|
2779
3288
|
files,
|
|
2780
|
-
steps: source.steps.map((
|
|
3289
|
+
steps: source.steps.length === 0 ? stack.slice(0, -1).map((_, pairIndex) => ({
|
|
3290
|
+
id: `${source.id}-revision-${pairIndex}`,
|
|
3291
|
+
title: "",
|
|
3292
|
+
body: "",
|
|
3293
|
+
file: files[0].path,
|
|
3294
|
+
pairIndex,
|
|
3295
|
+
side: "right"
|
|
3296
|
+
})) : source.steps.map((step) => {
|
|
2781
3297
|
var _a, _b;
|
|
2782
3298
|
return {
|
|
2783
3299
|
id: step.id,
|
|
@@ -2889,21 +3405,51 @@ function buildChapters(records, sceneIdByPath) {
|
|
|
2889
3405
|
}
|
|
2890
3406
|
function chapterForPath(filePath) {
|
|
2891
3407
|
const lower = filePath.toLowerCase();
|
|
2892
|
-
const
|
|
2893
|
-
if (
|
|
3408
|
+
const role = classifyChangeFileRole(filePath);
|
|
3409
|
+
if (role === "documentation" || lower.includes("architecture")) {
|
|
2894
3410
|
return CHAPTERS.context;
|
|
2895
3411
|
}
|
|
2896
|
-
if (
|
|
3412
|
+
if (role === "test") {
|
|
2897
3413
|
return CHAPTERS.proof;
|
|
2898
3414
|
}
|
|
2899
3415
|
if (/(^|\/)(models?|schema|types?|contracts?)\.[^/]+$/.test(lower)) {
|
|
2900
3416
|
return CHAPTERS.contracts;
|
|
2901
3417
|
}
|
|
2902
|
-
if (
|
|
3418
|
+
if (role === "dependency" || role === "generated") {
|
|
2903
3419
|
return CHAPTERS.packaging;
|
|
2904
3420
|
}
|
|
2905
3421
|
return CHAPTERS.behavior;
|
|
2906
3422
|
}
|
|
3423
|
+
function buildGeneratedTourLanding(scenes, chapters, attention, summary) {
|
|
3424
|
+
const entry = attention.entryPath ? `Start here: ${attention.entryPath} \u2014 ${attention.entryReason}` : `No single entry-point \u2014 ${attention.entryReason}`;
|
|
3425
|
+
const focus = attention.focusPaths.length > 0 ? `Pay attention: ${attention.focusPaths.join(", ")}` : "Pay attention: no additional production files were identified.";
|
|
3426
|
+
const skipped = attention.backgroundPaths.length + attention.mechanicalPaths.length;
|
|
3427
|
+
const overview = {
|
|
3428
|
+
id: "change-overview",
|
|
3429
|
+
kind: "discussion",
|
|
3430
|
+
title: "Change overview",
|
|
3431
|
+
summary: `${summary.changedFiles} files (${summary.changedFiles - summary.binaryFiles} text, ${summary.binaryFiles} binary) \xB7 +${summary.additions} \u2212${summary.deletions} \xB7 ${summary.commitCount} commits`,
|
|
3432
|
+
bullets: [entry, focus, `Usually skip on a first pass: ${skipped} file${skipped === 1 ? "" : "s"}. All files remain available.`],
|
|
3433
|
+
tags: ["start here", "first pass"],
|
|
3434
|
+
takeaway: attention.entryPath ? `Next: open ${attention.entryPath}.` : "Next: open the first available textual change."
|
|
3435
|
+
};
|
|
3436
|
+
return {
|
|
3437
|
+
scenes: [overview, ...scenes],
|
|
3438
|
+
chapters: [
|
|
3439
|
+
{ id: "start-here", title: "Start here", sceneIds: [overview.id] },
|
|
3440
|
+
...chapters
|
|
3441
|
+
]
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
function findDeclarationNames(content) {
|
|
3445
|
+
const names = /* @__PURE__ */ new Set();
|
|
3446
|
+
for (const line of content.split("\n")) {
|
|
3447
|
+
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*)/);
|
|
3448
|
+
if (match) names.add(match[1]);
|
|
3449
|
+
if (names.size >= 25) break;
|
|
3450
|
+
}
|
|
3451
|
+
return [...names];
|
|
3452
|
+
}
|
|
2907
3453
|
function buildSceneNote(kind, additions, deletions, previousPath) {
|
|
2908
3454
|
const label = formatChangeKind(kind);
|
|
2909
3455
|
const rename = previousPath ? ` from ${previousPath}` : "";
|
|
@@ -2966,19 +3512,31 @@ function readGitLineStats(repoRoot, baseOid, headOid, filePath, previousPath) {
|
|
|
2966
3512
|
const [additionsText = "0", deletionsText = "0"] = output.trim().split(/\s+/, 3);
|
|
2967
3513
|
return {
|
|
2968
3514
|
additions: Number.parseInt(additionsText, 10) || 0,
|
|
2969
|
-
deletions: Number.parseInt(deletionsText, 10) || 0
|
|
3515
|
+
deletions: Number.parseInt(deletionsText, 10) || 0,
|
|
3516
|
+
binary: additionsText === "-" || deletionsText === "-"
|
|
2970
3517
|
};
|
|
2971
3518
|
}
|
|
2972
3519
|
|
|
2973
3520
|
// src/tourCoverage.ts
|
|
2974
|
-
var TOUR_COVERAGE_REPORT_VERSION =
|
|
3521
|
+
var TOUR_COVERAGE_REPORT_VERSION = 2;
|
|
3522
|
+
function buildTourAuthoringCoverage(report) {
|
|
3523
|
+
return {
|
|
3524
|
+
walkthrough: {
|
|
3525
|
+
scope: report.version === 2 ? "final" : "tour",
|
|
3526
|
+
coveredUnits: report.totals.coveredUnits,
|
|
3527
|
+
includedUnits: report.totals.includedUnits,
|
|
3528
|
+
coveragePercent: report.totals.coveragePercent
|
|
3529
|
+
},
|
|
3530
|
+
explanationAssignments: report.version === 2 ? report.explanationAssignments : []
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
2975
3533
|
var DEPTH_ORDER = {
|
|
2976
3534
|
mentioned: 0,
|
|
2977
3535
|
explained: 1,
|
|
2978
3536
|
contextualized: 2
|
|
2979
3537
|
};
|
|
2980
3538
|
function buildTourCoverageReport(startPath, sourceValue) {
|
|
2981
|
-
var _a, _b, _c, _d, _e;
|
|
3539
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2982
3540
|
const source = parseChangeTourSource(sourceValue);
|
|
2983
3541
|
const inventory = buildChangeInventory(startPath, {
|
|
2984
3542
|
headRef: (_a = source.range) == null ? void 0 : _a.head,
|
|
@@ -2994,7 +3552,9 @@ function buildTourCoverageReport(startPath, sourceValue) {
|
|
|
2994
3552
|
const includedUnits = textualUnits.filter((unit) => !excludedIds.has(unit.id));
|
|
2995
3553
|
const coverage = /* @__PURE__ */ new Map();
|
|
2996
3554
|
const contextualOnlyEvidence = [];
|
|
2997
|
-
|
|
3555
|
+
const percentageDecimalPlaces = manifest.version === 2 ? 2 : 0;
|
|
3556
|
+
const walkthroughScenes = manifest.version === 2 ? requireFinalScenes((_e = manifest.zoom) == null ? void 0 : _e.final.scenes) : manifest.scenes;
|
|
3557
|
+
for (const scene of walkthroughScenes) {
|
|
2998
3558
|
if (scene.kind !== "walkthrough") continue;
|
|
2999
3559
|
for (const step of scene.steps) {
|
|
3000
3560
|
const matches = includedUnits.filter((unit) => evidenceIntersectsUnit(step.focus, unit));
|
|
@@ -3015,11 +3575,10 @@ function buildTourCoverageReport(startPath, sourceValue) {
|
|
|
3015
3575
|
const highest = covered.depths.reduce((left, right) => DEPTH_ORDER[right] > DEPTH_ORDER[left] ? right : left);
|
|
3016
3576
|
depth[highest] += 1;
|
|
3017
3577
|
}
|
|
3018
|
-
const files = inventory.files.filter((file) => file.material === "text").map((file) => buildFileResult(file, excludedIds, coverage));
|
|
3578
|
+
const files = inventory.files.filter((file) => file.material === "text").map((file) => buildFileResult(file, excludedIds, coverage, percentageDecimalPlaces));
|
|
3019
3579
|
const includedChangedLines = includedUnits.reduce((total, unit) => total + unit.additions + unit.deletions, 0);
|
|
3020
3580
|
const coveredChangedLines = [...coverage.values()].reduce((total, entry) => total + entry.unit.additions + entry.unit.deletions, 0);
|
|
3021
|
-
|
|
3022
|
-
version: TOUR_COVERAGE_REPORT_VERSION,
|
|
3581
|
+
const report = {
|
|
3023
3582
|
range: {
|
|
3024
3583
|
baseRef: inventory.range.baseRef,
|
|
3025
3584
|
headRef: inventory.range.headRef,
|
|
@@ -3032,13 +3591,13 @@ function buildTourCoverageReport(startPath, sourceValue) {
|
|
|
3032
3591
|
includedUnits: includedUnits.length,
|
|
3033
3592
|
coveredUnits: coverage.size,
|
|
3034
3593
|
uncoveredUnits: includedUnits.length - coverage.size,
|
|
3035
|
-
coveragePercent: percentage(coverage.size, includedUnits.length),
|
|
3594
|
+
coveragePercent: percentage(coverage.size, includedUnits.length, percentageDecimalPlaces),
|
|
3036
3595
|
coveredChangedLines,
|
|
3037
3596
|
includedChangedLines
|
|
3038
3597
|
},
|
|
3039
3598
|
depth,
|
|
3040
3599
|
files,
|
|
3041
|
-
exclusions: (((
|
|
3600
|
+
exclusions: (((_f = source.coverage) == null ? void 0 : _f.exclusions) || []).map((exclusion) => {
|
|
3042
3601
|
var _a2;
|
|
3043
3602
|
return {
|
|
3044
3603
|
path: exclusion.path,
|
|
@@ -3049,6 +3608,34 @@ function buildTourCoverageReport(startPath, sourceValue) {
|
|
|
3049
3608
|
unsupported: inventory.files.filter((file) => file.material !== "text").map((file) => ({ path: file.path, material: file.material })),
|
|
3050
3609
|
contextualOnlyEvidence
|
|
3051
3610
|
};
|
|
3611
|
+
if (manifest.version === 1) return { version: 1, ...report };
|
|
3612
|
+
return {
|
|
3613
|
+
version: TOUR_COVERAGE_REPORT_VERSION,
|
|
3614
|
+
...report,
|
|
3615
|
+
walkthroughScope: "final",
|
|
3616
|
+
explanationAssignments: buildExplanationAssignments(source, inventory.files)
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
function requireFinalScenes(scenes) {
|
|
3620
|
+
if (!scenes) throw new Error("A v2 tour requires Final-mode scenes for coverage reporting.");
|
|
3621
|
+
return scenes;
|
|
3622
|
+
}
|
|
3623
|
+
function buildExplanationAssignments(source, files) {
|
|
3624
|
+
const materializableFiles = files.filter((file) => file.material === "text" && file.changeKind !== "renamed" && file.baseContent !== void 0 && file.headContent !== void 0);
|
|
3625
|
+
const totalUnits = materializableFiles.reduce((total, file) => total + file.units.length, 0);
|
|
3626
|
+
return source.chapters.flatMap((chapter) => chapter.scenes).filter((scene) => scene.kind === "deconstructed-diff").map((scene) => {
|
|
3627
|
+
const assignedUnits = scene.stages.reduce((stageTotal, stage) => stageTotal + stage.changes.reduce((changeTotal, change) => changeTotal + change.hunks.length, 0), 0);
|
|
3628
|
+
const excludedUnits = totalUnits - assignedUnits;
|
|
3629
|
+
const completeUnits = assignedUnits + excludedUnits;
|
|
3630
|
+
return {
|
|
3631
|
+
sceneId: scene.id,
|
|
3632
|
+
assignedUnits,
|
|
3633
|
+
excludedUnits,
|
|
3634
|
+
completeUnits,
|
|
3635
|
+
totalUnits,
|
|
3636
|
+
assignmentPercent: percentage(completeUnits, totalUnits)
|
|
3637
|
+
};
|
|
3638
|
+
});
|
|
3052
3639
|
}
|
|
3053
3640
|
function resolveExclusions(source, files) {
|
|
3054
3641
|
var _a, _b, _c;
|
|
@@ -3075,7 +3662,7 @@ function evidenceIntersectsUnit(focus, unit) {
|
|
|
3075
3662
|
const end = start + count - 1;
|
|
3076
3663
|
return focus.startLine <= end && focus.endLine >= start;
|
|
3077
3664
|
}
|
|
3078
|
-
function buildFileResult(file, excludedIds, coverage) {
|
|
3665
|
+
function buildFileResult(file, excludedIds, coverage, percentageDecimalPlaces) {
|
|
3079
3666
|
const included = file.units.filter((unit) => !excludedIds.has(unit.id));
|
|
3080
3667
|
const covered = included.filter((unit) => coverage.has(unit.id));
|
|
3081
3668
|
return {
|
|
@@ -3083,16 +3670,19 @@ function buildFileResult(file, excludedIds, coverage) {
|
|
|
3083
3670
|
originalUnits: file.units.length,
|
|
3084
3671
|
includedUnits: included.length,
|
|
3085
3672
|
coveredUnits: covered.length,
|
|
3086
|
-
coveragePercent: percentage(covered.length, included.length),
|
|
3673
|
+
coveragePercent: percentage(covered.length, included.length, percentageDecimalPlaces),
|
|
3087
3674
|
uncoveredHunks: included.filter((unit) => !coverage.has(unit.id)).map((unit) => unit.id)
|
|
3088
3675
|
};
|
|
3089
3676
|
}
|
|
3090
|
-
function percentage(numerator, denominator) {
|
|
3091
|
-
|
|
3677
|
+
function percentage(numerator, denominator, decimalPlaces = 0) {
|
|
3678
|
+
if (denominator === 0) return 100;
|
|
3679
|
+
const precision = 10 ** decimalPlaces;
|
|
3680
|
+
return Math.round(numerator / denominator * 100 * precision) / precision;
|
|
3092
3681
|
}
|
|
3093
3682
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3094
3683
|
0 && (module.exports = {
|
|
3095
3684
|
TOUR_COVERAGE_REPORT_VERSION,
|
|
3685
|
+
buildTourAuthoringCoverage,
|
|
3096
3686
|
buildTourCoverageReport
|
|
3097
3687
|
});
|
|
3098
3688
|
//# sourceMappingURL=tourCoverage.js.map
|