@mrsf/cli 0.6.0 → 0.7.0
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 +10 -7
- package/dist/browser.cjs +1101 -24
- package/dist/browser.cjs.map +4 -4
- package/dist/browser.d.ts +11 -0
- package/dist/browser.js +8 -0
- package/dist/evaluation/generate-reanchor-cases.d.ts +10 -0
- package/dist/evaluation/generate-reanchor-cases.js +301 -0
- package/dist/evaluation/reanchor-eval.d.ts +111 -0
- package/dist/evaluation/reanchor-eval.js +266 -0
- package/dist/evaluation/reanchor-gates.d.ts +29 -0
- package/dist/evaluation/reanchor-gates.js +47 -0
- package/dist/evaluation/run-reanchor-eval.d.ts +3 -0
- package/dist/evaluation/run-reanchor-eval.js +103 -0
- package/dist/evaluation/run-reanchor-generator.d.ts +3 -0
- package/dist/evaluation/run-reanchor-generator.js +63 -0
- package/dist/evaluation/run-reanchor-profile.d.ts +3 -0
- package/dist/evaluation/run-reanchor-profile.js +129 -0
- package/dist/index.cjs +1209 -34
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -0
- package/dist/lib/anchor-context.d.ts +46 -0
- package/dist/lib/anchor-context.js +474 -0
- package/dist/lib/confidence-calibration.d.ts +16 -0
- package/dist/lib/confidence-calibration.js +120 -0
- package/dist/lib/fuzzy.d.ts +14 -1
- package/dist/lib/fuzzy.js +140 -19
- package/dist/lib/git.d.ts +2 -0
- package/dist/lib/git.js +12 -0
- package/dist/lib/global-reconciliation.d.ts +11 -0
- package/dist/lib/global-reconciliation.js +145 -0
- package/dist/lib/reanchor-core.d.ts +7 -0
- package/dist/lib/reanchor-core.js +46 -4
- package/dist/lib/reanchor.js +82 -11
- package/dist/lib/revision-projection.d.ts +21 -0
- package/dist/lib/revision-projection.js +203 -0
- package/package.json +5 -1
package/dist/index.cjs
CHANGED
|
@@ -203,8 +203,11 @@ __export(index_exports, {
|
|
|
203
203
|
HIGH_THRESHOLD: () => HIGH_THRESHOLD2,
|
|
204
204
|
addComment: () => addComment2,
|
|
205
205
|
applyReanchorResults: () => applyReanchorResults2,
|
|
206
|
+
calibrateAnchorEvidence: () => calibrateAnchorEvidence2,
|
|
206
207
|
combinedScore: () => combinedScore2,
|
|
207
208
|
computeHash: () => computeHash2,
|
|
209
|
+
createAnchorContextIndex: () => createAnchorContextIndex2,
|
|
210
|
+
createRevisionProjection: () => createRevisionProjection2,
|
|
208
211
|
detectRenames: () => detectRenames2,
|
|
209
212
|
discoverAllSidecars: () => discoverAllSidecars2,
|
|
210
213
|
discoverSidecar: () => discoverSidecar3,
|
|
@@ -217,7 +220,7 @@ __export(index_exports, {
|
|
|
217
220
|
fuzzySearch: () => fuzzySearch2,
|
|
218
221
|
getCurrentCommit: () => getCurrentCommit2,
|
|
219
222
|
getDiff: () => getDiff2,
|
|
220
|
-
getFileAtCommit: () =>
|
|
223
|
+
getFileAtCommit: () => getFileAtCommit2,
|
|
221
224
|
getLineShift: () => getLineShift4,
|
|
222
225
|
getStagedFiles: () => getStagedFiles2,
|
|
223
226
|
getThreads: () => getThreads2,
|
|
@@ -241,9 +244,11 @@ __export(index_exports, {
|
|
|
241
244
|
reanchorDocumentLines: () => reanchorDocumentLines2,
|
|
242
245
|
reanchorDocumentText: () => reanchorDocumentText2,
|
|
243
246
|
reanchorFile: () => reanchorFile2,
|
|
247
|
+
reconcileCommentAnchors: () => reconcileCommentAnchors2,
|
|
244
248
|
removeComment: () => removeComment2,
|
|
245
249
|
resolveAnchor: () => resolveAnchor2,
|
|
246
250
|
resolveComment: () => resolveComment2,
|
|
251
|
+
resolveCommit: () => resolveCommit2,
|
|
247
252
|
resolveSidecarPaths: () => resolveSidecarPaths2,
|
|
248
253
|
sidecarToDocument: () => sidecarToDocument2,
|
|
249
254
|
summarize: () => summarize2,
|
|
@@ -1008,6 +1013,16 @@ async function validateFile(filePath, options = {}) {
|
|
|
1008
1013
|
|
|
1009
1014
|
// src/lib/fuzzy.ts
|
|
1010
1015
|
var import_fastest_levenshtein = require("fastest-levenshtein");
|
|
1016
|
+
var MAX_FUZZY_CANDIDATE_LINES = 64;
|
|
1017
|
+
function createFuzzySearchIndex(lines) {
|
|
1018
|
+
const tokenPostings = /* @__PURE__ */ new Map();
|
|
1019
|
+
const trigramPostings = /* @__PURE__ */ new Map();
|
|
1020
|
+
for (let line = 1; line < lines.length; line += 1) {
|
|
1021
|
+
addPostingSignals(tokenPostings, lexicalTokens(lines[line]), line);
|
|
1022
|
+
addPostingSignals(trigramPostings, characterTrigrams(lines[line]), line);
|
|
1023
|
+
}
|
|
1024
|
+
return { lines, tokenPostings, trigramPostings };
|
|
1025
|
+
}
|
|
1011
1026
|
function exactMatch(lines, needle) {
|
|
1012
1027
|
if (!needle) return [];
|
|
1013
1028
|
const results = [];
|
|
@@ -1130,22 +1145,48 @@ function combinedScore(needle, candidate) {
|
|
|
1130
1145
|
}
|
|
1131
1146
|
return tScore * 0.6 + lScore * 0.4;
|
|
1132
1147
|
}
|
|
1133
|
-
function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
|
|
1134
|
-
|
|
1148
|
+
function fuzzySearch(lines, needle, threshold = 0.6, hintLine, index) {
|
|
1149
|
+
return fuzzySearchThresholds(
|
|
1150
|
+
lines,
|
|
1151
|
+
needle,
|
|
1152
|
+
[threshold],
|
|
1153
|
+
hintLine,
|
|
1154
|
+
index
|
|
1155
|
+
).get(threshold) ?? [];
|
|
1156
|
+
}
|
|
1157
|
+
function fuzzySearchThresholds(lines, needle, thresholds, hintLine, index = createFuzzySearchIndex(lines)) {
|
|
1158
|
+
const uniqueThresholds = [...new Set(thresholds)];
|
|
1159
|
+
const results = /* @__PURE__ */ new Map();
|
|
1160
|
+
if (uniqueThresholds.length === 0) return results;
|
|
1161
|
+
if (!needle) {
|
|
1162
|
+
for (const threshold of uniqueThresholds) results.set(threshold, []);
|
|
1163
|
+
return results;
|
|
1164
|
+
}
|
|
1135
1165
|
const needleLines = needle.split("\n");
|
|
1136
1166
|
const needleLineCount = needleLines.length;
|
|
1137
1167
|
const candidates = [];
|
|
1168
|
+
const minimumThreshold = Math.min(...uniqueThresholds);
|
|
1169
|
+
const candidateLines = retrieveCandidateLines(index, needle, hintLine);
|
|
1138
1170
|
const minWindow = Math.max(1, Math.floor(needleLineCount * 0.7));
|
|
1139
1171
|
const maxWindow = Math.min(
|
|
1140
1172
|
lines.length - 1,
|
|
1141
1173
|
Math.ceil(needleLineCount * 1.3) + 1
|
|
1142
1174
|
);
|
|
1143
1175
|
for (let winSize = minWindow; winSize <= maxWindow; winSize++) {
|
|
1144
|
-
|
|
1176
|
+
const startLines = /* @__PURE__ */ new Set();
|
|
1177
|
+
for (const candidateLine of candidateLines) {
|
|
1178
|
+
for (let offset = 0; offset < winSize; offset += 1) {
|
|
1179
|
+
const startLine = candidateLine - offset;
|
|
1180
|
+
if (startLine >= 1 && startLine + winSize - 1 < lines.length) {
|
|
1181
|
+
startLines.add(startLine);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
for (const startLine of startLines) {
|
|
1145
1186
|
const windowLines = lines.slice(startLine, startLine + winSize);
|
|
1146
1187
|
const windowText = windowLines.join("\n");
|
|
1147
1188
|
const score = combinedScore(needle, windowText);
|
|
1148
|
-
if (score >=
|
|
1189
|
+
if (score >= minimumThreshold) {
|
|
1149
1190
|
candidates.push({
|
|
1150
1191
|
text: windowText,
|
|
1151
1192
|
line: startLine,
|
|
@@ -1158,17 +1199,18 @@ function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
|
|
|
1158
1199
|
}
|
|
1159
1200
|
}
|
|
1160
1201
|
if (needleLineCount === 1 && needle.length < 200) {
|
|
1161
|
-
for (
|
|
1202
|
+
for (const lineNum of candidateLines) {
|
|
1162
1203
|
const line = lines[lineNum];
|
|
1163
1204
|
if (!line) continue;
|
|
1164
1205
|
const winLen = needle.length;
|
|
1165
1206
|
const minWinLen = Math.max(3, Math.floor(winLen * 0.7));
|
|
1166
1207
|
const maxWinLen = Math.min(line.length, Math.ceil(winLen * 1.3));
|
|
1167
|
-
|
|
1208
|
+
const lengths = evenlySpacedIntegers(minWinLen, maxWinLen, 7);
|
|
1209
|
+
for (const len of lengths) {
|
|
1168
1210
|
for (let col = 0; col + len <= line.length; col++) {
|
|
1169
1211
|
const sub = line.substring(col, col + len);
|
|
1170
1212
|
const score = combinedScore(needle, sub);
|
|
1171
|
-
if (score >=
|
|
1213
|
+
if (score >= minimumThreshold) {
|
|
1172
1214
|
candidates.push({
|
|
1173
1215
|
text: sub,
|
|
1174
1216
|
line: lineNum,
|
|
@@ -1182,15 +1224,83 @@ function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
|
|
|
1182
1224
|
}
|
|
1183
1225
|
}
|
|
1184
1226
|
}
|
|
1185
|
-
|
|
1227
|
+
const deduped = deduplicateCandidates(candidates);
|
|
1228
|
+
const scored = deduped.map((candidate) => ({
|
|
1229
|
+
baseScore: candidate.score,
|
|
1230
|
+
candidate: applyProximityBonus(candidate, hintLine)
|
|
1231
|
+
}));
|
|
1232
|
+
for (const threshold of uniqueThresholds) {
|
|
1233
|
+
results.set(
|
|
1234
|
+
threshold,
|
|
1235
|
+
scored.filter((item) => item.baseScore >= threshold).map((item) => item.candidate).sort((left, right) => right.score - left.score)
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
return results;
|
|
1239
|
+
}
|
|
1240
|
+
function retrieveCandidateLines(index, needle, hintLine) {
|
|
1241
|
+
const votes = /* @__PURE__ */ new Map();
|
|
1242
|
+
const lineCount = Math.max(0, index.lines.length - 1);
|
|
1243
|
+
const signals = [
|
|
1244
|
+
...postingSignals(index.tokenPostings, lexicalTokens(needle), 2),
|
|
1245
|
+
...postingSignals(index.trigramPostings, characterTrigrams(needle), 1)
|
|
1246
|
+
].sort((left, right) => left.postings.length - right.postings.length).slice(0, 16);
|
|
1247
|
+
for (const signal of signals) {
|
|
1248
|
+
const rarity = Math.log1p(lineCount / signal.postings.length);
|
|
1249
|
+
for (const line of signal.postings) {
|
|
1250
|
+
votes.set(line, (votes.get(line) ?? 0) + signal.weight * rarity);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1186
1253
|
if (hintLine != null) {
|
|
1187
|
-
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1254
|
+
for (let offset = -4; offset <= 4; offset += 1) {
|
|
1255
|
+
const line = hintLine + offset;
|
|
1256
|
+
if (line >= 1 && line <= lineCount) {
|
|
1257
|
+
votes.set(line, (votes.get(line) ?? 0) + 0.25);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (votes.size === 0) {
|
|
1262
|
+
return Array.from({ length: lineCount }, (_, index2) => index2 + 1);
|
|
1192
1263
|
}
|
|
1193
|
-
return
|
|
1264
|
+
return [...votes.entries()].sort(
|
|
1265
|
+
(left, right) => right[1] - left[1] || distanceFromHint(left[0], hintLine) - distanceFromHint(right[0], hintLine) || left[0] - right[0]
|
|
1266
|
+
).slice(0, MAX_FUZZY_CANDIDATE_LINES).map(([line]) => line);
|
|
1267
|
+
}
|
|
1268
|
+
function postingSignals(postings, values, weight) {
|
|
1269
|
+
return [...new Set(values)].map((value) => ({ postings: postings.get(value) ?? [], weight })).filter((signal) => signal.postings.length > 0);
|
|
1270
|
+
}
|
|
1271
|
+
function addPostingSignals(postings, values, line) {
|
|
1272
|
+
for (const value of new Set(values)) {
|
|
1273
|
+
const lines = postings.get(value);
|
|
1274
|
+
if (lines) {
|
|
1275
|
+
lines.push(line);
|
|
1276
|
+
} else {
|
|
1277
|
+
postings.set(value, [line]);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
function lexicalTokens(text) {
|
|
1282
|
+
return text.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? [];
|
|
1283
|
+
}
|
|
1284
|
+
function characterTrigrams(text) {
|
|
1285
|
+
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1286
|
+
const characters = [...normalized];
|
|
1287
|
+
if (characters.length < 3) return normalized ? [normalized] : [];
|
|
1288
|
+
const trigrams = [];
|
|
1289
|
+
for (let index = 0; index <= characters.length - 3; index += 1) {
|
|
1290
|
+
trigrams.push(characters.slice(index, index + 3).join(""));
|
|
1291
|
+
}
|
|
1292
|
+
return trigrams;
|
|
1293
|
+
}
|
|
1294
|
+
function evenlySpacedIntegers(minimum, maximum, count) {
|
|
1295
|
+
if (maximum <= minimum) return [minimum];
|
|
1296
|
+
const values = /* @__PURE__ */ new Set();
|
|
1297
|
+
for (let index = 0; index < count; index += 1) {
|
|
1298
|
+
values.add(Math.round(minimum + (maximum - minimum) * index / (count - 1)));
|
|
1299
|
+
}
|
|
1300
|
+
return [...values];
|
|
1301
|
+
}
|
|
1302
|
+
function distanceFromHint(line, hintLine) {
|
|
1303
|
+
return hintLine == null ? 0 : Math.abs(line - hintLine);
|
|
1194
1304
|
}
|
|
1195
1305
|
function deduplicateCandidates(candidates) {
|
|
1196
1306
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -1203,6 +1313,15 @@ function deduplicateCandidates(candidates) {
|
|
|
1203
1313
|
}
|
|
1204
1314
|
return Array.from(seen.values());
|
|
1205
1315
|
}
|
|
1316
|
+
function applyProximityBonus(candidate, hintLine) {
|
|
1317
|
+
if (hintLine == null) return candidate;
|
|
1318
|
+
const distance = Math.abs(candidate.line - hintLine);
|
|
1319
|
+
const proximityBonus = 0.1 * Math.max(0, 1 - distance / 50);
|
|
1320
|
+
return {
|
|
1321
|
+
...candidate,
|
|
1322
|
+
score: Math.min(1, candidate.score + proximityBonus)
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1206
1325
|
|
|
1207
1326
|
// src/lib/git.ts
|
|
1208
1327
|
var import_node_child_process = require("node:child_process");
|
|
@@ -1246,6 +1365,19 @@ async function getCurrentCommit(repoRoot) {
|
|
|
1246
1365
|
return null;
|
|
1247
1366
|
}
|
|
1248
1367
|
}
|
|
1368
|
+
async function resolveCommit(revision, repoRoot) {
|
|
1369
|
+
if (!await isGitAvailable()) return null;
|
|
1370
|
+
try {
|
|
1371
|
+
const { stdout } = await execFile(
|
|
1372
|
+
"git",
|
|
1373
|
+
["rev-parse", "--verify", `${revision}^{commit}`],
|
|
1374
|
+
{ cwd: repoRoot, timeout: GIT_TIMEOUT }
|
|
1375
|
+
);
|
|
1376
|
+
return stdout.trim();
|
|
1377
|
+
} catch {
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1249
1381
|
async function isStale(commentCommit, repoRoot) {
|
|
1250
1382
|
const head = await getCurrentCommit(repoRoot);
|
|
1251
1383
|
if (!head) return false;
|
|
@@ -1359,6 +1491,783 @@ async function detectRenames(fromCommit, toCommit, repoRoot) {
|
|
|
1359
1491
|
}
|
|
1360
1492
|
}
|
|
1361
1493
|
|
|
1494
|
+
// src/lib/revision-projection.ts
|
|
1495
|
+
var CONTEXT_RADIUS = 8;
|
|
1496
|
+
function createRevisionProjection(sourceLines, targetLines) {
|
|
1497
|
+
const sourceOccurrences = collectLineOccurrences(sourceLines);
|
|
1498
|
+
const targetOccurrences = collectLineOccurrences(targetLines);
|
|
1499
|
+
const lineMap = /* @__PURE__ */ new Map();
|
|
1500
|
+
for (const [text, sourceLineNumbers] of sourceOccurrences) {
|
|
1501
|
+
const targetLineNumbers = targetOccurrences.get(text);
|
|
1502
|
+
if (sourceLineNumbers.length === 1 && targetLineNumbers?.length === 1) {
|
|
1503
|
+
lineMap.set(sourceLineNumbers[0], targetLineNumbers[0]);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return { sourceLines, targetLines, lineMap };
|
|
1507
|
+
}
|
|
1508
|
+
function projectCommentAnchor(comment, projection, threshold) {
|
|
1509
|
+
if (comment.line == null || !comment.selected_text) return void 0;
|
|
1510
|
+
const sourceText = extractText(
|
|
1511
|
+
projection.sourceLines,
|
|
1512
|
+
comment.line,
|
|
1513
|
+
comment.end_line,
|
|
1514
|
+
comment.start_column,
|
|
1515
|
+
comment.end_column
|
|
1516
|
+
);
|
|
1517
|
+
if (sourceText !== comment.selected_text) return void 0;
|
|
1518
|
+
const exactCandidates = exactMatch(
|
|
1519
|
+
projection.targetLines,
|
|
1520
|
+
comment.selected_text
|
|
1521
|
+
);
|
|
1522
|
+
if (exactCandidates.length === 1) {
|
|
1523
|
+
const candidate = exactCandidates[0];
|
|
1524
|
+
const contextSupport = countIndependentExactSupport(
|
|
1525
|
+
comment,
|
|
1526
|
+
candidate.line,
|
|
1527
|
+
projection
|
|
1528
|
+
);
|
|
1529
|
+
if (contextSupport >= 2) {
|
|
1530
|
+
return {
|
|
1531
|
+
line: candidate.line,
|
|
1532
|
+
endLine: candidate.endLine,
|
|
1533
|
+
startColumn: candidate.startColumn,
|
|
1534
|
+
endColumn: candidate.endColumn,
|
|
1535
|
+
text: candidate.text,
|
|
1536
|
+
score: 1,
|
|
1537
|
+
exact: true,
|
|
1538
|
+
contextSupport,
|
|
1539
|
+
contextMargin: 1,
|
|
1540
|
+
reason: "Source revision and neighboring line evidence confirm exact relocation."
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
if (exactCandidates.length > 0) return void 0;
|
|
1545
|
+
const projected = projectLineFromContext(comment, projection);
|
|
1546
|
+
if (projected == null) return void 0;
|
|
1547
|
+
const projectedLine = projected.line;
|
|
1548
|
+
const lineSpan = (comment.end_line ?? comment.line) - comment.line;
|
|
1549
|
+
const projectedEndLine = projectedLine + lineSpan;
|
|
1550
|
+
if (projectedLine < 1 || projectedEndLine >= projection.targetLines.length) {
|
|
1551
|
+
return void 0;
|
|
1552
|
+
}
|
|
1553
|
+
const columns = projectColumns(comment, projection, projectedLine);
|
|
1554
|
+
const targetText = extractText(
|
|
1555
|
+
projection.targetLines,
|
|
1556
|
+
projectedLine,
|
|
1557
|
+
projectedEndLine,
|
|
1558
|
+
columns.startColumn,
|
|
1559
|
+
columns.endColumn
|
|
1560
|
+
);
|
|
1561
|
+
if (targetText == null) return void 0;
|
|
1562
|
+
const score = Math.min(
|
|
1563
|
+
1,
|
|
1564
|
+
combinedScore(comment.selected_text, targetText) + 0.1
|
|
1565
|
+
);
|
|
1566
|
+
if (score < threshold) return void 0;
|
|
1567
|
+
return {
|
|
1568
|
+
line: projectedLine,
|
|
1569
|
+
endLine: projectedEndLine,
|
|
1570
|
+
startColumn: columns.startColumn,
|
|
1571
|
+
endColumn: columns.endColumn,
|
|
1572
|
+
text: targetText,
|
|
1573
|
+
score,
|
|
1574
|
+
exact: targetText === comment.selected_text,
|
|
1575
|
+
contextSupport: projected.support,
|
|
1576
|
+
contextMargin: projected.margin,
|
|
1577
|
+
reason: "Source revision context projects the edited anchor range."
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function collectLineOccurrences(lines) {
|
|
1581
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
1582
|
+
for (let line = 1; line < lines.length; line += 1) {
|
|
1583
|
+
const text = lines[line];
|
|
1584
|
+
if (!text.trim()) continue;
|
|
1585
|
+
const existing = occurrences.get(text);
|
|
1586
|
+
if (existing) {
|
|
1587
|
+
existing.push(line);
|
|
1588
|
+
} else {
|
|
1589
|
+
occurrences.set(text, [line]);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return occurrences;
|
|
1593
|
+
}
|
|
1594
|
+
function countIndependentExactSupport(comment, candidateLine, projection) {
|
|
1595
|
+
const sourceEndLine = comment.end_line ?? comment.line;
|
|
1596
|
+
const expectedShift = candidateLine - comment.line;
|
|
1597
|
+
if (comment.start_column != null || comment.end_column != null) {
|
|
1598
|
+
const mappedContainerLine = projection.lineMap.get(comment.line);
|
|
1599
|
+
if (mappedContainerLine === candidateLine) return 2;
|
|
1600
|
+
}
|
|
1601
|
+
let support = 0;
|
|
1602
|
+
for (let distance = 1; distance <= CONTEXT_RADIUS; distance += 1) {
|
|
1603
|
+
for (const sourceLine of [
|
|
1604
|
+
comment.line - distance,
|
|
1605
|
+
sourceEndLine + distance
|
|
1606
|
+
]) {
|
|
1607
|
+
if (sourceLine < 1 || sourceLine >= projection.sourceLines.length) {
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
const targetLine = projection.lineMap.get(sourceLine);
|
|
1611
|
+
if (targetLine != null && targetLine - sourceLine === expectedShift) {
|
|
1612
|
+
support += 1;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return support;
|
|
1617
|
+
}
|
|
1618
|
+
function projectLineFromContext(comment, projection) {
|
|
1619
|
+
const sourceLine = comment.line;
|
|
1620
|
+
const sourceEndLine = comment.end_line ?? sourceLine;
|
|
1621
|
+
const votes = /* @__PURE__ */ new Map();
|
|
1622
|
+
for (let distance = 1; distance <= CONTEXT_RADIUS; distance += 1) {
|
|
1623
|
+
for (const contextLine of [
|
|
1624
|
+
sourceLine - distance,
|
|
1625
|
+
sourceEndLine + distance
|
|
1626
|
+
]) {
|
|
1627
|
+
if (contextLine < 1 || contextLine >= projection.sourceLines.length) {
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
const targetLine = projection.lineMap.get(contextLine);
|
|
1631
|
+
if (targetLine == null) continue;
|
|
1632
|
+
const shift = targetLine - contextLine;
|
|
1633
|
+
const vote = votes.get(shift);
|
|
1634
|
+
if (vote) {
|
|
1635
|
+
vote.count += 1;
|
|
1636
|
+
vote.nearest = Math.min(vote.nearest, distance);
|
|
1637
|
+
} else {
|
|
1638
|
+
votes.set(shift, { count: 1, nearest: distance });
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const ranked = [...votes.entries()].sort(
|
|
1643
|
+
(left, right) => right[1].count - left[1].count || left[1].nearest - right[1].nearest || Math.abs(left[0]) - Math.abs(right[0])
|
|
1644
|
+
);
|
|
1645
|
+
const best = ranked[0];
|
|
1646
|
+
if (!best) return void 0;
|
|
1647
|
+
if (best[1].count < 2 && best[1].nearest > 3) return void 0;
|
|
1648
|
+
if (ranked[1] && ranked[1][1].count === best[1].count && ranked[1][1].nearest === best[1].nearest) {
|
|
1649
|
+
return void 0;
|
|
1650
|
+
}
|
|
1651
|
+
const runnerUpCount = ranked[1]?.[1].count ?? 0;
|
|
1652
|
+
return {
|
|
1653
|
+
line: sourceLine + best[0],
|
|
1654
|
+
support: best[1].count,
|
|
1655
|
+
margin: (best[1].count - runnerUpCount) / best[1].count
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
function projectColumns(comment, projection, targetLine) {
|
|
1659
|
+
if (comment.line == null || comment.end_line != null && comment.end_line !== comment.line || comment.start_column == null || comment.end_column == null) {
|
|
1660
|
+
return {
|
|
1661
|
+
startColumn: comment.start_column,
|
|
1662
|
+
endColumn: comment.end_column
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
const sourceLine = projection.sourceLines[comment.line];
|
|
1666
|
+
const targetLineText = projection.targetLines[targetLine];
|
|
1667
|
+
const prefix = sourceLine.slice(0, comment.start_column);
|
|
1668
|
+
const suffix = sourceLine.slice(comment.end_column);
|
|
1669
|
+
const startColumn = targetLineText.startsWith(prefix) ? prefix.length : Math.min(comment.start_column, targetLineText.length);
|
|
1670
|
+
const endColumn = targetLineText.endsWith(suffix) ? targetLineText.length - suffix.length : Math.min(
|
|
1671
|
+
targetLineText.length,
|
|
1672
|
+
startColumn + (comment.end_column - comment.start_column)
|
|
1673
|
+
);
|
|
1674
|
+
return { startColumn, endColumn };
|
|
1675
|
+
}
|
|
1676
|
+
function extractText(lines, line, endLine, startColumn, endColumn) {
|
|
1677
|
+
const finalLine = endLine ?? line;
|
|
1678
|
+
if (line < 1 || finalLine >= lines.length) return null;
|
|
1679
|
+
if (line === finalLine) {
|
|
1680
|
+
const text = lines[line];
|
|
1681
|
+
return startColumn != null && endColumn != null ? text.slice(startColumn, endColumn) : text;
|
|
1682
|
+
}
|
|
1683
|
+
const result = [];
|
|
1684
|
+
for (let current = line; current <= finalLine; current += 1) {
|
|
1685
|
+
let text = lines[current];
|
|
1686
|
+
if (current === line && startColumn != null) text = text.slice(startColumn);
|
|
1687
|
+
if (current === finalLine && endColumn != null) text = text.slice(0, endColumn);
|
|
1688
|
+
result.push(text);
|
|
1689
|
+
}
|
|
1690
|
+
return result.join("\n");
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// src/lib/anchor-context.ts
|
|
1694
|
+
var MATCH_THRESHOLD = 0.35;
|
|
1695
|
+
var AMBIGUITY_MARGIN = 0.03;
|
|
1696
|
+
var MAX_CONTEXT_CANDIDATE_BLOCKS = 64;
|
|
1697
|
+
function createAnchorContextIndex(sourceLines, targetLines) {
|
|
1698
|
+
return {
|
|
1699
|
+
source: createDocumentBlockIndex(sourceLines),
|
|
1700
|
+
target: createDocumentBlockIndex(targetLines)
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
function resolveContextAnchor(comment, index) {
|
|
1704
|
+
if (comment.line == null || !comment.selected_text) return void 0;
|
|
1705
|
+
const sourceBlockIndex = index.source.lineToBlock.get(comment.line);
|
|
1706
|
+
if (sourceBlockIndex == null) return void 0;
|
|
1707
|
+
const sourceBlock = index.source.blocks[sourceBlockIndex];
|
|
1708
|
+
const sourceText = extractText2(
|
|
1709
|
+
index.source.lines,
|
|
1710
|
+
comment.line,
|
|
1711
|
+
comment.end_line,
|
|
1712
|
+
comment.start_column,
|
|
1713
|
+
comment.end_column
|
|
1714
|
+
);
|
|
1715
|
+
if (sourceText !== comment.selected_text) return void 0;
|
|
1716
|
+
const textAtCurrentPosition = extractText2(
|
|
1717
|
+
index.target.lines,
|
|
1718
|
+
comment.line,
|
|
1719
|
+
comment.end_line,
|
|
1720
|
+
comment.start_column,
|
|
1721
|
+
comment.end_column
|
|
1722
|
+
);
|
|
1723
|
+
if (textAtCurrentPosition === comment.selected_text) {
|
|
1724
|
+
return {
|
|
1725
|
+
status: "anchored",
|
|
1726
|
+
score: 1,
|
|
1727
|
+
line: comment.line,
|
|
1728
|
+
endLine: comment.end_line ?? comment.line,
|
|
1729
|
+
startColumn: comment.start_column,
|
|
1730
|
+
endColumn: comment.end_column,
|
|
1731
|
+
text: comment.selected_text,
|
|
1732
|
+
candidateMargin: 1,
|
|
1733
|
+
reason: "Source-verified anchor remains exact at its stored position."
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
const candidates = findContextAnchorCandidates(comment, index);
|
|
1737
|
+
const best = candidates[0];
|
|
1738
|
+
if (!best) {
|
|
1739
|
+
if (index.target.lines.slice(1).join("\n").includes(comment.selected_text)) {
|
|
1740
|
+
return void 0;
|
|
1741
|
+
}
|
|
1742
|
+
return {
|
|
1743
|
+
status: "orphaned",
|
|
1744
|
+
score: 0,
|
|
1745
|
+
candidateMargin: 1,
|
|
1746
|
+
reason: "Source block has no plausible structural or contextual match."
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
if (candidates[1] && best.score - candidates[1].score < AMBIGUITY_MARGIN) {
|
|
1750
|
+
return {
|
|
1751
|
+
status: "ambiguous",
|
|
1752
|
+
score: best.score,
|
|
1753
|
+
line: best.line,
|
|
1754
|
+
endLine: best.endLine,
|
|
1755
|
+
candidateMargin: best.score - candidates[1].score,
|
|
1756
|
+
reason: `Structural candidates are too close (${best.score.toFixed(3)} vs ${candidates[1].score.toFixed(3)}).`
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
const repeatedExactText = best.exact && countOccurrences(
|
|
1760
|
+
index.target.lines.slice(1).join("\n"),
|
|
1761
|
+
comment.selected_text
|
|
1762
|
+
) > 1;
|
|
1763
|
+
return {
|
|
1764
|
+
status: best.exact && !repeatedExactText ? "anchored" : "fuzzy",
|
|
1765
|
+
score: best.exact && !repeatedExactText ? 1 : best.score,
|
|
1766
|
+
line: best.line,
|
|
1767
|
+
endLine: best.endLine,
|
|
1768
|
+
startColumn: best.startColumn,
|
|
1769
|
+
endColumn: best.endColumn,
|
|
1770
|
+
text: best.text,
|
|
1771
|
+
candidateMargin: candidates[1] ? best.score - candidates[1].score : 1,
|
|
1772
|
+
reason: best.exact && !repeatedExactText ? "Markdown structure and bidirectional context disambiguate the exact anchor." : repeatedExactText ? "Markdown context selects one repeated exact anchor tentatively." : "Markdown structure and bidirectional context locate the edited anchor."
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
function findContextAnchorCandidates(comment, index) {
|
|
1776
|
+
if (comment.line == null || !comment.selected_text) return [];
|
|
1777
|
+
const sourceBlockIndex = index.source.lineToBlock.get(comment.line);
|
|
1778
|
+
if (sourceBlockIndex == null) return [];
|
|
1779
|
+
const sourceBlock = index.source.blocks[sourceBlockIndex];
|
|
1780
|
+
const sourceText = extractText2(
|
|
1781
|
+
index.source.lines,
|
|
1782
|
+
comment.line,
|
|
1783
|
+
comment.end_line,
|
|
1784
|
+
comment.start_column,
|
|
1785
|
+
comment.end_column
|
|
1786
|
+
);
|
|
1787
|
+
if (sourceText !== comment.selected_text) return [];
|
|
1788
|
+
return createCandidateWindows(
|
|
1789
|
+
sourceBlock,
|
|
1790
|
+
sourceBlockIndex,
|
|
1791
|
+
comment.line,
|
|
1792
|
+
index
|
|
1793
|
+
).map((candidate) => ({
|
|
1794
|
+
...candidate,
|
|
1795
|
+
score: scoreCandidate(
|
|
1796
|
+
sourceBlock,
|
|
1797
|
+
sourceBlockIndex,
|
|
1798
|
+
candidate,
|
|
1799
|
+
index,
|
|
1800
|
+
comment.line
|
|
1801
|
+
)
|
|
1802
|
+
})).filter((candidate) => candidate.score >= MATCH_THRESHOLD).sort(
|
|
1803
|
+
(left, right) => right.score - left.score || Math.abs(left.startLine - comment.line) - Math.abs(right.startLine - comment.line)
|
|
1804
|
+
).map((candidate) => {
|
|
1805
|
+
const range = resolveCandidateRange(
|
|
1806
|
+
comment,
|
|
1807
|
+
sourceBlock,
|
|
1808
|
+
candidate,
|
|
1809
|
+
index.target
|
|
1810
|
+
);
|
|
1811
|
+
return {
|
|
1812
|
+
score: candidate.score,
|
|
1813
|
+
line: range.line,
|
|
1814
|
+
endLine: range.endLine,
|
|
1815
|
+
startColumn: range.startColumn,
|
|
1816
|
+
endColumn: range.endColumn,
|
|
1817
|
+
text: range.text,
|
|
1818
|
+
exact: range.text === comment.selected_text
|
|
1819
|
+
};
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
function getAnchorContextScope(comment, index) {
|
|
1823
|
+
if (comment.line == null) return void 0;
|
|
1824
|
+
const blockIndex = index.source.lineToBlock.get(comment.line);
|
|
1825
|
+
if (blockIndex == null) return void 0;
|
|
1826
|
+
return index.source.blocks[blockIndex].headingPath.join("");
|
|
1827
|
+
}
|
|
1828
|
+
function createDocumentBlockIndex(lines) {
|
|
1829
|
+
const blocks = [];
|
|
1830
|
+
const lineToBlock = /* @__PURE__ */ new Map();
|
|
1831
|
+
const headings = [];
|
|
1832
|
+
let line = 1;
|
|
1833
|
+
while (line < lines.length) {
|
|
1834
|
+
if (!lines[line].trim()) {
|
|
1835
|
+
line += 1;
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
const startLine = line;
|
|
1839
|
+
const marker = classifyLine(lines[line]);
|
|
1840
|
+
if (marker.type === "heading") {
|
|
1841
|
+
while (headings.length > 0 && headings[headings.length - 1].level >= marker.headingLevel) {
|
|
1842
|
+
headings.pop();
|
|
1843
|
+
}
|
|
1844
|
+
const block = makeBlock(
|
|
1845
|
+
lines,
|
|
1846
|
+
startLine,
|
|
1847
|
+
startLine,
|
|
1848
|
+
"heading",
|
|
1849
|
+
headings.map((heading) => heading.title)
|
|
1850
|
+
);
|
|
1851
|
+
blocks.push(block);
|
|
1852
|
+
headings.push({ level: marker.headingLevel, title: marker.headingTitle });
|
|
1853
|
+
line += 1;
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
if (marker.type === "code") {
|
|
1857
|
+
line += 1;
|
|
1858
|
+
while (line < lines.length && !lines[line].trimStart().startsWith("```")) {
|
|
1859
|
+
line += 1;
|
|
1860
|
+
}
|
|
1861
|
+
if (line < lines.length) line += 1;
|
|
1862
|
+
} else {
|
|
1863
|
+
line += 1;
|
|
1864
|
+
while (line < lines.length && lines[line].trim() && continuesBlock(marker.type, lines[line])) {
|
|
1865
|
+
line += 1;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
blocks.push(makeBlock(
|
|
1869
|
+
lines,
|
|
1870
|
+
startLine,
|
|
1871
|
+
line - 1,
|
|
1872
|
+
marker.type,
|
|
1873
|
+
headings.map((heading) => heading.title)
|
|
1874
|
+
));
|
|
1875
|
+
}
|
|
1876
|
+
for (const [blockIndex, block] of blocks.entries()) {
|
|
1877
|
+
for (let blockLine = block.startLine; blockLine <= block.endLine; blockLine += 1) {
|
|
1878
|
+
lineToBlock.set(blockLine, blockIndex);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return {
|
|
1882
|
+
lines,
|
|
1883
|
+
blocks,
|
|
1884
|
+
lineToBlock,
|
|
1885
|
+
tokenPostings: createTokenPostings(blocks)
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
function makeBlock(lines, startLine, endLine, type, headingPath) {
|
|
1889
|
+
return {
|
|
1890
|
+
startLine,
|
|
1891
|
+
endLine,
|
|
1892
|
+
type,
|
|
1893
|
+
text: lines.slice(startLine, endLine + 1).join("\n"),
|
|
1894
|
+
headingPath
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
function classifyLine(line) {
|
|
1898
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
1899
|
+
if (heading) {
|
|
1900
|
+
return {
|
|
1901
|
+
type: "heading",
|
|
1902
|
+
headingLevel: heading[1].length,
|
|
1903
|
+
headingTitle: heading[2].trim()
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
if (line.trimStart().startsWith("```")) {
|
|
1907
|
+
return { type: "code", headingLevel: 0, headingTitle: "" };
|
|
1908
|
+
}
|
|
1909
|
+
if (/^\s*(?:[-*+]|\d+\.)\s+/.test(line)) {
|
|
1910
|
+
return { type: "list", headingLevel: 0, headingTitle: "" };
|
|
1911
|
+
}
|
|
1912
|
+
if (/^\s*\|/.test(line)) {
|
|
1913
|
+
return { type: "table", headingLevel: 0, headingTitle: "" };
|
|
1914
|
+
}
|
|
1915
|
+
if (/^\s*>/.test(line)) {
|
|
1916
|
+
return { type: "blockquote", headingLevel: 0, headingTitle: "" };
|
|
1917
|
+
}
|
|
1918
|
+
return { type: "paragraph", headingLevel: 0, headingTitle: "" };
|
|
1919
|
+
}
|
|
1920
|
+
function continuesBlock(type, line) {
|
|
1921
|
+
if (type === "list") return /^\s*(?:[-*+]|\d+\.)\s+/.test(line);
|
|
1922
|
+
if (type === "table") return /^\s*\|/.test(line);
|
|
1923
|
+
if (type === "blockquote") return /^\s*>/.test(line);
|
|
1924
|
+
if (type === "paragraph") {
|
|
1925
|
+
const next = classifyLine(line);
|
|
1926
|
+
return next.type === "paragraph";
|
|
1927
|
+
}
|
|
1928
|
+
return false;
|
|
1929
|
+
}
|
|
1930
|
+
function createCandidateWindows(sourceBlock, sourceBlockIndex, originalLine, index) {
|
|
1931
|
+
const target = index.target;
|
|
1932
|
+
const candidates = [];
|
|
1933
|
+
for (const start of retrieveCandidateBlocks(
|
|
1934
|
+
sourceBlock,
|
|
1935
|
+
sourceBlockIndex,
|
|
1936
|
+
originalLine,
|
|
1937
|
+
index
|
|
1938
|
+
)) {
|
|
1939
|
+
const block = target.blocks[start];
|
|
1940
|
+
candidates.push(toCandidateWindow(target, start, start));
|
|
1941
|
+
if (sourceBlock.type === "paragraph" && block.type === "paragraph" && target.blocks[start + 1]?.type === "paragraph" && samePath(block.headingPath, target.blocks[start + 1].headingPath)) {
|
|
1942
|
+
candidates.push(toCandidateWindow(target, start, start + 1));
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
return candidates;
|
|
1946
|
+
}
|
|
1947
|
+
function createTokenPostings(blocks) {
|
|
1948
|
+
const postings = /* @__PURE__ */ new Map();
|
|
1949
|
+
for (const [blockIndex, block] of blocks.entries()) {
|
|
1950
|
+
for (const token of new Set(tokenize2(block.text))) {
|
|
1951
|
+
const blocksForToken = postings.get(token);
|
|
1952
|
+
if (blocksForToken) {
|
|
1953
|
+
blocksForToken.push(blockIndex);
|
|
1954
|
+
} else {
|
|
1955
|
+
postings.set(token, [blockIndex]);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
return postings;
|
|
1960
|
+
}
|
|
1961
|
+
function retrieveCandidateBlocks(sourceBlock, sourceBlockIndex, originalLine, index) {
|
|
1962
|
+
const votes = /* @__PURE__ */ new Map();
|
|
1963
|
+
const targetCount = index.target.blocks.length;
|
|
1964
|
+
const addEvidence = (text, targetOffset, weight) => {
|
|
1965
|
+
if (!text) return;
|
|
1966
|
+
const rankedTokens = [...new Set(tokenize2(text))].map((token) => ({
|
|
1967
|
+
token,
|
|
1968
|
+
postings: index.target.tokenPostings.get(token) ?? []
|
|
1969
|
+
})).filter((item) => item.postings.length > 0).sort((left, right) => left.postings.length - right.postings.length).slice(0, 12);
|
|
1970
|
+
for (const { postings } of rankedTokens) {
|
|
1971
|
+
const rarity = Math.log1p(targetCount / postings.length);
|
|
1972
|
+
for (const posting of postings) {
|
|
1973
|
+
const candidate = posting + targetOffset;
|
|
1974
|
+
if (candidate >= 0 && candidate < targetCount) {
|
|
1975
|
+
votes.set(candidate, (votes.get(candidate) ?? 0) + weight * rarity);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
addEvidence(sourceBlock.text, 0, 1);
|
|
1981
|
+
addEvidence(index.source.blocks[sourceBlockIndex - 1]?.text, 1, 0.7);
|
|
1982
|
+
addEvidence(index.source.blocks[sourceBlockIndex + 1]?.text, -1, 0.7);
|
|
1983
|
+
const nearbyBlock = closestBlockToLine(index.target.blocks, originalLine);
|
|
1984
|
+
if (nearbyBlock != null) {
|
|
1985
|
+
for (let offset = -2; offset <= 2; offset += 1) {
|
|
1986
|
+
const candidate = nearbyBlock + offset;
|
|
1987
|
+
if (candidate >= 0 && candidate < targetCount) {
|
|
1988
|
+
votes.set(candidate, (votes.get(candidate) ?? 0) + 0.25);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return [...votes.entries()].sort(
|
|
1993
|
+
(left, right) => right[1] - left[1] || Math.abs(index.target.blocks[left[0]].startLine - originalLine) - Math.abs(index.target.blocks[right[0]].startLine - originalLine) || left[0] - right[0]
|
|
1994
|
+
).slice(0, MAX_CONTEXT_CANDIDATE_BLOCKS).map(([blockIndex]) => blockIndex);
|
|
1995
|
+
}
|
|
1996
|
+
function closestBlockToLine(blocks, line) {
|
|
1997
|
+
if (blocks.length === 0) return void 0;
|
|
1998
|
+
let low = 0;
|
|
1999
|
+
let high = blocks.length - 1;
|
|
2000
|
+
while (low <= high) {
|
|
2001
|
+
const middle = Math.floor((low + high) / 2);
|
|
2002
|
+
const block = blocks[middle];
|
|
2003
|
+
if (line < block.startLine) {
|
|
2004
|
+
high = middle - 1;
|
|
2005
|
+
} else if (line > block.endLine) {
|
|
2006
|
+
low = middle + 1;
|
|
2007
|
+
} else {
|
|
2008
|
+
return middle;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
if (low >= blocks.length) return blocks.length - 1;
|
|
2012
|
+
if (high < 0) return 0;
|
|
2013
|
+
return Math.abs(blocks[low].startLine - line) < Math.abs(blocks[high].endLine - line) ? low : high;
|
|
2014
|
+
}
|
|
2015
|
+
function toCandidateWindow(target, startBlock, endBlock) {
|
|
2016
|
+
const first = target.blocks[startBlock];
|
|
2017
|
+
const last = target.blocks[endBlock];
|
|
2018
|
+
return {
|
|
2019
|
+
startBlock,
|
|
2020
|
+
endBlock,
|
|
2021
|
+
startLine: first.startLine,
|
|
2022
|
+
endLine: last.endLine,
|
|
2023
|
+
type: first.type,
|
|
2024
|
+
text: target.lines.slice(first.startLine, last.endLine + 1).join("\n"),
|
|
2025
|
+
headingPath: first.headingPath,
|
|
2026
|
+
score: 0
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
function scoreCandidate(sourceBlock, sourceBlockIndex, candidate, index, originalLine) {
|
|
2030
|
+
const content = textSimilarity(sourceBlock.text, candidate.text);
|
|
2031
|
+
const type = sourceBlock.type === candidate.type ? 1 : 0;
|
|
2032
|
+
const heading = pathSimilarity(sourceBlock.headingPath, candidate.headingPath);
|
|
2033
|
+
const previous = neighborSimilarity(
|
|
2034
|
+
index.source.blocks[sourceBlockIndex - 1],
|
|
2035
|
+
index.target.blocks[candidate.startBlock - 1]
|
|
2036
|
+
);
|
|
2037
|
+
const next = neighborSimilarity(
|
|
2038
|
+
index.source.blocks[sourceBlockIndex + 1],
|
|
2039
|
+
index.target.blocks[candidate.endBlock + 1]
|
|
2040
|
+
);
|
|
2041
|
+
const proximity = Math.max(
|
|
2042
|
+
0,
|
|
2043
|
+
1 - Math.abs(candidate.startLine - originalLine) / 100
|
|
2044
|
+
);
|
|
2045
|
+
return Math.min(
|
|
2046
|
+
1,
|
|
2047
|
+
content * 0.45 + type * 0.1 + heading * 0.1 + previous * 0.15 + next * 0.15 + proximity * 0.05
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
function neighborSimilarity(source, target) {
|
|
2051
|
+
if (!source || !target) return 0;
|
|
2052
|
+
return textSimilarity(source.text, target.text);
|
|
2053
|
+
}
|
|
2054
|
+
function textSimilarity(left, right) {
|
|
2055
|
+
return Math.max(
|
|
2056
|
+
tokenDice(left, right),
|
|
2057
|
+
combinedScore(normalizeText(left), normalizeText(right))
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
function pathSimilarity(left, right) {
|
|
2061
|
+
if (left.length === 0 || right.length === 0) return 0;
|
|
2062
|
+
return tokenDice(left.join(" "), right.join(" "));
|
|
2063
|
+
}
|
|
2064
|
+
function tokenDice(left, right) {
|
|
2065
|
+
const leftTokens = tokenize2(left);
|
|
2066
|
+
const rightTokens = tokenize2(right);
|
|
2067
|
+
if (leftTokens.length === 0 || rightTokens.length === 0) return 0;
|
|
2068
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
2069
|
+
for (const token of rightTokens) {
|
|
2070
|
+
remaining.set(token, (remaining.get(token) ?? 0) + 1);
|
|
2071
|
+
}
|
|
2072
|
+
let overlap = 0;
|
|
2073
|
+
for (const token of leftTokens) {
|
|
2074
|
+
const count = remaining.get(token) ?? 0;
|
|
2075
|
+
if (count > 0) {
|
|
2076
|
+
overlap += 1;
|
|
2077
|
+
remaining.set(token, count - 1);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
return 2 * overlap / (leftTokens.length + rightTokens.length);
|
|
2081
|
+
}
|
|
2082
|
+
function tokenize2(text) {
|
|
2083
|
+
return normalizeText(text).match(/[\p{L}\p{N}_-]+/gu) ?? [];
|
|
2084
|
+
}
|
|
2085
|
+
function normalizeText(text) {
|
|
2086
|
+
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2087
|
+
}
|
|
2088
|
+
function samePath(left, right) {
|
|
2089
|
+
return left.length === right.length && left.every((item, index) => item === right[index]);
|
|
2090
|
+
}
|
|
2091
|
+
function resolveCandidateRange(comment, sourceBlock, candidate, target) {
|
|
2092
|
+
const exactIndex = candidate.text.indexOf(comment.selected_text);
|
|
2093
|
+
const selectedIsWholeBlock = comment.line === sourceBlock.startLine && (comment.end_line ?? comment.line) === sourceBlock.endLine && comment.start_column == null && comment.end_column == null && comment.selected_text === sourceBlock.text;
|
|
2094
|
+
const sourceOccurrenceIsUnique = countOccurrences(sourceBlock.text, comment.selected_text) === 1;
|
|
2095
|
+
if (exactIndex >= 0 && (selectedIsWholeBlock || sourceOccurrenceIsUnique)) {
|
|
2096
|
+
return exactRange(
|
|
2097
|
+
candidate.startLine,
|
|
2098
|
+
candidate.text,
|
|
2099
|
+
comment.selected_text,
|
|
2100
|
+
exactIndex
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
if (selectedIsWholeBlock) {
|
|
2104
|
+
return {
|
|
2105
|
+
line: candidate.startLine,
|
|
2106
|
+
endLine: candidate.endLine,
|
|
2107
|
+
text: candidate.text
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
const relativeLine = comment.line - sourceBlock.startLine;
|
|
2111
|
+
const line = Math.min(candidate.endLine, candidate.startLine + relativeLine);
|
|
2112
|
+
const lineSpan = (comment.end_line ?? comment.line) - comment.line;
|
|
2113
|
+
const endLine = Math.min(candidate.endLine, line + lineSpan);
|
|
2114
|
+
const startColumn = comment.start_column;
|
|
2115
|
+
const endColumn = comment.end_column;
|
|
2116
|
+
return {
|
|
2117
|
+
line,
|
|
2118
|
+
endLine,
|
|
2119
|
+
startColumn,
|
|
2120
|
+
endColumn,
|
|
2121
|
+
text: extractText2(
|
|
2122
|
+
target.lines,
|
|
2123
|
+
line,
|
|
2124
|
+
endLine,
|
|
2125
|
+
startColumn,
|
|
2126
|
+
endColumn
|
|
2127
|
+
) ?? ""
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
function countOccurrences(text, needle) {
|
|
2131
|
+
if (!needle) return 0;
|
|
2132
|
+
let count = 0;
|
|
2133
|
+
let offset = 0;
|
|
2134
|
+
while (offset <= text.length - needle.length) {
|
|
2135
|
+
const index = text.indexOf(needle, offset);
|
|
2136
|
+
if (index < 0) break;
|
|
2137
|
+
count += 1;
|
|
2138
|
+
offset = index + 1;
|
|
2139
|
+
}
|
|
2140
|
+
return count;
|
|
2141
|
+
}
|
|
2142
|
+
function exactRange(startLine, candidateText, selectedText, index) {
|
|
2143
|
+
const before = candidateText.slice(0, index).split("\n");
|
|
2144
|
+
const selectedLines = selectedText.split("\n");
|
|
2145
|
+
const line = startLine + before.length - 1;
|
|
2146
|
+
const startColumn = before.at(-1)?.length ?? 0;
|
|
2147
|
+
const finalSelectedLineLength = selectedLines.at(-1)?.length ?? 0;
|
|
2148
|
+
return {
|
|
2149
|
+
line,
|
|
2150
|
+
endLine: line + selectedLines.length - 1,
|
|
2151
|
+
startColumn,
|
|
2152
|
+
endColumn: selectedLines.length === 1 ? startColumn + finalSelectedLineLength : finalSelectedLineLength,
|
|
2153
|
+
text: selectedText
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
function extractText2(lines, line, endLine, startColumn, endColumn) {
|
|
2157
|
+
const finalLine = endLine ?? line;
|
|
2158
|
+
if (line < 1 || finalLine >= lines.length) return null;
|
|
2159
|
+
if (line === finalLine) {
|
|
2160
|
+
const text = lines[line];
|
|
2161
|
+
return startColumn != null && endColumn != null ? text.slice(startColumn, endColumn) : text;
|
|
2162
|
+
}
|
|
2163
|
+
const result = [];
|
|
2164
|
+
for (let current = line; current <= finalLine; current += 1) {
|
|
2165
|
+
let text = lines[current];
|
|
2166
|
+
if (current === line && startColumn != null) text = text.slice(startColumn);
|
|
2167
|
+
if (current === finalLine && endColumn != null) text = text.slice(0, endColumn);
|
|
2168
|
+
result.push(text);
|
|
2169
|
+
}
|
|
2170
|
+
return result.join("\n");
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// src/lib/confidence-calibration.ts
|
|
2174
|
+
function calibrateAnchorEvidence(commentId, selectedText, projected, contextual) {
|
|
2175
|
+
if (projected?.exact) {
|
|
2176
|
+
return {
|
|
2177
|
+
band: "certain",
|
|
2178
|
+
result: projectedResult(commentId, selectedText, projected)
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
if (contextual?.status === "anchored") {
|
|
2182
|
+
return {
|
|
2183
|
+
band: "certain",
|
|
2184
|
+
result: contextualResult(commentId, selectedText, contextual)
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
if (projected && contextual) {
|
|
2188
|
+
if (sameRange(projected, contextual)) {
|
|
2189
|
+
const result = contextualResult(commentId, selectedText, contextual);
|
|
2190
|
+
result.status = "fuzzy";
|
|
2191
|
+
result.score = combineIndependentScores(projected.score, contextual.score);
|
|
2192
|
+
result.reason = `Probable anchor: revision projection and Markdown context agree (confidence ${result.score.toFixed(3)}).`;
|
|
2193
|
+
return { band: "probable", result };
|
|
2194
|
+
}
|
|
2195
|
+
if (contextual.status === "orphaned" && !isStrongProjection(projected)) {
|
|
2196
|
+
return {
|
|
2197
|
+
band: "orphaned",
|
|
2198
|
+
result: contextualResult(commentId, selectedText, contextual)
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
if (contextual.status === "fuzzy" && contextual.candidateMargin >= 0.15 && contextual.score >= projected.score + 0.1) {
|
|
2202
|
+
return {
|
|
2203
|
+
band: "probable",
|
|
2204
|
+
result: contextualResult(commentId, selectedText, contextual)
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
return {
|
|
2208
|
+
band: "ambiguous",
|
|
2209
|
+
result: {
|
|
2210
|
+
commentId,
|
|
2211
|
+
status: "ambiguous",
|
|
2212
|
+
score: Math.max(projected.score, contextual.score),
|
|
2213
|
+
reason: `Ambiguous evidence: revision projection points to line ${projected.line}, while Markdown context points to ${contextual.line ?? "no location"}.`
|
|
2214
|
+
}
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
if (contextual) {
|
|
2218
|
+
const band = contextual.status === "orphaned" ? "orphaned" : contextual.status === "ambiguous" ? "ambiguous" : "probable";
|
|
2219
|
+
return {
|
|
2220
|
+
band,
|
|
2221
|
+
result: contextualResult(commentId, selectedText, contextual)
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
if (projected) {
|
|
2225
|
+
if (!isStrongProjection(projected)) return void 0;
|
|
2226
|
+
return {
|
|
2227
|
+
band: "probable",
|
|
2228
|
+
result: projectedResult(commentId, selectedText, projected)
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
return void 0;
|
|
2232
|
+
}
|
|
2233
|
+
function isStrongProjection(projected) {
|
|
2234
|
+
return projected.contextSupport >= 3 || projected.contextSupport >= 2 && projected.contextMargin >= 0.5;
|
|
2235
|
+
}
|
|
2236
|
+
function sameRange(projected, contextual) {
|
|
2237
|
+
return projected.line === contextual.line && projected.endLine === contextual.endLine;
|
|
2238
|
+
}
|
|
2239
|
+
function combineIndependentScores(left, right) {
|
|
2240
|
+
return Math.min(0.95, (left + right) / 2 + 0.05);
|
|
2241
|
+
}
|
|
2242
|
+
function projectedResult(commentId, selectedText, projected) {
|
|
2243
|
+
return {
|
|
2244
|
+
commentId,
|
|
2245
|
+
status: projected.exact ? "anchored" : "fuzzy",
|
|
2246
|
+
score: projected.score,
|
|
2247
|
+
newLine: projected.line,
|
|
2248
|
+
newEndLine: projected.endLine,
|
|
2249
|
+
newStartColumn: projected.startColumn,
|
|
2250
|
+
newEndColumn: projected.endColumn,
|
|
2251
|
+
anchoredText: projected.exact ? void 0 : projected.text,
|
|
2252
|
+
previousSelectedText: projected.exact ? void 0 : selectedText,
|
|
2253
|
+
reason: projected.reason
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
function contextualResult(commentId, selectedText, contextual) {
|
|
2257
|
+
return {
|
|
2258
|
+
commentId,
|
|
2259
|
+
status: contextual.status,
|
|
2260
|
+
score: contextual.score,
|
|
2261
|
+
newLine: contextual.line,
|
|
2262
|
+
newEndLine: contextual.endLine,
|
|
2263
|
+
newStartColumn: contextual.startColumn,
|
|
2264
|
+
newEndColumn: contextual.endColumn,
|
|
2265
|
+
anchoredText: contextual.status === "fuzzy" ? contextual.text : void 0,
|
|
2266
|
+
previousSelectedText: contextual.status === "fuzzy" ? selectedText : void 0,
|
|
2267
|
+
reason: contextual.reason
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
|
|
1362
2271
|
// src/lib/reanchor-core.ts
|
|
1363
2272
|
var HIGH_THRESHOLD = 0.8;
|
|
1364
2273
|
var DEFAULT_THRESHOLD = 0.6;
|
|
@@ -1371,6 +2280,12 @@ function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
1371
2280
|
const proximityWindow = opts.proximityWindow ?? DEFAULT_PROXIMITY_WINDOW;
|
|
1372
2281
|
const commentId = comment.id;
|
|
1373
2282
|
const selectedText = comment.selected_text;
|
|
2283
|
+
let fuzzyCandidateSets;
|
|
2284
|
+
let fuzzySearchIndex = opts.fuzzySearchIndex;
|
|
2285
|
+
const getFuzzySearchIndex = () => {
|
|
2286
|
+
fuzzySearchIndex ??= opts.getFuzzySearchIndex?.() ?? createFuzzySearchIndex(documentLines);
|
|
2287
|
+
return fuzzySearchIndex;
|
|
2288
|
+
};
|
|
1374
2289
|
if (!selectedText && comment.line == null) {
|
|
1375
2290
|
return {
|
|
1376
2291
|
commentId,
|
|
@@ -1398,7 +2313,7 @@ function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
1398
2313
|
const shiftedLine = comment.line + shift;
|
|
1399
2314
|
const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
|
|
1400
2315
|
const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : void 0;
|
|
1401
|
-
const textAtShifted =
|
|
2316
|
+
const textAtShifted = extractText3(
|
|
1402
2317
|
documentLines,
|
|
1403
2318
|
shiftedLine,
|
|
1404
2319
|
shiftedEndLine,
|
|
@@ -1417,8 +2332,39 @@ function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
1417
2332
|
}
|
|
1418
2333
|
}
|
|
1419
2334
|
}
|
|
2335
|
+
const projected = selectedText && opts.revisionProjection ? projectCommentAnchor(
|
|
2336
|
+
comment,
|
|
2337
|
+
opts.revisionProjection,
|
|
2338
|
+
threshold
|
|
2339
|
+
) : void 0;
|
|
2340
|
+
if (selectedText && projected?.exact) {
|
|
2341
|
+
const exactCalibration = calibrateAnchorEvidence(
|
|
2342
|
+
commentId,
|
|
2343
|
+
selectedText,
|
|
2344
|
+
projected
|
|
2345
|
+
);
|
|
2346
|
+
if (exactCalibration) return exactCalibration.result;
|
|
2347
|
+
}
|
|
2348
|
+
const contextual = selectedText && opts.anchorContext ? resolveContextAnchor(comment, opts.anchorContext) : void 0;
|
|
2349
|
+
if (selectedText && (projected || contextual)) {
|
|
2350
|
+
const calibrated = calibrateAnchorEvidence(
|
|
2351
|
+
commentId,
|
|
2352
|
+
selectedText,
|
|
2353
|
+
projected,
|
|
2354
|
+
contextual
|
|
2355
|
+
);
|
|
2356
|
+
if (calibrated) return calibrated.result;
|
|
2357
|
+
}
|
|
1420
2358
|
if (selectedText) {
|
|
1421
2359
|
const exactCandidates = exactMatch(documentLines, selectedText);
|
|
2360
|
+
if (exactCandidates.length > 1 && comment.line == null) {
|
|
2361
|
+
return {
|
|
2362
|
+
commentId,
|
|
2363
|
+
status: "ambiguous",
|
|
2364
|
+
score: 1,
|
|
2365
|
+
reason: `Ambiguous: ${exactCandidates.length} exact matches and no position or source context to disambiguate them.`
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
1422
2368
|
let chosen;
|
|
1423
2369
|
let chosenReason = "";
|
|
1424
2370
|
if (exactCandidates.length === 1) {
|
|
@@ -1430,7 +2376,7 @@ function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
1430
2376
|
}
|
|
1431
2377
|
if (chosen) {
|
|
1432
2378
|
if (isImplausibleExactRelocation(comment, chosen, documentLines, proximityWindow)) {
|
|
1433
|
-
const textAtOrigin =
|
|
2379
|
+
const textAtOrigin = extractText3(
|
|
1434
2380
|
documentLines,
|
|
1435
2381
|
comment.line,
|
|
1436
2382
|
comment.end_line,
|
|
@@ -1477,12 +2423,14 @@ function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
1477
2423
|
reason: "Normalized whitespace match."
|
|
1478
2424
|
};
|
|
1479
2425
|
}
|
|
1480
|
-
|
|
2426
|
+
fuzzyCandidateSets = fuzzySearchThresholds(
|
|
1481
2427
|
documentLines,
|
|
1482
2428
|
selectedText,
|
|
1483
|
-
HIGH_THRESHOLD,
|
|
1484
|
-
comment.line
|
|
2429
|
+
[HIGH_THRESHOLD, threshold],
|
|
2430
|
+
comment.line,
|
|
2431
|
+
getFuzzySearchIndex()
|
|
1485
2432
|
);
|
|
2433
|
+
const fuzzyCandidates = fuzzyCandidateSets.get(HIGH_THRESHOLD) ?? [];
|
|
1486
2434
|
if (fuzzyCandidates.length === 1 || fuzzyCandidates.length > 0 && fuzzyCandidates[0].score >= HIGH_THRESHOLD) {
|
|
1487
2435
|
const best = fuzzyCandidates.length === 1 ? fuzzyCandidates[0] : closestToLine(fuzzyCandidates, comment.line ?? 1);
|
|
1488
2436
|
return {
|
|
@@ -1531,11 +2479,12 @@ function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
1531
2479
|
}
|
|
1532
2480
|
}
|
|
1533
2481
|
if (selectedText) {
|
|
1534
|
-
const lowCandidates = fuzzySearch(
|
|
2482
|
+
const lowCandidates = fuzzyCandidateSets?.get(threshold) ?? fuzzySearch(
|
|
1535
2483
|
documentLines,
|
|
1536
2484
|
selectedText,
|
|
1537
2485
|
threshold,
|
|
1538
|
-
comment.line
|
|
2486
|
+
comment.line,
|
|
2487
|
+
getFuzzySearchIndex()
|
|
1539
2488
|
);
|
|
1540
2489
|
if (lowCandidates.length === 1) {
|
|
1541
2490
|
const candidate = lowCandidates[0];
|
|
@@ -1572,7 +2521,14 @@ function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
1572
2521
|
};
|
|
1573
2522
|
}
|
|
1574
2523
|
function reanchorDocumentLines(doc, documentLines, opts = {}) {
|
|
1575
|
-
|
|
2524
|
+
let fuzzySearchIndex;
|
|
2525
|
+
const getFuzzySearchIndex = () => {
|
|
2526
|
+
fuzzySearchIndex ??= createFuzzySearchIndex(documentLines);
|
|
2527
|
+
return fuzzySearchIndex;
|
|
2528
|
+
};
|
|
2529
|
+
return doc.comments.map(
|
|
2530
|
+
(comment) => reanchorComment(comment, documentLines, { ...opts, getFuzzySearchIndex })
|
|
2531
|
+
);
|
|
1576
2532
|
}
|
|
1577
2533
|
function reanchorDocumentText(doc, documentText, opts = {}) {
|
|
1578
2534
|
return reanchorDocumentLines(doc, toReanchorLines(documentText), opts);
|
|
@@ -1691,7 +2647,7 @@ function applyReanchorResults(doc, results, opts = {}) {
|
|
|
1691
2647
|
}
|
|
1692
2648
|
return changed;
|
|
1693
2649
|
}
|
|
1694
|
-
function
|
|
2650
|
+
function extractText3(lines, line, endLine, startColumn, endColumn) {
|
|
1695
2651
|
const startIdx = line;
|
|
1696
2652
|
const endIdx = endLine ?? line;
|
|
1697
2653
|
if (startIdx < 1 || endIdx >= lines.length) return null;
|
|
@@ -1720,7 +2676,7 @@ function isImplausibleExactRelocation(comment, candidate, lines, proximityWindow
|
|
|
1720
2676
|
if (comment.line == null) return false;
|
|
1721
2677
|
if (comment.line <= 0 || comment.line >= lines.length) return false;
|
|
1722
2678
|
if (Math.abs(candidate.line - comment.line) <= proximityWindow) return false;
|
|
1723
|
-
const textAtOrigin =
|
|
2679
|
+
const textAtOrigin = extractText3(
|
|
1724
2680
|
lines,
|
|
1725
2681
|
comment.line,
|
|
1726
2682
|
comment.end_line,
|
|
@@ -1746,6 +2702,137 @@ function getLineShift2(diffHunks, line) {
|
|
|
1746
2702
|
return { shift, modified };
|
|
1747
2703
|
}
|
|
1748
2704
|
|
|
2705
|
+
// src/lib/global-reconciliation.ts
|
|
2706
|
+
var LOCAL_LANDMARK_WINDOW = 30;
|
|
2707
|
+
var MIN_LANDMARK_SUPPORT = 0.65;
|
|
2708
|
+
var MIN_SUPPORTING_LANDMARKS = 2;
|
|
2709
|
+
var MIN_GLOBAL_MARGIN = 0.08;
|
|
2710
|
+
var MAX_RECONCILIATION_ROUNDS = 4;
|
|
2711
|
+
function reconcileCommentAnchors(comments, results, anchorContext) {
|
|
2712
|
+
const reconciled = results.map((result) => ({ ...result }));
|
|
2713
|
+
if (comments.length < MIN_SUPPORTING_LANDMARKS + 1 || !reconciled.some((result) => result.status === "ambiguous")) {
|
|
2714
|
+
return reconciled;
|
|
2715
|
+
}
|
|
2716
|
+
const resultIndexes = new Map(
|
|
2717
|
+
reconciled.map((result, index) => [result.commentId, index])
|
|
2718
|
+
);
|
|
2719
|
+
const landmarks = collectLandmarks(comments, reconciled, anchorContext);
|
|
2720
|
+
const landmarkCounts = /* @__PURE__ */ new Map();
|
|
2721
|
+
for (const landmark of landmarks) {
|
|
2722
|
+
landmarkCounts.set(
|
|
2723
|
+
landmark.scope,
|
|
2724
|
+
(landmarkCounts.get(landmark.scope) ?? 0) + 1
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
for (let round = 0; round < MAX_RECONCILIATION_ROUNDS; round += 1) {
|
|
2728
|
+
let changed = false;
|
|
2729
|
+
for (const comment of comments) {
|
|
2730
|
+
if (comment.line == null || !comment.selected_text) continue;
|
|
2731
|
+
const scope = getAnchorContextScope(comment, anchorContext);
|
|
2732
|
+
if (scope == null) continue;
|
|
2733
|
+
if ((landmarkCounts.get(scope) ?? 0) < MIN_SUPPORTING_LANDMARKS) {
|
|
2734
|
+
continue;
|
|
2735
|
+
}
|
|
2736
|
+
const resultIndex = resultIndexes.get(comment.id);
|
|
2737
|
+
if (resultIndex == null) continue;
|
|
2738
|
+
const result = reconciled[resultIndex];
|
|
2739
|
+
if (result.status !== "ambiguous") continue;
|
|
2740
|
+
const candidates = deduplicateCandidates2(
|
|
2741
|
+
findContextAnchorCandidates(comment, anchorContext)
|
|
2742
|
+
).slice(0, 8);
|
|
2743
|
+
if (candidates.length < 2) continue;
|
|
2744
|
+
const ranked = candidates.map(
|
|
2745
|
+
(candidate) => scoreWithLandmarks(
|
|
2746
|
+
comment.line,
|
|
2747
|
+
scope,
|
|
2748
|
+
candidate,
|
|
2749
|
+
landmarks
|
|
2750
|
+
)
|
|
2751
|
+
).sort(
|
|
2752
|
+
(left, right) => right.globalScore - left.globalScore || right.candidate.score - left.candidate.score || left.candidate.line - right.candidate.line
|
|
2753
|
+
);
|
|
2754
|
+
const best = ranked[0];
|
|
2755
|
+
const second = ranked[1];
|
|
2756
|
+
if (!best.candidate.exact || best.supportingLandmarks < MIN_SUPPORTING_LANDMARKS || best.support < MIN_LANDMARK_SUPPORT || best.globalScore - second.globalScore < MIN_GLOBAL_MARGIN) {
|
|
2757
|
+
continue;
|
|
2758
|
+
}
|
|
2759
|
+
reconciled[resultIndex] = {
|
|
2760
|
+
commentId: comment.id,
|
|
2761
|
+
status: "anchored",
|
|
2762
|
+
score: Math.min(0.99, 0.8 + best.support * 0.19),
|
|
2763
|
+
newLine: best.candidate.line,
|
|
2764
|
+
newEndLine: best.candidate.endLine,
|
|
2765
|
+
newStartColumn: best.candidate.startColumn,
|
|
2766
|
+
newEndColumn: best.candidate.endColumn,
|
|
2767
|
+
reason: `Global reconciliation selected this candidate from ${best.supportingLandmarks} nearby landmark(s) (support ${best.support.toFixed(3)}, margin ${(best.globalScore - second.globalScore).toFixed(3)}).`
|
|
2768
|
+
};
|
|
2769
|
+
changed = true;
|
|
2770
|
+
landmarks.push({
|
|
2771
|
+
sourceLine: comment.line,
|
|
2772
|
+
targetLine: best.candidate.line,
|
|
2773
|
+
scope
|
|
2774
|
+
});
|
|
2775
|
+
landmarkCounts.set(scope, (landmarkCounts.get(scope) ?? 0) + 1);
|
|
2776
|
+
}
|
|
2777
|
+
if (!changed) break;
|
|
2778
|
+
}
|
|
2779
|
+
return reconciled;
|
|
2780
|
+
}
|
|
2781
|
+
function collectLandmarks(comments, results, anchorContext) {
|
|
2782
|
+
const resultMap = new Map(results.map((result) => [result.commentId, result]));
|
|
2783
|
+
return comments.flatMap((comment) => {
|
|
2784
|
+
const result = resultMap.get(comment.id);
|
|
2785
|
+
if (comment.line == null || result?.newLine == null || result.status !== "anchored" && result.status !== "shifted" || result.score < 0.99) {
|
|
2786
|
+
return [];
|
|
2787
|
+
}
|
|
2788
|
+
const scope = getAnchorContextScope(comment, anchorContext);
|
|
2789
|
+
return scope == null ? [] : [{ sourceLine: comment.line, targetLine: result.newLine, scope }];
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
function scoreWithLandmarks(sourceLine, scope, candidate, landmarks) {
|
|
2793
|
+
let weightedSupport = 0;
|
|
2794
|
+
let totalWeight = 0;
|
|
2795
|
+
let supportingLandmarks = 0;
|
|
2796
|
+
const candidateShift = candidate.line - sourceLine;
|
|
2797
|
+
for (const landmark of landmarks) {
|
|
2798
|
+
if (landmark.scope !== scope) continue;
|
|
2799
|
+
const sourceDistance = Math.abs(sourceLine - landmark.sourceLine);
|
|
2800
|
+
if (sourceDistance === 0 || sourceDistance > LOCAL_LANDMARK_WINDOW) continue;
|
|
2801
|
+
const landmarkShift = landmark.targetLine - landmark.sourceLine;
|
|
2802
|
+
const shiftError = Math.abs(candidateShift - landmarkShift);
|
|
2803
|
+
const agreement = Math.max(0, 1 - shiftError / 8);
|
|
2804
|
+
const weight = 1 / (1 + sourceDistance / 4);
|
|
2805
|
+
weightedSupport += agreement * weight;
|
|
2806
|
+
totalWeight += weight;
|
|
2807
|
+
if (agreement >= MIN_LANDMARK_SUPPORT) supportingLandmarks += 1;
|
|
2808
|
+
}
|
|
2809
|
+
const support = totalWeight > 0 ? weightedSupport / totalWeight : 0;
|
|
2810
|
+
return {
|
|
2811
|
+
candidate,
|
|
2812
|
+
globalScore: candidate.score * 0.5 + support * 0.45 + (candidate.exact ? 0.05 : 0),
|
|
2813
|
+
support,
|
|
2814
|
+
supportingLandmarks
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
function deduplicateCandidates2(candidates) {
|
|
2818
|
+
const unique = /* @__PURE__ */ new Map();
|
|
2819
|
+
for (const candidate of candidates) {
|
|
2820
|
+
const key = [
|
|
2821
|
+
candidate.line,
|
|
2822
|
+
candidate.endLine,
|
|
2823
|
+
candidate.startColumn ?? "",
|
|
2824
|
+
candidate.endColumn ?? ""
|
|
2825
|
+
].join(":");
|
|
2826
|
+
const existing = unique.get(key);
|
|
2827
|
+
if (!existing || candidate.score > existing.score) {
|
|
2828
|
+
unique.set(key, candidate);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
return [...unique.values()].sort(
|
|
2832
|
+
(left, right) => right.score - left.score || left.line - right.line
|
|
2833
|
+
);
|
|
2834
|
+
}
|
|
2835
|
+
|
|
1749
2836
|
// src/lib/reanchor.ts
|
|
1750
2837
|
init_parser();
|
|
1751
2838
|
init_parser();
|
|
@@ -1754,31 +2841,109 @@ async function reanchorDocument(doc, documentLines, opts = {}) {
|
|
|
1754
2841
|
const results = [];
|
|
1755
2842
|
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
1756
2843
|
const proximityWindow = opts.proximityWindow;
|
|
1757
|
-
let
|
|
1758
|
-
|
|
2844
|
+
let fuzzySearchIndex;
|
|
2845
|
+
const getFuzzySearchIndex = () => {
|
|
2846
|
+
fuzzySearchIndex ??= createFuzzySearchIndex(documentLines);
|
|
2847
|
+
return fuzzySearchIndex;
|
|
2848
|
+
};
|
|
1759
2849
|
if (!opts.noGit && await isGitAvailable()) {
|
|
1760
2850
|
const repoRoot = opts.repoRoot ?? await findRepoRoot(opts.cwd);
|
|
1761
2851
|
if (repoRoot && opts.documentPath) {
|
|
1762
2852
|
const relPath = import_node_path6.default.relative(repoRoot, opts.documentPath);
|
|
1763
2853
|
const head = await getCurrentCommit(repoRoot);
|
|
1764
2854
|
const globalFrom = opts.fromCommit;
|
|
2855
|
+
const diffCache = /* @__PURE__ */ new Map();
|
|
2856
|
+
const projectionCache = /* @__PURE__ */ new Map();
|
|
2857
|
+
const contextCache = /* @__PURE__ */ new Map();
|
|
2858
|
+
const canonicalCommitCache = /* @__PURE__ */ new Map();
|
|
2859
|
+
const reconciliationGroups = /* @__PURE__ */ new Map();
|
|
1765
2860
|
for (const comment of doc.comments) {
|
|
1766
|
-
const
|
|
1767
|
-
if (
|
|
1768
|
-
|
|
1769
|
-
const
|
|
2861
|
+
const rawCommentCommit = globalFrom ?? comment.commit;
|
|
2862
|
+
if (rawCommentCommit && head) {
|
|
2863
|
+
const cachedCommit = canonicalCommitCache.get(rawCommentCommit);
|
|
2864
|
+
const commentCommit = cachedCommit ?? await resolveCommit(rawCommentCommit, repoRoot) ?? rawCommentCommit;
|
|
2865
|
+
if (!cachedCommit) {
|
|
2866
|
+
canonicalCommitCache.set(rawCommentCommit, commentCommit);
|
|
2867
|
+
}
|
|
2868
|
+
if (commentCommit === head) {
|
|
2869
|
+
results.push(
|
|
2870
|
+
reanchorComment(comment, documentLines, {
|
|
2871
|
+
threshold,
|
|
2872
|
+
commitIsStale: false,
|
|
2873
|
+
proximityWindow,
|
|
2874
|
+
getFuzzySearchIndex
|
|
2875
|
+
})
|
|
2876
|
+
);
|
|
2877
|
+
continue;
|
|
2878
|
+
}
|
|
2879
|
+
let hunks = diffCache.get(commentCommit);
|
|
2880
|
+
if (!hunks) {
|
|
2881
|
+
hunks = await getDiff(commentCommit, head, relPath, repoRoot);
|
|
2882
|
+
diffCache.set(commentCommit, hunks);
|
|
2883
|
+
}
|
|
2884
|
+
let revisionProjection = projectionCache.get(commentCommit);
|
|
2885
|
+
let anchorContext = contextCache.get(commentCommit);
|
|
2886
|
+
if (!projectionCache.has(commentCommit)) {
|
|
2887
|
+
const sourceText = await getFileAtCommit(
|
|
2888
|
+
commentCommit,
|
|
2889
|
+
relPath,
|
|
2890
|
+
repoRoot
|
|
2891
|
+
);
|
|
2892
|
+
revisionProjection = sourceText == null ? void 0 : createRevisionProjection(
|
|
2893
|
+
toReanchorLines(sourceText),
|
|
2894
|
+
documentLines
|
|
2895
|
+
);
|
|
2896
|
+
anchorContext = sourceText == null ? void 0 : createAnchorContextIndex(
|
|
2897
|
+
toReanchorLines(sourceText),
|
|
2898
|
+
documentLines
|
|
2899
|
+
);
|
|
2900
|
+
projectionCache.set(commentCommit, revisionProjection);
|
|
2901
|
+
contextCache.set(commentCommit, anchorContext);
|
|
2902
|
+
}
|
|
1770
2903
|
const result = reanchorComment(comment, documentLines, {
|
|
1771
2904
|
diffHunks: hunks,
|
|
1772
2905
|
threshold,
|
|
1773
2906
|
commitIsStale: true,
|
|
1774
|
-
proximityWindow
|
|
2907
|
+
proximityWindow,
|
|
2908
|
+
revisionProjection,
|
|
2909
|
+
anchorContext,
|
|
2910
|
+
getFuzzySearchIndex
|
|
1775
2911
|
});
|
|
2912
|
+
const resultIndex = results.length;
|
|
1776
2913
|
results.push(result);
|
|
2914
|
+
if (anchorContext) {
|
|
2915
|
+
const group = reconciliationGroups.get(commentCommit);
|
|
2916
|
+
if (group) {
|
|
2917
|
+
group.comments.push(comment);
|
|
2918
|
+
group.resultIndexes.push(resultIndex);
|
|
2919
|
+
} else {
|
|
2920
|
+
reconciliationGroups.set(commentCommit, {
|
|
2921
|
+
comments: [comment],
|
|
2922
|
+
resultIndexes: [resultIndex],
|
|
2923
|
+
anchorContext
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
1777
2927
|
continue;
|
|
1778
2928
|
}
|
|
1779
2929
|
results.push(
|
|
1780
|
-
reanchorComment(comment, documentLines, {
|
|
2930
|
+
reanchorComment(comment, documentLines, {
|
|
2931
|
+
threshold,
|
|
2932
|
+
commitIsStale: false,
|
|
2933
|
+
proximityWindow,
|
|
2934
|
+
getFuzzySearchIndex
|
|
2935
|
+
})
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
for (const group of reconciliationGroups.values()) {
|
|
2939
|
+
const reconciled = reconcileCommentAnchors(
|
|
2940
|
+
group.comments,
|
|
2941
|
+
group.resultIndexes.map((index) => results[index]),
|
|
2942
|
+
group.anchorContext
|
|
1781
2943
|
);
|
|
2944
|
+
for (const [offset, resultIndex] of group.resultIndexes.entries()) {
|
|
2945
|
+
results[resultIndex] = reconciled[offset];
|
|
2946
|
+
}
|
|
1782
2947
|
}
|
|
1783
2948
|
return results;
|
|
1784
2949
|
}
|
|
@@ -2091,10 +3256,11 @@ var combinedScore2 = combinedScore;
|
|
|
2091
3256
|
var isGitAvailable2 = isGitAvailable;
|
|
2092
3257
|
var findRepoRoot3 = findRepoRoot;
|
|
2093
3258
|
var getCurrentCommit2 = getCurrentCommit;
|
|
3259
|
+
var resolveCommit2 = resolveCommit;
|
|
2094
3260
|
var isStale2 = isStale;
|
|
2095
3261
|
var getDiff2 = getDiff;
|
|
2096
3262
|
var getLineShift4 = getLineShift;
|
|
2097
|
-
var
|
|
3263
|
+
var getFileAtCommit2 = getFileAtCommit;
|
|
2098
3264
|
var getStagedFiles2 = getStagedFiles;
|
|
2099
3265
|
var detectRenames2 = detectRenames;
|
|
2100
3266
|
var parseDiffHunks3 = parseDiffHunks;
|
|
@@ -2108,6 +3274,10 @@ var resolveAnchor2 = resolveAnchor;
|
|
|
2108
3274
|
var reanchorDocument2 = reanchorDocument;
|
|
2109
3275
|
var applyReanchorResults2 = applyReanchorResults;
|
|
2110
3276
|
var reanchorFile2 = reanchorFile;
|
|
3277
|
+
var createAnchorContextIndex2 = createAnchorContextIndex;
|
|
3278
|
+
var reconcileCommentAnchors2 = reconcileCommentAnchors;
|
|
3279
|
+
var createRevisionProjection2 = createRevisionProjection;
|
|
3280
|
+
var calibrateAnchorEvidence2 = calibrateAnchorEvidence;
|
|
2111
3281
|
var addComment2 = addComment;
|
|
2112
3282
|
var editComment2 = editComment;
|
|
2113
3283
|
var normalizeCommentExtensions2 = normalizeCommentExtensions;
|
|
@@ -2127,8 +3297,11 @@ var newCommentId2 = newCommentId;
|
|
|
2127
3297
|
HIGH_THRESHOLD,
|
|
2128
3298
|
addComment,
|
|
2129
3299
|
applyReanchorResults,
|
|
3300
|
+
calibrateAnchorEvidence,
|
|
2130
3301
|
combinedScore,
|
|
2131
3302
|
computeHash,
|
|
3303
|
+
createAnchorContextIndex,
|
|
3304
|
+
createRevisionProjection,
|
|
2132
3305
|
detectRenames,
|
|
2133
3306
|
discoverAllSidecars,
|
|
2134
3307
|
discoverSidecar,
|
|
@@ -2165,9 +3338,11 @@ var newCommentId2 = newCommentId;
|
|
|
2165
3338
|
reanchorDocumentLines,
|
|
2166
3339
|
reanchorDocumentText,
|
|
2167
3340
|
reanchorFile,
|
|
3341
|
+
reconcileCommentAnchors,
|
|
2168
3342
|
removeComment,
|
|
2169
3343
|
resolveAnchor,
|
|
2170
3344
|
resolveComment,
|
|
3345
|
+
resolveCommit,
|
|
2171
3346
|
resolveSidecarPaths,
|
|
2172
3347
|
sidecarToDocument,
|
|
2173
3348
|
summarize,
|