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