@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/README.md +1 -1
- package/docs/releasing.md +5 -0
- package/media/webview.js +1 -1
- package/out/changeTour.js +1319 -33
- package/out/standalone-main.js +1311 -31
- package/out/tourCoverage.js +1319 -33
- package/package.json +1 -1
- package/web/presenter.css +12 -0
- package/web/web-host.js +5 -5
package/out/tourCoverage.js
CHANGED
|
@@ -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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
|
1320
|
-
if (!
|
|
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: { ...
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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,
|