@davmash/bygone 0.8.12 → 0.8.13

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.
@@ -629,16 +629,41 @@ function validateDeconstructedScene(value, index) {
629
629
  }
630
630
  validateMultiPanelFiles(value.files, panels.length, index);
631
631
  validateMultiPanelSteps(value.steps, panels.length, index, true);
632
- if (Array.isArray(value.steps) && value.steps.length !== panels.length - 1) {
633
- throw new Error(`scenes[${index}].steps must match the number of explanation stages.`);
634
- }
635
632
  if (Array.isArray(value.steps)) {
633
+ let previousPairIndex = -1;
636
634
  value.steps.forEach((step, stepIndex) => {
637
- const panel = panels[stepIndex + 1];
638
- if (!isRecord(step) || !isRecord(panel) || panel.stageId !== step.id) {
639
- throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
635
+ if (!isRecord(step)) return;
636
+ const pairIndex = Number(step.pairIndex);
637
+ if (pairIndex < previousPairIndex) {
638
+ throw new Error(`scenes[${index}].steps must remain grouped in explanation stage order.`);
639
+ }
640
+ previousPairIndex = pairIndex;
641
+ if (step.stageId !== void 0) requireString(step.stageId, `scenes[${index}].steps[${stepIndex}].stageId`);
642
+ if (step.stageIndex !== void 0) requireNonNegativeInteger(step.stageIndex, `scenes[${index}].steps[${stepIndex}].stageIndex`);
643
+ if (step.focusIndex !== void 0) requireNonNegativeInteger(step.focusIndex, `scenes[${index}].steps[${stepIndex}].focusIndex`);
644
+ if (step.focusCount !== void 0) requirePositiveInteger(step.focusCount, `scenes[${index}].steps[${stepIndex}].focusCount`);
645
+ if (step.stageIndex !== void 0 && Number(step.stageIndex) !== pairIndex) {
646
+ throw new Error(`scenes[${index}].steps[${stepIndex}].stageIndex must match pairIndex.`);
647
+ }
648
+ if (step.focusIndex !== void 0 && step.focusCount !== void 0 && Number(step.focusIndex) >= Number(step.focusCount)) {
649
+ throw new Error(`scenes[${index}].steps[${stepIndex}].focusIndex must be less than focusCount.`);
640
650
  }
641
651
  });
652
+ for (let stageIndex = 0; stageIndex < panels.length - 1; stageIndex += 1) {
653
+ const panel = panels[stageIndex + 1];
654
+ const stageSteps = value.steps.filter((step) => isRecord(step) && Number(step.pairIndex) === stageIndex);
655
+ if (!isRecord(panel) || stageSteps.length === 0) {
656
+ throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
657
+ }
658
+ const firstStep = stageSteps[0];
659
+ const fallbackStageId = isRecord(firstStep) ? firstStep.id : void 0;
660
+ if (stageSteps.some((step) => {
661
+ if (!isRecord(step)) return true;
662
+ return (step.stageId ?? fallbackStageId) !== panel.stageId;
663
+ })) {
664
+ throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
665
+ }
666
+ }
642
667
  }
643
668
  }
644
669
  function validateMultiPanelFiles(value, panelCount, index) {
@@ -667,7 +692,6 @@ function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
667
692
  if (deconstructed) {
668
693
  requireStringArray(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
669
694
  if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
670
- if (Number(step.pairIndex) !== stepIndex) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex must match its explanation stage.`);
671
695
  }
672
696
  if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
673
697
  if (step.startLine !== void 0) requirePositiveInteger(step.startLine, `scenes[${index}].steps[${stepIndex}].startLine`);
@@ -1068,6 +1092,7 @@ function compileDeconstructedScene(inventory, scene) {
1068
1092
  for (const { stage, claims } of stageClaims) {
1069
1093
  const introducedHunks = [];
1070
1094
  const introducedFiles = [];
1095
+ const introducedChanges = [];
1071
1096
  for (const claim of claims) {
1072
1097
  const cumulative = cumulativeByFile.get(claim.file) || /* @__PURE__ */ new Set();
1073
1098
  claim.ids.forEach((id) => {
@@ -1076,6 +1101,12 @@ function compileDeconstructedScene(inventory, scene) {
1076
1101
  });
1077
1102
  cumulativeByFile.set(claim.file, cumulative);
1078
1103
  if (!introducedFiles.includes(claim.file.path)) introducedFiles.push(claim.file.path);
1104
+ const introducedChange = introducedChanges.find((change) => change.file === claim.file.path);
1105
+ if (introducedChange) {
1106
+ introducedChange.hunks.push(...claim.ids);
1107
+ } else {
1108
+ introducedChanges.push({ file: claim.file.path, hunks: [...claim.ids] });
1109
+ }
1079
1110
  }
1080
1111
  stages.push({
1081
1112
  id: stage.id,
@@ -1083,6 +1114,7 @@ function compileDeconstructedScene(inventory, scene) {
1083
1114
  narration: stage.narration,
1084
1115
  introducedHunks,
1085
1116
  introducedFiles,
1117
+ introducedChanges,
1086
1118
  cumulativeHunks: [...cumulativeByFile.values()].flatMap((ids) => [...ids]),
1087
1119
  files: materializableFiles.map((file) => buildFileState(
1088
1120
  file,
@@ -1148,6 +1180,1201 @@ function buildFileState(file, selected, wholeFileExcluded) {
1148
1180
  };
1149
1181
  }
1150
1182
 
1183
+ // node_modules/diff/lib/index.mjs
1184
+ function Diff() {
1185
+ }
1186
+ Diff.prototype = {
1187
+ diff: function diff(oldString, newString) {
1188
+ var _options$timeout;
1189
+ var options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
1190
+ var callback = options.callback;
1191
+ if (typeof options === "function") {
1192
+ callback = options;
1193
+ options = {};
1194
+ }
1195
+ this.options = options;
1196
+ var self = this;
1197
+ function done(value) {
1198
+ if (callback) {
1199
+ setTimeout(function() {
1200
+ callback(void 0, value);
1201
+ }, 0);
1202
+ return true;
1203
+ } else {
1204
+ return value;
1205
+ }
1206
+ }
1207
+ oldString = this.castInput(oldString);
1208
+ newString = this.castInput(newString);
1209
+ oldString = this.removeEmpty(this.tokenize(oldString));
1210
+ newString = this.removeEmpty(this.tokenize(newString));
1211
+ var newLen = newString.length, oldLen = oldString.length;
1212
+ var editLength = 1;
1213
+ var maxEditLength = newLen + oldLen;
1214
+ if (options.maxEditLength) {
1215
+ maxEditLength = Math.min(maxEditLength, options.maxEditLength);
1216
+ }
1217
+ var maxExecutionTime = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : Infinity;
1218
+ var abortAfterTimestamp = Date.now() + maxExecutionTime;
1219
+ var bestPath = [{
1220
+ oldPos: -1,
1221
+ lastComponent: void 0
1222
+ }];
1223
+ var newPos = this.extractCommon(bestPath[0], newString, oldString, 0);
1224
+ if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
1225
+ return done([{
1226
+ value: this.join(newString),
1227
+ count: newString.length
1228
+ }]);
1229
+ }
1230
+ var minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
1231
+ function execEditLength() {
1232
+ for (var diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
1233
+ var basePath = void 0;
1234
+ var removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
1235
+ if (removePath) {
1236
+ bestPath[diagonalPath - 1] = void 0;
1237
+ }
1238
+ var canAdd = false;
1239
+ if (addPath) {
1240
+ var addPathNewPos = addPath.oldPos - diagonalPath;
1241
+ canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
1242
+ }
1243
+ var canRemove = removePath && removePath.oldPos + 1 < oldLen;
1244
+ if (!canAdd && !canRemove) {
1245
+ bestPath[diagonalPath] = void 0;
1246
+ continue;
1247
+ }
1248
+ if (!canRemove || canAdd && removePath.oldPos + 1 < addPath.oldPos) {
1249
+ basePath = self.addToPath(addPath, true, void 0, 0);
1250
+ } else {
1251
+ basePath = self.addToPath(removePath, void 0, true, 1);
1252
+ }
1253
+ newPos = self.extractCommon(basePath, newString, oldString, diagonalPath);
1254
+ if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
1255
+ return done(buildValues(self, basePath.lastComponent, newString, oldString, self.useLongestToken));
1256
+ } else {
1257
+ bestPath[diagonalPath] = basePath;
1258
+ if (basePath.oldPos + 1 >= oldLen) {
1259
+ maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
1260
+ }
1261
+ if (newPos + 1 >= newLen) {
1262
+ minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
1263
+ }
1264
+ }
1265
+ }
1266
+ editLength++;
1267
+ }
1268
+ if (callback) {
1269
+ (function exec() {
1270
+ setTimeout(function() {
1271
+ if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
1272
+ return callback();
1273
+ }
1274
+ if (!execEditLength()) {
1275
+ exec();
1276
+ }
1277
+ }, 0);
1278
+ })();
1279
+ } else {
1280
+ while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
1281
+ var ret = execEditLength();
1282
+ if (ret) {
1283
+ return ret;
1284
+ }
1285
+ }
1286
+ }
1287
+ },
1288
+ addToPath: function addToPath(path, added, removed, oldPosInc) {
1289
+ var last = path.lastComponent;
1290
+ if (last && last.added === added && last.removed === removed) {
1291
+ return {
1292
+ oldPos: path.oldPos + oldPosInc,
1293
+ lastComponent: {
1294
+ count: last.count + 1,
1295
+ added,
1296
+ removed,
1297
+ previousComponent: last.previousComponent
1298
+ }
1299
+ };
1300
+ } else {
1301
+ return {
1302
+ oldPos: path.oldPos + oldPosInc,
1303
+ lastComponent: {
1304
+ count: 1,
1305
+ added,
1306
+ removed,
1307
+ previousComponent: last
1308
+ }
1309
+ };
1310
+ }
1311
+ },
1312
+ extractCommon: function extractCommon(basePath, newString, oldString, diagonalPath) {
1313
+ var newLen = newString.length, oldLen = oldString.length, oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
1314
+ while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newString[newPos + 1], oldString[oldPos + 1])) {
1315
+ newPos++;
1316
+ oldPos++;
1317
+ commonCount++;
1318
+ }
1319
+ if (commonCount) {
1320
+ basePath.lastComponent = {
1321
+ count: commonCount,
1322
+ previousComponent: basePath.lastComponent
1323
+ };
1324
+ }
1325
+ basePath.oldPos = oldPos;
1326
+ return newPos;
1327
+ },
1328
+ equals: function equals(left, right) {
1329
+ if (this.options.comparator) {
1330
+ return this.options.comparator(left, right);
1331
+ } else {
1332
+ return left === right || this.options.ignoreCase && left.toLowerCase() === right.toLowerCase();
1333
+ }
1334
+ },
1335
+ removeEmpty: function removeEmpty(array) {
1336
+ var ret = [];
1337
+ for (var i = 0; i < array.length; i++) {
1338
+ if (array[i]) {
1339
+ ret.push(array[i]);
1340
+ }
1341
+ }
1342
+ return ret;
1343
+ },
1344
+ castInput: function castInput(value) {
1345
+ return value;
1346
+ },
1347
+ tokenize: function tokenize(value) {
1348
+ return value.split("");
1349
+ },
1350
+ join: function join(chars) {
1351
+ return chars.join("");
1352
+ }
1353
+ };
1354
+ function buildValues(diff2, lastComponent, newString, oldString, useLongestToken) {
1355
+ var components = [];
1356
+ var nextComponent;
1357
+ while (lastComponent) {
1358
+ components.push(lastComponent);
1359
+ nextComponent = lastComponent.previousComponent;
1360
+ delete lastComponent.previousComponent;
1361
+ lastComponent = nextComponent;
1362
+ }
1363
+ components.reverse();
1364
+ var componentPos = 0, componentLen = components.length, newPos = 0, oldPos = 0;
1365
+ for (; componentPos < componentLen; componentPos++) {
1366
+ var component = components[componentPos];
1367
+ if (!component.removed) {
1368
+ if (!component.added && useLongestToken) {
1369
+ var value = newString.slice(newPos, newPos + component.count);
1370
+ value = value.map(function(value2, i) {
1371
+ var oldValue = oldString[oldPos + i];
1372
+ return oldValue.length > value2.length ? oldValue : value2;
1373
+ });
1374
+ component.value = diff2.join(value);
1375
+ } else {
1376
+ component.value = diff2.join(newString.slice(newPos, newPos + component.count));
1377
+ }
1378
+ newPos += component.count;
1379
+ if (!component.added) {
1380
+ oldPos += component.count;
1381
+ }
1382
+ } else {
1383
+ component.value = diff2.join(oldString.slice(oldPos, oldPos + component.count));
1384
+ oldPos += component.count;
1385
+ if (componentPos && components[componentPos - 1].added) {
1386
+ var tmp = components[componentPos - 1];
1387
+ components[componentPos - 1] = components[componentPos];
1388
+ components[componentPos] = tmp;
1389
+ }
1390
+ }
1391
+ }
1392
+ var finalComponent = components[componentLen - 1];
1393
+ if (componentLen > 1 && typeof finalComponent.value === "string" && (finalComponent.added || finalComponent.removed) && diff2.equals("", finalComponent.value)) {
1394
+ components[componentLen - 2].value += finalComponent.value;
1395
+ components.pop();
1396
+ }
1397
+ return components;
1398
+ }
1399
+ var characterDiff = new Diff();
1400
+ function diffChars(oldStr, newStr, options) {
1401
+ return characterDiff.diff(oldStr, newStr, options);
1402
+ }
1403
+ var extendedWordChars = /^[A-Za-z\xC0-\u02C6\u02C8-\u02D7\u02DE-\u02FF\u1E00-\u1EFF]+$/;
1404
+ var reWhitespace = /\S/;
1405
+ var wordDiff = new Diff();
1406
+ wordDiff.equals = function(left, right) {
1407
+ if (this.options.ignoreCase) {
1408
+ left = left.toLowerCase();
1409
+ right = right.toLowerCase();
1410
+ }
1411
+ return left === right || this.options.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right);
1412
+ };
1413
+ wordDiff.tokenize = function(value) {
1414
+ var tokens = value.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/);
1415
+ for (var i = 0; i < tokens.length - 1; i++) {
1416
+ if (!tokens[i + 1] && tokens[i + 2] && extendedWordChars.test(tokens[i]) && extendedWordChars.test(tokens[i + 2])) {
1417
+ tokens[i] += tokens[i + 2];
1418
+ tokens.splice(i + 1, 2);
1419
+ i--;
1420
+ }
1421
+ }
1422
+ return tokens;
1423
+ };
1424
+ function diffWordsWithSpace(oldStr, newStr, options) {
1425
+ return wordDiff.diff(oldStr, newStr, options);
1426
+ }
1427
+ var lineDiff = new Diff();
1428
+ lineDiff.tokenize = function(value) {
1429
+ if (this.options.stripTrailingCr) {
1430
+ value = value.replace(/\r\n/g, "\n");
1431
+ }
1432
+ var retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
1433
+ if (!linesAndNewlines[linesAndNewlines.length - 1]) {
1434
+ linesAndNewlines.pop();
1435
+ }
1436
+ for (var i = 0; i < linesAndNewlines.length; i++) {
1437
+ var line = linesAndNewlines[i];
1438
+ if (i % 2 && !this.options.newlineIsToken) {
1439
+ retLines[retLines.length - 1] += line;
1440
+ } else {
1441
+ if (this.options.ignoreWhitespace) {
1442
+ line = line.trim();
1443
+ }
1444
+ retLines.push(line);
1445
+ }
1446
+ }
1447
+ return retLines;
1448
+ };
1449
+ var sentenceDiff = new Diff();
1450
+ sentenceDiff.tokenize = function(value) {
1451
+ return value.split(/(\S.+?[.!?])(?=\s+|$)/);
1452
+ };
1453
+ var cssDiff = new Diff();
1454
+ cssDiff.tokenize = function(value) {
1455
+ return value.split(/([{}:;,]|\s+)/);
1456
+ };
1457
+ function _typeof(obj) {
1458
+ "@babel/helpers - typeof";
1459
+ if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
1460
+ _typeof = function(obj2) {
1461
+ return typeof obj2;
1462
+ };
1463
+ } else {
1464
+ _typeof = function(obj2) {
1465
+ return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
1466
+ };
1467
+ }
1468
+ return _typeof(obj);
1469
+ }
1470
+ var objectPrototypeToString = Object.prototype.toString;
1471
+ var jsonDiff = new Diff();
1472
+ jsonDiff.useLongestToken = true;
1473
+ jsonDiff.tokenize = lineDiff.tokenize;
1474
+ jsonDiff.castInput = function(value) {
1475
+ var _this$options = this.options, undefinedReplacement = _this$options.undefinedReplacement, _this$options$stringi = _this$options.stringifyReplacer, stringifyReplacer = _this$options$stringi === void 0 ? function(k, v) {
1476
+ return typeof v === "undefined" ? undefinedReplacement : v;
1477
+ } : _this$options$stringi;
1478
+ return typeof value === "string" ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, " ");
1479
+ };
1480
+ jsonDiff.equals = function(left, right) {
1481
+ return Diff.prototype.equals.call(jsonDiff, left.replace(/,([\r\n])/g, "$1"), right.replace(/,([\r\n])/g, "$1"));
1482
+ };
1483
+ function canonicalize(obj, stack, replacementStack, replacer, key) {
1484
+ stack = stack || [];
1485
+ replacementStack = replacementStack || [];
1486
+ if (replacer) {
1487
+ obj = replacer(key, obj);
1488
+ }
1489
+ var i;
1490
+ for (i = 0; i < stack.length; i += 1) {
1491
+ if (stack[i] === obj) {
1492
+ return replacementStack[i];
1493
+ }
1494
+ }
1495
+ var canonicalizedObj;
1496
+ if ("[object Array]" === objectPrototypeToString.call(obj)) {
1497
+ stack.push(obj);
1498
+ canonicalizedObj = new Array(obj.length);
1499
+ replacementStack.push(canonicalizedObj);
1500
+ for (i = 0; i < obj.length; i += 1) {
1501
+ canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key);
1502
+ }
1503
+ stack.pop();
1504
+ replacementStack.pop();
1505
+ return canonicalizedObj;
1506
+ }
1507
+ if (obj && obj.toJSON) {
1508
+ obj = obj.toJSON();
1509
+ }
1510
+ if (_typeof(obj) === "object" && obj !== null) {
1511
+ stack.push(obj);
1512
+ canonicalizedObj = {};
1513
+ replacementStack.push(canonicalizedObj);
1514
+ var sortedKeys = [], _key;
1515
+ for (_key in obj) {
1516
+ if (obj.hasOwnProperty(_key)) {
1517
+ sortedKeys.push(_key);
1518
+ }
1519
+ }
1520
+ sortedKeys.sort();
1521
+ for (i = 0; i < sortedKeys.length; i += 1) {
1522
+ _key = sortedKeys[i];
1523
+ canonicalizedObj[_key] = canonicalize(obj[_key], stack, replacementStack, replacer, _key);
1524
+ }
1525
+ stack.pop();
1526
+ replacementStack.pop();
1527
+ } else {
1528
+ canonicalizedObj = obj;
1529
+ }
1530
+ return canonicalizedObj;
1531
+ }
1532
+ var arrayDiff = new Diff();
1533
+ arrayDiff.tokenize = function(value) {
1534
+ return value.slice();
1535
+ };
1536
+ arrayDiff.join = arrayDiff.removeEmpty = function(value) {
1537
+ return value;
1538
+ };
1539
+ function diffArrays(oldArr, newArr, callback) {
1540
+ return arrayDiff.diff(oldArr, newArr, callback);
1541
+ }
1542
+
1543
+ // src/diffEngine.ts
1544
+ var DEFAULT_MAX_INLINE_HIGHLIGHT_LINE_LENGTH = 500;
1545
+ var DEFAULT_DIFF_TIMEOUT_MS = 100;
1546
+ var MAX_INLINE_HIGHLIGHT_LINE_LENGTH = readPositiveIntegerEnv(
1547
+ "BYGONE_MAX_INLINE_HIGHLIGHT_LINE_LENGTH",
1548
+ DEFAULT_MAX_INLINE_HIGHLIGHT_LINE_LENGTH
1549
+ );
1550
+ var DIFF_TIMEOUT_MS = readPositiveIntegerEnv("BYGONE_DIFF_TIMEOUT_MS", DEFAULT_DIFF_TIMEOUT_MS);
1551
+ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
1552
+ const leftLines = normalizeLines(leftContent);
1553
+ const rightLines = normalizeLines(rightContent);
1554
+ const changes = diffArrays(leftLines, rightLines, {
1555
+ timeout: options.timeoutMs ?? DIFF_TIMEOUT_MS
1556
+ });
1557
+ if (!changes) {
1558
+ return buildConservativeTwoWayDiffModel(leftLines, rightLines);
1559
+ }
1560
+ const rows = [];
1561
+ const renderedLeftLines = [];
1562
+ const renderedRightLines = [];
1563
+ const blocks = [];
1564
+ let leftLineNumber = 1;
1565
+ let rightLineNumber = 1;
1566
+ for (let index = 0; index < changes.length; index++) {
1567
+ const change = changes[index];
1568
+ const removedLines = change.removed ? change.value : [];
1569
+ const addedLines = change.added ? change.value : [];
1570
+ if (!change.added && !change.removed) {
1571
+ for (const line of change.value) {
1572
+ renderedLeftLines.push(makeDiffLine("context", line, leftLineNumber));
1573
+ renderedRightLines.push(makeDiffLine("context", line, rightLineNumber));
1574
+ rows.push(makeDiffRow(
1575
+ makeDiffCell("context", line, leftLineNumber++),
1576
+ makeDiffCell("context", line, rightLineNumber++)
1577
+ ));
1578
+ }
1579
+ continue;
1580
+ }
1581
+ if (change.removed && index + 1 < changes.length && changes[index + 1].added) {
1582
+ const nextChange = changes[index + 1];
1583
+ const leftStart = renderedLeftLines.length;
1584
+ const rightStart = renderedRightLines.length;
1585
+ const alignedLines = alignReplacementLines(removedLines, nextChange.value);
1586
+ for (const { left: removedLine, right: addedLine } of alignedLines) {
1587
+ let renderedLeftLine;
1588
+ let renderedRightLine;
1589
+ if (removedLine !== void 0) {
1590
+ renderedLeftLine = makeDiffLine("removed", removedLine, leftLineNumber);
1591
+ renderedLeftLines.push(renderedLeftLine);
1592
+ }
1593
+ if (addedLine !== void 0) {
1594
+ renderedRightLine = makeDiffLine("added", addedLine, rightLineNumber);
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));
1606
+ index++;
1607
+ continue;
1608
+ }
1609
+ if (change.removed) {
1610
+ const leftStart = renderedLeftLines.length;
1611
+ const rightStart = renderedRightLines.length;
1612
+ for (const line of removedLines) {
1613
+ renderedLeftLines.push(makeDiffLine("removed", line, leftLineNumber));
1614
+ rows.push(makeDiffRow(
1615
+ makeDiffCell("removed", line, leftLineNumber++),
1616
+ makePlaceholder()
1617
+ ));
1618
+ }
1619
+ blocks.push(makeDiffBlock("delete", leftStart, renderedLeftLines.length, rightStart, renderedRightLines.length));
1620
+ continue;
1621
+ }
1622
+ if (change.added) {
1623
+ const leftStart = renderedLeftLines.length;
1624
+ const rightStart = renderedRightLines.length;
1625
+ for (const line of addedLines) {
1626
+ renderedRightLines.push(makeDiffLine("added", line, rightLineNumber));
1627
+ rows.push(makeDiffRow(
1628
+ makePlaceholder(),
1629
+ makeDiffCell("added", line, rightLineNumber++)
1630
+ ));
1631
+ }
1632
+ blocks.push(makeDiffBlock("insert", leftStart, renderedLeftLines.length, rightStart, renderedRightLines.length));
1633
+ }
1634
+ }
1635
+ const hasChanges = rows.some((row) => row.left.kind !== "context" || row.right.kind !== "context");
1636
+ return {
1637
+ rows,
1638
+ leftLines: renderedLeftLines,
1639
+ rightLines: renderedRightLines,
1640
+ blocks,
1641
+ hasChanges
1642
+ };
1643
+ }
1644
+ function buildConservativeTwoWayDiffModel(leftLines, rightLines) {
1645
+ const renderedLeftLines = leftLines.map((line, index) => makeDiffLine("removed", line, index + 1));
1646
+ const renderedRightLines = rightLines.map((line, index) => makeDiffLine("added", line, index + 1));
1647
+ const rows = [
1648
+ ...leftLines.map((line, index) => makeDiffRow(
1649
+ makeDiffCell("removed", line, index + 1),
1650
+ makePlaceholder()
1651
+ )),
1652
+ ...rightLines.map((line, index) => makeDiffRow(
1653
+ makePlaceholder(),
1654
+ makeDiffCell("added", line, index + 1)
1655
+ ))
1656
+ ];
1657
+ let blockKind = "replace";
1658
+ if (leftLines.length === 0) {
1659
+ blockKind = "insert";
1660
+ } else if (rightLines.length === 0) {
1661
+ blockKind = "delete";
1662
+ }
1663
+ return {
1664
+ rows,
1665
+ leftLines: renderedLeftLines,
1666
+ rightLines: renderedRightLines,
1667
+ blocks: rows.length === 0 ? [] : [makeDiffBlock(blockKind, 0, leftLines.length, 0, rightLines.length)],
1668
+ hasChanges: rows.length > 0
1669
+ };
1670
+ }
1671
+ function normalizeLines(content) {
1672
+ if (content.length === 0) {
1673
+ return [];
1674
+ }
1675
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
1676
+ if (lines[lines.length - 1] === "") {
1677
+ lines.pop();
1678
+ }
1679
+ return lines;
1680
+ }
1681
+ var MAX_ALIGNMENT_CELLS = 1e4;
1682
+ var MAX_SCORING_LINE_LENGTH = 2e3;
1683
+ var MINIMUM_MATCH_SCORE = 0.52;
1684
+ var MINIMUM_SINGLE_PAIR_SCORE = 0.35;
1685
+ var MINIMUM_CONTEXTUAL_MATCH_SCORE = 0.38;
1686
+ var MINIMUM_CONTEXTUAL_RUN_SCORE = 0.33;
1687
+ var HIGH_CONFIDENCE_MATCH_SCORE = 0.86;
1688
+ var AMBIGUITY_MARGIN = 0.08;
1689
+ var CONTEXTUAL_AMBIGUITY_MARGIN = 0.05;
1690
+ var GAP_PENALTY = -0.12;
1691
+ var MAX_BOUNDED_CANDIDATES = 5e3;
1692
+ var MAX_LARGE_HUNK_ANCHORS = 2e3;
1693
+ var MAX_RARE_TOKEN_OCCURRENCES = 4;
1694
+ var MAX_CONTEXTUAL_HUNK_LINES = 8;
1695
+ var MAX_CONTEXTUAL_LENGTH_DELTA = 4;
1696
+ function alignReplacementLines(leftLines, rightLines) {
1697
+ if (leftLines.length === 0) {
1698
+ return rightLines.map((right) => ({ right }));
1699
+ }
1700
+ if (rightLines.length === 0) {
1701
+ return leftLines.map((left) => ({ left }));
1702
+ }
1703
+ if (leftLines.length === 1 && rightLines.length === 1) {
1704
+ 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]))) {
1706
+ return [{ left: leftLines[0], right: rightLines[0] }];
1707
+ }
1708
+ return [{ left: leftLines[0] }, { right: rightLines[0] }];
1709
+ }
1710
+ if (leftLines.length * rightLines.length > MAX_ALIGNMENT_CELLS) {
1711
+ return alignLargeReplacementLines(leftLines, rightLines);
1712
+ }
1713
+ const pairScores = buildPairScores(leftLines, rightLines);
1714
+ const scores = Array.from(
1715
+ { length: leftLines.length + 1 },
1716
+ () => new Array(rightLines.length + 1).fill(Number.NEGATIVE_INFINITY)
1717
+ );
1718
+ const moves = Array.from(
1719
+ { length: leftLines.length + 1 },
1720
+ () => new Array(rightLines.length + 1).fill(null)
1721
+ );
1722
+ scores[0][0] = 0;
1723
+ for (let leftIndex2 = 1; leftIndex2 <= leftLines.length; leftIndex2++) {
1724
+ scores[leftIndex2][0] = scores[leftIndex2 - 1][0] + GAP_PENALTY;
1725
+ moves[leftIndex2][0] = "left";
1726
+ }
1727
+ for (let rightIndex2 = 1; rightIndex2 <= rightLines.length; rightIndex2++) {
1728
+ scores[0][rightIndex2] = scores[0][rightIndex2 - 1] + GAP_PENALTY;
1729
+ moves[0][rightIndex2] = "right";
1730
+ }
1731
+ for (let leftIndex2 = 1; leftIndex2 <= leftLines.length; leftIndex2++) {
1732
+ for (let rightIndex2 = 1; rightIndex2 <= rightLines.length; rightIndex2++) {
1733
+ const pairScore = pairScores[leftIndex2 - 1][rightIndex2 - 1];
1734
+ const candidates = [
1735
+ { move: "left", score: scores[leftIndex2 - 1][rightIndex2] + GAP_PENALTY },
1736
+ { move: "right", score: scores[leftIndex2][rightIndex2 - 1] + GAP_PENALTY },
1737
+ {
1738
+ move: "match",
1739
+ score: pairScore === null ? Number.NEGATIVE_INFINITY : scores[leftIndex2 - 1][rightIndex2 - 1] + pairScore
1740
+ }
1741
+ ];
1742
+ const best = candidates.reduce((current, candidate) => candidate.score > current.score ? candidate : current);
1743
+ scores[leftIndex2][rightIndex2] = best.score;
1744
+ moves[leftIndex2][rightIndex2] = best.move;
1745
+ }
1746
+ }
1747
+ const aligned = [];
1748
+ let leftIndex = leftLines.length;
1749
+ let rightIndex = rightLines.length;
1750
+ while (leftIndex > 0 || rightIndex > 0) {
1751
+ const move = moves[leftIndex][rightIndex];
1752
+ if (move === "match") {
1753
+ aligned.push({ left: leftLines[--leftIndex], right: rightLines[--rightIndex] });
1754
+ } else if (move === "left") {
1755
+ aligned.push({ left: leftLines[--leftIndex] });
1756
+ } else {
1757
+ aligned.push({ right: rightLines[--rightIndex] });
1758
+ }
1759
+ }
1760
+ return aligned.reverse();
1761
+ }
1762
+ function scoreReplacementLinePair(left, right) {
1763
+ const normalizedLeft = normalizeMatchingContent(left);
1764
+ const normalizedRight = normalizeMatchingContent(right);
1765
+ const leftInformative = isInformativeLine(normalizedLeft);
1766
+ const rightInformative = isInformativeLine(normalizedRight);
1767
+ if (!leftInformative || !rightInformative || normalizedLeft.length > MAX_SCORING_LINE_LENGTH || normalizedRight.length > MAX_SCORING_LINE_LENGTH) {
1768
+ return {
1769
+ score: 0,
1770
+ eligible: false,
1771
+ characterSimilarity: 0,
1772
+ tokenSimilarity: 0,
1773
+ lengthSimilarity: 0
1774
+ };
1775
+ }
1776
+ if (normalizedLeft === normalizedRight) {
1777
+ return {
1778
+ score: 1,
1779
+ eligible: true,
1780
+ characterSimilarity: 1,
1781
+ tokenSimilarity: 1,
1782
+ lengthSimilarity: 1
1783
+ };
1784
+ }
1785
+ const leftTokens = tokenizeMatchingContent(normalizedLeft);
1786
+ const rightTokens = tokenizeMatchingContent(normalizedRight);
1787
+ const characterSimilarity = lineSimilarity(normalizedLeft, normalizedRight);
1788
+ const tokenSimilarity = multisetDiceSimilarity(leftTokens, rightTokens);
1789
+ const lengthSimilarity = Math.min(normalizedLeft.length, normalizedRight.length) / Math.max(normalizedLeft.length, normalizedRight.length);
1790
+ const boundarySimilarity = tokenBoundarySimilarity(leftTokens, rightTokens);
1791
+ const score = characterSimilarity * 0.52 + tokenSimilarity * 0.32 + lengthSimilarity * 0.1 + boundarySimilarity * 0.06;
1792
+ const hasContentEvidence = characterSimilarity >= 0.52 || tokenSimilarity >= 0.45 || characterSimilarity >= 0.48 && tokenSimilarity >= 0.28;
1793
+ return {
1794
+ score,
1795
+ eligible: hasContentEvidence && score >= MINIMUM_MATCH_SCORE,
1796
+ characterSimilarity,
1797
+ tokenSimilarity,
1798
+ lengthSimilarity
1799
+ };
1800
+ }
1801
+ function buildPairScores(leftLines, rightLines) {
1802
+ const scored = leftLines.map((left) => rightLines.map((right) => scoreReplacementLinePair(left, right)));
1803
+ const structuralPairs = collectUniqueStructuralPairs(leftLines, rightLines);
1804
+ const leftRankings = scored.map((row) => rankEligibleScores(row));
1805
+ const rightRankings = rightLines.map((_right, rightIndex) => rankEligibleScores(scored.map((row) => row[rightIndex])));
1806
+ const accepted = scored.map((row, leftIndex) => row.map((candidate, rightIndex) => {
1807
+ if (structuralPairs.has(`${leftIndex}:${rightIndex}`)) {
1808
+ return Math.max(candidate.score, 1);
1809
+ }
1810
+ if (!candidate.eligible) {
1811
+ return null;
1812
+ }
1813
+ const leftRanking = leftRankings[leftIndex];
1814
+ const rightRanking = rightRankings[rightIndex];
1815
+ const isBestOnBothSides = candidate.score === leftRanking.best && candidate.score === rightRanking.best;
1816
+ const requiredMargin = candidate.score >= HIGH_CONFIDENCE_MATCH_SCORE ? AMBIGUITY_MARGIN / 2 : AMBIGUITY_MARGIN;
1817
+ const hasLeftMargin = leftRanking.best - leftRanking.second >= requiredMargin;
1818
+ const hasRightMargin = rightRanking.best - rightRanking.second >= requiredMargin;
1819
+ return isBestOnBothSides && hasLeftMargin && hasRightMargin ? candidate.score : null;
1820
+ }));
1821
+ if (Math.max(leftLines.length, rightLines.length) > MAX_CONTEXTUAL_HUNK_LINES || Math.abs(leftLines.length - rightLines.length) > MAX_CONTEXTUAL_LENGTH_DELTA) {
1822
+ return accepted;
1823
+ }
1824
+ const isContextualCandidate = (candidate, leftIndex, rightIndex) => candidate.score >= MINIMUM_CONTEXTUAL_MATCH_SCORE && haveCompatibleLineRoles(leftLines[leftIndex], rightLines[rightIndex]) && Math.abs(leftIndex - rightIndex) <= MAX_CONTEXTUAL_LENGTH_DELTA;
1825
+ const leftContextRankings = scored.map((row, leftIndex) => rankScores(row, (candidate, rightIndex) => isContextualCandidate(candidate, leftIndex, rightIndex)));
1826
+ const rightContextRankings = rightLines.map((_right, rightIndex) => rankScores(
1827
+ scored.map((row) => row[rightIndex]),
1828
+ (candidate, leftIndex) => isContextualCandidate(candidate, leftIndex, rightIndex)
1829
+ ));
1830
+ const isWeakRunCandidate = (candidate, leftIndex, rightIndex) => candidate.score >= MINIMUM_CONTEXTUAL_RUN_SCORE && haveCompatibleLineRoles(leftLines[leftIndex], rightLines[rightIndex]) && Math.abs(leftIndex - rightIndex) <= MAX_CONTEXTUAL_LENGTH_DELTA;
1831
+ const leftWeakRankings = scored.map((row, leftIndex) => rankScores(row, (candidate, rightIndex) => isWeakRunCandidate(candidate, leftIndex, rightIndex)));
1832
+ const rightWeakRankings = rightLines.map((_right, rightIndex) => rankScores(
1833
+ scored.map((row) => row[rightIndex]),
1834
+ (candidate, leftIndex) => isWeakRunCandidate(candidate, leftIndex, rightIndex)
1835
+ ));
1836
+ const isUnambiguousContextualCandidate = (leftIndex, rightIndex) => {
1837
+ const candidate = scored[leftIndex][rightIndex];
1838
+ if (!isContextualCandidate(candidate, leftIndex, rightIndex)) {
1839
+ return false;
1840
+ }
1841
+ const leftRanking = leftContextRankings[leftIndex];
1842
+ const rightRanking = rightContextRankings[rightIndex];
1843
+ return candidate.score === leftRanking.best && candidate.score === rightRanking.best && leftRanking.best - leftRanking.second >= CONTEXTUAL_AMBIGUITY_MARGIN && rightRanking.best - rightRanking.second >= CONTEXTUAL_AMBIGUITY_MARGIN;
1844
+ };
1845
+ const isUnambiguousWeakRunAnchor = (leftIndex, rightIndex) => {
1846
+ const candidate = scored[leftIndex][rightIndex];
1847
+ if (!isWeakRunCandidate(candidate, leftIndex, rightIndex)) {
1848
+ return false;
1849
+ }
1850
+ const leftRanking = leftWeakRankings[leftIndex];
1851
+ const rightRanking = rightWeakRankings[rightIndex];
1852
+ return candidate.score === leftRanking.best && candidate.score === rightRanking.best && leftRanking.best - leftRanking.second >= CONTEXTUAL_AMBIGUITY_MARGIN && rightRanking.best - rightRanking.second >= CONTEXTUAL_AMBIGUITY_MARGIN;
1853
+ };
1854
+ return accepted.map((row, leftIndex) => row.map((acceptedScore, rightIndex) => {
1855
+ if (acceptedScore !== null) {
1856
+ return acceptedScore;
1857
+ }
1858
+ const hasLocalSupport = isUnambiguousContextualCandidate(leftIndex, rightIndex) && (hasAcceptedDiagonalNeighbor(accepted, leftIndex, rightIndex) || hasContextualDiagonalNeighbor(
1859
+ scored,
1860
+ leftIndex,
1861
+ rightIndex,
1862
+ isUnambiguousContextualCandidate
1863
+ ));
1864
+ const hasRunSupport = hasAnchoredWeakDiagonalRun(
1865
+ scored,
1866
+ leftIndex,
1867
+ rightIndex,
1868
+ (candidateLeftIndex, candidateRightIndex) => isWeakRunCandidate(
1869
+ scored[candidateLeftIndex][candidateRightIndex],
1870
+ candidateLeftIndex,
1871
+ candidateRightIndex
1872
+ ),
1873
+ isUnambiguousWeakRunAnchor
1874
+ );
1875
+ if (!hasLocalSupport && !hasRunSupport) {
1876
+ return null;
1877
+ }
1878
+ return scored[leftIndex][rightIndex].score;
1879
+ }));
1880
+ }
1881
+ function hasAcceptedDiagonalNeighbor(accepted, leftIndex, rightIndex) {
1882
+ return leftIndex > 0 && rightIndex > 0 && accepted[leftIndex - 1][rightIndex - 1] !== null || leftIndex + 1 < accepted.length && rightIndex + 1 < accepted[leftIndex + 1].length && accepted[leftIndex + 1][rightIndex + 1] !== null;
1883
+ }
1884
+ function hasContextualDiagonalNeighbor(scored, leftIndex, rightIndex, isCandidate) {
1885
+ return leftIndex > 0 && rightIndex > 0 && isCandidate(leftIndex - 1, rightIndex - 1) || leftIndex + 1 < scored.length && rightIndex + 1 < scored[leftIndex + 1].length && isCandidate(leftIndex + 1, rightIndex + 1);
1886
+ }
1887
+ function hasAnchoredWeakDiagonalRun(scored, leftIndex, rightIndex, isCandidate, isAnchor) {
1888
+ let startLeft = leftIndex;
1889
+ let startRight = rightIndex;
1890
+ while (startLeft > 0 && startRight > 0 && isCandidate(startLeft - 1, startRight - 1)) {
1891
+ startLeft--;
1892
+ startRight--;
1893
+ }
1894
+ let endLeft = leftIndex;
1895
+ let endRight = rightIndex;
1896
+ let scoreTotal = scored[leftIndex][rightIndex].score;
1897
+ let length = 1;
1898
+ while (endLeft + 1 < scored.length && endRight + 1 < scored[endLeft + 1].length && isCandidate(endLeft + 1, endRight + 1)) {
1899
+ endLeft++;
1900
+ endRight++;
1901
+ scoreTotal += scored[endLeft][endRight].score;
1902
+ length++;
1903
+ }
1904
+ for (let scanLeft = leftIndex - 1, scanRight = rightIndex - 1; scanLeft >= startLeft && scanRight >= startRight; scanLeft--, scanRight--) {
1905
+ scoreTotal += scored[scanLeft][scanRight].score;
1906
+ length++;
1907
+ }
1908
+ return length >= 3 && scoreTotal / length >= MINIMUM_CONTEXTUAL_MATCH_SCORE && isAnchor(startLeft, startRight) && isAnchor(endLeft, endRight);
1909
+ }
1910
+ function rankScores(scores, include) {
1911
+ let best = Number.NEGATIVE_INFINITY;
1912
+ let second = Number.NEGATIVE_INFINITY;
1913
+ scores.forEach((candidate, index) => {
1914
+ if (!include(candidate, index)) {
1915
+ return;
1916
+ }
1917
+ if (candidate.score > best) {
1918
+ second = best;
1919
+ best = candidate.score;
1920
+ } else if (candidate.score > second) {
1921
+ second = candidate.score;
1922
+ }
1923
+ });
1924
+ return { best, second };
1925
+ }
1926
+ function rankEligibleScores(scores) {
1927
+ let best = Number.NEGATIVE_INFINITY;
1928
+ let second = Number.NEGATIVE_INFINITY;
1929
+ for (const candidate of scores) {
1930
+ if (!candidate.eligible) {
1931
+ continue;
1932
+ }
1933
+ if (candidate.score > best) {
1934
+ second = best;
1935
+ best = candidate.score;
1936
+ } else if (candidate.score > second) {
1937
+ second = candidate.score;
1938
+ }
1939
+ }
1940
+ return { best, second };
1941
+ }
1942
+ function alignLargeReplacementLines(leftLines, rightLines) {
1943
+ const anchors = collectLargeHunkAnchors(leftLines, rightLines);
1944
+ const aligned = [];
1945
+ let leftStart = 0;
1946
+ let rightStart = 0;
1947
+ for (const anchor of anchors) {
1948
+ appendBoundedSegment(
1949
+ aligned,
1950
+ leftLines.slice(leftStart, anchor.leftIndex),
1951
+ rightLines.slice(rightStart, anchor.rightIndex)
1952
+ );
1953
+ aligned.push({ left: leftLines[anchor.leftIndex], right: rightLines[anchor.rightIndex] });
1954
+ leftStart = anchor.leftIndex + 1;
1955
+ rightStart = anchor.rightIndex + 1;
1956
+ }
1957
+ appendBoundedSegment(aligned, leftLines.slice(leftStart), rightLines.slice(rightStart));
1958
+ return aligned;
1959
+ }
1960
+ function collectLargeHunkAnchors(leftLines, rightLines) {
1961
+ const candidatePairs = collectRareTokenCandidatePairs(leftLines, rightLines);
1962
+ if (candidatePairs === null) {
1963
+ return [];
1964
+ }
1965
+ const structuralPairs = collectUniqueStructuralPairs(leftLines, rightLines);
1966
+ const scored = candidatePairs.map(({ leftIndex, rightIndex, exact }) => ({
1967
+ leftIndex,
1968
+ rightIndex,
1969
+ exact,
1970
+ structural: structuralPairs.has(`${leftIndex}:${rightIndex}`),
1971
+ match: scoreReplacementLinePair(leftLines[leftIndex], rightLines[rightIndex])
1972
+ })).filter((candidate) => candidate.exact || candidate.structural || candidate.match.eligible);
1973
+ const leftRankings = rankAnchorCandidates(scored, "leftIndex");
1974
+ const rightRankings = rankAnchorCandidates(scored, "rightIndex");
1975
+ const confident = scored.filter((candidate) => {
1976
+ if (candidate.exact || candidate.structural) {
1977
+ return true;
1978
+ }
1979
+ const leftRanking = leftRankings.get(candidate.leftIndex);
1980
+ const rightRanking = rightRankings.get(candidate.rightIndex);
1981
+ if (!leftRanking || !rightRanking || candidate.match.score !== leftRanking.best || candidate.match.score !== rightRanking.best) {
1982
+ return false;
1983
+ }
1984
+ const requiredMargin = candidate.match.score >= HIGH_CONFIDENCE_MATCH_SCORE ? AMBIGUITY_MARGIN / 2 : AMBIGUITY_MARGIN;
1985
+ return leftRanking.best - leftRanking.second >= requiredMargin && rightRanking.best - rightRanking.second >= requiredMargin;
1986
+ });
1987
+ const anchors = selectMaximumWeightMonotonicAnchors(confident, rightLines.length);
1988
+ return anchors.length <= MAX_LARGE_HUNK_ANCHORS ? anchors : [];
1989
+ }
1990
+ function collectRareTokenCandidatePairs(leftLines, rightLines) {
1991
+ const leftExact = buildLinePostings(leftLines, normalizeMatchingContent);
1992
+ const rightExact = buildLinePostings(rightLines, normalizeMatchingContent);
1993
+ const pairs = /* @__PURE__ */ new Map();
1994
+ for (const [line, leftIndices] of leftExact) {
1995
+ const rightIndices = rightExact.get(line);
1996
+ if (!isInformativeLine(line) || leftIndices.length !== 1 || (rightIndices == null ? void 0 : rightIndices.length) !== 1) {
1997
+ continue;
1998
+ }
1999
+ const leftIndex = leftIndices[0];
2000
+ const rightIndex = rightIndices[0];
2001
+ pairs.set(`${leftIndex}:${rightIndex}`, { leftIndex, rightIndex, exact: true });
2002
+ if (pairs.size > MAX_BOUNDED_CANDIDATES) {
2003
+ return null;
2004
+ }
2005
+ }
2006
+ const leftTokens = buildTokenPostings(leftLines);
2007
+ const rightTokens = buildTokenPostings(rightLines);
2008
+ for (const [token, leftIndices] of leftTokens) {
2009
+ const rightIndices = rightTokens.get(token);
2010
+ if (!rightIndices || leftIndices.length > MAX_RARE_TOKEN_OCCURRENCES || rightIndices.length > MAX_RARE_TOKEN_OCCURRENCES) {
2011
+ continue;
2012
+ }
2013
+ for (const leftIndex of leftIndices) {
2014
+ for (const rightIndex of rightIndices) {
2015
+ const key = `${leftIndex}:${rightIndex}`;
2016
+ if (!pairs.has(key)) {
2017
+ pairs.set(key, { leftIndex, rightIndex, exact: false });
2018
+ if (pairs.size > MAX_BOUNDED_CANDIDATES) {
2019
+ return null;
2020
+ }
2021
+ }
2022
+ }
2023
+ }
2024
+ }
2025
+ return [...pairs.values()];
2026
+ }
2027
+ function buildLinePostings(lines, keyForLine) {
2028
+ const postings = /* @__PURE__ */ new Map();
2029
+ lines.forEach((line, index) => {
2030
+ const key = keyForLine(line);
2031
+ const indices = postings.get(key) ?? [];
2032
+ indices.push(index);
2033
+ postings.set(key, indices);
2034
+ });
2035
+ return postings;
2036
+ }
2037
+ function buildTokenPostings(lines) {
2038
+ const postings = /* @__PURE__ */ new Map();
2039
+ lines.forEach((line, index) => {
2040
+ const tokens = new Set(tokenizeMatchingContent(normalizeMatchingContent(line)).filter(isMeaningfulAnchorToken));
2041
+ for (const token of tokens) {
2042
+ const indices = postings.get(token) ?? [];
2043
+ indices.push(index);
2044
+ postings.set(token, indices);
2045
+ }
2046
+ });
2047
+ return postings;
2048
+ }
2049
+ function isMeaningfulAnchorToken(token) {
2050
+ return token.length >= 2 && /[\p{L}\p{N}_$]/u.test(token);
2051
+ }
2052
+ function rankAnchorCandidates(candidates, indexKey) {
2053
+ const grouped = /* @__PURE__ */ new Map();
2054
+ for (const candidate of candidates) {
2055
+ const scores = grouped.get(candidate[indexKey]) ?? [];
2056
+ scores.push(candidate.match);
2057
+ grouped.set(candidate[indexKey], scores);
2058
+ }
2059
+ return new Map([...grouped].map(([index, scores]) => [index, rankEligibleScores(scores)]));
2060
+ }
2061
+ function selectMaximumWeightMonotonicAnchors(candidates, rightLength) {
2062
+ const ordered = [...candidates].sort((left, right) => left.leftIndex - right.leftIndex || left.rightIndex - right.rightIndex);
2063
+ const treeScores = new Array(rightLength + 2).fill(0);
2064
+ const treeCandidates = new Array(rightLength + 2).fill(-1);
2065
+ const bestScores = new Array(ordered.length).fill(0);
2066
+ const previous = new Array(ordered.length).fill(-1);
2067
+ for (let start = 0; start < ordered.length; ) {
2068
+ let end = start + 1;
2069
+ while (end < ordered.length && ordered[end].leftIndex === ordered[start].leftIndex) {
2070
+ end++;
2071
+ }
2072
+ for (let index = start; index < end; index++) {
2073
+ const prior = queryAnchorTree(treeScores, treeCandidates, ordered[index].rightIndex);
2074
+ const anchorBonus = ordered[index].exact || ordered[index].structural ? 0.5 : 0;
2075
+ bestScores[index] = prior.score + ordered[index].match.score + anchorBonus;
2076
+ previous[index] = prior.candidateIndex;
2077
+ }
2078
+ for (let index = start; index < end; index++) {
2079
+ updateAnchorTree(
2080
+ treeScores,
2081
+ treeCandidates,
2082
+ ordered[index].rightIndex + 1,
2083
+ bestScores[index],
2084
+ index
2085
+ );
2086
+ }
2087
+ start = end;
2088
+ }
2089
+ const best = queryAnchorTree(treeScores, treeCandidates, rightLength + 1);
2090
+ const anchors = [];
2091
+ for (let index = best.candidateIndex; index >= 0; index = previous[index]) {
2092
+ anchors.push({ leftIndex: ordered[index].leftIndex, rightIndex: ordered[index].rightIndex });
2093
+ }
2094
+ return anchors.reverse();
2095
+ }
2096
+ function queryAnchorTree(scores, candidates, position) {
2097
+ let score = 0;
2098
+ let candidateIndex = -1;
2099
+ for (let index = position; index > 0; index -= index & -index) {
2100
+ if (scores[index] > score) {
2101
+ score = scores[index];
2102
+ candidateIndex = candidates[index];
2103
+ }
2104
+ }
2105
+ return { score, candidateIndex };
2106
+ }
2107
+ function updateAnchorTree(scores, candidates, position, score, candidateIndex) {
2108
+ for (let index = position; index < scores.length; index += index & -index) {
2109
+ if (score > scores[index]) {
2110
+ scores[index] = score;
2111
+ candidates[index] = candidateIndex;
2112
+ }
2113
+ }
2114
+ }
2115
+ function appendBoundedSegment(aligned, leftLines, rightLines) {
2116
+ if (leftLines.length * rightLines.length <= MAX_ALIGNMENT_CELLS) {
2117
+ aligned.push(...alignReplacementLines(leftLines, rightLines));
2118
+ return;
2119
+ }
2120
+ aligned.push(...leftLines.map((left) => ({ left })));
2121
+ aligned.push(...rightLines.map((right) => ({ right })));
2122
+ }
2123
+ function normalizeMatchingContent(line) {
2124
+ return line.trim().replace(/\s+/g, " ");
2125
+ }
2126
+ function collectUniqueStructuralPairs(leftLines, rightLines) {
2127
+ const leftPostings = buildStructuralPostings(leftLines);
2128
+ const rightPostings = buildStructuralPostings(rightLines);
2129
+ const pairs = /* @__PURE__ */ new Set();
2130
+ for (const [key, leftIndices] of leftPostings) {
2131
+ const rightIndices = rightPostings.get(key);
2132
+ if (leftIndices.length === 1 && (rightIndices == null ? void 0 : rightIndices.length) === 1) {
2133
+ pairs.add(`${leftIndices[0]}:${rightIndices[0]}`);
2134
+ }
2135
+ }
2136
+ return pairs;
2137
+ }
2138
+ function buildStructuralPostings(lines) {
2139
+ const postings = /* @__PURE__ */ new Map();
2140
+ lines.forEach((line, index) => {
2141
+ const key = declarationAnchorKey(line);
2142
+ if (!key) {
2143
+ return;
2144
+ }
2145
+ const indices = postings.get(key) ?? [];
2146
+ indices.push(index);
2147
+ postings.set(key, indices);
2148
+ });
2149
+ return postings;
2150
+ }
2151
+ function haveCompatibleLineRoles(left, right) {
2152
+ const leftKey = declarationAnchorKey(left);
2153
+ const rightKey = declarationAnchorKey(right);
2154
+ if (!leftKey || !rightKey) {
2155
+ return leftKey === rightKey;
2156
+ }
2157
+ return leftKey.split(":", 1)[0] === rightKey.split(":", 1)[0];
2158
+ }
2159
+ function declarationAnchorKey(line) {
2160
+ const normalized = normalizeMatchingContent(line);
2161
+ const patterns = [
2162
+ ["function", /^(?:(?:export|default)\s+)*(?:async\s+)?function\s*\*?\s*([\p{L}_$][\p{L}\p{N}_$]*)\s*\(/u],
2163
+ ["function", /^(?:async\s+)?def\s+([\p{L}_][\p{L}\p{N}_]*)\s*\(/u],
2164
+ ["function", /^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([\p{L}_][\p{L}\p{N}_]*)\s*(?:<[^>]*>)?\s*\(/u],
2165
+ ["function", /^func\s+(?:\([^)]*\)\s*)?([\p{L}_][\p{L}\p{N}_]*)\s*\(/u],
2166
+ ["class", /^(?:(?:export|default|abstract|public)\s+)*class\s+([\p{L}_$][\p{L}\p{N}_$]*)\b/u]
2167
+ ];
2168
+ for (const [kind, pattern] of patterns) {
2169
+ const match = normalized.match(pattern);
2170
+ if (match) {
2171
+ return `${kind}:${match[1]}`;
2172
+ }
2173
+ }
2174
+ return null;
2175
+ }
2176
+ function isInformativeLine(line) {
2177
+ if (line.length < 3) {
2178
+ return false;
2179
+ }
2180
+ return /[\p{L}\p{N}_$]/u.test(line) && !/^[\s()[\]{};,.:+\-*/=>|&!?'"`]+$/.test(line);
2181
+ }
2182
+ function tokenizeMatchingContent(line) {
2183
+ return line.toLowerCase().match(new RegExp("[\\p{L}_$][\\p{L}\\p{N}_$]*|\\p{N}+(?:\\.\\p{N}+)?|[^\\s\\p{L}\\p{N}_$]", "gu")) ?? [];
2184
+ }
2185
+ function multisetDiceSimilarity(leftTokens, rightTokens) {
2186
+ if (leftTokens.length + rightTokens.length === 0) {
2187
+ return 1;
2188
+ }
2189
+ const remaining = /* @__PURE__ */ new Map();
2190
+ leftTokens.forEach((token) => remaining.set(token, (remaining.get(token) ?? 0) + 1));
2191
+ let common = 0;
2192
+ for (const token of rightTokens) {
2193
+ const count = remaining.get(token) ?? 0;
2194
+ if (count > 0) {
2195
+ common++;
2196
+ remaining.set(token, count - 1);
2197
+ }
2198
+ }
2199
+ return 2 * common / (leftTokens.length + rightTokens.length);
2200
+ }
2201
+ function tokenBoundarySimilarity(leftTokens, rightTokens) {
2202
+ if (leftTokens.length === 0 || rightTokens.length === 0) {
2203
+ return 0;
2204
+ }
2205
+ const sameStart = leftTokens[0] === rightTokens[0] ? 0.5 : 0;
2206
+ const sameEnd = leftTokens[leftTokens.length - 1] === rightTokens[rightTokens.length - 1] ? 0.5 : 0;
2207
+ return sameStart + sameEnd;
2208
+ }
2209
+ function lineSimilarity(left, right) {
2210
+ const maxLength = left.length + right.length;
2211
+ if (maxLength === 0) {
2212
+ return 1;
2213
+ }
2214
+ const commonLength = diffChars(left, right).filter((change) => !change.added && !change.removed).reduce((total, change) => total + change.value.length, 0);
2215
+ return 2 * commonLength / maxLength;
2216
+ }
2217
+ function applyInlineHighlightPair(leftLine, rightLine) {
2218
+ if (leftLine.content.length > MAX_INLINE_HIGHLIGHT_LINE_LENGTH || rightLine.content.length > MAX_INLINE_HIGHLIGHT_LINE_LENGTH) {
2219
+ return;
2220
+ }
2221
+ const { leftSegments, rightSegments, hasInlineChanges } = buildInlineSegments(leftLine.content, rightLine.content);
2222
+ if (hasInlineChanges) {
2223
+ leftLine.segments = leftSegments;
2224
+ rightLine.segments = rightSegments;
2225
+ }
2226
+ }
2227
+ function buildInlineSegments(leftContent, rightContent) {
2228
+ const changes = diffWordsWithSpace(leftContent, rightContent);
2229
+ const leftSegments = [];
2230
+ const rightSegments = [];
2231
+ let hasInlineChanges = false;
2232
+ for (const change of changes) {
2233
+ const value = change.value;
2234
+ if (!change.added && !change.removed) {
2235
+ const contextSegment = {
2236
+ kind: "context",
2237
+ text: value,
2238
+ emphasis: false
2239
+ };
2240
+ leftSegments.push(contextSegment);
2241
+ rightSegments.push(contextSegment);
2242
+ continue;
2243
+ }
2244
+ const emphasis = /[^\s]/.test(value);
2245
+ hasInlineChanges = hasInlineChanges || emphasis;
2246
+ if (change.removed) {
2247
+ leftSegments.push({
2248
+ kind: "removed",
2249
+ text: value,
2250
+ emphasis
2251
+ });
2252
+ }
2253
+ if (change.added) {
2254
+ rightSegments.push({
2255
+ kind: "added",
2256
+ text: value,
2257
+ emphasis
2258
+ });
2259
+ }
2260
+ }
2261
+ return {
2262
+ leftSegments,
2263
+ rightSegments,
2264
+ hasInlineChanges
2265
+ };
2266
+ }
2267
+ function makePlaceholder() {
2268
+ return {
2269
+ kind: "placeholder",
2270
+ content: "",
2271
+ lineNumber: null
2272
+ };
2273
+ }
2274
+ function makeDiffCell(kind, content, lineNumber) {
2275
+ return { kind, content, lineNumber };
2276
+ }
2277
+ function makeDiffLine(kind, content, lineNumber) {
2278
+ return { kind, content, lineNumber };
2279
+ }
2280
+ function makeDiffRow(left, right) {
2281
+ return { left, right };
2282
+ }
2283
+ function makeDiffBlock(kind, leftStart, leftEnd, rightStart, rightEnd) {
2284
+ return { kind, leftStart, leftEnd, rightStart, rightEnd };
2285
+ }
2286
+ function readPositiveIntegerEnv(name, fallback) {
2287
+ var _a;
2288
+ const value = typeof process !== "undefined" ? (_a = process.env) == null ? void 0 : _a[name] : void 0;
2289
+ const parsed = Number.parseInt(value ?? "", 10);
2290
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
2291
+ }
2292
+
2293
+ // src/tourAnnotations.ts
2294
+ var MAX_AUTOMATIC_TOUR_FOCUS_RANGES = 4;
2295
+ var NEARBY_TOUR_FOCUS_LINE_GAP = 8;
2296
+ var LARGE_TOUR_FOCUS_RANGE_LINES = 120;
2297
+ var TOUR_FOCUS_BOUNDARY_SEARCH_LINES = 12;
2298
+ function getChangeSourceRange(diffModel, blockIndex, side) {
2299
+ var _a, _b, _c;
2300
+ const block = diffModel == null ? void 0 : diffModel.blocks[blockIndex];
2301
+ const lines = side === "left" ? diffModel == null ? void 0 : diffModel.leftLines : diffModel == null ? void 0 : diffModel.rightLines;
2302
+ if (!block || !(lines == null ? void 0 : lines.length)) {
2303
+ return void 0;
2304
+ }
2305
+ const start = side === "left" ? block.leftStart : block.rightStart;
2306
+ const end = side === "left" ? block.leftEnd : block.rightEnd;
2307
+ const changedLines = lines.slice(start, end);
2308
+ const startLine = ((_a = changedLines[0]) == null ? void 0 : _a.lineNumber) ?? ((_b = lines[Math.min(start, lines.length - 1)]) == null ? void 0 : _b.lineNumber);
2309
+ if (!Number.isInteger(startLine)) {
2310
+ return void 0;
2311
+ }
2312
+ return {
2313
+ startLine,
2314
+ endLine: ((_c = changedLines.at(-1)) == null ? void 0 : _c.lineNumber) ?? startLine
2315
+ };
2316
+ }
2317
+ function buildTourFocusRanges(diffModel, side) {
2318
+ const ranges = diffModel.blocks.map((_, blockIndex) => getChangeSourceRange(diffModel, blockIndex, side)).filter((range) => Boolean(range));
2319
+ if (ranges.length === 0) return [];
2320
+ const nearby = ranges.reduce((grouped, range) => {
2321
+ const previous = grouped.at(-1);
2322
+ if (previous && range.startLine - previous.endLine <= NEARBY_TOUR_FOCUS_LINE_GAP) {
2323
+ previous.endLine = Math.max(previous.endLine, range.endLine);
2324
+ } else {
2325
+ grouped.push({ ...range });
2326
+ }
2327
+ return grouped;
2328
+ }, []);
2329
+ const capped = capFocusRanges(nearby, MAX_AUTOMATIC_TOUR_FOCUS_RANGES);
2330
+ if (capped.length !== 1) return capped;
2331
+ const [only] = capped;
2332
+ const lineCount = only.endLine - only.startLine + 1;
2333
+ if (lineCount < LARGE_TOUR_FOCUS_RANGE_LINES) return capped;
2334
+ const lines = side === "left" ? diffModel.leftLines : diffModel.rightLines;
2335
+ const partCount = Math.min(
2336
+ MAX_AUTOMATIC_TOUR_FOCUS_RANGES,
2337
+ Math.ceil(lineCount / LARGE_TOUR_FOCUS_RANGE_LINES)
2338
+ );
2339
+ const boundaries = [];
2340
+ for (let partIndex = 1; partIndex < partCount; partIndex += 1) {
2341
+ const target = only.startLine + Math.round(lineCount * partIndex / partCount) - 1;
2342
+ boundaries.push(findBlankLineBoundary(lines, target, only.startLine, only.endLine));
2343
+ }
2344
+ const uniqueBoundaries = [...new Set(boundaries)].filter((line) => line >= only.startLine && line < only.endLine).sort((left, right) => left - right);
2345
+ const split = [];
2346
+ let startLine = only.startLine;
2347
+ for (const endLine of uniqueBoundaries) {
2348
+ split.push({ startLine, endLine });
2349
+ startLine = endLine + 1;
2350
+ }
2351
+ split.push({ startLine, endLine: only.endLine });
2352
+ return split;
2353
+ }
2354
+ function capFocusRanges(ranges, maximum) {
2355
+ if (ranges.length <= maximum) return ranges.map((range) => ({ ...range }));
2356
+ const capped = [];
2357
+ for (let index = 0; index < maximum; index += 1) {
2358
+ const startIndex = Math.floor(index * ranges.length / maximum);
2359
+ const endIndex = Math.floor((index + 1) * ranges.length / maximum) - 1;
2360
+ capped.push({
2361
+ startLine: ranges[startIndex].startLine,
2362
+ endLine: ranges[endIndex].endLine
2363
+ });
2364
+ }
2365
+ return capped;
2366
+ }
2367
+ function findBlankLineBoundary(lines, target, minimum, maximum) {
2368
+ for (let distance = 0; distance <= TOUR_FOCUS_BOUNDARY_SEARCH_LINES; distance += 1) {
2369
+ for (const candidate of distance === 0 ? [target] : [target + distance, target - distance]) {
2370
+ if (candidate < minimum || candidate >= maximum) continue;
2371
+ const line = lines.find((entry) => entry.lineNumber === candidate);
2372
+ if ((line == null ? void 0 : line.content.trim()) === "") return candidate;
2373
+ }
2374
+ }
2375
+ return Math.min(Math.max(target, minimum), maximum - 1);
2376
+ }
2377
+
1151
2378
  // src/changeTourContext.ts
1152
2379
  var DEFAULT_MAX_PATCH_BYTES = 128 * 1024;
1153
2380
  var DEFAULT_MAX_TOTAL_PATCH_BYTES = 2 * 1024 * 1024;
@@ -1290,7 +2517,8 @@ function applySource(source, defaultScenes, repoRoot) {
1290
2517
  repoRoot,
1291
2518
  authoredScene,
1292
2519
  authoredScene.base || ((_a = source.range) == null ? void 0 : _a.base),
1293
- authoredScene.target || ((_b = source.range) == null ? void 0 : _b.head)
2520
+ authoredScene.target || ((_b = source.range) == null ? void 0 : _b.head),
2521
+ [...available.keys()]
1294
2522
  );
1295
2523
  scenes.push(deconstructedScene);
1296
2524
  sceneIds.push(deconstructedScene.id);
@@ -1316,8 +2544,8 @@ function applySource(source, defaultScenes, repoRoot) {
1316
2544
  takeaway: authoredScene.takeaway,
1317
2545
  steps: authoredScene.steps.map((step) => {
1318
2546
  const focus = requireResolvedAnchor(resolvedAnchors, step.focus);
1319
- const diff = available.get(focus.path);
1320
- if (!diff) throw new Error(`Step ${step.id} has no text diff for ${focus.path}.`);
2547
+ const diff2 = available.get(focus.path);
2548
+ if (!diff2) throw new Error(`Step ${step.id} has no text diff for ${focus.path}.`);
1321
2549
  return {
1322
2550
  id: step.id,
1323
2551
  title: step.title,
@@ -1325,7 +2553,7 @@ function applySource(source, defaultScenes, repoRoot) {
1325
2553
  depth: step.depth,
1326
2554
  focus,
1327
2555
  connection: step.connection ? connections.get(step.connection) : void 0,
1328
- diff: { ...diff, id: `${authoredScene.id}-${step.id}` }
2556
+ diff: { ...diff2, id: `${authoredScene.id}-${step.id}` }
1329
2557
  };
1330
2558
  })
1331
2559
  };
@@ -1336,7 +2564,7 @@ function applySource(source, defaultScenes, repoRoot) {
1336
2564
  }
1337
2565
  return { scenes, chapters };
1338
2566
  }
1339
- function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef) {
2567
+ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, availableFilePaths = []) {
1340
2568
  const compiled = buildDeconstructedScene(repoRoot, source, { baseRef, headRef: targetRef });
1341
2569
  const panels = [{
1342
2570
  id: "explanation-baseline",
@@ -1348,9 +2576,15 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef) {
1348
2576
  role: "stage",
1349
2577
  stageId: stage.id
1350
2578
  }))];
1351
- const includedPaths = [...new Set(compiled.stages.flatMap((stage) => stage.introducedFiles))];
2579
+ const materializablePaths = new Set(compiled.baselineFiles.map((file) => file.path));
2580
+ const includedPaths = availableFilePaths.length > 0 ? availableFilePaths.filter((filePath) => materializablePaths.has(filePath)).sort() : [...materializablePaths].sort();
2581
+ const missingIntroducedPath = compiled.stages.flatMap((stage) => stage.introducedFiles).find((filePath) => !includedPaths.includes(filePath));
2582
+ if (missingIntroducedPath) {
2583
+ throw new Error(`Deconstructed tour file is not renderable: ${missingIntroducedPath}`);
2584
+ }
2585
+ const introducedPaths = new Set(compiled.stages.flatMap((stage) => stage.introducedFiles));
1352
2586
  let totalContentBytes = 0;
1353
- const files = includedPaths.map((filePath) => {
2587
+ const files = includedPaths.flatMap((filePath) => {
1354
2588
  const states = [
1355
2589
  compiled.baselineFiles.find((file) => file.path === filePath),
1356
2590
  ...compiled.stages.map((stage) => stage.files.find((file) => file.path === filePath))
@@ -1358,18 +2592,23 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef) {
1358
2592
  if (states.some((state) => !state)) {
1359
2593
  throw new Error(`Deconstructed scene ${source.id} is missing virtual state for ${filePath}.`);
1360
2594
  }
1361
- return {
2595
+ const fileContentBytes = states.reduce((total, state) => total + Buffer.byteLength((state == null ? void 0 : state.content) || "", "utf8"), 0);
2596
+ const exceedsFileLimit = states.some((state) => {
2597
+ const content = Buffer.from((state == null ? void 0 : state.content) || "", "utf8");
2598
+ return content.length > DEFAULT_MAX_TOUR_FILE_BYTES || hasLineLongerThan(content, DEFAULT_MAX_TOUR_LINE_BYTES);
2599
+ });
2600
+ const exceedsSceneLimit = totalContentBytes + fileContentBytes > DEFAULT_MAX_STACK_CONTENT_BYTES;
2601
+ if (exceedsFileLimit || exceedsSceneLimit) {
2602
+ if (introducedPaths.has(filePath)) {
2603
+ throw new Error(exceedsFileLimit ? `Deconstructed tour file is too large to present: ${filePath}` : `Deconstructed tour content exceeds ${DEFAULT_MAX_STACK_CONTENT_BYTES} bytes.`);
2604
+ }
2605
+ return [];
2606
+ }
2607
+ totalContentBytes += fileContentBytes;
2608
+ return [{
1362
2609
  path: filePath,
1363
2610
  panels: states.map((state, index) => {
1364
2611
  if (!state) throw new Error(`Missing virtual state for ${filePath}.`);
1365
- const content = Buffer.from(state.content, "utf8");
1366
- if (content.length > DEFAULT_MAX_TOUR_FILE_BYTES || hasLineLongerThan(content, DEFAULT_MAX_TOUR_LINE_BYTES)) {
1367
- throw new Error(`Deconstructed tour file is too large to present: ${filePath}`);
1368
- }
1369
- totalContentBytes += content.length;
1370
- if (totalContentBytes > DEFAULT_MAX_STACK_CONTENT_BYTES) {
1371
- throw new Error(`Deconstructed tour content exceeds ${DEFAULT_MAX_STACK_CONTENT_BYTES} bytes.`);
1372
- }
1373
2612
  return {
1374
2613
  id: panels[index].id,
1375
2614
  label: `${panels[index].label} / ${filePath}${state.exists ? "" : " (absent)"}`,
@@ -1378,9 +2617,10 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef) {
1378
2617
  exists: state.exists
1379
2618
  };
1380
2619
  })
1381
- };
2620
+ }];
1382
2621
  });
1383
2622
  if (files.length === 0) throw new Error(`Deconstructed scene ${source.id} has no staged files.`);
2623
+ const steps = buildDeconstructedFocusSteps(source.id, compiled.stages, files);
1384
2624
  return {
1385
2625
  id: source.id,
1386
2626
  kind: "deconstructed-diff",
@@ -1398,17 +2638,63 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef) {
1398
2638
  },
1399
2639
  panels,
1400
2640
  files,
1401
- steps: compiled.stages.map((stage, index) => ({
1402
- id: stage.id,
1403
- title: stage.title,
1404
- body: stage.narration,
1405
- file: stage.introducedFiles[0],
1406
- pairIndex: index,
1407
- side: "right",
1408
- introducedHunks: stage.introducedHunks
1409
- }))
2641
+ steps
1410
2642
  };
1411
2643
  }
2644
+ function buildDeconstructedFocusSteps(sceneId, stages, files) {
2645
+ const reservedIds = new Set(stages.map((stage) => stage.id));
2646
+ const usedIds = /* @__PURE__ */ new Set();
2647
+ return stages.flatMap((stage, stageIndex) => {
2648
+ const focuses = stage.introducedChanges.flatMap((change) => {
2649
+ const file = files.find((candidate) => candidate.path === change.file);
2650
+ const left = file == null ? void 0 : file.panels[stageIndex];
2651
+ const right = file == null ? void 0 : file.panels[stageIndex + 1];
2652
+ if (!file || !left || !right) {
2653
+ throw new Error(`Deconstructed scene ${sceneId} is missing comparison state for ${change.file}.`);
2654
+ }
2655
+ const diffModel = buildTwoWayDiffModel(left.content, right.content);
2656
+ const side = left.exists && !right.exists ? "left" : "right";
2657
+ const ranges = buildTourFocusRanges(diffModel, side);
2658
+ if (ranges.length === 0) {
2659
+ throw new Error(`Deconstructed stage ${stage.id} has no visible change for ${change.file}.`);
2660
+ }
2661
+ return ranges.map((range) => ({
2662
+ file: change.file,
2663
+ hunks: change.hunks,
2664
+ side,
2665
+ ...range
2666
+ }));
2667
+ });
2668
+ return focuses.map((focus, focusIndex) => ({
2669
+ id: focusIndex === 0 ? claimDeconstructedStageId(stage.id, usedIds) : claimDeconstructedFocusId(`${stage.id}-focus-${focusIndex + 1}`, reservedIds, usedIds),
2670
+ title: focuses.length === 1 ? stage.title : `${stage.title} \xB7 ${focus.file} \xB7 Focus ${focusIndex + 1}/${focuses.length}`,
2671
+ body: stage.narration,
2672
+ file: focus.file,
2673
+ pairIndex: stageIndex,
2674
+ side: focus.side,
2675
+ startLine: focus.startLine,
2676
+ endLine: focus.endLine,
2677
+ introducedHunks: [...focus.hunks],
2678
+ stageId: stage.id,
2679
+ stageIndex,
2680
+ focusIndex,
2681
+ focusCount: focuses.length
2682
+ }));
2683
+ });
2684
+ }
2685
+ function claimDeconstructedStageId(stageId, usedIds) {
2686
+ usedIds.add(stageId);
2687
+ return stageId;
2688
+ }
2689
+ function claimDeconstructedFocusId(preferredId, reservedIds, usedIds) {
2690
+ let candidate = preferredId;
2691
+ let suffix = 2;
2692
+ while (usedIds.has(candidate) || reservedIds.has(candidate)) {
2693
+ candidate = `${preferredId}-${suffix++}`;
2694
+ }
2695
+ usedIds.add(candidate);
2696
+ return candidate;
2697
+ }
1412
2698
  function buildStackedScene(repoRoot, source, availableFilePaths) {
1413
2699
  const stack = source.stack.map((entry) => ({
1414
2700
  id: entry.id,