@davmash/bygone 0.8.11 → 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/CHANGELOG.md +7 -0
- package/README.md +3 -3
- package/docs/releasing.md +23 -1
- package/media/webview.css +1 -1
- package/media/webview.js +173 -172
- 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/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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
1543
|
-
if (!
|
|
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: { ...
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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,
|