@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.
package/out/changeTour.js CHANGED
@@ -418,16 +418,41 @@ function validateDeconstructedScene(value, index) {
418
418
  }
419
419
  validateMultiPanelFiles(value.files, panels.length, index);
420
420
  validateMultiPanelSteps(value.steps, panels.length, index, true);
421
- if (Array.isArray(value.steps) && value.steps.length !== panels.length - 1) {
422
- throw new Error(`scenes[${index}].steps must match the number of explanation stages.`);
423
- }
424
421
  if (Array.isArray(value.steps)) {
422
+ let previousPairIndex = -1;
425
423
  value.steps.forEach((step, stepIndex) => {
426
- const panel = panels[stepIndex + 1];
427
- if (!isRecord(step) || !isRecord(panel) || panel.stageId !== step.id) {
428
- throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
424
+ if (!isRecord(step)) return;
425
+ const pairIndex = Number(step.pairIndex);
426
+ if (pairIndex < previousPairIndex) {
427
+ throw new Error(`scenes[${index}].steps must remain grouped in explanation stage order.`);
428
+ }
429
+ previousPairIndex = pairIndex;
430
+ if (step.stageId !== void 0) requireString(step.stageId, `scenes[${index}].steps[${stepIndex}].stageId`);
431
+ if (step.stageIndex !== void 0) requireNonNegativeInteger(step.stageIndex, `scenes[${index}].steps[${stepIndex}].stageIndex`);
432
+ if (step.focusIndex !== void 0) requireNonNegativeInteger(step.focusIndex, `scenes[${index}].steps[${stepIndex}].focusIndex`);
433
+ if (step.focusCount !== void 0) requirePositiveInteger(step.focusCount, `scenes[${index}].steps[${stepIndex}].focusCount`);
434
+ if (step.stageIndex !== void 0 && Number(step.stageIndex) !== pairIndex) {
435
+ throw new Error(`scenes[${index}].steps[${stepIndex}].stageIndex must match pairIndex.`);
436
+ }
437
+ if (step.focusIndex !== void 0 && step.focusCount !== void 0 && Number(step.focusIndex) >= Number(step.focusCount)) {
438
+ throw new Error(`scenes[${index}].steps[${stepIndex}].focusIndex must be less than focusCount.`);
429
439
  }
430
440
  });
441
+ for (let stageIndex = 0; stageIndex < panels.length - 1; stageIndex += 1) {
442
+ const panel = panels[stageIndex + 1];
443
+ const stageSteps = value.steps.filter((step) => isRecord(step) && Number(step.pairIndex) === stageIndex);
444
+ if (!isRecord(panel) || stageSteps.length === 0) {
445
+ throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
446
+ }
447
+ const firstStep = stageSteps[0];
448
+ const fallbackStageId = isRecord(firstStep) ? firstStep.id : void 0;
449
+ if (stageSteps.some((step) => {
450
+ if (!isRecord(step)) return true;
451
+ return (step.stageId ?? fallbackStageId) !== panel.stageId;
452
+ })) {
453
+ throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
454
+ }
455
+ }
431
456
  }
432
457
  }
433
458
  function validateMultiPanelFiles(value, panelCount, index) {
@@ -456,7 +481,6 @@ function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
456
481
  if (deconstructed) {
457
482
  requireStringArray(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
458
483
  if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
459
- if (Number(step.pairIndex) !== stepIndex) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex must match its explanation stage.`);
460
484
  }
461
485
  if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
462
486
  if (step.startLine !== void 0) requirePositiveInteger(step.startLine, `scenes[${index}].steps[${stepIndex}].startLine`);
@@ -1102,6 +1126,7 @@ function compileDeconstructedScene(inventory, scene) {
1102
1126
  for (const { stage, claims } of stageClaims) {
1103
1127
  const introducedHunks = [];
1104
1128
  const introducedFiles = [];
1129
+ const introducedChanges = [];
1105
1130
  for (const claim of claims) {
1106
1131
  const cumulative = cumulativeByFile.get(claim.file) || /* @__PURE__ */ new Set();
1107
1132
  claim.ids.forEach((id) => {
@@ -1110,6 +1135,12 @@ function compileDeconstructedScene(inventory, scene) {
1110
1135
  });
1111
1136
  cumulativeByFile.set(claim.file, cumulative);
1112
1137
  if (!introducedFiles.includes(claim.file.path)) introducedFiles.push(claim.file.path);
1138
+ const introducedChange = introducedChanges.find((change) => change.file === claim.file.path);
1139
+ if (introducedChange) {
1140
+ introducedChange.hunks.push(...claim.ids);
1141
+ } else {
1142
+ introducedChanges.push({ file: claim.file.path, hunks: [...claim.ids] });
1143
+ }
1113
1144
  }
1114
1145
  stages.push({
1115
1146
  id: stage.id,
@@ -1117,6 +1148,7 @@ function compileDeconstructedScene(inventory, scene) {
1117
1148
  narration: stage.narration,
1118
1149
  introducedHunks,
1119
1150
  introducedFiles,
1151
+ introducedChanges,
1120
1152
  cumulativeHunks: [...cumulativeByFile.values()].flatMap((ids) => [...ids]),
1121
1153
  files: materializableFiles.map((file) => buildFileState(
1122
1154
  file,
@@ -1182,6 +1214,1201 @@ function buildFileState(file, selected, wholeFileExcluded) {
1182
1214
  };
1183
1215
  }
1184
1216
 
1217
+ // node_modules/diff/lib/index.mjs
1218
+ function Diff() {
1219
+ }
1220
+ Diff.prototype = {
1221
+ diff: function diff(oldString, newString) {
1222
+ var _options$timeout;
1223
+ var options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
1224
+ var callback = options.callback;
1225
+ if (typeof options === "function") {
1226
+ callback = options;
1227
+ options = {};
1228
+ }
1229
+ this.options = options;
1230
+ var self = this;
1231
+ function done(value) {
1232
+ if (callback) {
1233
+ setTimeout(function() {
1234
+ callback(void 0, value);
1235
+ }, 0);
1236
+ return true;
1237
+ } else {
1238
+ return value;
1239
+ }
1240
+ }
1241
+ oldString = this.castInput(oldString);
1242
+ newString = this.castInput(newString);
1243
+ oldString = this.removeEmpty(this.tokenize(oldString));
1244
+ newString = this.removeEmpty(this.tokenize(newString));
1245
+ var newLen = newString.length, oldLen = oldString.length;
1246
+ var editLength = 1;
1247
+ var maxEditLength = newLen + oldLen;
1248
+ if (options.maxEditLength) {
1249
+ maxEditLength = Math.min(maxEditLength, options.maxEditLength);
1250
+ }
1251
+ var maxExecutionTime = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : Infinity;
1252
+ var abortAfterTimestamp = Date.now() + maxExecutionTime;
1253
+ var bestPath = [{
1254
+ oldPos: -1,
1255
+ lastComponent: void 0
1256
+ }];
1257
+ var newPos = this.extractCommon(bestPath[0], newString, oldString, 0);
1258
+ if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
1259
+ return done([{
1260
+ value: this.join(newString),
1261
+ count: newString.length
1262
+ }]);
1263
+ }
1264
+ var minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
1265
+ function execEditLength() {
1266
+ for (var diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
1267
+ var basePath = void 0;
1268
+ var removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
1269
+ if (removePath) {
1270
+ bestPath[diagonalPath - 1] = void 0;
1271
+ }
1272
+ var canAdd = false;
1273
+ if (addPath) {
1274
+ var addPathNewPos = addPath.oldPos - diagonalPath;
1275
+ canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
1276
+ }
1277
+ var canRemove = removePath && removePath.oldPos + 1 < oldLen;
1278
+ if (!canAdd && !canRemove) {
1279
+ bestPath[diagonalPath] = void 0;
1280
+ continue;
1281
+ }
1282
+ if (!canRemove || canAdd && removePath.oldPos + 1 < addPath.oldPos) {
1283
+ basePath = self.addToPath(addPath, true, void 0, 0);
1284
+ } else {
1285
+ basePath = self.addToPath(removePath, void 0, true, 1);
1286
+ }
1287
+ newPos = self.extractCommon(basePath, newString, oldString, diagonalPath);
1288
+ if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
1289
+ return done(buildValues(self, basePath.lastComponent, newString, oldString, self.useLongestToken));
1290
+ } else {
1291
+ bestPath[diagonalPath] = basePath;
1292
+ if (basePath.oldPos + 1 >= oldLen) {
1293
+ maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
1294
+ }
1295
+ if (newPos + 1 >= newLen) {
1296
+ minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
1297
+ }
1298
+ }
1299
+ }
1300
+ editLength++;
1301
+ }
1302
+ if (callback) {
1303
+ (function exec() {
1304
+ setTimeout(function() {
1305
+ if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
1306
+ return callback();
1307
+ }
1308
+ if (!execEditLength()) {
1309
+ exec();
1310
+ }
1311
+ }, 0);
1312
+ })();
1313
+ } else {
1314
+ while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
1315
+ var ret = execEditLength();
1316
+ if (ret) {
1317
+ return ret;
1318
+ }
1319
+ }
1320
+ }
1321
+ },
1322
+ addToPath: function addToPath(path, added, removed, oldPosInc) {
1323
+ var last = path.lastComponent;
1324
+ if (last && last.added === added && last.removed === removed) {
1325
+ return {
1326
+ oldPos: path.oldPos + oldPosInc,
1327
+ lastComponent: {
1328
+ count: last.count + 1,
1329
+ added,
1330
+ removed,
1331
+ previousComponent: last.previousComponent
1332
+ }
1333
+ };
1334
+ } else {
1335
+ return {
1336
+ oldPos: path.oldPos + oldPosInc,
1337
+ lastComponent: {
1338
+ count: 1,
1339
+ added,
1340
+ removed,
1341
+ previousComponent: last
1342
+ }
1343
+ };
1344
+ }
1345
+ },
1346
+ extractCommon: function extractCommon(basePath, newString, oldString, diagonalPath) {
1347
+ var newLen = newString.length, oldLen = oldString.length, oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
1348
+ while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newString[newPos + 1], oldString[oldPos + 1])) {
1349
+ newPos++;
1350
+ oldPos++;
1351
+ commonCount++;
1352
+ }
1353
+ if (commonCount) {
1354
+ basePath.lastComponent = {
1355
+ count: commonCount,
1356
+ previousComponent: basePath.lastComponent
1357
+ };
1358
+ }
1359
+ basePath.oldPos = oldPos;
1360
+ return newPos;
1361
+ },
1362
+ equals: function equals(left, right) {
1363
+ if (this.options.comparator) {
1364
+ return this.options.comparator(left, right);
1365
+ } else {
1366
+ return left === right || this.options.ignoreCase && left.toLowerCase() === right.toLowerCase();
1367
+ }
1368
+ },
1369
+ removeEmpty: function removeEmpty(array) {
1370
+ var ret = [];
1371
+ for (var i = 0; i < array.length; i++) {
1372
+ if (array[i]) {
1373
+ ret.push(array[i]);
1374
+ }
1375
+ }
1376
+ return ret;
1377
+ },
1378
+ castInput: function castInput(value) {
1379
+ return value;
1380
+ },
1381
+ tokenize: function tokenize(value) {
1382
+ return value.split("");
1383
+ },
1384
+ join: function join(chars) {
1385
+ return chars.join("");
1386
+ }
1387
+ };
1388
+ function buildValues(diff2, lastComponent, newString, oldString, useLongestToken) {
1389
+ var components = [];
1390
+ var nextComponent;
1391
+ while (lastComponent) {
1392
+ components.push(lastComponent);
1393
+ nextComponent = lastComponent.previousComponent;
1394
+ delete lastComponent.previousComponent;
1395
+ lastComponent = nextComponent;
1396
+ }
1397
+ components.reverse();
1398
+ var componentPos = 0, componentLen = components.length, newPos = 0, oldPos = 0;
1399
+ for (; componentPos < componentLen; componentPos++) {
1400
+ var component = components[componentPos];
1401
+ if (!component.removed) {
1402
+ if (!component.added && useLongestToken) {
1403
+ var value = newString.slice(newPos, newPos + component.count);
1404
+ value = value.map(function(value2, i) {
1405
+ var oldValue = oldString[oldPos + i];
1406
+ return oldValue.length > value2.length ? oldValue : value2;
1407
+ });
1408
+ component.value = diff2.join(value);
1409
+ } else {
1410
+ component.value = diff2.join(newString.slice(newPos, newPos + component.count));
1411
+ }
1412
+ newPos += component.count;
1413
+ if (!component.added) {
1414
+ oldPos += component.count;
1415
+ }
1416
+ } else {
1417
+ component.value = diff2.join(oldString.slice(oldPos, oldPos + component.count));
1418
+ oldPos += component.count;
1419
+ if (componentPos && components[componentPos - 1].added) {
1420
+ var tmp = components[componentPos - 1];
1421
+ components[componentPos - 1] = components[componentPos];
1422
+ components[componentPos] = tmp;
1423
+ }
1424
+ }
1425
+ }
1426
+ var finalComponent = components[componentLen - 1];
1427
+ if (componentLen > 1 && typeof finalComponent.value === "string" && (finalComponent.added || finalComponent.removed) && diff2.equals("", finalComponent.value)) {
1428
+ components[componentLen - 2].value += finalComponent.value;
1429
+ components.pop();
1430
+ }
1431
+ return components;
1432
+ }
1433
+ var characterDiff = new Diff();
1434
+ function diffChars(oldStr, newStr, options) {
1435
+ return characterDiff.diff(oldStr, newStr, options);
1436
+ }
1437
+ var extendedWordChars = /^[A-Za-z\xC0-\u02C6\u02C8-\u02D7\u02DE-\u02FF\u1E00-\u1EFF]+$/;
1438
+ var reWhitespace = /\S/;
1439
+ var wordDiff = new Diff();
1440
+ wordDiff.equals = function(left, right) {
1441
+ if (this.options.ignoreCase) {
1442
+ left = left.toLowerCase();
1443
+ right = right.toLowerCase();
1444
+ }
1445
+ return left === right || this.options.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right);
1446
+ };
1447
+ wordDiff.tokenize = function(value) {
1448
+ var tokens = value.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/);
1449
+ for (var i = 0; i < tokens.length - 1; i++) {
1450
+ if (!tokens[i + 1] && tokens[i + 2] && extendedWordChars.test(tokens[i]) && extendedWordChars.test(tokens[i + 2])) {
1451
+ tokens[i] += tokens[i + 2];
1452
+ tokens.splice(i + 1, 2);
1453
+ i--;
1454
+ }
1455
+ }
1456
+ return tokens;
1457
+ };
1458
+ function diffWordsWithSpace(oldStr, newStr, options) {
1459
+ return wordDiff.diff(oldStr, newStr, options);
1460
+ }
1461
+ var lineDiff = new Diff();
1462
+ lineDiff.tokenize = function(value) {
1463
+ if (this.options.stripTrailingCr) {
1464
+ value = value.replace(/\r\n/g, "\n");
1465
+ }
1466
+ var retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
1467
+ if (!linesAndNewlines[linesAndNewlines.length - 1]) {
1468
+ linesAndNewlines.pop();
1469
+ }
1470
+ for (var i = 0; i < linesAndNewlines.length; i++) {
1471
+ var line = linesAndNewlines[i];
1472
+ if (i % 2 && !this.options.newlineIsToken) {
1473
+ retLines[retLines.length - 1] += line;
1474
+ } else {
1475
+ if (this.options.ignoreWhitespace) {
1476
+ line = line.trim();
1477
+ }
1478
+ retLines.push(line);
1479
+ }
1480
+ }
1481
+ return retLines;
1482
+ };
1483
+ var sentenceDiff = new Diff();
1484
+ sentenceDiff.tokenize = function(value) {
1485
+ return value.split(/(\S.+?[.!?])(?=\s+|$)/);
1486
+ };
1487
+ var cssDiff = new Diff();
1488
+ cssDiff.tokenize = function(value) {
1489
+ return value.split(/([{}:;,]|\s+)/);
1490
+ };
1491
+ function _typeof(obj) {
1492
+ "@babel/helpers - typeof";
1493
+ if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
1494
+ _typeof = function(obj2) {
1495
+ return typeof obj2;
1496
+ };
1497
+ } else {
1498
+ _typeof = function(obj2) {
1499
+ return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
1500
+ };
1501
+ }
1502
+ return _typeof(obj);
1503
+ }
1504
+ var objectPrototypeToString = Object.prototype.toString;
1505
+ var jsonDiff = new Diff();
1506
+ jsonDiff.useLongestToken = true;
1507
+ jsonDiff.tokenize = lineDiff.tokenize;
1508
+ jsonDiff.castInput = function(value) {
1509
+ var _this$options = this.options, undefinedReplacement = _this$options.undefinedReplacement, _this$options$stringi = _this$options.stringifyReplacer, stringifyReplacer = _this$options$stringi === void 0 ? function(k, v) {
1510
+ return typeof v === "undefined" ? undefinedReplacement : v;
1511
+ } : _this$options$stringi;
1512
+ return typeof value === "string" ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, " ");
1513
+ };
1514
+ jsonDiff.equals = function(left, right) {
1515
+ return Diff.prototype.equals.call(jsonDiff, left.replace(/,([\r\n])/g, "$1"), right.replace(/,([\r\n])/g, "$1"));
1516
+ };
1517
+ function canonicalize(obj, stack, replacementStack, replacer, key) {
1518
+ stack = stack || [];
1519
+ replacementStack = replacementStack || [];
1520
+ if (replacer) {
1521
+ obj = replacer(key, obj);
1522
+ }
1523
+ var i;
1524
+ for (i = 0; i < stack.length; i += 1) {
1525
+ if (stack[i] === obj) {
1526
+ return replacementStack[i];
1527
+ }
1528
+ }
1529
+ var canonicalizedObj;
1530
+ if ("[object Array]" === objectPrototypeToString.call(obj)) {
1531
+ stack.push(obj);
1532
+ canonicalizedObj = new Array(obj.length);
1533
+ replacementStack.push(canonicalizedObj);
1534
+ for (i = 0; i < obj.length; i += 1) {
1535
+ canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key);
1536
+ }
1537
+ stack.pop();
1538
+ replacementStack.pop();
1539
+ return canonicalizedObj;
1540
+ }
1541
+ if (obj && obj.toJSON) {
1542
+ obj = obj.toJSON();
1543
+ }
1544
+ if (_typeof(obj) === "object" && obj !== null) {
1545
+ stack.push(obj);
1546
+ canonicalizedObj = {};
1547
+ replacementStack.push(canonicalizedObj);
1548
+ var sortedKeys = [], _key;
1549
+ for (_key in obj) {
1550
+ if (obj.hasOwnProperty(_key)) {
1551
+ sortedKeys.push(_key);
1552
+ }
1553
+ }
1554
+ sortedKeys.sort();
1555
+ for (i = 0; i < sortedKeys.length; i += 1) {
1556
+ _key = sortedKeys[i];
1557
+ canonicalizedObj[_key] = canonicalize(obj[_key], stack, replacementStack, replacer, _key);
1558
+ }
1559
+ stack.pop();
1560
+ replacementStack.pop();
1561
+ } else {
1562
+ canonicalizedObj = obj;
1563
+ }
1564
+ return canonicalizedObj;
1565
+ }
1566
+ var arrayDiff = new Diff();
1567
+ arrayDiff.tokenize = function(value) {
1568
+ return value.slice();
1569
+ };
1570
+ arrayDiff.join = arrayDiff.removeEmpty = function(value) {
1571
+ return value;
1572
+ };
1573
+ function diffArrays(oldArr, newArr, callback) {
1574
+ return arrayDiff.diff(oldArr, newArr, callback);
1575
+ }
1576
+
1577
+ // src/diffEngine.ts
1578
+ var DEFAULT_MAX_INLINE_HIGHLIGHT_LINE_LENGTH = 500;
1579
+ var DEFAULT_DIFF_TIMEOUT_MS = 100;
1580
+ var MAX_INLINE_HIGHLIGHT_LINE_LENGTH = readPositiveIntegerEnv(
1581
+ "BYGONE_MAX_INLINE_HIGHLIGHT_LINE_LENGTH",
1582
+ DEFAULT_MAX_INLINE_HIGHLIGHT_LINE_LENGTH
1583
+ );
1584
+ var DIFF_TIMEOUT_MS = readPositiveIntegerEnv("BYGONE_DIFF_TIMEOUT_MS", DEFAULT_DIFF_TIMEOUT_MS);
1585
+ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
1586
+ const leftLines = normalizeLines(leftContent);
1587
+ const rightLines = normalizeLines(rightContent);
1588
+ const changes = diffArrays(leftLines, rightLines, {
1589
+ timeout: options.timeoutMs ?? DIFF_TIMEOUT_MS
1590
+ });
1591
+ if (!changes) {
1592
+ return buildConservativeTwoWayDiffModel(leftLines, rightLines);
1593
+ }
1594
+ const rows = [];
1595
+ const renderedLeftLines = [];
1596
+ const renderedRightLines = [];
1597
+ const blocks = [];
1598
+ let leftLineNumber = 1;
1599
+ let rightLineNumber = 1;
1600
+ for (let index = 0; index < changes.length; index++) {
1601
+ const change = changes[index];
1602
+ const removedLines = change.removed ? change.value : [];
1603
+ const addedLines = change.added ? change.value : [];
1604
+ if (!change.added && !change.removed) {
1605
+ for (const line of change.value) {
1606
+ renderedLeftLines.push(makeDiffLine("context", line, leftLineNumber));
1607
+ renderedRightLines.push(makeDiffLine("context", line, rightLineNumber));
1608
+ rows.push(makeDiffRow(
1609
+ makeDiffCell("context", line, leftLineNumber++),
1610
+ makeDiffCell("context", line, rightLineNumber++)
1611
+ ));
1612
+ }
1613
+ continue;
1614
+ }
1615
+ if (change.removed && index + 1 < changes.length && changes[index + 1].added) {
1616
+ const nextChange = changes[index + 1];
1617
+ const leftStart = renderedLeftLines.length;
1618
+ const rightStart = renderedRightLines.length;
1619
+ const alignedLines = alignReplacementLines(removedLines, nextChange.value);
1620
+ for (const { left: removedLine, right: addedLine } of alignedLines) {
1621
+ let renderedLeftLine;
1622
+ let renderedRightLine;
1623
+ if (removedLine !== void 0) {
1624
+ renderedLeftLine = makeDiffLine("removed", removedLine, leftLineNumber);
1625
+ renderedLeftLines.push(renderedLeftLine);
1626
+ }
1627
+ if (addedLine !== void 0) {
1628
+ renderedRightLine = makeDiffLine("added", addedLine, rightLineNumber);
1629
+ renderedRightLines.push(renderedRightLine);
1630
+ }
1631
+ if (renderedLeftLine && renderedRightLine) {
1632
+ applyInlineHighlightPair(renderedLeftLine, renderedRightLine);
1633
+ }
1634
+ rows.push(makeDiffRow(
1635
+ removedLine === void 0 ? makePlaceholder() : makeDiffCell("removed", removedLine, leftLineNumber++),
1636
+ addedLine === void 0 ? makePlaceholder() : makeDiffCell("added", addedLine, rightLineNumber++)
1637
+ ));
1638
+ }
1639
+ blocks.push(makeDiffBlock("replace", leftStart, renderedLeftLines.length, rightStart, renderedRightLines.length));
1640
+ index++;
1641
+ continue;
1642
+ }
1643
+ if (change.removed) {
1644
+ const leftStart = renderedLeftLines.length;
1645
+ const rightStart = renderedRightLines.length;
1646
+ for (const line of removedLines) {
1647
+ renderedLeftLines.push(makeDiffLine("removed", line, leftLineNumber));
1648
+ rows.push(makeDiffRow(
1649
+ makeDiffCell("removed", line, leftLineNumber++),
1650
+ makePlaceholder()
1651
+ ));
1652
+ }
1653
+ blocks.push(makeDiffBlock("delete", leftStart, renderedLeftLines.length, rightStart, renderedRightLines.length));
1654
+ continue;
1655
+ }
1656
+ if (change.added) {
1657
+ const leftStart = renderedLeftLines.length;
1658
+ const rightStart = renderedRightLines.length;
1659
+ for (const line of addedLines) {
1660
+ renderedRightLines.push(makeDiffLine("added", line, rightLineNumber));
1661
+ rows.push(makeDiffRow(
1662
+ makePlaceholder(),
1663
+ makeDiffCell("added", line, rightLineNumber++)
1664
+ ));
1665
+ }
1666
+ blocks.push(makeDiffBlock("insert", leftStart, renderedLeftLines.length, rightStart, renderedRightLines.length));
1667
+ }
1668
+ }
1669
+ const hasChanges = rows.some((row) => row.left.kind !== "context" || row.right.kind !== "context");
1670
+ return {
1671
+ rows,
1672
+ leftLines: renderedLeftLines,
1673
+ rightLines: renderedRightLines,
1674
+ blocks,
1675
+ hasChanges
1676
+ };
1677
+ }
1678
+ function buildConservativeTwoWayDiffModel(leftLines, rightLines) {
1679
+ const renderedLeftLines = leftLines.map((line, index) => makeDiffLine("removed", line, index + 1));
1680
+ const renderedRightLines = rightLines.map((line, index) => makeDiffLine("added", line, index + 1));
1681
+ const rows = [
1682
+ ...leftLines.map((line, index) => makeDiffRow(
1683
+ makeDiffCell("removed", line, index + 1),
1684
+ makePlaceholder()
1685
+ )),
1686
+ ...rightLines.map((line, index) => makeDiffRow(
1687
+ makePlaceholder(),
1688
+ makeDiffCell("added", line, index + 1)
1689
+ ))
1690
+ ];
1691
+ let blockKind = "replace";
1692
+ if (leftLines.length === 0) {
1693
+ blockKind = "insert";
1694
+ } else if (rightLines.length === 0) {
1695
+ blockKind = "delete";
1696
+ }
1697
+ return {
1698
+ rows,
1699
+ leftLines: renderedLeftLines,
1700
+ rightLines: renderedRightLines,
1701
+ blocks: rows.length === 0 ? [] : [makeDiffBlock(blockKind, 0, leftLines.length, 0, rightLines.length)],
1702
+ hasChanges: rows.length > 0
1703
+ };
1704
+ }
1705
+ function normalizeLines(content) {
1706
+ if (content.length === 0) {
1707
+ return [];
1708
+ }
1709
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
1710
+ if (lines[lines.length - 1] === "") {
1711
+ lines.pop();
1712
+ }
1713
+ return lines;
1714
+ }
1715
+ var MAX_ALIGNMENT_CELLS = 1e4;
1716
+ var MAX_SCORING_LINE_LENGTH = 2e3;
1717
+ var MINIMUM_MATCH_SCORE = 0.52;
1718
+ var MINIMUM_SINGLE_PAIR_SCORE = 0.35;
1719
+ var MINIMUM_CONTEXTUAL_MATCH_SCORE = 0.38;
1720
+ var MINIMUM_CONTEXTUAL_RUN_SCORE = 0.33;
1721
+ var HIGH_CONFIDENCE_MATCH_SCORE = 0.86;
1722
+ var AMBIGUITY_MARGIN = 0.08;
1723
+ var CONTEXTUAL_AMBIGUITY_MARGIN = 0.05;
1724
+ var GAP_PENALTY = -0.12;
1725
+ var MAX_BOUNDED_CANDIDATES = 5e3;
1726
+ var MAX_LARGE_HUNK_ANCHORS = 2e3;
1727
+ var MAX_RARE_TOKEN_OCCURRENCES = 4;
1728
+ var MAX_CONTEXTUAL_HUNK_LINES = 8;
1729
+ var MAX_CONTEXTUAL_LENGTH_DELTA = 4;
1730
+ function alignReplacementLines(leftLines, rightLines) {
1731
+ if (leftLines.length === 0) {
1732
+ return rightLines.map((right) => ({ right }));
1733
+ }
1734
+ if (rightLines.length === 0) {
1735
+ return leftLines.map((left) => ({ left }));
1736
+ }
1737
+ if (leftLines.length === 1 && rightLines.length === 1) {
1738
+ const singletonScore = scoreReplacementLinePair(leftLines[0], rightLines[0]);
1739
+ if ((singletonScore.eligible || singletonScore.score >= MINIMUM_SINGLE_PAIR_SCORE) && haveCompatibleLineRoles(leftLines[0], rightLines[0]) && isInformativeLine(normalizeMatchingContent(leftLines[0])) && isInformativeLine(normalizeMatchingContent(rightLines[0]))) {
1740
+ return [{ left: leftLines[0], right: rightLines[0] }];
1741
+ }
1742
+ return [{ left: leftLines[0] }, { right: rightLines[0] }];
1743
+ }
1744
+ if (leftLines.length * rightLines.length > MAX_ALIGNMENT_CELLS) {
1745
+ return alignLargeReplacementLines(leftLines, rightLines);
1746
+ }
1747
+ const pairScores = buildPairScores(leftLines, rightLines);
1748
+ const scores = Array.from(
1749
+ { length: leftLines.length + 1 },
1750
+ () => new Array(rightLines.length + 1).fill(Number.NEGATIVE_INFINITY)
1751
+ );
1752
+ const moves = Array.from(
1753
+ { length: leftLines.length + 1 },
1754
+ () => new Array(rightLines.length + 1).fill(null)
1755
+ );
1756
+ scores[0][0] = 0;
1757
+ for (let leftIndex2 = 1; leftIndex2 <= leftLines.length; leftIndex2++) {
1758
+ scores[leftIndex2][0] = scores[leftIndex2 - 1][0] + GAP_PENALTY;
1759
+ moves[leftIndex2][0] = "left";
1760
+ }
1761
+ for (let rightIndex2 = 1; rightIndex2 <= rightLines.length; rightIndex2++) {
1762
+ scores[0][rightIndex2] = scores[0][rightIndex2 - 1] + GAP_PENALTY;
1763
+ moves[0][rightIndex2] = "right";
1764
+ }
1765
+ for (let leftIndex2 = 1; leftIndex2 <= leftLines.length; leftIndex2++) {
1766
+ for (let rightIndex2 = 1; rightIndex2 <= rightLines.length; rightIndex2++) {
1767
+ const pairScore = pairScores[leftIndex2 - 1][rightIndex2 - 1];
1768
+ const candidates = [
1769
+ { move: "left", score: scores[leftIndex2 - 1][rightIndex2] + GAP_PENALTY },
1770
+ { move: "right", score: scores[leftIndex2][rightIndex2 - 1] + GAP_PENALTY },
1771
+ {
1772
+ move: "match",
1773
+ score: pairScore === null ? Number.NEGATIVE_INFINITY : scores[leftIndex2 - 1][rightIndex2 - 1] + pairScore
1774
+ }
1775
+ ];
1776
+ const best = candidates.reduce((current, candidate) => candidate.score > current.score ? candidate : current);
1777
+ scores[leftIndex2][rightIndex2] = best.score;
1778
+ moves[leftIndex2][rightIndex2] = best.move;
1779
+ }
1780
+ }
1781
+ const aligned = [];
1782
+ let leftIndex = leftLines.length;
1783
+ let rightIndex = rightLines.length;
1784
+ while (leftIndex > 0 || rightIndex > 0) {
1785
+ const move = moves[leftIndex][rightIndex];
1786
+ if (move === "match") {
1787
+ aligned.push({ left: leftLines[--leftIndex], right: rightLines[--rightIndex] });
1788
+ } else if (move === "left") {
1789
+ aligned.push({ left: leftLines[--leftIndex] });
1790
+ } else {
1791
+ aligned.push({ right: rightLines[--rightIndex] });
1792
+ }
1793
+ }
1794
+ return aligned.reverse();
1795
+ }
1796
+ function scoreReplacementLinePair(left, right) {
1797
+ const normalizedLeft = normalizeMatchingContent(left);
1798
+ const normalizedRight = normalizeMatchingContent(right);
1799
+ const leftInformative = isInformativeLine(normalizedLeft);
1800
+ const rightInformative = isInformativeLine(normalizedRight);
1801
+ if (!leftInformative || !rightInformative || normalizedLeft.length > MAX_SCORING_LINE_LENGTH || normalizedRight.length > MAX_SCORING_LINE_LENGTH) {
1802
+ return {
1803
+ score: 0,
1804
+ eligible: false,
1805
+ characterSimilarity: 0,
1806
+ tokenSimilarity: 0,
1807
+ lengthSimilarity: 0
1808
+ };
1809
+ }
1810
+ if (normalizedLeft === normalizedRight) {
1811
+ return {
1812
+ score: 1,
1813
+ eligible: true,
1814
+ characterSimilarity: 1,
1815
+ tokenSimilarity: 1,
1816
+ lengthSimilarity: 1
1817
+ };
1818
+ }
1819
+ const leftTokens = tokenizeMatchingContent(normalizedLeft);
1820
+ const rightTokens = tokenizeMatchingContent(normalizedRight);
1821
+ const characterSimilarity = lineSimilarity(normalizedLeft, normalizedRight);
1822
+ const tokenSimilarity = multisetDiceSimilarity(leftTokens, rightTokens);
1823
+ const lengthSimilarity = Math.min(normalizedLeft.length, normalizedRight.length) / Math.max(normalizedLeft.length, normalizedRight.length);
1824
+ const boundarySimilarity = tokenBoundarySimilarity(leftTokens, rightTokens);
1825
+ const score = characterSimilarity * 0.52 + tokenSimilarity * 0.32 + lengthSimilarity * 0.1 + boundarySimilarity * 0.06;
1826
+ const hasContentEvidence = characterSimilarity >= 0.52 || tokenSimilarity >= 0.45 || characterSimilarity >= 0.48 && tokenSimilarity >= 0.28;
1827
+ return {
1828
+ score,
1829
+ eligible: hasContentEvidence && score >= MINIMUM_MATCH_SCORE,
1830
+ characterSimilarity,
1831
+ tokenSimilarity,
1832
+ lengthSimilarity
1833
+ };
1834
+ }
1835
+ function buildPairScores(leftLines, rightLines) {
1836
+ const scored = leftLines.map((left) => rightLines.map((right) => scoreReplacementLinePair(left, right)));
1837
+ const structuralPairs = collectUniqueStructuralPairs(leftLines, rightLines);
1838
+ const leftRankings = scored.map((row) => rankEligibleScores(row));
1839
+ const rightRankings = rightLines.map((_right, rightIndex) => rankEligibleScores(scored.map((row) => row[rightIndex])));
1840
+ const accepted = scored.map((row, leftIndex) => row.map((candidate, rightIndex) => {
1841
+ if (structuralPairs.has(`${leftIndex}:${rightIndex}`)) {
1842
+ return Math.max(candidate.score, 1);
1843
+ }
1844
+ if (!candidate.eligible) {
1845
+ return null;
1846
+ }
1847
+ const leftRanking = leftRankings[leftIndex];
1848
+ const rightRanking = rightRankings[rightIndex];
1849
+ const isBestOnBothSides = candidate.score === leftRanking.best && candidate.score === rightRanking.best;
1850
+ const requiredMargin = candidate.score >= HIGH_CONFIDENCE_MATCH_SCORE ? AMBIGUITY_MARGIN / 2 : AMBIGUITY_MARGIN;
1851
+ const hasLeftMargin = leftRanking.best - leftRanking.second >= requiredMargin;
1852
+ const hasRightMargin = rightRanking.best - rightRanking.second >= requiredMargin;
1853
+ return isBestOnBothSides && hasLeftMargin && hasRightMargin ? candidate.score : null;
1854
+ }));
1855
+ if (Math.max(leftLines.length, rightLines.length) > MAX_CONTEXTUAL_HUNK_LINES || Math.abs(leftLines.length - rightLines.length) > MAX_CONTEXTUAL_LENGTH_DELTA) {
1856
+ return accepted;
1857
+ }
1858
+ const isContextualCandidate = (candidate, leftIndex, rightIndex) => candidate.score >= MINIMUM_CONTEXTUAL_MATCH_SCORE && haveCompatibleLineRoles(leftLines[leftIndex], rightLines[rightIndex]) && Math.abs(leftIndex - rightIndex) <= MAX_CONTEXTUAL_LENGTH_DELTA;
1859
+ const leftContextRankings = scored.map((row, leftIndex) => rankScores(row, (candidate, rightIndex) => isContextualCandidate(candidate, leftIndex, rightIndex)));
1860
+ const rightContextRankings = rightLines.map((_right, rightIndex) => rankScores(
1861
+ scored.map((row) => row[rightIndex]),
1862
+ (candidate, leftIndex) => isContextualCandidate(candidate, leftIndex, rightIndex)
1863
+ ));
1864
+ const isWeakRunCandidate = (candidate, leftIndex, rightIndex) => candidate.score >= MINIMUM_CONTEXTUAL_RUN_SCORE && haveCompatibleLineRoles(leftLines[leftIndex], rightLines[rightIndex]) && Math.abs(leftIndex - rightIndex) <= MAX_CONTEXTUAL_LENGTH_DELTA;
1865
+ const leftWeakRankings = scored.map((row, leftIndex) => rankScores(row, (candidate, rightIndex) => isWeakRunCandidate(candidate, leftIndex, rightIndex)));
1866
+ const rightWeakRankings = rightLines.map((_right, rightIndex) => rankScores(
1867
+ scored.map((row) => row[rightIndex]),
1868
+ (candidate, leftIndex) => isWeakRunCandidate(candidate, leftIndex, rightIndex)
1869
+ ));
1870
+ const isUnambiguousContextualCandidate = (leftIndex, rightIndex) => {
1871
+ const candidate = scored[leftIndex][rightIndex];
1872
+ if (!isContextualCandidate(candidate, leftIndex, rightIndex)) {
1873
+ return false;
1874
+ }
1875
+ const leftRanking = leftContextRankings[leftIndex];
1876
+ const rightRanking = rightContextRankings[rightIndex];
1877
+ return candidate.score === leftRanking.best && candidate.score === rightRanking.best && leftRanking.best - leftRanking.second >= CONTEXTUAL_AMBIGUITY_MARGIN && rightRanking.best - rightRanking.second >= CONTEXTUAL_AMBIGUITY_MARGIN;
1878
+ };
1879
+ const isUnambiguousWeakRunAnchor = (leftIndex, rightIndex) => {
1880
+ const candidate = scored[leftIndex][rightIndex];
1881
+ if (!isWeakRunCandidate(candidate, leftIndex, rightIndex)) {
1882
+ return false;
1883
+ }
1884
+ const leftRanking = leftWeakRankings[leftIndex];
1885
+ const rightRanking = rightWeakRankings[rightIndex];
1886
+ return candidate.score === leftRanking.best && candidate.score === rightRanking.best && leftRanking.best - leftRanking.second >= CONTEXTUAL_AMBIGUITY_MARGIN && rightRanking.best - rightRanking.second >= CONTEXTUAL_AMBIGUITY_MARGIN;
1887
+ };
1888
+ return accepted.map((row, leftIndex) => row.map((acceptedScore, rightIndex) => {
1889
+ if (acceptedScore !== null) {
1890
+ return acceptedScore;
1891
+ }
1892
+ const hasLocalSupport = isUnambiguousContextualCandidate(leftIndex, rightIndex) && (hasAcceptedDiagonalNeighbor(accepted, leftIndex, rightIndex) || hasContextualDiagonalNeighbor(
1893
+ scored,
1894
+ leftIndex,
1895
+ rightIndex,
1896
+ isUnambiguousContextualCandidate
1897
+ ));
1898
+ const hasRunSupport = hasAnchoredWeakDiagonalRun(
1899
+ scored,
1900
+ leftIndex,
1901
+ rightIndex,
1902
+ (candidateLeftIndex, candidateRightIndex) => isWeakRunCandidate(
1903
+ scored[candidateLeftIndex][candidateRightIndex],
1904
+ candidateLeftIndex,
1905
+ candidateRightIndex
1906
+ ),
1907
+ isUnambiguousWeakRunAnchor
1908
+ );
1909
+ if (!hasLocalSupport && !hasRunSupport) {
1910
+ return null;
1911
+ }
1912
+ return scored[leftIndex][rightIndex].score;
1913
+ }));
1914
+ }
1915
+ function hasAcceptedDiagonalNeighbor(accepted, leftIndex, rightIndex) {
1916
+ 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;
1917
+ }
1918
+ function hasContextualDiagonalNeighbor(scored, leftIndex, rightIndex, isCandidate) {
1919
+ 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);
1920
+ }
1921
+ function hasAnchoredWeakDiagonalRun(scored, leftIndex, rightIndex, isCandidate, isAnchor) {
1922
+ let startLeft = leftIndex;
1923
+ let startRight = rightIndex;
1924
+ while (startLeft > 0 && startRight > 0 && isCandidate(startLeft - 1, startRight - 1)) {
1925
+ startLeft--;
1926
+ startRight--;
1927
+ }
1928
+ let endLeft = leftIndex;
1929
+ let endRight = rightIndex;
1930
+ let scoreTotal = scored[leftIndex][rightIndex].score;
1931
+ let length = 1;
1932
+ while (endLeft + 1 < scored.length && endRight + 1 < scored[endLeft + 1].length && isCandidate(endLeft + 1, endRight + 1)) {
1933
+ endLeft++;
1934
+ endRight++;
1935
+ scoreTotal += scored[endLeft][endRight].score;
1936
+ length++;
1937
+ }
1938
+ for (let scanLeft = leftIndex - 1, scanRight = rightIndex - 1; scanLeft >= startLeft && scanRight >= startRight; scanLeft--, scanRight--) {
1939
+ scoreTotal += scored[scanLeft][scanRight].score;
1940
+ length++;
1941
+ }
1942
+ return length >= 3 && scoreTotal / length >= MINIMUM_CONTEXTUAL_MATCH_SCORE && isAnchor(startLeft, startRight) && isAnchor(endLeft, endRight);
1943
+ }
1944
+ function rankScores(scores, include) {
1945
+ let best = Number.NEGATIVE_INFINITY;
1946
+ let second = Number.NEGATIVE_INFINITY;
1947
+ scores.forEach((candidate, index) => {
1948
+ if (!include(candidate, index)) {
1949
+ return;
1950
+ }
1951
+ if (candidate.score > best) {
1952
+ second = best;
1953
+ best = candidate.score;
1954
+ } else if (candidate.score > second) {
1955
+ second = candidate.score;
1956
+ }
1957
+ });
1958
+ return { best, second };
1959
+ }
1960
+ function rankEligibleScores(scores) {
1961
+ let best = Number.NEGATIVE_INFINITY;
1962
+ let second = Number.NEGATIVE_INFINITY;
1963
+ for (const candidate of scores) {
1964
+ if (!candidate.eligible) {
1965
+ continue;
1966
+ }
1967
+ if (candidate.score > best) {
1968
+ second = best;
1969
+ best = candidate.score;
1970
+ } else if (candidate.score > second) {
1971
+ second = candidate.score;
1972
+ }
1973
+ }
1974
+ return { best, second };
1975
+ }
1976
+ function alignLargeReplacementLines(leftLines, rightLines) {
1977
+ const anchors = collectLargeHunkAnchors(leftLines, rightLines);
1978
+ const aligned = [];
1979
+ let leftStart = 0;
1980
+ let rightStart = 0;
1981
+ for (const anchor of anchors) {
1982
+ appendBoundedSegment(
1983
+ aligned,
1984
+ leftLines.slice(leftStart, anchor.leftIndex),
1985
+ rightLines.slice(rightStart, anchor.rightIndex)
1986
+ );
1987
+ aligned.push({ left: leftLines[anchor.leftIndex], right: rightLines[anchor.rightIndex] });
1988
+ leftStart = anchor.leftIndex + 1;
1989
+ rightStart = anchor.rightIndex + 1;
1990
+ }
1991
+ appendBoundedSegment(aligned, leftLines.slice(leftStart), rightLines.slice(rightStart));
1992
+ return aligned;
1993
+ }
1994
+ function collectLargeHunkAnchors(leftLines, rightLines) {
1995
+ const candidatePairs = collectRareTokenCandidatePairs(leftLines, rightLines);
1996
+ if (candidatePairs === null) {
1997
+ return [];
1998
+ }
1999
+ const structuralPairs = collectUniqueStructuralPairs(leftLines, rightLines);
2000
+ const scored = candidatePairs.map(({ leftIndex, rightIndex, exact }) => ({
2001
+ leftIndex,
2002
+ rightIndex,
2003
+ exact,
2004
+ structural: structuralPairs.has(`${leftIndex}:${rightIndex}`),
2005
+ match: scoreReplacementLinePair(leftLines[leftIndex], rightLines[rightIndex])
2006
+ })).filter((candidate) => candidate.exact || candidate.structural || candidate.match.eligible);
2007
+ const leftRankings = rankAnchorCandidates(scored, "leftIndex");
2008
+ const rightRankings = rankAnchorCandidates(scored, "rightIndex");
2009
+ const confident = scored.filter((candidate) => {
2010
+ if (candidate.exact || candidate.structural) {
2011
+ return true;
2012
+ }
2013
+ const leftRanking = leftRankings.get(candidate.leftIndex);
2014
+ const rightRanking = rightRankings.get(candidate.rightIndex);
2015
+ if (!leftRanking || !rightRanking || candidate.match.score !== leftRanking.best || candidate.match.score !== rightRanking.best) {
2016
+ return false;
2017
+ }
2018
+ const requiredMargin = candidate.match.score >= HIGH_CONFIDENCE_MATCH_SCORE ? AMBIGUITY_MARGIN / 2 : AMBIGUITY_MARGIN;
2019
+ return leftRanking.best - leftRanking.second >= requiredMargin && rightRanking.best - rightRanking.second >= requiredMargin;
2020
+ });
2021
+ const anchors = selectMaximumWeightMonotonicAnchors(confident, rightLines.length);
2022
+ return anchors.length <= MAX_LARGE_HUNK_ANCHORS ? anchors : [];
2023
+ }
2024
+ function collectRareTokenCandidatePairs(leftLines, rightLines) {
2025
+ const leftExact = buildLinePostings(leftLines, normalizeMatchingContent);
2026
+ const rightExact = buildLinePostings(rightLines, normalizeMatchingContent);
2027
+ const pairs = /* @__PURE__ */ new Map();
2028
+ for (const [line, leftIndices] of leftExact) {
2029
+ const rightIndices = rightExact.get(line);
2030
+ if (!isInformativeLine(line) || leftIndices.length !== 1 || (rightIndices == null ? void 0 : rightIndices.length) !== 1) {
2031
+ continue;
2032
+ }
2033
+ const leftIndex = leftIndices[0];
2034
+ const rightIndex = rightIndices[0];
2035
+ pairs.set(`${leftIndex}:${rightIndex}`, { leftIndex, rightIndex, exact: true });
2036
+ if (pairs.size > MAX_BOUNDED_CANDIDATES) {
2037
+ return null;
2038
+ }
2039
+ }
2040
+ const leftTokens = buildTokenPostings(leftLines);
2041
+ const rightTokens = buildTokenPostings(rightLines);
2042
+ for (const [token, leftIndices] of leftTokens) {
2043
+ const rightIndices = rightTokens.get(token);
2044
+ if (!rightIndices || leftIndices.length > MAX_RARE_TOKEN_OCCURRENCES || rightIndices.length > MAX_RARE_TOKEN_OCCURRENCES) {
2045
+ continue;
2046
+ }
2047
+ for (const leftIndex of leftIndices) {
2048
+ for (const rightIndex of rightIndices) {
2049
+ const key = `${leftIndex}:${rightIndex}`;
2050
+ if (!pairs.has(key)) {
2051
+ pairs.set(key, { leftIndex, rightIndex, exact: false });
2052
+ if (pairs.size > MAX_BOUNDED_CANDIDATES) {
2053
+ return null;
2054
+ }
2055
+ }
2056
+ }
2057
+ }
2058
+ }
2059
+ return [...pairs.values()];
2060
+ }
2061
+ function buildLinePostings(lines, keyForLine) {
2062
+ const postings = /* @__PURE__ */ new Map();
2063
+ lines.forEach((line, index) => {
2064
+ const key = keyForLine(line);
2065
+ const indices = postings.get(key) ?? [];
2066
+ indices.push(index);
2067
+ postings.set(key, indices);
2068
+ });
2069
+ return postings;
2070
+ }
2071
+ function buildTokenPostings(lines) {
2072
+ const postings = /* @__PURE__ */ new Map();
2073
+ lines.forEach((line, index) => {
2074
+ const tokens = new Set(tokenizeMatchingContent(normalizeMatchingContent(line)).filter(isMeaningfulAnchorToken));
2075
+ for (const token of tokens) {
2076
+ const indices = postings.get(token) ?? [];
2077
+ indices.push(index);
2078
+ postings.set(token, indices);
2079
+ }
2080
+ });
2081
+ return postings;
2082
+ }
2083
+ function isMeaningfulAnchorToken(token) {
2084
+ return token.length >= 2 && /[\p{L}\p{N}_$]/u.test(token);
2085
+ }
2086
+ function rankAnchorCandidates(candidates, indexKey) {
2087
+ const grouped = /* @__PURE__ */ new Map();
2088
+ for (const candidate of candidates) {
2089
+ const scores = grouped.get(candidate[indexKey]) ?? [];
2090
+ scores.push(candidate.match);
2091
+ grouped.set(candidate[indexKey], scores);
2092
+ }
2093
+ return new Map([...grouped].map(([index, scores]) => [index, rankEligibleScores(scores)]));
2094
+ }
2095
+ function selectMaximumWeightMonotonicAnchors(candidates, rightLength) {
2096
+ const ordered = [...candidates].sort((left, right) => left.leftIndex - right.leftIndex || left.rightIndex - right.rightIndex);
2097
+ const treeScores = new Array(rightLength + 2).fill(0);
2098
+ const treeCandidates = new Array(rightLength + 2).fill(-1);
2099
+ const bestScores = new Array(ordered.length).fill(0);
2100
+ const previous = new Array(ordered.length).fill(-1);
2101
+ for (let start = 0; start < ordered.length; ) {
2102
+ let end = start + 1;
2103
+ while (end < ordered.length && ordered[end].leftIndex === ordered[start].leftIndex) {
2104
+ end++;
2105
+ }
2106
+ for (let index = start; index < end; index++) {
2107
+ const prior = queryAnchorTree(treeScores, treeCandidates, ordered[index].rightIndex);
2108
+ const anchorBonus = ordered[index].exact || ordered[index].structural ? 0.5 : 0;
2109
+ bestScores[index] = prior.score + ordered[index].match.score + anchorBonus;
2110
+ previous[index] = prior.candidateIndex;
2111
+ }
2112
+ for (let index = start; index < end; index++) {
2113
+ updateAnchorTree(
2114
+ treeScores,
2115
+ treeCandidates,
2116
+ ordered[index].rightIndex + 1,
2117
+ bestScores[index],
2118
+ index
2119
+ );
2120
+ }
2121
+ start = end;
2122
+ }
2123
+ const best = queryAnchorTree(treeScores, treeCandidates, rightLength + 1);
2124
+ const anchors = [];
2125
+ for (let index = best.candidateIndex; index >= 0; index = previous[index]) {
2126
+ anchors.push({ leftIndex: ordered[index].leftIndex, rightIndex: ordered[index].rightIndex });
2127
+ }
2128
+ return anchors.reverse();
2129
+ }
2130
+ function queryAnchorTree(scores, candidates, position) {
2131
+ let score = 0;
2132
+ let candidateIndex = -1;
2133
+ for (let index = position; index > 0; index -= index & -index) {
2134
+ if (scores[index] > score) {
2135
+ score = scores[index];
2136
+ candidateIndex = candidates[index];
2137
+ }
2138
+ }
2139
+ return { score, candidateIndex };
2140
+ }
2141
+ function updateAnchorTree(scores, candidates, position, score, candidateIndex) {
2142
+ for (let index = position; index < scores.length; index += index & -index) {
2143
+ if (score > scores[index]) {
2144
+ scores[index] = score;
2145
+ candidates[index] = candidateIndex;
2146
+ }
2147
+ }
2148
+ }
2149
+ function appendBoundedSegment(aligned, leftLines, rightLines) {
2150
+ if (leftLines.length * rightLines.length <= MAX_ALIGNMENT_CELLS) {
2151
+ aligned.push(...alignReplacementLines(leftLines, rightLines));
2152
+ return;
2153
+ }
2154
+ aligned.push(...leftLines.map((left) => ({ left })));
2155
+ aligned.push(...rightLines.map((right) => ({ right })));
2156
+ }
2157
+ function normalizeMatchingContent(line) {
2158
+ return line.trim().replace(/\s+/g, " ");
2159
+ }
2160
+ function collectUniqueStructuralPairs(leftLines, rightLines) {
2161
+ const leftPostings = buildStructuralPostings(leftLines);
2162
+ const rightPostings = buildStructuralPostings(rightLines);
2163
+ const pairs = /* @__PURE__ */ new Set();
2164
+ for (const [key, leftIndices] of leftPostings) {
2165
+ const rightIndices = rightPostings.get(key);
2166
+ if (leftIndices.length === 1 && (rightIndices == null ? void 0 : rightIndices.length) === 1) {
2167
+ pairs.add(`${leftIndices[0]}:${rightIndices[0]}`);
2168
+ }
2169
+ }
2170
+ return pairs;
2171
+ }
2172
+ function buildStructuralPostings(lines) {
2173
+ const postings = /* @__PURE__ */ new Map();
2174
+ lines.forEach((line, index) => {
2175
+ const key = declarationAnchorKey(line);
2176
+ if (!key) {
2177
+ return;
2178
+ }
2179
+ const indices = postings.get(key) ?? [];
2180
+ indices.push(index);
2181
+ postings.set(key, indices);
2182
+ });
2183
+ return postings;
2184
+ }
2185
+ function haveCompatibleLineRoles(left, right) {
2186
+ const leftKey = declarationAnchorKey(left);
2187
+ const rightKey = declarationAnchorKey(right);
2188
+ if (!leftKey || !rightKey) {
2189
+ return leftKey === rightKey;
2190
+ }
2191
+ return leftKey.split(":", 1)[0] === rightKey.split(":", 1)[0];
2192
+ }
2193
+ function declarationAnchorKey(line) {
2194
+ const normalized = normalizeMatchingContent(line);
2195
+ const patterns = [
2196
+ ["function", /^(?:(?:export|default)\s+)*(?:async\s+)?function\s*\*?\s*([\p{L}_$][\p{L}\p{N}_$]*)\s*\(/u],
2197
+ ["function", /^(?:async\s+)?def\s+([\p{L}_][\p{L}\p{N}_]*)\s*\(/u],
2198
+ ["function", /^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([\p{L}_][\p{L}\p{N}_]*)\s*(?:<[^>]*>)?\s*\(/u],
2199
+ ["function", /^func\s+(?:\([^)]*\)\s*)?([\p{L}_][\p{L}\p{N}_]*)\s*\(/u],
2200
+ ["class", /^(?:(?:export|default|abstract|public)\s+)*class\s+([\p{L}_$][\p{L}\p{N}_$]*)\b/u]
2201
+ ];
2202
+ for (const [kind, pattern] of patterns) {
2203
+ const match = normalized.match(pattern);
2204
+ if (match) {
2205
+ return `${kind}:${match[1]}`;
2206
+ }
2207
+ }
2208
+ return null;
2209
+ }
2210
+ function isInformativeLine(line) {
2211
+ if (line.length < 3) {
2212
+ return false;
2213
+ }
2214
+ return /[\p{L}\p{N}_$]/u.test(line) && !/^[\s()[\]{};,.:+\-*/=>|&!?'"`]+$/.test(line);
2215
+ }
2216
+ function tokenizeMatchingContent(line) {
2217
+ return line.toLowerCase().match(new RegExp("[\\p{L}_$][\\p{L}\\p{N}_$]*|\\p{N}+(?:\\.\\p{N}+)?|[^\\s\\p{L}\\p{N}_$]", "gu")) ?? [];
2218
+ }
2219
+ function multisetDiceSimilarity(leftTokens, rightTokens) {
2220
+ if (leftTokens.length + rightTokens.length === 0) {
2221
+ return 1;
2222
+ }
2223
+ const remaining = /* @__PURE__ */ new Map();
2224
+ leftTokens.forEach((token) => remaining.set(token, (remaining.get(token) ?? 0) + 1));
2225
+ let common = 0;
2226
+ for (const token of rightTokens) {
2227
+ const count = remaining.get(token) ?? 0;
2228
+ if (count > 0) {
2229
+ common++;
2230
+ remaining.set(token, count - 1);
2231
+ }
2232
+ }
2233
+ return 2 * common / (leftTokens.length + rightTokens.length);
2234
+ }
2235
+ function tokenBoundarySimilarity(leftTokens, rightTokens) {
2236
+ if (leftTokens.length === 0 || rightTokens.length === 0) {
2237
+ return 0;
2238
+ }
2239
+ const sameStart = leftTokens[0] === rightTokens[0] ? 0.5 : 0;
2240
+ const sameEnd = leftTokens[leftTokens.length - 1] === rightTokens[rightTokens.length - 1] ? 0.5 : 0;
2241
+ return sameStart + sameEnd;
2242
+ }
2243
+ function lineSimilarity(left, right) {
2244
+ const maxLength = left.length + right.length;
2245
+ if (maxLength === 0) {
2246
+ return 1;
2247
+ }
2248
+ const commonLength = diffChars(left, right).filter((change) => !change.added && !change.removed).reduce((total, change) => total + change.value.length, 0);
2249
+ return 2 * commonLength / maxLength;
2250
+ }
2251
+ function applyInlineHighlightPair(leftLine, rightLine) {
2252
+ if (leftLine.content.length > MAX_INLINE_HIGHLIGHT_LINE_LENGTH || rightLine.content.length > MAX_INLINE_HIGHLIGHT_LINE_LENGTH) {
2253
+ return;
2254
+ }
2255
+ const { leftSegments, rightSegments, hasInlineChanges } = buildInlineSegments(leftLine.content, rightLine.content);
2256
+ if (hasInlineChanges) {
2257
+ leftLine.segments = leftSegments;
2258
+ rightLine.segments = rightSegments;
2259
+ }
2260
+ }
2261
+ function buildInlineSegments(leftContent, rightContent) {
2262
+ const changes = diffWordsWithSpace(leftContent, rightContent);
2263
+ const leftSegments = [];
2264
+ const rightSegments = [];
2265
+ let hasInlineChanges = false;
2266
+ for (const change of changes) {
2267
+ const value = change.value;
2268
+ if (!change.added && !change.removed) {
2269
+ const contextSegment = {
2270
+ kind: "context",
2271
+ text: value,
2272
+ emphasis: false
2273
+ };
2274
+ leftSegments.push(contextSegment);
2275
+ rightSegments.push(contextSegment);
2276
+ continue;
2277
+ }
2278
+ const emphasis = /[^\s]/.test(value);
2279
+ hasInlineChanges = hasInlineChanges || emphasis;
2280
+ if (change.removed) {
2281
+ leftSegments.push({
2282
+ kind: "removed",
2283
+ text: value,
2284
+ emphasis
2285
+ });
2286
+ }
2287
+ if (change.added) {
2288
+ rightSegments.push({
2289
+ kind: "added",
2290
+ text: value,
2291
+ emphasis
2292
+ });
2293
+ }
2294
+ }
2295
+ return {
2296
+ leftSegments,
2297
+ rightSegments,
2298
+ hasInlineChanges
2299
+ };
2300
+ }
2301
+ function makePlaceholder() {
2302
+ return {
2303
+ kind: "placeholder",
2304
+ content: "",
2305
+ lineNumber: null
2306
+ };
2307
+ }
2308
+ function makeDiffCell(kind, content, lineNumber) {
2309
+ return { kind, content, lineNumber };
2310
+ }
2311
+ function makeDiffLine(kind, content, lineNumber) {
2312
+ return { kind, content, lineNumber };
2313
+ }
2314
+ function makeDiffRow(left, right) {
2315
+ return { left, right };
2316
+ }
2317
+ function makeDiffBlock(kind, leftStart, leftEnd, rightStart, rightEnd) {
2318
+ return { kind, leftStart, leftEnd, rightStart, rightEnd };
2319
+ }
2320
+ function readPositiveIntegerEnv(name, fallback) {
2321
+ var _a;
2322
+ const value = typeof process !== "undefined" ? (_a = process.env) == null ? void 0 : _a[name] : void 0;
2323
+ const parsed = Number.parseInt(value ?? "", 10);
2324
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
2325
+ }
2326
+
2327
+ // src/tourAnnotations.ts
2328
+ var MAX_AUTOMATIC_TOUR_FOCUS_RANGES = 4;
2329
+ var NEARBY_TOUR_FOCUS_LINE_GAP = 8;
2330
+ var LARGE_TOUR_FOCUS_RANGE_LINES = 120;
2331
+ var TOUR_FOCUS_BOUNDARY_SEARCH_LINES = 12;
2332
+ function getChangeSourceRange(diffModel, blockIndex, side) {
2333
+ var _a, _b, _c;
2334
+ const block = diffModel == null ? void 0 : diffModel.blocks[blockIndex];
2335
+ const lines = side === "left" ? diffModel == null ? void 0 : diffModel.leftLines : diffModel == null ? void 0 : diffModel.rightLines;
2336
+ if (!block || !(lines == null ? void 0 : lines.length)) {
2337
+ return void 0;
2338
+ }
2339
+ const start = side === "left" ? block.leftStart : block.rightStart;
2340
+ const end = side === "left" ? block.leftEnd : block.rightEnd;
2341
+ const changedLines = lines.slice(start, end);
2342
+ const startLine = ((_a = changedLines[0]) == null ? void 0 : _a.lineNumber) ?? ((_b = lines[Math.min(start, lines.length - 1)]) == null ? void 0 : _b.lineNumber);
2343
+ if (!Number.isInteger(startLine)) {
2344
+ return void 0;
2345
+ }
2346
+ return {
2347
+ startLine,
2348
+ endLine: ((_c = changedLines.at(-1)) == null ? void 0 : _c.lineNumber) ?? startLine
2349
+ };
2350
+ }
2351
+ function buildTourFocusRanges(diffModel, side) {
2352
+ const ranges = diffModel.blocks.map((_, blockIndex) => getChangeSourceRange(diffModel, blockIndex, side)).filter((range) => Boolean(range));
2353
+ if (ranges.length === 0) return [];
2354
+ const nearby = ranges.reduce((grouped, range) => {
2355
+ const previous = grouped.at(-1);
2356
+ if (previous && range.startLine - previous.endLine <= NEARBY_TOUR_FOCUS_LINE_GAP) {
2357
+ previous.endLine = Math.max(previous.endLine, range.endLine);
2358
+ } else {
2359
+ grouped.push({ ...range });
2360
+ }
2361
+ return grouped;
2362
+ }, []);
2363
+ const capped = capFocusRanges(nearby, MAX_AUTOMATIC_TOUR_FOCUS_RANGES);
2364
+ if (capped.length !== 1) return capped;
2365
+ const [only] = capped;
2366
+ const lineCount = only.endLine - only.startLine + 1;
2367
+ if (lineCount < LARGE_TOUR_FOCUS_RANGE_LINES) return capped;
2368
+ const lines = side === "left" ? diffModel.leftLines : diffModel.rightLines;
2369
+ const partCount = Math.min(
2370
+ MAX_AUTOMATIC_TOUR_FOCUS_RANGES,
2371
+ Math.ceil(lineCount / LARGE_TOUR_FOCUS_RANGE_LINES)
2372
+ );
2373
+ const boundaries = [];
2374
+ for (let partIndex = 1; partIndex < partCount; partIndex += 1) {
2375
+ const target = only.startLine + Math.round(lineCount * partIndex / partCount) - 1;
2376
+ boundaries.push(findBlankLineBoundary(lines, target, only.startLine, only.endLine));
2377
+ }
2378
+ const uniqueBoundaries = [...new Set(boundaries)].filter((line) => line >= only.startLine && line < only.endLine).sort((left, right) => left - right);
2379
+ const split = [];
2380
+ let startLine = only.startLine;
2381
+ for (const endLine of uniqueBoundaries) {
2382
+ split.push({ startLine, endLine });
2383
+ startLine = endLine + 1;
2384
+ }
2385
+ split.push({ startLine, endLine: only.endLine });
2386
+ return split;
2387
+ }
2388
+ function capFocusRanges(ranges, maximum) {
2389
+ if (ranges.length <= maximum) return ranges.map((range) => ({ ...range }));
2390
+ const capped = [];
2391
+ for (let index = 0; index < maximum; index += 1) {
2392
+ const startIndex = Math.floor(index * ranges.length / maximum);
2393
+ const endIndex = Math.floor((index + 1) * ranges.length / maximum) - 1;
2394
+ capped.push({
2395
+ startLine: ranges[startIndex].startLine,
2396
+ endLine: ranges[endIndex].endLine
2397
+ });
2398
+ }
2399
+ return capped;
2400
+ }
2401
+ function findBlankLineBoundary(lines, target, minimum, maximum) {
2402
+ for (let distance = 0; distance <= TOUR_FOCUS_BOUNDARY_SEARCH_LINES; distance += 1) {
2403
+ for (const candidate of distance === 0 ? [target] : [target + distance, target - distance]) {
2404
+ if (candidate < minimum || candidate >= maximum) continue;
2405
+ const line = lines.find((entry) => entry.lineNumber === candidate);
2406
+ if ((line == null ? void 0 : line.content.trim()) === "") return candidate;
2407
+ }
2408
+ }
2409
+ return Math.min(Math.max(target, minimum), maximum - 1);
2410
+ }
2411
+
1185
2412
  // src/changeTourContext.ts
1186
2413
  var import_child_process3 = require("child_process");
1187
2414
  var CHANGE_TOUR_CONTEXT_VERSION = 1;
@@ -1513,7 +2740,8 @@ function applySource(source, defaultScenes, repoRoot) {
1513
2740
  repoRoot,
1514
2741
  authoredScene,
1515
2742
  authoredScene.base || ((_a = source.range) == null ? void 0 : _a.base),
1516
- authoredScene.target || ((_b = source.range) == null ? void 0 : _b.head)
2743
+ authoredScene.target || ((_b = source.range) == null ? void 0 : _b.head),
2744
+ [...available.keys()]
1517
2745
  );
1518
2746
  scenes.push(deconstructedScene);
1519
2747
  sceneIds.push(deconstructedScene.id);
@@ -1539,8 +2767,8 @@ function applySource(source, defaultScenes, repoRoot) {
1539
2767
  takeaway: authoredScene.takeaway,
1540
2768
  steps: authoredScene.steps.map((step) => {
1541
2769
  const focus = requireResolvedAnchor(resolvedAnchors, step.focus);
1542
- const diff = available.get(focus.path);
1543
- if (!diff) throw new Error(`Step ${step.id} has no text diff for ${focus.path}.`);
2770
+ const diff2 = available.get(focus.path);
2771
+ if (!diff2) throw new Error(`Step ${step.id} has no text diff for ${focus.path}.`);
1544
2772
  return {
1545
2773
  id: step.id,
1546
2774
  title: step.title,
@@ -1548,7 +2776,7 @@ function applySource(source, defaultScenes, repoRoot) {
1548
2776
  depth: step.depth,
1549
2777
  focus,
1550
2778
  connection: step.connection ? connections.get(step.connection) : void 0,
1551
- diff: { ...diff, id: `${authoredScene.id}-${step.id}` }
2779
+ diff: { ...diff2, id: `${authoredScene.id}-${step.id}` }
1552
2780
  };
1553
2781
  })
1554
2782
  };
@@ -1559,7 +2787,7 @@ function applySource(source, defaultScenes, repoRoot) {
1559
2787
  }
1560
2788
  return { scenes, chapters };
1561
2789
  }
1562
- function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef) {
2790
+ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, availableFilePaths = []) {
1563
2791
  const compiled = buildDeconstructedScene(repoRoot, source, { baseRef, headRef: targetRef });
1564
2792
  const panels = [{
1565
2793
  id: "explanation-baseline",
@@ -1571,9 +2799,15 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef) {
1571
2799
  role: "stage",
1572
2800
  stageId: stage.id
1573
2801
  }))];
1574
- const includedPaths = [...new Set(compiled.stages.flatMap((stage) => stage.introducedFiles))];
2802
+ const materializablePaths = new Set(compiled.baselineFiles.map((file) => file.path));
2803
+ const includedPaths = availableFilePaths.length > 0 ? availableFilePaths.filter((filePath) => materializablePaths.has(filePath)).sort() : [...materializablePaths].sort();
2804
+ const missingIntroducedPath = compiled.stages.flatMap((stage) => stage.introducedFiles).find((filePath) => !includedPaths.includes(filePath));
2805
+ if (missingIntroducedPath) {
2806
+ throw new Error(`Deconstructed tour file is not renderable: ${missingIntroducedPath}`);
2807
+ }
2808
+ const introducedPaths = new Set(compiled.stages.flatMap((stage) => stage.introducedFiles));
1575
2809
  let totalContentBytes = 0;
1576
- const files = includedPaths.map((filePath) => {
2810
+ const files = includedPaths.flatMap((filePath) => {
1577
2811
  const states = [
1578
2812
  compiled.baselineFiles.find((file) => file.path === filePath),
1579
2813
  ...compiled.stages.map((stage) => stage.files.find((file) => file.path === filePath))
@@ -1581,18 +2815,23 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef) {
1581
2815
  if (states.some((state) => !state)) {
1582
2816
  throw new Error(`Deconstructed scene ${source.id} is missing virtual state for ${filePath}.`);
1583
2817
  }
1584
- return {
2818
+ const fileContentBytes = states.reduce((total, state) => total + Buffer.byteLength((state == null ? void 0 : state.content) || "", "utf8"), 0);
2819
+ const exceedsFileLimit = states.some((state) => {
2820
+ const content = Buffer.from((state == null ? void 0 : state.content) || "", "utf8");
2821
+ return content.length > DEFAULT_MAX_TOUR_FILE_BYTES || hasLineLongerThan(content, DEFAULT_MAX_TOUR_LINE_BYTES);
2822
+ });
2823
+ const exceedsSceneLimit = totalContentBytes + fileContentBytes > DEFAULT_MAX_STACK_CONTENT_BYTES;
2824
+ if (exceedsFileLimit || exceedsSceneLimit) {
2825
+ if (introducedPaths.has(filePath)) {
2826
+ throw new Error(exceedsFileLimit ? `Deconstructed tour file is too large to present: ${filePath}` : `Deconstructed tour content exceeds ${DEFAULT_MAX_STACK_CONTENT_BYTES} bytes.`);
2827
+ }
2828
+ return [];
2829
+ }
2830
+ totalContentBytes += fileContentBytes;
2831
+ return [{
1585
2832
  path: filePath,
1586
2833
  panels: states.map((state, index) => {
1587
2834
  if (!state) throw new Error(`Missing virtual state for ${filePath}.`);
1588
- const content = Buffer.from(state.content, "utf8");
1589
- if (content.length > DEFAULT_MAX_TOUR_FILE_BYTES || hasLineLongerThan(content, DEFAULT_MAX_TOUR_LINE_BYTES)) {
1590
- throw new Error(`Deconstructed tour file is too large to present: ${filePath}`);
1591
- }
1592
- totalContentBytes += content.length;
1593
- if (totalContentBytes > DEFAULT_MAX_STACK_CONTENT_BYTES) {
1594
- throw new Error(`Deconstructed tour content exceeds ${DEFAULT_MAX_STACK_CONTENT_BYTES} bytes.`);
1595
- }
1596
2835
  return {
1597
2836
  id: panels[index].id,
1598
2837
  label: `${panels[index].label} / ${filePath}${state.exists ? "" : " (absent)"}`,
@@ -1601,9 +2840,10 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef) {
1601
2840
  exists: state.exists
1602
2841
  };
1603
2842
  })
1604
- };
2843
+ }];
1605
2844
  });
1606
2845
  if (files.length === 0) throw new Error(`Deconstructed scene ${source.id} has no staged files.`);
2846
+ const steps = buildDeconstructedFocusSteps(source.id, compiled.stages, files);
1607
2847
  return {
1608
2848
  id: source.id,
1609
2849
  kind: "deconstructed-diff",
@@ -1621,17 +2861,63 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef) {
1621
2861
  },
1622
2862
  panels,
1623
2863
  files,
1624
- steps: compiled.stages.map((stage, index) => ({
1625
- id: stage.id,
1626
- title: stage.title,
1627
- body: stage.narration,
1628
- file: stage.introducedFiles[0],
1629
- pairIndex: index,
1630
- side: "right",
1631
- introducedHunks: stage.introducedHunks
1632
- }))
2864
+ steps
1633
2865
  };
1634
2866
  }
2867
+ function buildDeconstructedFocusSteps(sceneId, stages, files) {
2868
+ const reservedIds = new Set(stages.map((stage) => stage.id));
2869
+ const usedIds = /* @__PURE__ */ new Set();
2870
+ return stages.flatMap((stage, stageIndex) => {
2871
+ const focuses = stage.introducedChanges.flatMap((change) => {
2872
+ const file = files.find((candidate) => candidate.path === change.file);
2873
+ const left = file == null ? void 0 : file.panels[stageIndex];
2874
+ const right = file == null ? void 0 : file.panels[stageIndex + 1];
2875
+ if (!file || !left || !right) {
2876
+ throw new Error(`Deconstructed scene ${sceneId} is missing comparison state for ${change.file}.`);
2877
+ }
2878
+ const diffModel = buildTwoWayDiffModel(left.content, right.content);
2879
+ const side = left.exists && !right.exists ? "left" : "right";
2880
+ const ranges = buildTourFocusRanges(diffModel, side);
2881
+ if (ranges.length === 0) {
2882
+ throw new Error(`Deconstructed stage ${stage.id} has no visible change for ${change.file}.`);
2883
+ }
2884
+ return ranges.map((range) => ({
2885
+ file: change.file,
2886
+ hunks: change.hunks,
2887
+ side,
2888
+ ...range
2889
+ }));
2890
+ });
2891
+ return focuses.map((focus, focusIndex) => ({
2892
+ id: focusIndex === 0 ? claimDeconstructedStageId(stage.id, usedIds) : claimDeconstructedFocusId(`${stage.id}-focus-${focusIndex + 1}`, reservedIds, usedIds),
2893
+ title: focuses.length === 1 ? stage.title : `${stage.title} \xB7 ${focus.file} \xB7 Focus ${focusIndex + 1}/${focuses.length}`,
2894
+ body: stage.narration,
2895
+ file: focus.file,
2896
+ pairIndex: stageIndex,
2897
+ side: focus.side,
2898
+ startLine: focus.startLine,
2899
+ endLine: focus.endLine,
2900
+ introducedHunks: [...focus.hunks],
2901
+ stageId: stage.id,
2902
+ stageIndex,
2903
+ focusIndex,
2904
+ focusCount: focuses.length
2905
+ }));
2906
+ });
2907
+ }
2908
+ function claimDeconstructedStageId(stageId, usedIds) {
2909
+ usedIds.add(stageId);
2910
+ return stageId;
2911
+ }
2912
+ function claimDeconstructedFocusId(preferredId, reservedIds, usedIds) {
2913
+ let candidate = preferredId;
2914
+ let suffix = 2;
2915
+ while (usedIds.has(candidate) || reservedIds.has(candidate)) {
2916
+ candidate = `${preferredId}-${suffix++}`;
2917
+ }
2918
+ usedIds.add(candidate);
2919
+ return candidate;
2920
+ }
1635
2921
  function buildStackedScene(repoRoot, source, availableFilePaths) {
1636
2922
  const stack = source.stack.map((entry) => ({
1637
2923
  id: entry.id,