@davmash/bygone 0.8.16 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/out/changeTour.js CHANGED
@@ -235,17 +235,29 @@ function readGitMaxBuffer() {
235
235
  }
236
236
 
237
237
  // src/changeTourSource.ts
238
- var CHANGE_TOUR_SOURCE_VERSION = 2;
238
+ var CHANGE_TOUR_SOURCE_VERSION = 3;
239
239
  function parseChangeTourSource(value) {
240
240
  if (isRecord(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_SOURCE_VERSION) {
241
241
  throw new Error(
242
242
  `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.`
243
243
  );
244
244
  }
245
- if (!isRecord(value) || value.version !== 1 && value.version !== CHANGE_TOUR_SOURCE_VERSION) {
245
+ if (!isRecord(value) || value.version !== 1 && value.version !== 2 && value.version !== CHANGE_TOUR_SOURCE_VERSION) {
246
246
  throw new Error("Unsupported or missing change-tour source version.");
247
247
  }
248
- requireOnlyKeys(value, ["version", "title", "windowTitle", "sourceUrl", "range", "anchors", "connections", "chapters", "coverage"], "source");
248
+ requireOnlyKeys(value, [
249
+ "version",
250
+ "title",
251
+ "windowTitle",
252
+ "sourceUrl",
253
+ "range",
254
+ "anchors",
255
+ "connections",
256
+ "chapters",
257
+ "tours",
258
+ "coverage",
259
+ "review"
260
+ ], "source");
249
261
  optionalString(value.title, "title");
250
262
  optionalString(value.windowTitle, "windowTitle");
251
263
  optionalString(value.sourceUrl, "sourceUrl");
@@ -309,30 +321,81 @@ function parseChangeTourSource(value) {
309
321
  }
310
322
  connectionIds.add(id);
311
323
  }
324
+ const authoredWalkthroughSteps = validateSourceChapters(
325
+ value.chapters,
326
+ "chapters",
327
+ value.version,
328
+ value.anchors,
329
+ connectionIds,
330
+ "root"
331
+ );
332
+ if (value.tours !== void 0) {
333
+ if (value.version !== 3) throw new Error("Independent tours require version 3.");
334
+ if (!isRecord(value.tours)) throw new Error("tours must be an object.");
335
+ requireOnlyKeys(value.tours, ["historical", "deconstructed"], "tours");
336
+ if (value.tours.historical === void 0 && value.tours.deconstructed === void 0) {
337
+ throw new Error("tours must contain a historical or deconstructed tour.");
338
+ }
339
+ for (const mode of ["historical", "deconstructed"]) {
340
+ const tour = value.tours[mode];
341
+ if (tour === void 0) continue;
342
+ const path = `tours.${mode}`;
343
+ if (!isRecord(tour)) throw new Error(`${path} must be an object.`);
344
+ requireOnlyKeys(tour, ["chapters"], path);
345
+ validateSourceChapters(tour.chapters, `${path}.chapters`, value.version, value.anchors, connectionIds, mode);
346
+ }
347
+ }
348
+ if (value.review !== void 0) {
349
+ if (value.version !== 3) throw new Error("Review notes require version 3.");
350
+ validateChangeTourReview(value.review, { authoredWalkthroughSteps });
351
+ }
352
+ return { ...value, connections: rawConnections };
353
+ }
354
+ function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, connectionIds, mode) {
355
+ if (!Array.isArray(chaptersValue) || chaptersValue.length === 0) {
356
+ throw new Error(`${chaptersPath} must be a non-empty array.`);
357
+ }
312
358
  const chapterIds = /* @__PURE__ */ new Set();
313
359
  const sceneIds = /* @__PURE__ */ new Set();
314
- for (const [chapterIndex, chapter] of value.chapters.entries()) {
360
+ const authoredWalkthroughSteps = /* @__PURE__ */ new Map();
361
+ for (const [chapterIndex, chapter] of chaptersValue.entries()) {
362
+ const chapterPath = `${chaptersPath}[${chapterIndex}]`;
315
363
  if (!isRecord(chapter) || !Array.isArray(chapter.scenes) || chapter.scenes.length === 0) {
316
- throw new Error(`chapters[${chapterIndex}] must contain a non-empty scenes array.`);
364
+ throw new Error(`${chapterPath} must contain a non-empty scenes array.`);
317
365
  }
318
- requireString(chapter.id, `chapters[${chapterIndex}].id`);
319
- requireString(chapter.title, `chapters[${chapterIndex}].title`);
320
- requireOnlyKeys(chapter, ["id", "title", "scenes"], `chapters[${chapterIndex}]`);
366
+ requireString(chapter.id, `${chapterPath}.id`);
367
+ requireString(chapter.title, `${chapterPath}.title`);
368
+ requireOnlyKeys(chapter, ["id", "title", "scenes"], chapterPath);
321
369
  if (chapterIds.has(chapter.id)) throw new Error(`Duplicate chapter id: ${chapter.id}`);
322
370
  chapterIds.add(chapter.id);
323
371
  for (const [sceneIndex, scene] of chapter.scenes.entries()) {
324
- const path = `chapters[${chapterIndex}].scenes[${sceneIndex}]`;
372
+ const path = `${chapterPath}.scenes[${sceneIndex}]`;
325
373
  if (!isRecord(scene)) throw new Error(`${path} must be an object.`);
326
374
  requireString(scene.id, `${path}.id`);
327
375
  requireString(scene.title, `${path}.title`);
328
376
  if (sceneIds.has(scene.id)) throw new Error(`Duplicate scene id: ${scene.id}`);
329
377
  sceneIds.add(scene.id);
378
+ if (scene.overview !== void 0 && version !== 3) {
379
+ throw new Error(`${path}.overview requires version 3.`);
380
+ }
330
381
  validateNarrative(scene, path);
331
382
  if (scene.kind === "deconstructed-diff") {
383
+ if (mode === "historical") {
384
+ throw new Error(`${path} cannot contain a deconstructed-diff scene in the historical tour.`);
385
+ }
332
386
  validateDeconstructedScene(scene, path);
333
- if (value.version === 2 || scene.stack !== void 0) validateRealStack(scene.stack, `${path}.stack`, 2);
334
- if (value.version === 2 || scene.steps !== void 0) {
335
- validateWalkthroughSteps(scene, path, value.anchors, connectionIds);
387
+ validateSceneOverview(scene.overview, `${path}.overview`, {
388
+ endpointIds: deconstructedOverviewEndpointIds(scene, path)
389
+ });
390
+ const requireRealEvidence = mode === "root" && version >= 2;
391
+ if (requireRealEvidence || scene.stack !== void 0) {
392
+ validateRealStack(scene.stack, `${path}.stack`, 2);
393
+ }
394
+ if (requireRealEvidence || scene.steps !== void 0) {
395
+ validateWalkthroughSteps(scene, path, anchors, connectionIds);
396
+ if (scene.steps !== void 0) {
397
+ authoredWalkthroughSteps.set(scene.id, collectStepIds(scene.steps));
398
+ }
336
399
  }
337
400
  continue;
338
401
  }
@@ -340,20 +403,110 @@ function parseChangeTourSource(value) {
340
403
  throw new Error(`${path} must contain a non-empty steps array.`);
341
404
  }
342
405
  if (scene.kind === "stacked-diff") {
343
- validateStackedScene(scene, path, value.version === 2 ? 2 : 3);
406
+ validateStackedScene(scene, path, mode === "root" ? version >= 2 ? 2 : 3 : 2);
407
+ validateSceneOverview(scene.overview, `${path}.overview`, {
408
+ endpointIds: stackedOverviewEndpointIds(scene, path)
409
+ });
344
410
  continue;
345
411
  }
346
412
  if (scene.kind !== void 0 && scene.kind !== "walkthrough") {
347
413
  throw new Error(`${path}.kind must be walkthrough, stacked-diff, or deconstructed-diff.`);
348
414
  }
349
- requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "steps"], path);
350
- validateWalkthroughSteps(scene, path, value.anchors, connectionIds);
415
+ requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "steps"], path);
416
+ validateSceneOverview(scene.overview, `${path}.overview`, { allowComparison: false });
417
+ validateWalkthroughSteps(scene, path, anchors, connectionIds);
418
+ authoredWalkthroughSteps.set(scene.id, collectStepIds(scene.steps));
419
+ }
420
+ }
421
+ if (mode === "deconstructed" && !chaptersValue.some((chapter) => isRecord(chapter) && Array.isArray(chapter.scenes) && chapter.scenes.some((scene) => isRecord(scene) && scene.kind === "deconstructed-diff"))) {
422
+ throw new Error(`${chaptersPath} must contain at least one deconstructed-diff scene.`);
423
+ }
424
+ return authoredWalkthroughSteps;
425
+ }
426
+ function stackedOverviewEndpointIds(scene, path) {
427
+ if (!Array.isArray(scene.stack)) {
428
+ throw new Error(`${path}.stack must be an array.`);
429
+ }
430
+ return scene.stack.filter(isRecord).map((entry) => typeof entry.id === "string" ? entry.id : "").filter((id) => id.length > 0);
431
+ }
432
+ function deconstructedOverviewEndpointIds(scene, path) {
433
+ if (!Array.isArray(scene.stages)) {
434
+ throw new Error(`${path}.stages must be an array.`);
435
+ }
436
+ return [
437
+ "explanation-baseline",
438
+ ...scene.stages.filter(isRecord).map((stage) => typeof stage.id === "string" ? `explanation-stage-${stage.id}` : "").filter((id) => id.length > 0)
439
+ ];
440
+ }
441
+ function validateChangeTourReview(value, context = {}) {
442
+ if (!isRecord(value)) {
443
+ throw new Error("review must be an object.");
444
+ }
445
+ requireOnlyKeys(value, ["baseOid", "headOid", "items"], "review");
446
+ requireFullGitOid(value.baseOid, "review.baseOid");
447
+ requireFullGitOid(value.headOid, "review.headOid");
448
+ if (!Array.isArray(value.items) || value.items.length === 0) {
449
+ throw new Error("review.items must be a non-empty array.");
450
+ }
451
+ if (context.expectedRange) {
452
+ if (normalizeGitOid(value.baseOid) !== normalizeGitOid(context.expectedRange.baseOid)) {
453
+ throw new Error(
454
+ `Stale review: review.baseOid ${value.baseOid} does not match the resolved range.mergeBaseOid ${context.expectedRange.baseOid}.`
455
+ );
456
+ }
457
+ if (normalizeGitOid(value.headOid) !== normalizeGitOid(context.expectedRange.headOid)) {
458
+ throw new Error(
459
+ `Stale review: review.headOid ${value.headOid} does not match the resolved range.headOid ${context.expectedRange.headOid}.`
460
+ );
461
+ }
462
+ }
463
+ const itemIds = /* @__PURE__ */ new Set();
464
+ for (const [index, item] of value.items.entries()) {
465
+ const itemPath = `review.items[${index}]`;
466
+ if (!isRecord(item)) throw new Error(`${itemPath} must be an object.`);
467
+ requireOnlyKeys(item, ["id", "kind", "title", "body", "evidence", "nextCheck"], itemPath);
468
+ requireReviewString(item.id, `${itemPath}.id`);
469
+ if (itemIds.has(item.id)) throw new Error(`Duplicate review item id: ${item.id}`);
470
+ itemIds.add(item.id);
471
+ if (typeof item.kind !== "string" || !["concept", "boundary", "tradeoff", "question"].includes(item.kind)) {
472
+ throw new Error(`${itemPath}.kind must be concept, boundary, tradeoff, or question.`);
473
+ }
474
+ requireReviewString(item.title, `${itemPath}.title`);
475
+ requireReviewString(item.body, `${itemPath}.body`);
476
+ if (!Array.isArray(item.evidence)) {
477
+ throw new Error(`${itemPath}.evidence must be an array.`);
478
+ }
479
+ if (item.kind !== "question" && item.evidence.length === 0) {
480
+ throw new Error(`${itemPath}.evidence must contain at least one reference for claims.`);
481
+ }
482
+ if (item.kind === "question" && item.nextCheck === void 0) {
483
+ throw new Error(`${itemPath}.nextCheck must be a non-empty string for question items.`);
484
+ }
485
+ if (item.nextCheck !== void 0) requireReviewString(item.nextCheck, `${itemPath}.nextCheck`);
486
+ for (const [evidenceIndex, evidence] of item.evidence.entries()) {
487
+ const evidencePath = `${itemPath}.evidence[${evidenceIndex}]`;
488
+ if (!isRecord(evidence)) throw new Error(`${evidencePath} must be an object.`);
489
+ requireOnlyKeys(evidence, ["sceneId", "stepId"], evidencePath);
490
+ requireReviewString(evidence.sceneId, `${evidencePath}.sceneId`);
491
+ requireReviewString(evidence.stepId, `${evidencePath}.stepId`);
492
+ if (context.authoredWalkthroughSteps) {
493
+ const stepIds = context.authoredWalkthroughSteps.get(evidence.sceneId);
494
+ if (!stepIds) {
495
+ throw new Error(
496
+ `${evidencePath} references unknown authored walkthrough scene ${evidence.sceneId}.`
497
+ );
498
+ }
499
+ if (!stepIds.has(evidence.stepId)) {
500
+ throw new Error(
501
+ `${evidencePath} references unknown authored walkthrough step ${evidence.sceneId}/${evidence.stepId}.`
502
+ );
503
+ }
504
+ }
351
505
  }
352
506
  }
353
- return { ...value, connections: rawConnections };
354
507
  }
355
508
  function validateDeconstructedScene(scene, path) {
356
- requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "base", "target", "stack", "steps", "stages", "exclusions"], path);
509
+ requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "base", "target", "stack", "steps", "stages", "exclusions"], path);
357
510
  optionalString(scene.base, `${path}.base`);
358
511
  optionalString(scene.target, `${path}.target`);
359
512
  if (!Array.isArray(scene.stages) || scene.stages.length === 0 || scene.stages.length > 12) {
@@ -439,7 +592,7 @@ function validateRealStack(stack, path, minimum) {
439
592
  }
440
593
  }
441
594
  function validateStackedScene(scene, path, minimum = 3) {
442
- requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "stack", "files", "steps"], path);
595
+ requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "stack", "files", "steps"], path);
443
596
  if (!Array.isArray(scene.stack) || scene.stack.length < minimum || scene.stack.length > 6) {
444
597
  throw new Error(`${path}.stack must contain between ${minimum} and 6 revisions.`);
445
598
  }
@@ -495,6 +648,53 @@ function validateStepRequirement(value, path) {
495
648
  throw new Error(`${path}.confidence must be high, medium, or low.`);
496
649
  }
497
650
  }
651
+ function validateSceneOverview(value, path, options = {}) {
652
+ if (value === void 0) return;
653
+ if (!isRecord(value)) throw new Error(`${path} must be an object.`);
654
+ requireOnlyKeys(value, ["kind", "path", "comparison"], path);
655
+ if (value.kind !== "directory-diff") {
656
+ throw new Error(`${path}.kind must be directory-diff.`);
657
+ }
658
+ if (value.path !== void 0) validateDirectoryOverviewPath(value.path, `${path}.path`);
659
+ if (value.comparison === void 0) return;
660
+ if (options.allowComparison === false) {
661
+ throw new Error(`${path}.comparison is not supported for walkthrough scenes; use the fixed base-to-head comparison.`);
662
+ }
663
+ if (!isRecord(value.comparison)) throw new Error(`${path}.comparison must be an object.`);
664
+ requireOnlyKeys(value.comparison, ["from", "to"], `${path}.comparison`);
665
+ requireString(value.comparison.from, `${path}.comparison.from`);
666
+ requireString(value.comparison.to, `${path}.comparison.to`);
667
+ if (value.comparison.from === value.comparison.to) {
668
+ throw new Error(`${path}.comparison.from and to must identify distinct endpoints.`);
669
+ }
670
+ if (options.endpointIds !== void 0) {
671
+ for (const endpoint of [value.comparison.from, value.comparison.to]) {
672
+ if (!options.endpointIds.includes(endpoint)) {
673
+ throw new Error(`${path}.comparison endpoint ${endpoint} is not defined by this scene.`);
674
+ }
675
+ }
676
+ }
677
+ }
678
+ function validateDirectoryOverviewPath(value, path) {
679
+ requireString(value, path);
680
+ if (value.includes("\\")) {
681
+ throw new Error(`${path} must use a relative POSIX directory path without backslashes.`);
682
+ }
683
+ if (value.startsWith("/") || /^[A-Za-z]:/.test(value)) {
684
+ throw new Error(`${path} must be relative to the repository root.`);
685
+ }
686
+ if (value.split("/").some((segment) => segment === "..")) {
687
+ throw new Error(`${path} must not contain parent-directory traversal.`);
688
+ }
689
+ }
690
+ function collectStepIds(value) {
691
+ const ids = /* @__PURE__ */ new Set();
692
+ if (!Array.isArray(value)) return ids;
693
+ for (const step of value) {
694
+ if (isRecord(step) && typeof step.id === "string") ids.add(step.id);
695
+ }
696
+ return ids;
697
+ }
498
698
  function validateNarrative(value, path) {
499
699
  requireString(value.summary, `${path}.summary`);
500
700
  requireStringArray(value.bullets, `${path}.bullets`);
@@ -509,6 +709,20 @@ function requireString(value, path) {
509
709
  throw new Error(`${path} must be a non-empty string.`);
510
710
  }
511
711
  }
712
+ function requireFullGitOid(value, path) {
713
+ requireString(value, path);
714
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(value)) {
715
+ throw new Error(`${path} must be a full 40- or 64-character hexadecimal Git OID.`);
716
+ }
717
+ }
718
+ function requireReviewString(value, path) {
719
+ if (typeof value !== "string" || value.trim().length === 0) {
720
+ throw new Error(`${path} must be a non-empty string.`);
721
+ }
722
+ }
723
+ function normalizeGitOid(value) {
724
+ return value.toLowerCase();
725
+ }
512
726
  function optionalString(value, path) {
513
727
  if (value !== void 0) {
514
728
  requireString(value, path);
@@ -527,14 +741,14 @@ function requireOnlyKeys(value, allowed, path) {
527
741
  }
528
742
 
529
743
  // src/changeTourManifest.ts
530
- var CHANGE_TOUR_MANIFEST_VERSION = 2;
744
+ var CHANGE_TOUR_MANIFEST_VERSION = 3;
531
745
  function parseChangeTourManifest(value) {
532
746
  if (isRecord2(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_MANIFEST_VERSION) {
533
747
  throw new Error(
534
748
  `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.`
535
749
  );
536
750
  }
537
- if (!isRecord2(value) || value.version !== 1 && value.version !== CHANGE_TOUR_MANIFEST_VERSION) {
751
+ if (!isRecord2(value) || value.version !== 1 && value.version !== 2 && value.version !== CHANGE_TOUR_MANIFEST_VERSION) {
538
752
  throw new Error(`Unsupported or missing change-tour manifest version.`);
539
753
  }
540
754
  requireString2(value.title, "title");
@@ -566,7 +780,7 @@ function parseChangeTourManifest(value) {
566
780
  }
567
781
  const sceneIds = /* @__PURE__ */ new Set();
568
782
  for (const [index, candidate] of value.scenes.entries()) {
569
- validateScene(candidate, index);
783
+ validateScene(candidate, index, value.version);
570
784
  if (sceneIds.has(candidate.id)) {
571
785
  throw new Error(`Duplicate change-tour scene id: ${candidate.id}`);
572
786
  }
@@ -598,14 +812,44 @@ function parseChangeTourManifest(value) {
598
812
  if (value.authoringCoverage !== void 0) {
599
813
  validateAuthoringCoverage(value.authoringCoverage, value.version);
600
814
  }
601
- if (value.version === 2) validateZoom(value);
815
+ if (value.tours !== void 0) {
816
+ if (value.version !== 3) throw new Error("Independent tours require manifest version 3.");
817
+ validateModeTours(value.tours, value.version);
818
+ }
819
+ if (value.version !== 1) validateZoom(value, value.version);
820
+ if (value.review !== void 0) {
821
+ if (value.version !== 3) throw new Error("Review notes require manifest version 3.");
822
+ const evidenceScenes = value.version >= 2 && isRecord2(value.zoom) && isRecord2(value.zoom.final) ? value.zoom.final.scenes : value.scenes;
823
+ validateChangeTourReview(value.review, {
824
+ expectedRange: {
825
+ baseOid: String(value.range.mergeBaseOid),
826
+ headOid: String(value.range.headOid)
827
+ },
828
+ authoredWalkthroughSteps: collectAuthoredWalkthroughStepIds(evidenceScenes)
829
+ });
830
+ }
602
831
  return { ...value, files };
603
832
  }
833
+ function collectAuthoredWalkthroughStepIds(value) {
834
+ const scenes = /* @__PURE__ */ new Map();
835
+ if (!Array.isArray(value)) return scenes;
836
+ for (const scene of value) {
837
+ if (!isRecord2(scene) || scene.kind !== "walkthrough" || !Array.isArray(scene.steps) || typeof scene.id !== "string") {
838
+ continue;
839
+ }
840
+ const stepIds = /* @__PURE__ */ new Set();
841
+ for (const step of scene.steps) {
842
+ if (isRecord2(step) && typeof step.id === "string") stepIds.add(step.id);
843
+ }
844
+ scenes.set(scene.id, stepIds);
845
+ }
846
+ return scenes;
847
+ }
604
848
  function validateAuthoringCoverage(value, version) {
605
849
  if (!isRecord2(value) || !isRecord2(value.walkthrough) || !Array.isArray(value.explanationAssignments)) {
606
850
  throw new Error("authoringCoverage must contain walkthrough and explanationAssignments.");
607
851
  }
608
- const expectedScope = version === 2 ? "final" : "tour";
852
+ const expectedScope = version >= 2 ? "final" : "tour";
609
853
  if (value.walkthrough.scope !== expectedScope) {
610
854
  throw new Error(`authoringCoverage.walkthrough.scope must be ${expectedScope}.`);
611
855
  }
@@ -629,6 +873,48 @@ function validateAuthoringCoverage(value, version) {
629
873
  }
630
874
  }
631
875
  }
876
+ function validateModeTours(value, version) {
877
+ if (!isRecord2(value)) throw new Error("Change-tour manifest tours must be an object.");
878
+ if (value.historical === void 0 && value.deconstructed === void 0) {
879
+ throw new Error("Change-tour manifest tours must contain a historical or deconstructed tour.");
880
+ }
881
+ for (const mode of ["historical", "deconstructed"]) {
882
+ const tour = value[mode];
883
+ if (tour === void 0) continue;
884
+ if (!isRecord2(tour) || !Array.isArray(tour.chapters) || !Array.isArray(tour.scenes) || tour.chapters.length === 0 || tour.scenes.length === 0) {
885
+ throw new Error(`Change-tour manifest tours.${mode} must contain non-empty chapters and scenes arrays.`);
886
+ }
887
+ const sceneIds = /* @__PURE__ */ new Set();
888
+ let hasDeconstructed = false;
889
+ for (const [index, scene] of tour.scenes.entries()) {
890
+ validateScene(scene, index, version);
891
+ if (sceneIds.has(scene.id)) {
892
+ throw new Error(`Duplicate ${mode} tour scene id: ${scene.id}`);
893
+ }
894
+ sceneIds.add(scene.id);
895
+ if (scene.kind === "deconstructed-diff") hasDeconstructed = true;
896
+ if (mode === "historical" && scene.kind === "deconstructed-diff") {
897
+ throw new Error("Historical tours cannot contain synthetic deconstructed scenes.");
898
+ }
899
+ }
900
+ if (mode === "deconstructed" && !hasDeconstructed) {
901
+ throw new Error("Deconstructed tours must contain a constructed deconstructed-diff scene.");
902
+ }
903
+ for (const [index, chapter] of tour.chapters.entries()) {
904
+ if (!isRecord2(chapter)) {
905
+ throw new Error(`tours.${mode}.chapters[${index}] must be an object.`);
906
+ }
907
+ requireString2(chapter.id, `tours.${mode}.chapters[${index}].id`);
908
+ requireString2(chapter.title, `tours.${mode}.chapters[${index}].title`);
909
+ requireStringArray2(chapter.sceneIds, `tours.${mode}.chapters[${index}].sceneIds`);
910
+ for (const sceneId of chapter.sceneIds) {
911
+ if (!sceneIds.has(sceneId)) {
912
+ throw new Error(`tours.${mode} chapter references unknown scene id: ${sceneId}`);
913
+ }
914
+ }
915
+ }
916
+ }
917
+ }
632
918
  function parseChangeTourStory(value) {
633
919
  if (!isRecord2(value) || !Array.isArray(value.scenes)) {
634
920
  throw new Error("Change-tour story must contain a scenes array.");
@@ -660,7 +946,7 @@ function parseChangeTourStory(value) {
660
946
  }
661
947
  return value;
662
948
  }
663
- function validateZoom(value) {
949
+ function validateZoom(value, version) {
664
950
  if (!isRecord2(value.repository) || typeof value.repository.root !== "string" || !value.repository.root) {
665
951
  throw new Error("A v2 tour requires its originating repository.root.");
666
952
  }
@@ -671,7 +957,7 @@ function validateZoom(value) {
671
957
  const revisions = value.zoom.revisions;
672
958
  const ids = /* @__PURE__ */ new Set();
673
959
  for (const [index, scene] of revisions.entries()) {
674
- validateScene(scene, index);
960
+ validateScene(scene, index, version);
675
961
  if (scene.kind !== "stacked-diff") throw new Error("zoom.revisions must contain real stacked-diff scenes.");
676
962
  if (ids.has(scene.id)) throw new Error(`Duplicate zoom revision scene: ${scene.id}`);
677
963
  ids.add(scene.id);
@@ -701,7 +987,7 @@ function validateZoom(value) {
701
987
  }
702
988
  const finalSceneIds = /* @__PURE__ */ new Set();
703
989
  for (const [index, scene] of value.zoom.final.scenes.entries()) {
704
- validateScene(scene, index);
990
+ validateScene(scene, index, version);
705
991
  if (scene.kind === "stacked-diff" || scene.kind === "deconstructed-diff") {
706
992
  throw new Error("zoom.final must contain only non-stacked scenes.");
707
993
  }
@@ -720,18 +1006,23 @@ function validateZoom(value) {
720
1006
  }
721
1007
  }
722
1008
  }
723
- function validateScene(value, index) {
1009
+ function validateScene(value, index, version) {
724
1010
  if (!isRecord2(value) || !["text-diff", "discussion", "walkthrough", "stacked-diff", "deconstructed-diff"].includes(String(value.kind))) {
725
1011
  throw new Error(`scenes[${index}] must be a text-diff, discussion, walkthrough, stacked-diff, or deconstructed-diff scene.`);
726
1012
  }
727
1013
  for (const key of ["id", "title"]) {
728
1014
  requireString2(value[key], `scenes[${index}].${key}`);
729
1015
  }
1016
+ if (value.overview !== void 0 && version !== 3) {
1017
+ throw new Error(`scenes[${index}].overview requires manifest version 3.`);
1018
+ }
730
1019
  validateNarrative2(value, `scenes[${index}]`);
731
1020
  if (value.kind === "discussion") {
1021
+ validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
732
1022
  return;
733
1023
  }
734
1024
  if (value.kind === "walkthrough") {
1025
+ validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
735
1026
  if (!Array.isArray(value.steps) || value.steps.length === 0) {
736
1027
  throw new Error(`scenes[${index}].steps must be a non-empty array.`);
737
1028
  }
@@ -746,7 +1037,7 @@ function validateScene(value, index) {
746
1037
  }
747
1038
  validateStepRequirement(step.requirement, `${path}.requirement`);
748
1039
  validateResolvedAnchor(step.focus, `${path}.focus`);
749
- validateScene(step.diff, index);
1040
+ validateScene(step.diff, index, version);
750
1041
  if (step.diff.kind !== "text-diff") throw new Error(`${path}.diff must be a text-diff scene.`);
751
1042
  if (step.connection !== void 0) {
752
1043
  if (!isRecord2(step.connection)) throw new Error(`${path}.connection must be an object.`);
@@ -760,12 +1051,19 @@ function validateScene(value, index) {
760
1051
  }
761
1052
  if (value.kind === "stacked-diff") {
762
1053
  validateStackedScene2(value, index);
1054
+ validateSceneOverview(value.overview, `scenes[${index}].overview`, {
1055
+ endpointIds: value.stack.map((panel) => panel.id)
1056
+ });
763
1057
  return;
764
1058
  }
765
1059
  if (value.kind === "deconstructed-diff") {
766
1060
  validateDeconstructedScene2(value, index);
1061
+ validateSceneOverview(value.overview, `scenes[${index}].overview`, {
1062
+ endpointIds: value.panels.map((panel) => panel.id)
1063
+ });
767
1064
  return;
768
1065
  }
1066
+ validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
769
1067
  for (const key of ["path", "leftLabel", "rightLabel", "leftContent", "rightContent"]) {
770
1068
  requireString2(value[key], `scenes[${index}].${key}`);
771
1069
  }
@@ -883,7 +1181,7 @@ function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
883
1181
  function validateTourFile(value, index) {
884
1182
  if (!isRecord2(value)) throw new Error(`files[${index}] must be an object.`);
885
1183
  if (value.kind === "text-diff") {
886
- validateScene(value, index);
1184
+ validateScene(value, index, 1);
887
1185
  return;
888
1186
  }
889
1187
  if (value.kind !== "omitted") {
@@ -3010,12 +3308,21 @@ var CHAPTERS = {
3010
3308
  packaging: { id: "packaging", title: "Packaging and dependencies", priority: 4 }
3011
3309
  };
3012
3310
  function buildChangeTourManifest(startPath, options = {}) {
3013
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
3311
+ var _a, _b, _c, _d;
3312
+ const source = options.source ? parseChangeTourSource(options.source) : void 0;
3014
3313
  const range = resolveBranchReviewRange(
3015
3314
  startPath,
3016
- options.headRef ?? ((_b = (_a = options.source) == null ? void 0 : _a.range) == null ? void 0 : _b.head),
3017
- options.baseRef ?? ((_d = (_c = options.source) == null ? void 0 : _c.range) == null ? void 0 : _d.base)
3315
+ options.headRef ?? ((_a = source == null ? void 0 : source.range) == null ? void 0 : _a.head),
3316
+ options.baseRef ?? ((_b = source == null ? void 0 : source.range) == null ? void 0 : _b.base)
3018
3317
  );
3318
+ if (source == null ? void 0 : source.review) {
3319
+ validateChangeTourReview(source.review, {
3320
+ expectedRange: {
3321
+ baseOid: range.mergeBaseOid,
3322
+ headOid: range.headOid
3323
+ }
3324
+ });
3325
+ }
3019
3326
  const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_TOUR_FILE_BYTES;
3020
3327
  const maxLineBytes = options.maxLineBytes ?? DEFAULT_MAX_TOUR_LINE_BYTES;
3021
3328
  const omittedFiles = [];
@@ -3076,7 +3383,7 @@ function buildChangeTourManifest(startPath, options = {}) {
3076
3383
  files.sort((left, right) => left.path.localeCompare(right.path));
3077
3384
  sceneRecords.sort((left, right) => left.chapter.priority - right.chapter.priority || left.scene.path.localeCompare(right.scene.path));
3078
3385
  let defaultScenes = sceneRecords.map(({ scene }, index) => ({ ...scene, id: `file-${index + 1}` }));
3079
- if (options.story && options.source) {
3386
+ if (options.story && source) {
3080
3387
  throw new Error("A change tour can use either a legacy story or a source file, not both.");
3081
3388
  }
3082
3389
  const attention = buildChangeAttention(files.map((file) => ({
@@ -3096,7 +3403,7 @@ function buildChangeTourManifest(startPath, options = {}) {
3096
3403
  };
3097
3404
  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));
3098
3405
  }
3099
- const authored = options.source ? applySource(parseChangeTourSource(options.source), defaultScenes, range.repoRoot) : options.story ? applyStory(options.story, defaultScenes) : buildGeneratedTourLanding(
3406
+ const authored = source ? applySource(source, defaultScenes, range.repoRoot) : options.story ? applyStory(options.story, defaultScenes) : buildGeneratedTourLanding(
3100
3407
  defaultScenes,
3101
3408
  buildChapters(sceneRecords, new Map(defaultScenes.map((scene) => [scene.path, scene.id]))),
3102
3409
  attention,
@@ -3113,12 +3420,12 @@ function buildChangeTourManifest(startPath, options = {}) {
3113
3420
  const finalScenes = "finalScenes" in authored ? authored.finalScenes : scenes;
3114
3421
  const finalChapters = "finalChapters" in authored ? authored.finalChapters : chapters;
3115
3422
  const manifest = {
3116
- // Only authored v2 files opt into repository-bound zoom modes. Generated
3423
+ // Only authored v2+ files opt into repository-bound zoom modes. Generated
3117
3424
  // and legacy/story tours retain the portable v1 manifest contract.
3118
- version: ((_e = options.source) == null ? void 0 : _e.version) ?? 1,
3119
- title: ((_f = options.source) == null ? void 0 : _f.title) || ((_g = options.story) == null ? void 0 : _g.title) || options.title || `${range.headRef} against ${range.baseRef}`,
3120
- windowTitle: (_h = options.source) == null ? void 0 : _h.windowTitle,
3121
- sourceUrl: ((_i = options.source) == null ? void 0 : _i.sourceUrl) || ((_j = options.story) == null ? void 0 : _j.sourceUrl) || options.sourceUrl,
3425
+ version: (source == null ? void 0 : source.version) ?? 1,
3426
+ title: (source == null ? void 0 : source.title) || ((_c = options.story) == null ? void 0 : _c.title) || options.title || `${range.headRef} against ${range.baseRef}`,
3427
+ windowTitle: source == null ? void 0 : source.windowTitle,
3428
+ sourceUrl: (source == null ? void 0 : source.sourceUrl) || ((_d = options.story) == null ? void 0 : _d.sourceUrl) || options.sourceUrl,
3122
3429
  generatedAt: options.generatedAt || (/* @__PURE__ */ new Date()).toISOString(),
3123
3430
  range: {
3124
3431
  baseRef: range.baseRef,
@@ -3137,22 +3444,33 @@ function buildChangeTourManifest(startPath, options = {}) {
3137
3444
  commits: range.commits,
3138
3445
  files,
3139
3446
  chapters,
3140
- scenes
3447
+ scenes,
3448
+ ...(source == null ? void 0 : source.review) ? { review: source.review } : {}
3141
3449
  };
3142
- if (manifest.version === 2) {
3450
+ const authoredModeTours = (source == null ? void 0 : source.version) === 3 ? buildAuthoredModeTours(
3451
+ source,
3452
+ authored,
3453
+ defaultScenes,
3454
+ range.repoRoot,
3455
+ range.mergeBaseOid,
3456
+ range.headOid
3457
+ ) : void 0;
3458
+ if (authoredModeTours) manifest.tours = authoredModeTours;
3459
+ if (manifest.version >= 2) {
3143
3460
  manifest.repository = { root: range.repoRoot };
3144
3461
  const revisions = [];
3145
3462
  for (const scene of scenes) {
3146
3463
  let realStack;
3147
3464
  if (scene.kind === "stacked-diff") realStack = scene;
3148
3465
  if (scene.kind === "deconstructed-diff") {
3149
- const authoredScene = (_k = options.source) == null ? void 0 : _k.chapters.flatMap((chapter) => chapter.scenes).find((candidate) => candidate.id === scene.id);
3466
+ const authoredScene = source == null ? void 0 : source.chapters.flatMap((chapter) => chapter.scenes).find((candidate) => candidate.id === scene.id);
3150
3467
  if ((authoredScene == null ? void 0 : authoredScene.kind) !== "deconstructed-diff" || !authoredScene.stack) {
3151
- throw new Error(`Deconstructed scene ${scene.id} requires an explicit real revision stack in v2.`);
3468
+ throw new Error(`Deconstructed scene ${scene.id} requires an explicit real revision stack in v2+.`);
3152
3469
  }
3153
3470
  realStack = buildStackedScene(range.repoRoot, {
3154
3471
  ...authoredScene,
3155
3472
  kind: "stacked-diff",
3473
+ overview: compileSceneOverview(authoredScene.overview, false),
3156
3474
  stack: authoredScene.stack,
3157
3475
  files: scene.files.map((file) => file.path),
3158
3476
  steps: []
@@ -3185,6 +3503,9 @@ function buildChangeTourManifest(startPath, options = {}) {
3185
3503
  return parseChangeTourManifest(manifest);
3186
3504
  }
3187
3505
  function applySource(source, defaultScenes, repoRoot) {
3506
+ return applySourceChapters(source, source.chapters, defaultScenes, repoRoot);
3507
+ }
3508
+ function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot) {
3188
3509
  var _a, _b;
3189
3510
  const available = new Map(defaultScenes.map((scene) => [scene.path, scene]));
3190
3511
  const resolvedAnchors = /* @__PURE__ */ new Map();
@@ -3206,7 +3527,7 @@ function applySource(source, defaultScenes, repoRoot) {
3206
3527
  const chapters = [];
3207
3528
  const finalScenes = [];
3208
3529
  const finalChapters = [];
3209
- for (const chapter of source.chapters) {
3530
+ for (const chapter of chaptersToCompile) {
3210
3531
  const sceneIds = [];
3211
3532
  const finalSceneIds = [];
3212
3533
  for (const authoredScene of chapter.scenes) {
@@ -3260,7 +3581,60 @@ function applySource(source, defaultScenes, repoRoot) {
3260
3581
  }
3261
3582
  return { scenes, chapters, finalScenes, finalChapters };
3262
3583
  }
3584
+ function buildAuthoredModeTours(source, root, defaultScenes, repoRoot, mergeBaseOid, headOid) {
3585
+ var _a, _b;
3586
+ const tours = {};
3587
+ if ((_a = source.tours) == null ? void 0 : _a.historical) {
3588
+ const compiled = applySourceChapters(source, source.tours.historical.chapters, defaultScenes, repoRoot);
3589
+ validateHistoricalStackEndpoints(compiled.scenes, mergeBaseOid, headOid);
3590
+ tours.historical = { chapters: compiled.chapters, scenes: compiled.scenes };
3591
+ } else if (source.version === 3) {
3592
+ const fallback = buildHistoricalFallback(source.chapters, root);
3593
+ if (fallback) tours.historical = fallback;
3594
+ }
3595
+ if ((_b = source.tours) == null ? void 0 : _b.deconstructed) {
3596
+ const compiled = applySourceChapters(source, source.tours.deconstructed.chapters, defaultScenes, repoRoot);
3597
+ tours.deconstructed = { chapters: compiled.chapters, scenes: compiled.scenes };
3598
+ } else if (source.version === 3 && source.chapters.some((chapter) => chapter.scenes.some((scene) => scene.kind === "deconstructed-diff"))) {
3599
+ tours.deconstructed = { chapters: root.chapters, scenes: root.scenes };
3600
+ }
3601
+ return tours.historical || tours.deconstructed ? tours : void 0;
3602
+ }
3603
+ function validateHistoricalStackEndpoints(scenes, mergeBaseOid, headOid) {
3604
+ for (const scene of scenes) {
3605
+ if (scene.kind !== "stacked-diff") continue;
3606
+ if (scene.stack[0].oid !== mergeBaseOid || scene.stack[scene.stack.length - 1].oid !== headOid) {
3607
+ throw new Error(`Historical scene ${scene.id} real stack endpoints must match the tour's final diff range.`);
3608
+ }
3609
+ }
3610
+ }
3611
+ function buildHistoricalFallback(sourceChapters, root) {
3612
+ const regularScenes = new Map(root.scenes.map((scene) => [scene.id, scene]));
3613
+ const finalScenes = new Map(root.finalScenes.map((scene) => [scene.id, scene]));
3614
+ const scenes = [];
3615
+ const chapters = [];
3616
+ for (const chapter of sourceChapters) {
3617
+ const sceneIds = [];
3618
+ for (const authoredScene of chapter.scenes) {
3619
+ const scene = authoredScene.kind === "deconstructed-diff" ? authoredScene.steps ? finalScenes.get(authoredScene.id) : void 0 : regularScenes.get(authoredScene.id);
3620
+ if (!scene) continue;
3621
+ scenes.push(scene);
3622
+ sceneIds.push(scene.id);
3623
+ }
3624
+ if (sceneIds.length > 0) chapters.push({ id: chapter.id, title: chapter.title, sceneIds });
3625
+ }
3626
+ return scenes.length > 0 ? { chapters, scenes } : void 0;
3627
+ }
3628
+ function compileSceneOverview(overview, includeComparison = true) {
3629
+ if (!overview) return void 0;
3630
+ return {
3631
+ kind: overview.kind,
3632
+ ...overview.path === void 0 ? {} : { path: overview.path },
3633
+ ...includeComparison && overview.comparison ? { comparison: { ...overview.comparison } } : {}
3634
+ };
3635
+ }
3263
3636
  function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections) {
3637
+ const overview = compileSceneOverview(authoredScene.overview, false);
3264
3638
  return {
3265
3639
  id: authoredScene.id,
3266
3640
  kind: "walkthrough",
@@ -3269,6 +3643,7 @@ function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors
3269
3643
  bullets: authoredScene.bullets,
3270
3644
  tags: authoredScene.tags,
3271
3645
  takeaway: authoredScene.takeaway,
3646
+ ...overview ? { overview } : {},
3272
3647
  steps: authoredScene.steps.map((step) => {
3273
3648
  const focus = requireResolvedAnchor(resolvedAnchors, step.focus);
3274
3649
  const diff2 = available.get(focus.path);
@@ -3343,6 +3718,7 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, a
3343
3718
  });
3344
3719
  if (files.length === 0) throw new Error(`Deconstructed scene ${source.id} has no staged files.`);
3345
3720
  const steps = buildDeconstructedFocusSteps(source.id, compiled.stages, files);
3721
+ const overview = compileSceneOverview(source.overview);
3346
3722
  return {
3347
3723
  id: source.id,
3348
3724
  kind: "deconstructed-diff",
@@ -3351,6 +3727,7 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, a
3351
3727
  bullets: source.bullets,
3352
3728
  tags: source.tags,
3353
3729
  takeaway: source.takeaway,
3730
+ ...overview ? { overview } : {},
3354
3731
  stageLabel: "Explanation stages",
3355
3732
  realRange: {
3356
3733
  baseRef: baseRef || compiled.baseOid,
@@ -3492,6 +3869,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
3492
3869
  });
3493
3870
  if (files.length === 0) throw new Error(`Stacked scene ${source.id} has no materializable text files.`);
3494
3871
  const stackIds = stack.map((entry) => entry.id);
3872
+ const overview = compileSceneOverview(source.overview);
3495
3873
  return {
3496
3874
  id: source.id,
3497
3875
  kind: "stacked-diff",
@@ -3500,6 +3878,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
3500
3878
  bullets: source.bullets,
3501
3879
  tags: source.tags,
3502
3880
  takeaway: source.takeaway,
3881
+ ...overview ? { overview } : {},
3503
3882
  stack,
3504
3883
  files,
3505
3884
  steps: source.steps.length === 0 ? stack.slice(0, -1).map((_, pairIndex) => ({