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