@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.
Files changed (37) hide show
  1. package/README.md +10 -7
  2. package/dist/browser.cjs +1101 -24
  3. package/dist/browser.cjs.map +4 -4
  4. package/dist/browser.d.ts +11 -0
  5. package/dist/browser.js +8 -0
  6. package/dist/evaluation/generate-reanchor-cases.d.ts +10 -0
  7. package/dist/evaluation/generate-reanchor-cases.js +301 -0
  8. package/dist/evaluation/reanchor-eval.d.ts +111 -0
  9. package/dist/evaluation/reanchor-eval.js +266 -0
  10. package/dist/evaluation/reanchor-gates.d.ts +29 -0
  11. package/dist/evaluation/reanchor-gates.js +47 -0
  12. package/dist/evaluation/run-reanchor-eval.d.ts +3 -0
  13. package/dist/evaluation/run-reanchor-eval.js +103 -0
  14. package/dist/evaluation/run-reanchor-generator.d.ts +3 -0
  15. package/dist/evaluation/run-reanchor-generator.js +63 -0
  16. package/dist/evaluation/run-reanchor-profile.d.ts +3 -0
  17. package/dist/evaluation/run-reanchor-profile.js +129 -0
  18. package/dist/index.cjs +1209 -34
  19. package/dist/index.cjs.map +4 -4
  20. package/dist/index.d.ts +12 -0
  21. package/dist/index.js +9 -0
  22. package/dist/lib/anchor-context.d.ts +46 -0
  23. package/dist/lib/anchor-context.js +474 -0
  24. package/dist/lib/confidence-calibration.d.ts +16 -0
  25. package/dist/lib/confidence-calibration.js +120 -0
  26. package/dist/lib/fuzzy.d.ts +14 -1
  27. package/dist/lib/fuzzy.js +140 -19
  28. package/dist/lib/git.d.ts +2 -0
  29. package/dist/lib/git.js +12 -0
  30. package/dist/lib/global-reconciliation.d.ts +11 -0
  31. package/dist/lib/global-reconciliation.js +145 -0
  32. package/dist/lib/reanchor-core.d.ts +7 -0
  33. package/dist/lib/reanchor-core.js +46 -4
  34. package/dist/lib/reanchor.js +82 -11
  35. package/dist/lib/revision-projection.d.ts +21 -0
  36. package/dist/lib/revision-projection.js +203 -0
  37. 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: () => getFileAtCommit3,
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
- if (!needle) return [];
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
- for (let startLine = 1; startLine + winSize - 1 < lines.length; startLine++) {
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 >= threshold) {
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 (let lineNum = 1; lineNum < lines.length; lineNum++) {
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
- for (let len = minWinLen; len <= maxWinLen; len++) {
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 >= threshold) {
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
- let deduped = deduplicateCandidates(candidates);
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
- deduped = deduped.map((c) => {
1188
- const dist = Math.abs(c.line - hintLine);
1189
- const proximityBonus = 0.1 * Math.max(0, 1 - dist / 50);
1190
- return { ...c, score: Math.min(1, c.score + proximityBonus) };
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 deduped.sort((a, b) => b.score - a.score);
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 = extractText(
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 = extractText(
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
- const fuzzyCandidates = fuzzySearch(
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
- return doc.comments.map((comment) => reanchorComment(comment, documentLines, opts));
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 extractText(lines, line, endLine, startColumn, endColumn) {
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 = extractText(
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 diffHunks;
1758
- let commitIsStale = false;
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 commentCommit = globalFrom ?? comment.commit;
1767
- if (commentCommit && head && commentCommit !== head) {
1768
- commitIsStale = true;
1769
- const hunks = await getDiff(commentCommit, head, relPath, repoRoot);
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, { threshold, commitIsStale: false, proximityWindow })
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 getFileAtCommit3 = getFileAtCommit;
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,