@mrsf/cli 0.5.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 (45) hide show
  1. package/README.md +10 -7
  2. package/dist/browser.cjs +1273 -37
  3. package/dist/browser.cjs.map +4 -4
  4. package/dist/browser.d.ts +18 -1
  5. package/dist/browser.js +14 -0
  6. package/dist/commands/reanchor.js +3 -0
  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 +1307 -85
  20. package/dist/index.cjs.map +4 -4
  21. package/dist/index.d.ts +12 -0
  22. package/dist/index.js +9 -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 +2 -0
  30. package/dist/lib/git.js +12 -0
  31. package/dist/lib/global-reconciliation.d.ts +11 -0
  32. package/dist/lib/global-reconciliation.js +145 -0
  33. package/dist/lib/parser.d.ts +4 -19
  34. package/dist/lib/parser.js +4 -140
  35. package/dist/lib/reanchor-core.d.ts +20 -0
  36. package/dist/lib/reanchor-core.js +123 -20
  37. package/dist/lib/reanchor.js +85 -12
  38. package/dist/lib/revision-projection.d.ts +21 -0
  39. package/dist/lib/revision-projection.js +203 -0
  40. package/dist/lib/serialize.d.ts +37 -0
  41. package/dist/lib/serialize.js +170 -0
  42. package/dist/lib/types.d.ts +7 -0
  43. package/dist/lib/writer.d.ts +1 -8
  44. package/dist/lib/writer.js +5 -14
  45. package/package.json +5 -1
package/dist/index.cjs CHANGED
@@ -31,20 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  ));
32
32
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
33
 
34
- // src/lib/parser.ts
35
- var parser_exports = {};
36
- __export(parser_exports, {
37
- parseSidecar: () => parseSidecar,
38
- parseSidecarContent: () => parseSidecarContent,
39
- parseSidecarContentLenient: () => parseSidecarContentLenient,
40
- parseSidecarLenient: () => parseSidecarLenient,
41
- readDocumentLines: () => readDocumentLines
42
- });
43
- async function parseSidecar(filePath) {
44
- const abs = import_node_path3.default.resolve(filePath);
45
- const content = await (0, import_promises2.readFile)(abs, "utf-8");
46
- return parseSidecarContent(content, abs);
47
- }
34
+ // src/lib/serialize.ts
48
35
  function parseSidecarContent(content, filenameHint) {
49
36
  const trimmed = content.trim();
50
37
  let parsed;
@@ -67,16 +54,6 @@ function parseSidecarContent(content, filenameHint) {
67
54
  }
68
55
  return parsed;
69
56
  }
70
- async function parseSidecarLenient(filePath) {
71
- const abs = import_node_path3.default.resolve(filePath);
72
- let content;
73
- try {
74
- content = await (0, import_promises2.readFile)(abs, "utf-8");
75
- } catch (e) {
76
- return { doc: null, error: `Cannot read file: ${e.message}` };
77
- }
78
- return parseSidecarContentLenient(content, abs);
79
- }
80
57
  function parseSidecarContentLenient(content, filenameHint) {
81
58
  const trimmed = content.trim();
82
59
  if (!trimmed) {
@@ -163,18 +140,59 @@ function salvageYaml(content) {
163
140
  partialComments: salvaged.length > 0 ? salvaged : void 0
164
141
  };
165
142
  }
143
+ function toYaml(doc) {
144
+ const yamlDoc = new import_yaml.Document(doc);
145
+ return yamlDoc.toString({ lineWidth: 0 });
146
+ }
147
+ function toJson(doc) {
148
+ return JSON.stringify(doc, null, 2) + "\n";
149
+ }
150
+ var import_js_yaml2, import_yaml;
151
+ var init_serialize = __esm({
152
+ "src/lib/serialize.ts"() {
153
+ "use strict";
154
+ import_js_yaml2 = __toESM(require("js-yaml"), 1);
155
+ import_yaml = require("yaml");
156
+ }
157
+ });
158
+
159
+ // src/lib/parser.ts
160
+ var parser_exports = {};
161
+ __export(parser_exports, {
162
+ parseSidecar: () => parseSidecar,
163
+ parseSidecarContent: () => parseSidecarContent,
164
+ parseSidecarContentLenient: () => parseSidecarContentLenient,
165
+ parseSidecarLenient: () => parseSidecarLenient,
166
+ readDocumentLines: () => readDocumentLines
167
+ });
168
+ async function parseSidecar(filePath) {
169
+ const abs = import_node_path3.default.resolve(filePath);
170
+ const content = await (0, import_promises2.readFile)(abs, "utf-8");
171
+ return parseSidecarContent(content, abs);
172
+ }
173
+ async function parseSidecarLenient(filePath) {
174
+ const abs = import_node_path3.default.resolve(filePath);
175
+ let content;
176
+ try {
177
+ content = await (0, import_promises2.readFile)(abs, "utf-8");
178
+ } catch (e) {
179
+ return { doc: null, error: `Cannot read file: ${e.message}` };
180
+ }
181
+ return parseSidecarContentLenient(content, abs);
182
+ }
166
183
  async function readDocumentLines(filePath) {
167
184
  const content = await (0, import_promises2.readFile)(import_node_path3.default.resolve(filePath), "utf-8");
168
185
  const lines = content.replace(/\r\n?/g, "\n").split("\n");
169
186
  return ["", ...lines];
170
187
  }
171
- var import_promises2, import_node_path3, import_js_yaml2;
188
+ var import_promises2, import_node_path3;
172
189
  var init_parser = __esm({
173
190
  "src/lib/parser.ts"() {
174
191
  "use strict";
175
192
  import_promises2 = require("node:fs/promises");
176
193
  import_node_path3 = __toESM(require("node:path"), 1);
177
- import_js_yaml2 = __toESM(require("js-yaml"), 1);
194
+ init_serialize();
195
+ init_serialize();
178
196
  }
179
197
  });
180
198
 
@@ -185,8 +203,11 @@ __export(index_exports, {
185
203
  HIGH_THRESHOLD: () => HIGH_THRESHOLD2,
186
204
  addComment: () => addComment2,
187
205
  applyReanchorResults: () => applyReanchorResults2,
206
+ calibrateAnchorEvidence: () => calibrateAnchorEvidence2,
188
207
  combinedScore: () => combinedScore2,
189
208
  computeHash: () => computeHash2,
209
+ createAnchorContextIndex: () => createAnchorContextIndex2,
210
+ createRevisionProjection: () => createRevisionProjection2,
190
211
  detectRenames: () => detectRenames2,
191
212
  discoverAllSidecars: () => discoverAllSidecars2,
192
213
  discoverSidecar: () => discoverSidecar3,
@@ -199,7 +220,7 @@ __export(index_exports, {
199
220
  fuzzySearch: () => fuzzySearch2,
200
221
  getCurrentCommit: () => getCurrentCommit2,
201
222
  getDiff: () => getDiff2,
202
- getFileAtCommit: () => getFileAtCommit3,
223
+ getFileAtCommit: () => getFileAtCommit2,
203
224
  getLineShift: () => getLineShift4,
204
225
  getStagedFiles: () => getStagedFiles2,
205
226
  getThreads: () => getThreads2,
@@ -223,9 +244,11 @@ __export(index_exports, {
223
244
  reanchorDocumentLines: () => reanchorDocumentLines2,
224
245
  reanchorDocumentText: () => reanchorDocumentText2,
225
246
  reanchorFile: () => reanchorFile2,
247
+ reconcileCommentAnchors: () => reconcileCommentAnchors2,
226
248
  removeComment: () => removeComment2,
227
249
  resolveAnchor: () => resolveAnchor2,
228
250
  resolveComment: () => resolveComment2,
251
+ resolveCommit: () => resolveCommit2,
229
252
  resolveSidecarPaths: () => resolveSidecarPaths2,
230
253
  sidecarToDocument: () => sidecarToDocument2,
231
254
  summarize: () => summarize2,
@@ -358,7 +381,9 @@ var import_promises3 = require("node:fs/promises");
358
381
  var import_node_fs2 = require("node:fs");
359
382
  var import_node_crypto = require("node:crypto");
360
383
  var import_node_path4 = __toESM(require("node:path"), 1);
361
- var import_yaml = require("yaml");
384
+ var import_yaml2 = require("yaml");
385
+ init_serialize();
386
+ init_serialize();
362
387
  var writeQueue = /* @__PURE__ */ new Map();
363
388
  function enqueueWrite(abs, fn) {
364
389
  const prev = writeQueue.get(abs) ?? Promise.resolve();
@@ -558,13 +583,6 @@ function deepEqual(a, b) {
558
583
  if (typeof a === "number" && typeof b === "number") return a === b;
559
584
  return false;
560
585
  }
561
- function toYaml(doc) {
562
- const yamlDoc = new import_yaml.Document(doc);
563
- return yamlDoc.toString({ lineWidth: 0 });
564
- }
565
- function toJson(doc) {
566
- return JSON.stringify(doc, null, 2) + "\n";
567
- }
568
586
  async function writeSidecar(filePath, doc) {
569
587
  const abs = import_node_path4.default.resolve(filePath);
570
588
  return enqueueWrite(abs, () => writeSidecarInternal(abs, doc));
@@ -593,14 +611,14 @@ async function writeSidecarInternal(abs, doc) {
593
611
  }
594
612
  let tokens;
595
613
  try {
596
- tokens = [...new import_yaml.Parser().parse(raw)];
614
+ tokens = [...new import_yaml2.Parser().parse(raw)];
597
615
  } catch {
598
616
  await atomicWriteFile(abs, toYaml(doc));
599
617
  return;
600
618
  }
601
619
  let currentDoc = null;
602
620
  try {
603
- currentDoc = (0, import_yaml.parse)(raw);
621
+ currentDoc = (0, import_yaml2.parse)(raw);
604
622
  } catch {
605
623
  }
606
624
  if (currentDoc && !Array.isArray(currentDoc.comments)) {
@@ -687,7 +705,7 @@ async function writeSidecarInternal(abs, doc) {
687
705
  }
688
706
  }
689
707
  }
690
- const result = tokens.map((t) => import_yaml.CST.stringify(t)).join("");
708
+ const result = tokens.map((t) => import_yaml2.CST.stringify(t)).join("");
691
709
  await atomicWriteFile(abs, result);
692
710
  }
693
711
 
@@ -995,6 +1013,16 @@ async function validateFile(filePath, options = {}) {
995
1013
 
996
1014
  // src/lib/fuzzy.ts
997
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
+ }
998
1026
  function exactMatch(lines, needle) {
999
1027
  if (!needle) return [];
1000
1028
  const results = [];
@@ -1117,22 +1145,48 @@ function combinedScore(needle, candidate) {
1117
1145
  }
1118
1146
  return tScore * 0.6 + lScore * 0.4;
1119
1147
  }
1120
- function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
1121
- 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
+ }
1122
1165
  const needleLines = needle.split("\n");
1123
1166
  const needleLineCount = needleLines.length;
1124
1167
  const candidates = [];
1168
+ const minimumThreshold = Math.min(...uniqueThresholds);
1169
+ const candidateLines = retrieveCandidateLines(index, needle, hintLine);
1125
1170
  const minWindow = Math.max(1, Math.floor(needleLineCount * 0.7));
1126
1171
  const maxWindow = Math.min(
1127
1172
  lines.length - 1,
1128
1173
  Math.ceil(needleLineCount * 1.3) + 1
1129
1174
  );
1130
1175
  for (let winSize = minWindow; winSize <= maxWindow; winSize++) {
1131
- 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) {
1132
1186
  const windowLines = lines.slice(startLine, startLine + winSize);
1133
1187
  const windowText = windowLines.join("\n");
1134
1188
  const score = combinedScore(needle, windowText);
1135
- if (score >= threshold) {
1189
+ if (score >= minimumThreshold) {
1136
1190
  candidates.push({
1137
1191
  text: windowText,
1138
1192
  line: startLine,
@@ -1145,17 +1199,18 @@ function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
1145
1199
  }
1146
1200
  }
1147
1201
  if (needleLineCount === 1 && needle.length < 200) {
1148
- for (let lineNum = 1; lineNum < lines.length; lineNum++) {
1202
+ for (const lineNum of candidateLines) {
1149
1203
  const line = lines[lineNum];
1150
1204
  if (!line) continue;
1151
1205
  const winLen = needle.length;
1152
1206
  const minWinLen = Math.max(3, Math.floor(winLen * 0.7));
1153
1207
  const maxWinLen = Math.min(line.length, Math.ceil(winLen * 1.3));
1154
- for (let len = minWinLen; len <= maxWinLen; len++) {
1208
+ const lengths = evenlySpacedIntegers(minWinLen, maxWinLen, 7);
1209
+ for (const len of lengths) {
1155
1210
  for (let col = 0; col + len <= line.length; col++) {
1156
1211
  const sub = line.substring(col, col + len);
1157
1212
  const score = combinedScore(needle, sub);
1158
- if (score >= threshold) {
1213
+ if (score >= minimumThreshold) {
1159
1214
  candidates.push({
1160
1215
  text: sub,
1161
1216
  line: lineNum,
@@ -1169,15 +1224,83 @@ function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
1169
1224
  }
1170
1225
  }
1171
1226
  }
1172
- 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
+ }
1173
1253
  if (hintLine != null) {
1174
- deduped = deduped.map((c) => {
1175
- const dist = Math.abs(c.line - hintLine);
1176
- const proximityBonus = 0.1 * Math.max(0, 1 - dist / 50);
1177
- return { ...c, score: Math.min(1, c.score + proximityBonus) };
1178
- });
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);
1263
+ }
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(""));
1179
1291
  }
1180
- return deduped.sort((a, b) => b.score - a.score);
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);
1181
1304
  }
1182
1305
  function deduplicateCandidates(candidates) {
1183
1306
  const seen = /* @__PURE__ */ new Map();
@@ -1190,6 +1313,15 @@ function deduplicateCandidates(candidates) {
1190
1313
  }
1191
1314
  return Array.from(seen.values());
1192
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
+ }
1193
1325
 
1194
1326
  // src/lib/git.ts
1195
1327
  var import_node_child_process = require("node:child_process");
@@ -1233,6 +1365,19 @@ async function getCurrentCommit(repoRoot) {
1233
1365
  return null;
1234
1366
  }
1235
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
+ }
1236
1381
  async function isStale(commentCommit, repoRoot) {
1237
1382
  const head = await getCurrentCommit(repoRoot);
1238
1383
  if (!head) return false;
@@ -1346,16 +1491,801 @@ async function detectRenames(fromCommit, toCommit, repoRoot) {
1346
1491
  }
1347
1492
  }
1348
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
+
1349
2271
  // src/lib/reanchor-core.ts
1350
2272
  var HIGH_THRESHOLD = 0.8;
1351
2273
  var DEFAULT_THRESHOLD = 0.6;
2274
+ var DEFAULT_PROXIMITY_WINDOW = 5;
1352
2275
  function toReanchorLines(documentText) {
1353
2276
  return ["", ...documentText.replace(/\r\n/g, "\n").split("\n")];
1354
2277
  }
1355
2278
  function reanchorComment(comment, documentLines, opts = {}) {
1356
2279
  const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
2280
+ const proximityWindow = opts.proximityWindow ?? DEFAULT_PROXIMITY_WINDOW;
1357
2281
  const commentId = comment.id;
1358
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
+ };
1359
2289
  if (!selectedText && comment.line == null) {
1360
2290
  return {
1361
2291
  commentId,
@@ -1383,7 +2313,7 @@ function reanchorComment(comment, documentLines, opts = {}) {
1383
2313
  const shiftedLine = comment.line + shift;
1384
2314
  const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
1385
2315
  const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : void 0;
1386
- const textAtShifted = extractText(
2316
+ const textAtShifted = extractText3(
1387
2317
  documentLines,
1388
2318
  shiftedLine,
1389
2319
  shiftedEndLine,
@@ -1402,32 +2332,79 @@ function reanchorComment(comment, documentLines, opts = {}) {
1402
2332
  }
1403
2333
  }
1404
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
+ }
1405
2358
  if (selectedText) {
1406
2359
  const exactCandidates = exactMatch(documentLines, selectedText);
1407
- if (exactCandidates.length === 1) {
1408
- const candidate = exactCandidates[0];
2360
+ if (exactCandidates.length > 1 && comment.line == null) {
1409
2361
  return {
1410
2362
  commentId,
1411
- status: "anchored",
2363
+ status: "ambiguous",
1412
2364
  score: 1,
1413
- newLine: candidate.line,
1414
- newEndLine: candidate.endLine,
1415
- newStartColumn: candidate.startColumn,
1416
- newEndColumn: candidate.endColumn,
1417
- reason: "Exact text match (unique)."
2365
+ reason: `Ambiguous: ${exactCandidates.length} exact matches and no position or source context to disambiguate them.`
1418
2366
  };
1419
2367
  }
1420
- if (exactCandidates.length > 1 && comment.line != null) {
1421
- const best = closestToLine(exactCandidates, comment.line);
2368
+ let chosen;
2369
+ let chosenReason = "";
2370
+ if (exactCandidates.length === 1) {
2371
+ chosen = exactCandidates[0];
2372
+ chosenReason = "Exact text match (unique).";
2373
+ } else if (exactCandidates.length > 1 && comment.line != null) {
2374
+ chosen = closestToLine(exactCandidates, comment.line);
2375
+ chosenReason = `Exact text match (${exactCandidates.length} occurrences; chose nearest to original line ${comment.line}).`;
2376
+ }
2377
+ if (chosen) {
2378
+ if (isImplausibleExactRelocation(comment, chosen, documentLines, proximityWindow)) {
2379
+ const textAtOrigin = extractText3(
2380
+ documentLines,
2381
+ comment.line,
2382
+ comment.end_line,
2383
+ comment.start_column,
2384
+ comment.end_column
2385
+ );
2386
+ return {
2387
+ commentId,
2388
+ status: "fuzzy",
2389
+ score: 0.5,
2390
+ newLine: comment.line,
2391
+ newEndLine: comment.end_line,
2392
+ newStartColumn: comment.start_column,
2393
+ newEndColumn: comment.end_column,
2394
+ anchoredText: textAtOrigin ?? void 0,
2395
+ previousSelectedText: selectedText,
2396
+ reason: `Lone exact match at line ${chosen.line} is beyond the proximity window (\xB1${proximityWindow}) of original line ${comment.line} and the text at the original position changed; kept at original position, needs re-anchoring.`
2397
+ };
2398
+ }
1422
2399
  return {
1423
2400
  commentId,
1424
2401
  status: "anchored",
1425
2402
  score: 1,
1426
- newLine: best.line,
1427
- newEndLine: best.endLine,
1428
- newStartColumn: best.startColumn,
1429
- newEndColumn: best.endColumn,
1430
- reason: `Exact text match (${exactCandidates.length} occurrences; chose nearest to original line ${comment.line}).`
2403
+ newLine: chosen.line,
2404
+ newEndLine: chosen.endLine,
2405
+ newStartColumn: chosen.startColumn,
2406
+ newEndColumn: chosen.endColumn,
2407
+ reason: chosenReason
1431
2408
  };
1432
2409
  }
1433
2410
  const normCandidates = normalizedMatch(documentLines, selectedText);
@@ -1446,12 +2423,14 @@ function reanchorComment(comment, documentLines, opts = {}) {
1446
2423
  reason: "Normalized whitespace match."
1447
2424
  };
1448
2425
  }
1449
- const fuzzyCandidates = fuzzySearch(
2426
+ fuzzyCandidateSets = fuzzySearchThresholds(
1450
2427
  documentLines,
1451
2428
  selectedText,
1452
- HIGH_THRESHOLD,
1453
- comment.line
2429
+ [HIGH_THRESHOLD, threshold],
2430
+ comment.line,
2431
+ getFuzzySearchIndex()
1454
2432
  );
2433
+ const fuzzyCandidates = fuzzyCandidateSets.get(HIGH_THRESHOLD) ?? [];
1455
2434
  if (fuzzyCandidates.length === 1 || fuzzyCandidates.length > 0 && fuzzyCandidates[0].score >= HIGH_THRESHOLD) {
1456
2435
  const best = fuzzyCandidates.length === 1 ? fuzzyCandidates[0] : closestToLine(fuzzyCandidates, comment.line ?? 1);
1457
2436
  return {
@@ -1500,11 +2479,12 @@ function reanchorComment(comment, documentLines, opts = {}) {
1500
2479
  }
1501
2480
  }
1502
2481
  if (selectedText) {
1503
- const lowCandidates = fuzzySearch(
2482
+ const lowCandidates = fuzzyCandidateSets?.get(threshold) ?? fuzzySearch(
1504
2483
  documentLines,
1505
2484
  selectedText,
1506
2485
  threshold,
1507
- comment.line
2486
+ comment.line,
2487
+ getFuzzySearchIndex()
1508
2488
  );
1509
2489
  if (lowCandidates.length === 1) {
1510
2490
  const candidate = lowCandidates[0];
@@ -1541,7 +2521,14 @@ function reanchorComment(comment, documentLines, opts = {}) {
1541
2521
  };
1542
2522
  }
1543
2523
  function reanchorDocumentLines(doc, documentLines, opts = {}) {
1544
- 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
+ );
1545
2532
  }
1546
2533
  function reanchorDocumentText(doc, documentText, opts = {}) {
1547
2534
  return reanchorDocumentLines(doc, toReanchorLines(documentText), opts);
@@ -1660,7 +2647,7 @@ function applyReanchorResults(doc, results, opts = {}) {
1660
2647
  }
1661
2648
  return changed;
1662
2649
  }
1663
- function extractText(lines, line, endLine, startColumn, endColumn) {
2650
+ function extractText3(lines, line, endLine, startColumn, endColumn) {
1664
2651
  const startIdx = line;
1665
2652
  const endIdx = endLine ?? line;
1666
2653
  if (startIdx < 1 || endIdx >= lines.length) return null;
@@ -1685,6 +2672,20 @@ function closestToLine(candidates, targetLine) {
1685
2672
  (best, candidate) => Math.abs(candidate.line - targetLine) < Math.abs(best.line - targetLine) ? candidate : best
1686
2673
  );
1687
2674
  }
2675
+ function isImplausibleExactRelocation(comment, candidate, lines, proximityWindow) {
2676
+ if (comment.line == null) return false;
2677
+ if (comment.line <= 0 || comment.line >= lines.length) return false;
2678
+ if (Math.abs(candidate.line - comment.line) <= proximityWindow) return false;
2679
+ const textAtOrigin = extractText3(
2680
+ lines,
2681
+ comment.line,
2682
+ comment.end_line,
2683
+ comment.start_column,
2684
+ comment.end_column
2685
+ );
2686
+ if (textAtOrigin === comment.selected_text) return false;
2687
+ return true;
2688
+ }
1688
2689
  function getLineShift2(diffHunks, line) {
1689
2690
  let shift = 0;
1690
2691
  let modified = false;
@@ -1701,6 +2702,137 @@ function getLineShift2(diffHunks, line) {
1701
2702
  return { shift, modified };
1702
2703
  }
1703
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
+
1704
2836
  // src/lib/reanchor.ts
1705
2837
  init_parser();
1706
2838
  init_parser();
@@ -1708,35 +2840,115 @@ var import_node_path6 = __toESM(require("node:path"), 1);
1708
2840
  async function reanchorDocument(doc, documentLines, opts = {}) {
1709
2841
  const results = [];
1710
2842
  const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
1711
- let diffHunks;
1712
- let commitIsStale = false;
2843
+ const proximityWindow = opts.proximityWindow;
2844
+ let fuzzySearchIndex;
2845
+ const getFuzzySearchIndex = () => {
2846
+ fuzzySearchIndex ??= createFuzzySearchIndex(documentLines);
2847
+ return fuzzySearchIndex;
2848
+ };
1713
2849
  if (!opts.noGit && await isGitAvailable()) {
1714
2850
  const repoRoot = opts.repoRoot ?? await findRepoRoot(opts.cwd);
1715
2851
  if (repoRoot && opts.documentPath) {
1716
2852
  const relPath = import_node_path6.default.relative(repoRoot, opts.documentPath);
1717
2853
  const head = await getCurrentCommit(repoRoot);
1718
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();
1719
2860
  for (const comment of doc.comments) {
1720
- const commentCommit = globalFrom ?? comment.commit;
1721
- if (commentCommit && head && commentCommit !== head) {
1722
- commitIsStale = true;
1723
- 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
+ }
1724
2903
  const result = reanchorComment(comment, documentLines, {
1725
2904
  diffHunks: hunks,
1726
2905
  threshold,
1727
- commitIsStale: true
2906
+ commitIsStale: true,
2907
+ proximityWindow,
2908
+ revisionProjection,
2909
+ anchorContext,
2910
+ getFuzzySearchIndex
1728
2911
  });
2912
+ const resultIndex = results.length;
1729
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
+ }
1730
2927
  continue;
1731
2928
  }
1732
2929
  results.push(
1733
- reanchorComment(comment, documentLines, { threshold, commitIsStale: false })
2930
+ reanchorComment(comment, documentLines, {
2931
+ threshold,
2932
+ commitIsStale: false,
2933
+ proximityWindow,
2934
+ getFuzzySearchIndex
2935
+ })
1734
2936
  );
1735
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
2943
+ );
2944
+ for (const [offset, resultIndex] of group.resultIndexes.entries()) {
2945
+ results[resultIndex] = reconciled[offset];
2946
+ }
2947
+ }
1736
2948
  return results;
1737
2949
  }
1738
2950
  }
1739
- return reanchorDocumentLines(doc, documentLines, { threshold });
2951
+ return reanchorDocumentLines(doc, documentLines, { threshold, proximityWindow });
1740
2952
  }
1741
2953
  async function reanchorFile(sidecarPath, opts = {}) {
1742
2954
  const doc = await parseSidecar(sidecarPath);
@@ -2044,10 +3256,11 @@ var combinedScore2 = combinedScore;
2044
3256
  var isGitAvailable2 = isGitAvailable;
2045
3257
  var findRepoRoot3 = findRepoRoot;
2046
3258
  var getCurrentCommit2 = getCurrentCommit;
3259
+ var resolveCommit2 = resolveCommit;
2047
3260
  var isStale2 = isStale;
2048
3261
  var getDiff2 = getDiff;
2049
3262
  var getLineShift4 = getLineShift;
2050
- var getFileAtCommit3 = getFileAtCommit;
3263
+ var getFileAtCommit2 = getFileAtCommit;
2051
3264
  var getStagedFiles2 = getStagedFiles;
2052
3265
  var detectRenames2 = detectRenames;
2053
3266
  var parseDiffHunks3 = parseDiffHunks;
@@ -2061,6 +3274,10 @@ var resolveAnchor2 = resolveAnchor;
2061
3274
  var reanchorDocument2 = reanchorDocument;
2062
3275
  var applyReanchorResults2 = applyReanchorResults;
2063
3276
  var reanchorFile2 = reanchorFile;
3277
+ var createAnchorContextIndex2 = createAnchorContextIndex;
3278
+ var reconcileCommentAnchors2 = reconcileCommentAnchors;
3279
+ var createRevisionProjection2 = createRevisionProjection;
3280
+ var calibrateAnchorEvidence2 = calibrateAnchorEvidence;
2064
3281
  var addComment2 = addComment;
2065
3282
  var editComment2 = editComment;
2066
3283
  var normalizeCommentExtensions2 = normalizeCommentExtensions;
@@ -2080,8 +3297,11 @@ var newCommentId2 = newCommentId;
2080
3297
  HIGH_THRESHOLD,
2081
3298
  addComment,
2082
3299
  applyReanchorResults,
3300
+ calibrateAnchorEvidence,
2083
3301
  combinedScore,
2084
3302
  computeHash,
3303
+ createAnchorContextIndex,
3304
+ createRevisionProjection,
2085
3305
  detectRenames,
2086
3306
  discoverAllSidecars,
2087
3307
  discoverSidecar,
@@ -2118,9 +3338,11 @@ var newCommentId2 = newCommentId;
2118
3338
  reanchorDocumentLines,
2119
3339
  reanchorDocumentText,
2120
3340
  reanchorFile,
3341
+ reconcileCommentAnchors,
2121
3342
  removeComment,
2122
3343
  resolveAnchor,
2123
3344
  resolveComment,
3345
+ resolveCommit,
2124
3346
  resolveSidecarPaths,
2125
3347
  sidecarToDocument,
2126
3348
  summarize,