@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/browser.cjs CHANGED
@@ -33,7 +33,10 @@ __export(browser_exports, {
33
33
  DEFAULT_THRESHOLD: () => DEFAULT_THRESHOLD2,
34
34
  HIGH_THRESHOLD: () => HIGH_THRESHOLD2,
35
35
  applyReanchorResults: () => applyReanchorResults2,
36
+ calibrateAnchorEvidence: () => calibrateAnchorEvidence2,
36
37
  combinedScore: () => combinedScore2,
38
+ createAnchorContextIndex: () => createAnchorContextIndex2,
39
+ createRevisionProjection: () => createRevisionProjection2,
37
40
  exactMatch: () => exactMatch2,
38
41
  formatAuthor: () => formatAuthor2,
39
42
  fuzzySearch: () => fuzzySearch2,
@@ -47,6 +50,7 @@ __export(browser_exports, {
47
50
  reanchorComment: () => reanchorComment2,
48
51
  reanchorDocumentLines: () => reanchorDocumentLines2,
49
52
  reanchorDocumentText: () => reanchorDocumentText2,
53
+ reconcileCommentAnchors: () => reconcileCommentAnchors2,
50
54
  resolveAnchor: () => resolveAnchor2,
51
55
  toJson: () => toJson2,
52
56
  toReanchorLines: () => toReanchorLines2,
@@ -58,6 +62,16 @@ module.exports = __toCommonJS(browser_exports);
58
62
 
59
63
  // src/lib/fuzzy.ts
60
64
  var import_fastest_levenshtein = require("fastest-levenshtein");
65
+ var MAX_FUZZY_CANDIDATE_LINES = 64;
66
+ function createFuzzySearchIndex(lines) {
67
+ const tokenPostings = /* @__PURE__ */ new Map();
68
+ const trigramPostings = /* @__PURE__ */ new Map();
69
+ for (let line = 1; line < lines.length; line += 1) {
70
+ addPostingSignals(tokenPostings, lexicalTokens(lines[line]), line);
71
+ addPostingSignals(trigramPostings, characterTrigrams(lines[line]), line);
72
+ }
73
+ return { lines, tokenPostings, trigramPostings };
74
+ }
61
75
  function exactMatch(lines, needle) {
62
76
  if (!needle) return [];
63
77
  const results = [];
@@ -180,22 +194,48 @@ function combinedScore(needle, candidate) {
180
194
  }
181
195
  return tScore * 0.6 + lScore * 0.4;
182
196
  }
183
- function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
184
- if (!needle) return [];
197
+ function fuzzySearch(lines, needle, threshold = 0.6, hintLine, index) {
198
+ return fuzzySearchThresholds(
199
+ lines,
200
+ needle,
201
+ [threshold],
202
+ hintLine,
203
+ index
204
+ ).get(threshold) ?? [];
205
+ }
206
+ function fuzzySearchThresholds(lines, needle, thresholds, hintLine, index = createFuzzySearchIndex(lines)) {
207
+ const uniqueThresholds = [...new Set(thresholds)];
208
+ const results = /* @__PURE__ */ new Map();
209
+ if (uniqueThresholds.length === 0) return results;
210
+ if (!needle) {
211
+ for (const threshold of uniqueThresholds) results.set(threshold, []);
212
+ return results;
213
+ }
185
214
  const needleLines = needle.split("\n");
186
215
  const needleLineCount = needleLines.length;
187
216
  const candidates = [];
217
+ const minimumThreshold = Math.min(...uniqueThresholds);
218
+ const candidateLines = retrieveCandidateLines(index, needle, hintLine);
188
219
  const minWindow = Math.max(1, Math.floor(needleLineCount * 0.7));
189
220
  const maxWindow = Math.min(
190
221
  lines.length - 1,
191
222
  Math.ceil(needleLineCount * 1.3) + 1
192
223
  );
193
224
  for (let winSize = minWindow; winSize <= maxWindow; winSize++) {
194
- for (let startLine = 1; startLine + winSize - 1 < lines.length; startLine++) {
225
+ const startLines = /* @__PURE__ */ new Set();
226
+ for (const candidateLine of candidateLines) {
227
+ for (let offset = 0; offset < winSize; offset += 1) {
228
+ const startLine = candidateLine - offset;
229
+ if (startLine >= 1 && startLine + winSize - 1 < lines.length) {
230
+ startLines.add(startLine);
231
+ }
232
+ }
233
+ }
234
+ for (const startLine of startLines) {
195
235
  const windowLines = lines.slice(startLine, startLine + winSize);
196
236
  const windowText = windowLines.join("\n");
197
237
  const score = combinedScore(needle, windowText);
198
- if (score >= threshold) {
238
+ if (score >= minimumThreshold) {
199
239
  candidates.push({
200
240
  text: windowText,
201
241
  line: startLine,
@@ -208,17 +248,18 @@ function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
208
248
  }
209
249
  }
210
250
  if (needleLineCount === 1 && needle.length < 200) {
211
- for (let lineNum = 1; lineNum < lines.length; lineNum++) {
251
+ for (const lineNum of candidateLines) {
212
252
  const line = lines[lineNum];
213
253
  if (!line) continue;
214
254
  const winLen = needle.length;
215
255
  const minWinLen = Math.max(3, Math.floor(winLen * 0.7));
216
256
  const maxWinLen = Math.min(line.length, Math.ceil(winLen * 1.3));
217
- for (let len = minWinLen; len <= maxWinLen; len++) {
257
+ const lengths = evenlySpacedIntegers(minWinLen, maxWinLen, 7);
258
+ for (const len of lengths) {
218
259
  for (let col = 0; col + len <= line.length; col++) {
219
260
  const sub = line.substring(col, col + len);
220
261
  const score = combinedScore(needle, sub);
221
- if (score >= threshold) {
262
+ if (score >= minimumThreshold) {
222
263
  candidates.push({
223
264
  text: sub,
224
265
  line: lineNum,
@@ -232,15 +273,83 @@ function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
232
273
  }
233
274
  }
234
275
  }
235
- let deduped = deduplicateCandidates(candidates);
276
+ const deduped = deduplicateCandidates(candidates);
277
+ const scored = deduped.map((candidate) => ({
278
+ baseScore: candidate.score,
279
+ candidate: applyProximityBonus(candidate, hintLine)
280
+ }));
281
+ for (const threshold of uniqueThresholds) {
282
+ results.set(
283
+ threshold,
284
+ scored.filter((item) => item.baseScore >= threshold).map((item) => item.candidate).sort((left, right) => right.score - left.score)
285
+ );
286
+ }
287
+ return results;
288
+ }
289
+ function retrieveCandidateLines(index, needle, hintLine) {
290
+ const votes = /* @__PURE__ */ new Map();
291
+ const lineCount = Math.max(0, index.lines.length - 1);
292
+ const signals = [
293
+ ...postingSignals(index.tokenPostings, lexicalTokens(needle), 2),
294
+ ...postingSignals(index.trigramPostings, characterTrigrams(needle), 1)
295
+ ].sort((left, right) => left.postings.length - right.postings.length).slice(0, 16);
296
+ for (const signal of signals) {
297
+ const rarity = Math.log1p(lineCount / signal.postings.length);
298
+ for (const line of signal.postings) {
299
+ votes.set(line, (votes.get(line) ?? 0) + signal.weight * rarity);
300
+ }
301
+ }
236
302
  if (hintLine != null) {
237
- deduped = deduped.map((c) => {
238
- const dist = Math.abs(c.line - hintLine);
239
- const proximityBonus = 0.1 * Math.max(0, 1 - dist / 50);
240
- return { ...c, score: Math.min(1, c.score + proximityBonus) };
241
- });
303
+ for (let offset = -4; offset <= 4; offset += 1) {
304
+ const line = hintLine + offset;
305
+ if (line >= 1 && line <= lineCount) {
306
+ votes.set(line, (votes.get(line) ?? 0) + 0.25);
307
+ }
308
+ }
309
+ }
310
+ if (votes.size === 0) {
311
+ return Array.from({ length: lineCount }, (_, index2) => index2 + 1);
242
312
  }
243
- return deduped.sort((a, b) => b.score - a.score);
313
+ return [...votes.entries()].sort(
314
+ (left, right) => right[1] - left[1] || distanceFromHint(left[0], hintLine) - distanceFromHint(right[0], hintLine) || left[0] - right[0]
315
+ ).slice(0, MAX_FUZZY_CANDIDATE_LINES).map(([line]) => line);
316
+ }
317
+ function postingSignals(postings, values, weight) {
318
+ return [...new Set(values)].map((value) => ({ postings: postings.get(value) ?? [], weight })).filter((signal) => signal.postings.length > 0);
319
+ }
320
+ function addPostingSignals(postings, values, line) {
321
+ for (const value of new Set(values)) {
322
+ const lines = postings.get(value);
323
+ if (lines) {
324
+ lines.push(line);
325
+ } else {
326
+ postings.set(value, [line]);
327
+ }
328
+ }
329
+ }
330
+ function lexicalTokens(text) {
331
+ return text.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? [];
332
+ }
333
+ function characterTrigrams(text) {
334
+ const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
335
+ const characters = [...normalized];
336
+ if (characters.length < 3) return normalized ? [normalized] : [];
337
+ const trigrams = [];
338
+ for (let index = 0; index <= characters.length - 3; index += 1) {
339
+ trigrams.push(characters.slice(index, index + 3).join(""));
340
+ }
341
+ return trigrams;
342
+ }
343
+ function evenlySpacedIntegers(minimum, maximum, count) {
344
+ if (maximum <= minimum) return [minimum];
345
+ const values = /* @__PURE__ */ new Set();
346
+ for (let index = 0; index < count; index += 1) {
347
+ values.add(Math.round(minimum + (maximum - minimum) * index / (count - 1)));
348
+ }
349
+ return [...values];
350
+ }
351
+ function distanceFromHint(line, hintLine) {
352
+ return hintLine == null ? 0 : Math.abs(line - hintLine);
244
353
  }
245
354
  function deduplicateCandidates(candidates) {
246
355
  const seen = /* @__PURE__ */ new Map();
@@ -253,6 +362,15 @@ function deduplicateCandidates(candidates) {
253
362
  }
254
363
  return Array.from(seen.values());
255
364
  }
365
+ function applyProximityBonus(candidate, hintLine) {
366
+ if (hintLine == null) return candidate;
367
+ const distance = Math.abs(candidate.line - hintLine);
368
+ const proximityBonus = 0.1 * Math.max(0, 1 - distance / 50);
369
+ return {
370
+ ...candidate,
371
+ score: Math.min(1, candidate.score + proximityBonus)
372
+ };
373
+ }
256
374
 
257
375
  // src/lib/identity.ts
258
376
  var import_uuid = require("uuid");
@@ -273,6 +391,783 @@ function newCommentId() {
273
391
  return (0, import_uuid.v4)();
274
392
  }
275
393
 
394
+ // src/lib/revision-projection.ts
395
+ var CONTEXT_RADIUS = 8;
396
+ function createRevisionProjection(sourceLines, targetLines) {
397
+ const sourceOccurrences = collectLineOccurrences(sourceLines);
398
+ const targetOccurrences = collectLineOccurrences(targetLines);
399
+ const lineMap = /* @__PURE__ */ new Map();
400
+ for (const [text, sourceLineNumbers] of sourceOccurrences) {
401
+ const targetLineNumbers = targetOccurrences.get(text);
402
+ if (sourceLineNumbers.length === 1 && targetLineNumbers?.length === 1) {
403
+ lineMap.set(sourceLineNumbers[0], targetLineNumbers[0]);
404
+ }
405
+ }
406
+ return { sourceLines, targetLines, lineMap };
407
+ }
408
+ function projectCommentAnchor(comment, projection, threshold) {
409
+ if (comment.line == null || !comment.selected_text) return void 0;
410
+ const sourceText = extractText(
411
+ projection.sourceLines,
412
+ comment.line,
413
+ comment.end_line,
414
+ comment.start_column,
415
+ comment.end_column
416
+ );
417
+ if (sourceText !== comment.selected_text) return void 0;
418
+ const exactCandidates = exactMatch(
419
+ projection.targetLines,
420
+ comment.selected_text
421
+ );
422
+ if (exactCandidates.length === 1) {
423
+ const candidate = exactCandidates[0];
424
+ const contextSupport = countIndependentExactSupport(
425
+ comment,
426
+ candidate.line,
427
+ projection
428
+ );
429
+ if (contextSupport >= 2) {
430
+ return {
431
+ line: candidate.line,
432
+ endLine: candidate.endLine,
433
+ startColumn: candidate.startColumn,
434
+ endColumn: candidate.endColumn,
435
+ text: candidate.text,
436
+ score: 1,
437
+ exact: true,
438
+ contextSupport,
439
+ contextMargin: 1,
440
+ reason: "Source revision and neighboring line evidence confirm exact relocation."
441
+ };
442
+ }
443
+ }
444
+ if (exactCandidates.length > 0) return void 0;
445
+ const projected = projectLineFromContext(comment, projection);
446
+ if (projected == null) return void 0;
447
+ const projectedLine = projected.line;
448
+ const lineSpan = (comment.end_line ?? comment.line) - comment.line;
449
+ const projectedEndLine = projectedLine + lineSpan;
450
+ if (projectedLine < 1 || projectedEndLine >= projection.targetLines.length) {
451
+ return void 0;
452
+ }
453
+ const columns = projectColumns(comment, projection, projectedLine);
454
+ const targetText = extractText(
455
+ projection.targetLines,
456
+ projectedLine,
457
+ projectedEndLine,
458
+ columns.startColumn,
459
+ columns.endColumn
460
+ );
461
+ if (targetText == null) return void 0;
462
+ const score = Math.min(
463
+ 1,
464
+ combinedScore(comment.selected_text, targetText) + 0.1
465
+ );
466
+ if (score < threshold) return void 0;
467
+ return {
468
+ line: projectedLine,
469
+ endLine: projectedEndLine,
470
+ startColumn: columns.startColumn,
471
+ endColumn: columns.endColumn,
472
+ text: targetText,
473
+ score,
474
+ exact: targetText === comment.selected_text,
475
+ contextSupport: projected.support,
476
+ contextMargin: projected.margin,
477
+ reason: "Source revision context projects the edited anchor range."
478
+ };
479
+ }
480
+ function collectLineOccurrences(lines) {
481
+ const occurrences = /* @__PURE__ */ new Map();
482
+ for (let line = 1; line < lines.length; line += 1) {
483
+ const text = lines[line];
484
+ if (!text.trim()) continue;
485
+ const existing = occurrences.get(text);
486
+ if (existing) {
487
+ existing.push(line);
488
+ } else {
489
+ occurrences.set(text, [line]);
490
+ }
491
+ }
492
+ return occurrences;
493
+ }
494
+ function countIndependentExactSupport(comment, candidateLine, projection) {
495
+ const sourceEndLine = comment.end_line ?? comment.line;
496
+ const expectedShift = candidateLine - comment.line;
497
+ if (comment.start_column != null || comment.end_column != null) {
498
+ const mappedContainerLine = projection.lineMap.get(comment.line);
499
+ if (mappedContainerLine === candidateLine) return 2;
500
+ }
501
+ let support = 0;
502
+ for (let distance = 1; distance <= CONTEXT_RADIUS; distance += 1) {
503
+ for (const sourceLine of [
504
+ comment.line - distance,
505
+ sourceEndLine + distance
506
+ ]) {
507
+ if (sourceLine < 1 || sourceLine >= projection.sourceLines.length) {
508
+ continue;
509
+ }
510
+ const targetLine = projection.lineMap.get(sourceLine);
511
+ if (targetLine != null && targetLine - sourceLine === expectedShift) {
512
+ support += 1;
513
+ }
514
+ }
515
+ }
516
+ return support;
517
+ }
518
+ function projectLineFromContext(comment, projection) {
519
+ const sourceLine = comment.line;
520
+ const sourceEndLine = comment.end_line ?? sourceLine;
521
+ const votes = /* @__PURE__ */ new Map();
522
+ for (let distance = 1; distance <= CONTEXT_RADIUS; distance += 1) {
523
+ for (const contextLine of [
524
+ sourceLine - distance,
525
+ sourceEndLine + distance
526
+ ]) {
527
+ if (contextLine < 1 || contextLine >= projection.sourceLines.length) {
528
+ continue;
529
+ }
530
+ const targetLine = projection.lineMap.get(contextLine);
531
+ if (targetLine == null) continue;
532
+ const shift = targetLine - contextLine;
533
+ const vote = votes.get(shift);
534
+ if (vote) {
535
+ vote.count += 1;
536
+ vote.nearest = Math.min(vote.nearest, distance);
537
+ } else {
538
+ votes.set(shift, { count: 1, nearest: distance });
539
+ }
540
+ }
541
+ }
542
+ const ranked = [...votes.entries()].sort(
543
+ (left, right) => right[1].count - left[1].count || left[1].nearest - right[1].nearest || Math.abs(left[0]) - Math.abs(right[0])
544
+ );
545
+ const best = ranked[0];
546
+ if (!best) return void 0;
547
+ if (best[1].count < 2 && best[1].nearest > 3) return void 0;
548
+ if (ranked[1] && ranked[1][1].count === best[1].count && ranked[1][1].nearest === best[1].nearest) {
549
+ return void 0;
550
+ }
551
+ const runnerUpCount = ranked[1]?.[1].count ?? 0;
552
+ return {
553
+ line: sourceLine + best[0],
554
+ support: best[1].count,
555
+ margin: (best[1].count - runnerUpCount) / best[1].count
556
+ };
557
+ }
558
+ function projectColumns(comment, projection, targetLine) {
559
+ if (comment.line == null || comment.end_line != null && comment.end_line !== comment.line || comment.start_column == null || comment.end_column == null) {
560
+ return {
561
+ startColumn: comment.start_column,
562
+ endColumn: comment.end_column
563
+ };
564
+ }
565
+ const sourceLine = projection.sourceLines[comment.line];
566
+ const targetLineText = projection.targetLines[targetLine];
567
+ const prefix = sourceLine.slice(0, comment.start_column);
568
+ const suffix = sourceLine.slice(comment.end_column);
569
+ const startColumn = targetLineText.startsWith(prefix) ? prefix.length : Math.min(comment.start_column, targetLineText.length);
570
+ const endColumn = targetLineText.endsWith(suffix) ? targetLineText.length - suffix.length : Math.min(
571
+ targetLineText.length,
572
+ startColumn + (comment.end_column - comment.start_column)
573
+ );
574
+ return { startColumn, endColumn };
575
+ }
576
+ function extractText(lines, line, endLine, startColumn, endColumn) {
577
+ const finalLine = endLine ?? line;
578
+ if (line < 1 || finalLine >= lines.length) return null;
579
+ if (line === finalLine) {
580
+ const text = lines[line];
581
+ return startColumn != null && endColumn != null ? text.slice(startColumn, endColumn) : text;
582
+ }
583
+ const result = [];
584
+ for (let current = line; current <= finalLine; current += 1) {
585
+ let text = lines[current];
586
+ if (current === line && startColumn != null) text = text.slice(startColumn);
587
+ if (current === finalLine && endColumn != null) text = text.slice(0, endColumn);
588
+ result.push(text);
589
+ }
590
+ return result.join("\n");
591
+ }
592
+
593
+ // src/lib/anchor-context.ts
594
+ var MATCH_THRESHOLD = 0.35;
595
+ var AMBIGUITY_MARGIN = 0.03;
596
+ var MAX_CONTEXT_CANDIDATE_BLOCKS = 64;
597
+ function createAnchorContextIndex(sourceLines, targetLines) {
598
+ return {
599
+ source: createDocumentBlockIndex(sourceLines),
600
+ target: createDocumentBlockIndex(targetLines)
601
+ };
602
+ }
603
+ function resolveContextAnchor(comment, index) {
604
+ if (comment.line == null || !comment.selected_text) return void 0;
605
+ const sourceBlockIndex = index.source.lineToBlock.get(comment.line);
606
+ if (sourceBlockIndex == null) return void 0;
607
+ const sourceBlock = index.source.blocks[sourceBlockIndex];
608
+ const sourceText = extractText2(
609
+ index.source.lines,
610
+ comment.line,
611
+ comment.end_line,
612
+ comment.start_column,
613
+ comment.end_column
614
+ );
615
+ if (sourceText !== comment.selected_text) return void 0;
616
+ const textAtCurrentPosition = extractText2(
617
+ index.target.lines,
618
+ comment.line,
619
+ comment.end_line,
620
+ comment.start_column,
621
+ comment.end_column
622
+ );
623
+ if (textAtCurrentPosition === comment.selected_text) {
624
+ return {
625
+ status: "anchored",
626
+ score: 1,
627
+ line: comment.line,
628
+ endLine: comment.end_line ?? comment.line,
629
+ startColumn: comment.start_column,
630
+ endColumn: comment.end_column,
631
+ text: comment.selected_text,
632
+ candidateMargin: 1,
633
+ reason: "Source-verified anchor remains exact at its stored position."
634
+ };
635
+ }
636
+ const candidates = findContextAnchorCandidates(comment, index);
637
+ const best = candidates[0];
638
+ if (!best) {
639
+ if (index.target.lines.slice(1).join("\n").includes(comment.selected_text)) {
640
+ return void 0;
641
+ }
642
+ return {
643
+ status: "orphaned",
644
+ score: 0,
645
+ candidateMargin: 1,
646
+ reason: "Source block has no plausible structural or contextual match."
647
+ };
648
+ }
649
+ if (candidates[1] && best.score - candidates[1].score < AMBIGUITY_MARGIN) {
650
+ return {
651
+ status: "ambiguous",
652
+ score: best.score,
653
+ line: best.line,
654
+ endLine: best.endLine,
655
+ candidateMargin: best.score - candidates[1].score,
656
+ reason: `Structural candidates are too close (${best.score.toFixed(3)} vs ${candidates[1].score.toFixed(3)}).`
657
+ };
658
+ }
659
+ const repeatedExactText = best.exact && countOccurrences(
660
+ index.target.lines.slice(1).join("\n"),
661
+ comment.selected_text
662
+ ) > 1;
663
+ return {
664
+ status: best.exact && !repeatedExactText ? "anchored" : "fuzzy",
665
+ score: best.exact && !repeatedExactText ? 1 : best.score,
666
+ line: best.line,
667
+ endLine: best.endLine,
668
+ startColumn: best.startColumn,
669
+ endColumn: best.endColumn,
670
+ text: best.text,
671
+ candidateMargin: candidates[1] ? best.score - candidates[1].score : 1,
672
+ 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."
673
+ };
674
+ }
675
+ function findContextAnchorCandidates(comment, index) {
676
+ if (comment.line == null || !comment.selected_text) return [];
677
+ const sourceBlockIndex = index.source.lineToBlock.get(comment.line);
678
+ if (sourceBlockIndex == null) return [];
679
+ const sourceBlock = index.source.blocks[sourceBlockIndex];
680
+ const sourceText = extractText2(
681
+ index.source.lines,
682
+ comment.line,
683
+ comment.end_line,
684
+ comment.start_column,
685
+ comment.end_column
686
+ );
687
+ if (sourceText !== comment.selected_text) return [];
688
+ return createCandidateWindows(
689
+ sourceBlock,
690
+ sourceBlockIndex,
691
+ comment.line,
692
+ index
693
+ ).map((candidate) => ({
694
+ ...candidate,
695
+ score: scoreCandidate(
696
+ sourceBlock,
697
+ sourceBlockIndex,
698
+ candidate,
699
+ index,
700
+ comment.line
701
+ )
702
+ })).filter((candidate) => candidate.score >= MATCH_THRESHOLD).sort(
703
+ (left, right) => right.score - left.score || Math.abs(left.startLine - comment.line) - Math.abs(right.startLine - comment.line)
704
+ ).map((candidate) => {
705
+ const range = resolveCandidateRange(
706
+ comment,
707
+ sourceBlock,
708
+ candidate,
709
+ index.target
710
+ );
711
+ return {
712
+ score: candidate.score,
713
+ line: range.line,
714
+ endLine: range.endLine,
715
+ startColumn: range.startColumn,
716
+ endColumn: range.endColumn,
717
+ text: range.text,
718
+ exact: range.text === comment.selected_text
719
+ };
720
+ });
721
+ }
722
+ function getAnchorContextScope(comment, index) {
723
+ if (comment.line == null) return void 0;
724
+ const blockIndex = index.source.lineToBlock.get(comment.line);
725
+ if (blockIndex == null) return void 0;
726
+ return index.source.blocks[blockIndex].headingPath.join("");
727
+ }
728
+ function createDocumentBlockIndex(lines) {
729
+ const blocks = [];
730
+ const lineToBlock = /* @__PURE__ */ new Map();
731
+ const headings = [];
732
+ let line = 1;
733
+ while (line < lines.length) {
734
+ if (!lines[line].trim()) {
735
+ line += 1;
736
+ continue;
737
+ }
738
+ const startLine = line;
739
+ const marker = classifyLine(lines[line]);
740
+ if (marker.type === "heading") {
741
+ while (headings.length > 0 && headings[headings.length - 1].level >= marker.headingLevel) {
742
+ headings.pop();
743
+ }
744
+ const block = makeBlock(
745
+ lines,
746
+ startLine,
747
+ startLine,
748
+ "heading",
749
+ headings.map((heading) => heading.title)
750
+ );
751
+ blocks.push(block);
752
+ headings.push({ level: marker.headingLevel, title: marker.headingTitle });
753
+ line += 1;
754
+ continue;
755
+ }
756
+ if (marker.type === "code") {
757
+ line += 1;
758
+ while (line < lines.length && !lines[line].trimStart().startsWith("```")) {
759
+ line += 1;
760
+ }
761
+ if (line < lines.length) line += 1;
762
+ } else {
763
+ line += 1;
764
+ while (line < lines.length && lines[line].trim() && continuesBlock(marker.type, lines[line])) {
765
+ line += 1;
766
+ }
767
+ }
768
+ blocks.push(makeBlock(
769
+ lines,
770
+ startLine,
771
+ line - 1,
772
+ marker.type,
773
+ headings.map((heading) => heading.title)
774
+ ));
775
+ }
776
+ for (const [blockIndex, block] of blocks.entries()) {
777
+ for (let blockLine = block.startLine; blockLine <= block.endLine; blockLine += 1) {
778
+ lineToBlock.set(blockLine, blockIndex);
779
+ }
780
+ }
781
+ return {
782
+ lines,
783
+ blocks,
784
+ lineToBlock,
785
+ tokenPostings: createTokenPostings(blocks)
786
+ };
787
+ }
788
+ function makeBlock(lines, startLine, endLine, type, headingPath) {
789
+ return {
790
+ startLine,
791
+ endLine,
792
+ type,
793
+ text: lines.slice(startLine, endLine + 1).join("\n"),
794
+ headingPath
795
+ };
796
+ }
797
+ function classifyLine(line) {
798
+ const heading = line.match(/^(#{1,6})\s+(.+)$/);
799
+ if (heading) {
800
+ return {
801
+ type: "heading",
802
+ headingLevel: heading[1].length,
803
+ headingTitle: heading[2].trim()
804
+ };
805
+ }
806
+ if (line.trimStart().startsWith("```")) {
807
+ return { type: "code", headingLevel: 0, headingTitle: "" };
808
+ }
809
+ if (/^\s*(?:[-*+]|\d+\.)\s+/.test(line)) {
810
+ return { type: "list", headingLevel: 0, headingTitle: "" };
811
+ }
812
+ if (/^\s*\|/.test(line)) {
813
+ return { type: "table", headingLevel: 0, headingTitle: "" };
814
+ }
815
+ if (/^\s*>/.test(line)) {
816
+ return { type: "blockquote", headingLevel: 0, headingTitle: "" };
817
+ }
818
+ return { type: "paragraph", headingLevel: 0, headingTitle: "" };
819
+ }
820
+ function continuesBlock(type, line) {
821
+ if (type === "list") return /^\s*(?:[-*+]|\d+\.)\s+/.test(line);
822
+ if (type === "table") return /^\s*\|/.test(line);
823
+ if (type === "blockquote") return /^\s*>/.test(line);
824
+ if (type === "paragraph") {
825
+ const next = classifyLine(line);
826
+ return next.type === "paragraph";
827
+ }
828
+ return false;
829
+ }
830
+ function createCandidateWindows(sourceBlock, sourceBlockIndex, originalLine, index) {
831
+ const target = index.target;
832
+ const candidates = [];
833
+ for (const start of retrieveCandidateBlocks(
834
+ sourceBlock,
835
+ sourceBlockIndex,
836
+ originalLine,
837
+ index
838
+ )) {
839
+ const block = target.blocks[start];
840
+ candidates.push(toCandidateWindow(target, start, start));
841
+ if (sourceBlock.type === "paragraph" && block.type === "paragraph" && target.blocks[start + 1]?.type === "paragraph" && samePath(block.headingPath, target.blocks[start + 1].headingPath)) {
842
+ candidates.push(toCandidateWindow(target, start, start + 1));
843
+ }
844
+ }
845
+ return candidates;
846
+ }
847
+ function createTokenPostings(blocks) {
848
+ const postings = /* @__PURE__ */ new Map();
849
+ for (const [blockIndex, block] of blocks.entries()) {
850
+ for (const token of new Set(tokenize2(block.text))) {
851
+ const blocksForToken = postings.get(token);
852
+ if (blocksForToken) {
853
+ blocksForToken.push(blockIndex);
854
+ } else {
855
+ postings.set(token, [blockIndex]);
856
+ }
857
+ }
858
+ }
859
+ return postings;
860
+ }
861
+ function retrieveCandidateBlocks(sourceBlock, sourceBlockIndex, originalLine, index) {
862
+ const votes = /* @__PURE__ */ new Map();
863
+ const targetCount = index.target.blocks.length;
864
+ const addEvidence = (text, targetOffset, weight) => {
865
+ if (!text) return;
866
+ const rankedTokens = [...new Set(tokenize2(text))].map((token) => ({
867
+ token,
868
+ postings: index.target.tokenPostings.get(token) ?? []
869
+ })).filter((item) => item.postings.length > 0).sort((left, right) => left.postings.length - right.postings.length).slice(0, 12);
870
+ for (const { postings } of rankedTokens) {
871
+ const rarity = Math.log1p(targetCount / postings.length);
872
+ for (const posting of postings) {
873
+ const candidate = posting + targetOffset;
874
+ if (candidate >= 0 && candidate < targetCount) {
875
+ votes.set(candidate, (votes.get(candidate) ?? 0) + weight * rarity);
876
+ }
877
+ }
878
+ }
879
+ };
880
+ addEvidence(sourceBlock.text, 0, 1);
881
+ addEvidence(index.source.blocks[sourceBlockIndex - 1]?.text, 1, 0.7);
882
+ addEvidence(index.source.blocks[sourceBlockIndex + 1]?.text, -1, 0.7);
883
+ const nearbyBlock = closestBlockToLine(index.target.blocks, originalLine);
884
+ if (nearbyBlock != null) {
885
+ for (let offset = -2; offset <= 2; offset += 1) {
886
+ const candidate = nearbyBlock + offset;
887
+ if (candidate >= 0 && candidate < targetCount) {
888
+ votes.set(candidate, (votes.get(candidate) ?? 0) + 0.25);
889
+ }
890
+ }
891
+ }
892
+ return [...votes.entries()].sort(
893
+ (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]
894
+ ).slice(0, MAX_CONTEXT_CANDIDATE_BLOCKS).map(([blockIndex]) => blockIndex);
895
+ }
896
+ function closestBlockToLine(blocks, line) {
897
+ if (blocks.length === 0) return void 0;
898
+ let low = 0;
899
+ let high = blocks.length - 1;
900
+ while (low <= high) {
901
+ const middle = Math.floor((low + high) / 2);
902
+ const block = blocks[middle];
903
+ if (line < block.startLine) {
904
+ high = middle - 1;
905
+ } else if (line > block.endLine) {
906
+ low = middle + 1;
907
+ } else {
908
+ return middle;
909
+ }
910
+ }
911
+ if (low >= blocks.length) return blocks.length - 1;
912
+ if (high < 0) return 0;
913
+ return Math.abs(blocks[low].startLine - line) < Math.abs(blocks[high].endLine - line) ? low : high;
914
+ }
915
+ function toCandidateWindow(target, startBlock, endBlock) {
916
+ const first = target.blocks[startBlock];
917
+ const last = target.blocks[endBlock];
918
+ return {
919
+ startBlock,
920
+ endBlock,
921
+ startLine: first.startLine,
922
+ endLine: last.endLine,
923
+ type: first.type,
924
+ text: target.lines.slice(first.startLine, last.endLine + 1).join("\n"),
925
+ headingPath: first.headingPath,
926
+ score: 0
927
+ };
928
+ }
929
+ function scoreCandidate(sourceBlock, sourceBlockIndex, candidate, index, originalLine) {
930
+ const content = textSimilarity(sourceBlock.text, candidate.text);
931
+ const type = sourceBlock.type === candidate.type ? 1 : 0;
932
+ const heading = pathSimilarity(sourceBlock.headingPath, candidate.headingPath);
933
+ const previous = neighborSimilarity(
934
+ index.source.blocks[sourceBlockIndex - 1],
935
+ index.target.blocks[candidate.startBlock - 1]
936
+ );
937
+ const next = neighborSimilarity(
938
+ index.source.blocks[sourceBlockIndex + 1],
939
+ index.target.blocks[candidate.endBlock + 1]
940
+ );
941
+ const proximity = Math.max(
942
+ 0,
943
+ 1 - Math.abs(candidate.startLine - originalLine) / 100
944
+ );
945
+ return Math.min(
946
+ 1,
947
+ content * 0.45 + type * 0.1 + heading * 0.1 + previous * 0.15 + next * 0.15 + proximity * 0.05
948
+ );
949
+ }
950
+ function neighborSimilarity(source, target) {
951
+ if (!source || !target) return 0;
952
+ return textSimilarity(source.text, target.text);
953
+ }
954
+ function textSimilarity(left, right) {
955
+ return Math.max(
956
+ tokenDice(left, right),
957
+ combinedScore(normalizeText(left), normalizeText(right))
958
+ );
959
+ }
960
+ function pathSimilarity(left, right) {
961
+ if (left.length === 0 || right.length === 0) return 0;
962
+ return tokenDice(left.join(" "), right.join(" "));
963
+ }
964
+ function tokenDice(left, right) {
965
+ const leftTokens = tokenize2(left);
966
+ const rightTokens = tokenize2(right);
967
+ if (leftTokens.length === 0 || rightTokens.length === 0) return 0;
968
+ const remaining = /* @__PURE__ */ new Map();
969
+ for (const token of rightTokens) {
970
+ remaining.set(token, (remaining.get(token) ?? 0) + 1);
971
+ }
972
+ let overlap = 0;
973
+ for (const token of leftTokens) {
974
+ const count = remaining.get(token) ?? 0;
975
+ if (count > 0) {
976
+ overlap += 1;
977
+ remaining.set(token, count - 1);
978
+ }
979
+ }
980
+ return 2 * overlap / (leftTokens.length + rightTokens.length);
981
+ }
982
+ function tokenize2(text) {
983
+ return normalizeText(text).match(/[\p{L}\p{N}_-]+/gu) ?? [];
984
+ }
985
+ function normalizeText(text) {
986
+ return text.toLowerCase().replace(/\s+/g, " ").trim();
987
+ }
988
+ function samePath(left, right) {
989
+ return left.length === right.length && left.every((item, index) => item === right[index]);
990
+ }
991
+ function resolveCandidateRange(comment, sourceBlock, candidate, target) {
992
+ const exactIndex = candidate.text.indexOf(comment.selected_text);
993
+ 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;
994
+ const sourceOccurrenceIsUnique = countOccurrences(sourceBlock.text, comment.selected_text) === 1;
995
+ if (exactIndex >= 0 && (selectedIsWholeBlock || sourceOccurrenceIsUnique)) {
996
+ return exactRange(
997
+ candidate.startLine,
998
+ candidate.text,
999
+ comment.selected_text,
1000
+ exactIndex
1001
+ );
1002
+ }
1003
+ if (selectedIsWholeBlock) {
1004
+ return {
1005
+ line: candidate.startLine,
1006
+ endLine: candidate.endLine,
1007
+ text: candidate.text
1008
+ };
1009
+ }
1010
+ const relativeLine = comment.line - sourceBlock.startLine;
1011
+ const line = Math.min(candidate.endLine, candidate.startLine + relativeLine);
1012
+ const lineSpan = (comment.end_line ?? comment.line) - comment.line;
1013
+ const endLine = Math.min(candidate.endLine, line + lineSpan);
1014
+ const startColumn = comment.start_column;
1015
+ const endColumn = comment.end_column;
1016
+ return {
1017
+ line,
1018
+ endLine,
1019
+ startColumn,
1020
+ endColumn,
1021
+ text: extractText2(
1022
+ target.lines,
1023
+ line,
1024
+ endLine,
1025
+ startColumn,
1026
+ endColumn
1027
+ ) ?? ""
1028
+ };
1029
+ }
1030
+ function countOccurrences(text, needle) {
1031
+ if (!needle) return 0;
1032
+ let count = 0;
1033
+ let offset = 0;
1034
+ while (offset <= text.length - needle.length) {
1035
+ const index = text.indexOf(needle, offset);
1036
+ if (index < 0) break;
1037
+ count += 1;
1038
+ offset = index + 1;
1039
+ }
1040
+ return count;
1041
+ }
1042
+ function exactRange(startLine, candidateText, selectedText, index) {
1043
+ const before = candidateText.slice(0, index).split("\n");
1044
+ const selectedLines = selectedText.split("\n");
1045
+ const line = startLine + before.length - 1;
1046
+ const startColumn = before.at(-1)?.length ?? 0;
1047
+ const finalSelectedLineLength = selectedLines.at(-1)?.length ?? 0;
1048
+ return {
1049
+ line,
1050
+ endLine: line + selectedLines.length - 1,
1051
+ startColumn,
1052
+ endColumn: selectedLines.length === 1 ? startColumn + finalSelectedLineLength : finalSelectedLineLength,
1053
+ text: selectedText
1054
+ };
1055
+ }
1056
+ function extractText2(lines, line, endLine, startColumn, endColumn) {
1057
+ const finalLine = endLine ?? line;
1058
+ if (line < 1 || finalLine >= lines.length) return null;
1059
+ if (line === finalLine) {
1060
+ const text = lines[line];
1061
+ return startColumn != null && endColumn != null ? text.slice(startColumn, endColumn) : text;
1062
+ }
1063
+ const result = [];
1064
+ for (let current = line; current <= finalLine; current += 1) {
1065
+ let text = lines[current];
1066
+ if (current === line && startColumn != null) text = text.slice(startColumn);
1067
+ if (current === finalLine && endColumn != null) text = text.slice(0, endColumn);
1068
+ result.push(text);
1069
+ }
1070
+ return result.join("\n");
1071
+ }
1072
+
1073
+ // src/lib/confidence-calibration.ts
1074
+ function calibrateAnchorEvidence(commentId, selectedText, projected, contextual) {
1075
+ if (projected?.exact) {
1076
+ return {
1077
+ band: "certain",
1078
+ result: projectedResult(commentId, selectedText, projected)
1079
+ };
1080
+ }
1081
+ if (contextual?.status === "anchored") {
1082
+ return {
1083
+ band: "certain",
1084
+ result: contextualResult(commentId, selectedText, contextual)
1085
+ };
1086
+ }
1087
+ if (projected && contextual) {
1088
+ if (sameRange(projected, contextual)) {
1089
+ const result = contextualResult(commentId, selectedText, contextual);
1090
+ result.status = "fuzzy";
1091
+ result.score = combineIndependentScores(projected.score, contextual.score);
1092
+ result.reason = `Probable anchor: revision projection and Markdown context agree (confidence ${result.score.toFixed(3)}).`;
1093
+ return { band: "probable", result };
1094
+ }
1095
+ if (contextual.status === "orphaned" && !isStrongProjection(projected)) {
1096
+ return {
1097
+ band: "orphaned",
1098
+ result: contextualResult(commentId, selectedText, contextual)
1099
+ };
1100
+ }
1101
+ if (contextual.status === "fuzzy" && contextual.candidateMargin >= 0.15 && contextual.score >= projected.score + 0.1) {
1102
+ return {
1103
+ band: "probable",
1104
+ result: contextualResult(commentId, selectedText, contextual)
1105
+ };
1106
+ }
1107
+ return {
1108
+ band: "ambiguous",
1109
+ result: {
1110
+ commentId,
1111
+ status: "ambiguous",
1112
+ score: Math.max(projected.score, contextual.score),
1113
+ reason: `Ambiguous evidence: revision projection points to line ${projected.line}, while Markdown context points to ${contextual.line ?? "no location"}.`
1114
+ }
1115
+ };
1116
+ }
1117
+ if (contextual) {
1118
+ const band = contextual.status === "orphaned" ? "orphaned" : contextual.status === "ambiguous" ? "ambiguous" : "probable";
1119
+ return {
1120
+ band,
1121
+ result: contextualResult(commentId, selectedText, contextual)
1122
+ };
1123
+ }
1124
+ if (projected) {
1125
+ if (!isStrongProjection(projected)) return void 0;
1126
+ return {
1127
+ band: "probable",
1128
+ result: projectedResult(commentId, selectedText, projected)
1129
+ };
1130
+ }
1131
+ return void 0;
1132
+ }
1133
+ function isStrongProjection(projected) {
1134
+ return projected.contextSupport >= 3 || projected.contextSupport >= 2 && projected.contextMargin >= 0.5;
1135
+ }
1136
+ function sameRange(projected, contextual) {
1137
+ return projected.line === contextual.line && projected.endLine === contextual.endLine;
1138
+ }
1139
+ function combineIndependentScores(left, right) {
1140
+ return Math.min(0.95, (left + right) / 2 + 0.05);
1141
+ }
1142
+ function projectedResult(commentId, selectedText, projected) {
1143
+ return {
1144
+ commentId,
1145
+ status: projected.exact ? "anchored" : "fuzzy",
1146
+ score: projected.score,
1147
+ newLine: projected.line,
1148
+ newEndLine: projected.endLine,
1149
+ newStartColumn: projected.startColumn,
1150
+ newEndColumn: projected.endColumn,
1151
+ anchoredText: projected.exact ? void 0 : projected.text,
1152
+ previousSelectedText: projected.exact ? void 0 : selectedText,
1153
+ reason: projected.reason
1154
+ };
1155
+ }
1156
+ function contextualResult(commentId, selectedText, contextual) {
1157
+ return {
1158
+ commentId,
1159
+ status: contextual.status,
1160
+ score: contextual.score,
1161
+ newLine: contextual.line,
1162
+ newEndLine: contextual.endLine,
1163
+ newStartColumn: contextual.startColumn,
1164
+ newEndColumn: contextual.endColumn,
1165
+ anchoredText: contextual.status === "fuzzy" ? contextual.text : void 0,
1166
+ previousSelectedText: contextual.status === "fuzzy" ? selectedText : void 0,
1167
+ reason: contextual.reason
1168
+ };
1169
+ }
1170
+
276
1171
  // src/lib/reanchor-core.ts
277
1172
  var HIGH_THRESHOLD = 0.8;
278
1173
  var DEFAULT_THRESHOLD = 0.6;
@@ -285,6 +1180,12 @@ function reanchorComment(comment, documentLines, opts = {}) {
285
1180
  const proximityWindow = opts.proximityWindow ?? DEFAULT_PROXIMITY_WINDOW;
286
1181
  const commentId = comment.id;
287
1182
  const selectedText = comment.selected_text;
1183
+ let fuzzyCandidateSets;
1184
+ let fuzzySearchIndex = opts.fuzzySearchIndex;
1185
+ const getFuzzySearchIndex = () => {
1186
+ fuzzySearchIndex ??= opts.getFuzzySearchIndex?.() ?? createFuzzySearchIndex(documentLines);
1187
+ return fuzzySearchIndex;
1188
+ };
288
1189
  if (!selectedText && comment.line == null) {
289
1190
  return {
290
1191
  commentId,
@@ -312,7 +1213,7 @@ function reanchorComment(comment, documentLines, opts = {}) {
312
1213
  const shiftedLine = comment.line + shift;
313
1214
  const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
314
1215
  const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : void 0;
315
- const textAtShifted = extractText(
1216
+ const textAtShifted = extractText3(
316
1217
  documentLines,
317
1218
  shiftedLine,
318
1219
  shiftedEndLine,
@@ -331,8 +1232,39 @@ function reanchorComment(comment, documentLines, opts = {}) {
331
1232
  }
332
1233
  }
333
1234
  }
1235
+ const projected = selectedText && opts.revisionProjection ? projectCommentAnchor(
1236
+ comment,
1237
+ opts.revisionProjection,
1238
+ threshold
1239
+ ) : void 0;
1240
+ if (selectedText && projected?.exact) {
1241
+ const exactCalibration = calibrateAnchorEvidence(
1242
+ commentId,
1243
+ selectedText,
1244
+ projected
1245
+ );
1246
+ if (exactCalibration) return exactCalibration.result;
1247
+ }
1248
+ const contextual = selectedText && opts.anchorContext ? resolveContextAnchor(comment, opts.anchorContext) : void 0;
1249
+ if (selectedText && (projected || contextual)) {
1250
+ const calibrated = calibrateAnchorEvidence(
1251
+ commentId,
1252
+ selectedText,
1253
+ projected,
1254
+ contextual
1255
+ );
1256
+ if (calibrated) return calibrated.result;
1257
+ }
334
1258
  if (selectedText) {
335
1259
  const exactCandidates = exactMatch(documentLines, selectedText);
1260
+ if (exactCandidates.length > 1 && comment.line == null) {
1261
+ return {
1262
+ commentId,
1263
+ status: "ambiguous",
1264
+ score: 1,
1265
+ reason: `Ambiguous: ${exactCandidates.length} exact matches and no position or source context to disambiguate them.`
1266
+ };
1267
+ }
336
1268
  let chosen;
337
1269
  let chosenReason = "";
338
1270
  if (exactCandidates.length === 1) {
@@ -344,7 +1276,7 @@ function reanchorComment(comment, documentLines, opts = {}) {
344
1276
  }
345
1277
  if (chosen) {
346
1278
  if (isImplausibleExactRelocation(comment, chosen, documentLines, proximityWindow)) {
347
- const textAtOrigin = extractText(
1279
+ const textAtOrigin = extractText3(
348
1280
  documentLines,
349
1281
  comment.line,
350
1282
  comment.end_line,
@@ -391,12 +1323,14 @@ function reanchorComment(comment, documentLines, opts = {}) {
391
1323
  reason: "Normalized whitespace match."
392
1324
  };
393
1325
  }
394
- const fuzzyCandidates = fuzzySearch(
1326
+ fuzzyCandidateSets = fuzzySearchThresholds(
395
1327
  documentLines,
396
1328
  selectedText,
397
- HIGH_THRESHOLD,
398
- comment.line
1329
+ [HIGH_THRESHOLD, threshold],
1330
+ comment.line,
1331
+ getFuzzySearchIndex()
399
1332
  );
1333
+ const fuzzyCandidates = fuzzyCandidateSets.get(HIGH_THRESHOLD) ?? [];
400
1334
  if (fuzzyCandidates.length === 1 || fuzzyCandidates.length > 0 && fuzzyCandidates[0].score >= HIGH_THRESHOLD) {
401
1335
  const best = fuzzyCandidates.length === 1 ? fuzzyCandidates[0] : closestToLine(fuzzyCandidates, comment.line ?? 1);
402
1336
  return {
@@ -445,11 +1379,12 @@ function reanchorComment(comment, documentLines, opts = {}) {
445
1379
  }
446
1380
  }
447
1381
  if (selectedText) {
448
- const lowCandidates = fuzzySearch(
1382
+ const lowCandidates = fuzzyCandidateSets?.get(threshold) ?? fuzzySearch(
449
1383
  documentLines,
450
1384
  selectedText,
451
1385
  threshold,
452
- comment.line
1386
+ comment.line,
1387
+ getFuzzySearchIndex()
453
1388
  );
454
1389
  if (lowCandidates.length === 1) {
455
1390
  const candidate = lowCandidates[0];
@@ -486,7 +1421,14 @@ function reanchorComment(comment, documentLines, opts = {}) {
486
1421
  };
487
1422
  }
488
1423
  function reanchorDocumentLines(doc, documentLines, opts = {}) {
489
- return doc.comments.map((comment) => reanchorComment(comment, documentLines, opts));
1424
+ let fuzzySearchIndex;
1425
+ const getFuzzySearchIndex = () => {
1426
+ fuzzySearchIndex ??= createFuzzySearchIndex(documentLines);
1427
+ return fuzzySearchIndex;
1428
+ };
1429
+ return doc.comments.map(
1430
+ (comment) => reanchorComment(comment, documentLines, { ...opts, getFuzzySearchIndex })
1431
+ );
490
1432
  }
491
1433
  function reanchorDocumentText(doc, documentText, opts = {}) {
492
1434
  return reanchorDocumentLines(doc, toReanchorLines(documentText), opts);
@@ -605,7 +1547,7 @@ function applyReanchorResults(doc, results, opts = {}) {
605
1547
  }
606
1548
  return changed;
607
1549
  }
608
- function extractText(lines, line, endLine, startColumn, endColumn) {
1550
+ function extractText3(lines, line, endLine, startColumn, endColumn) {
609
1551
  const startIdx = line;
610
1552
  const endIdx = endLine ?? line;
611
1553
  if (startIdx < 1 || endIdx >= lines.length) return null;
@@ -634,7 +1576,7 @@ function isImplausibleExactRelocation(comment, candidate, lines, proximityWindow
634
1576
  if (comment.line == null) return false;
635
1577
  if (comment.line <= 0 || comment.line >= lines.length) return false;
636
1578
  if (Math.abs(candidate.line - comment.line) <= proximityWindow) return false;
637
- const textAtOrigin = extractText(
1579
+ const textAtOrigin = extractText3(
638
1580
  lines,
639
1581
  comment.line,
640
1582
  comment.end_line,
@@ -660,6 +1602,137 @@ function getLineShift(diffHunks, line) {
660
1602
  return { shift, modified };
661
1603
  }
662
1604
 
1605
+ // src/lib/global-reconciliation.ts
1606
+ var LOCAL_LANDMARK_WINDOW = 30;
1607
+ var MIN_LANDMARK_SUPPORT = 0.65;
1608
+ var MIN_SUPPORTING_LANDMARKS = 2;
1609
+ var MIN_GLOBAL_MARGIN = 0.08;
1610
+ var MAX_RECONCILIATION_ROUNDS = 4;
1611
+ function reconcileCommentAnchors(comments, results, anchorContext) {
1612
+ const reconciled = results.map((result) => ({ ...result }));
1613
+ if (comments.length < MIN_SUPPORTING_LANDMARKS + 1 || !reconciled.some((result) => result.status === "ambiguous")) {
1614
+ return reconciled;
1615
+ }
1616
+ const resultIndexes = new Map(
1617
+ reconciled.map((result, index) => [result.commentId, index])
1618
+ );
1619
+ const landmarks = collectLandmarks(comments, reconciled, anchorContext);
1620
+ const landmarkCounts = /* @__PURE__ */ new Map();
1621
+ for (const landmark of landmarks) {
1622
+ landmarkCounts.set(
1623
+ landmark.scope,
1624
+ (landmarkCounts.get(landmark.scope) ?? 0) + 1
1625
+ );
1626
+ }
1627
+ for (let round = 0; round < MAX_RECONCILIATION_ROUNDS; round += 1) {
1628
+ let changed = false;
1629
+ for (const comment of comments) {
1630
+ if (comment.line == null || !comment.selected_text) continue;
1631
+ const scope = getAnchorContextScope(comment, anchorContext);
1632
+ if (scope == null) continue;
1633
+ if ((landmarkCounts.get(scope) ?? 0) < MIN_SUPPORTING_LANDMARKS) {
1634
+ continue;
1635
+ }
1636
+ const resultIndex = resultIndexes.get(comment.id);
1637
+ if (resultIndex == null) continue;
1638
+ const result = reconciled[resultIndex];
1639
+ if (result.status !== "ambiguous") continue;
1640
+ const candidates = deduplicateCandidates2(
1641
+ findContextAnchorCandidates(comment, anchorContext)
1642
+ ).slice(0, 8);
1643
+ if (candidates.length < 2) continue;
1644
+ const ranked = candidates.map(
1645
+ (candidate) => scoreWithLandmarks(
1646
+ comment.line,
1647
+ scope,
1648
+ candidate,
1649
+ landmarks
1650
+ )
1651
+ ).sort(
1652
+ (left, right) => right.globalScore - left.globalScore || right.candidate.score - left.candidate.score || left.candidate.line - right.candidate.line
1653
+ );
1654
+ const best = ranked[0];
1655
+ const second = ranked[1];
1656
+ if (!best.candidate.exact || best.supportingLandmarks < MIN_SUPPORTING_LANDMARKS || best.support < MIN_LANDMARK_SUPPORT || best.globalScore - second.globalScore < MIN_GLOBAL_MARGIN) {
1657
+ continue;
1658
+ }
1659
+ reconciled[resultIndex] = {
1660
+ commentId: comment.id,
1661
+ status: "anchored",
1662
+ score: Math.min(0.99, 0.8 + best.support * 0.19),
1663
+ newLine: best.candidate.line,
1664
+ newEndLine: best.candidate.endLine,
1665
+ newStartColumn: best.candidate.startColumn,
1666
+ newEndColumn: best.candidate.endColumn,
1667
+ reason: `Global reconciliation selected this candidate from ${best.supportingLandmarks} nearby landmark(s) (support ${best.support.toFixed(3)}, margin ${(best.globalScore - second.globalScore).toFixed(3)}).`
1668
+ };
1669
+ changed = true;
1670
+ landmarks.push({
1671
+ sourceLine: comment.line,
1672
+ targetLine: best.candidate.line,
1673
+ scope
1674
+ });
1675
+ landmarkCounts.set(scope, (landmarkCounts.get(scope) ?? 0) + 1);
1676
+ }
1677
+ if (!changed) break;
1678
+ }
1679
+ return reconciled;
1680
+ }
1681
+ function collectLandmarks(comments, results, anchorContext) {
1682
+ const resultMap = new Map(results.map((result) => [result.commentId, result]));
1683
+ return comments.flatMap((comment) => {
1684
+ const result = resultMap.get(comment.id);
1685
+ if (comment.line == null || result?.newLine == null || result.status !== "anchored" && result.status !== "shifted" || result.score < 0.99) {
1686
+ return [];
1687
+ }
1688
+ const scope = getAnchorContextScope(comment, anchorContext);
1689
+ return scope == null ? [] : [{ sourceLine: comment.line, targetLine: result.newLine, scope }];
1690
+ });
1691
+ }
1692
+ function scoreWithLandmarks(sourceLine, scope, candidate, landmarks) {
1693
+ let weightedSupport = 0;
1694
+ let totalWeight = 0;
1695
+ let supportingLandmarks = 0;
1696
+ const candidateShift = candidate.line - sourceLine;
1697
+ for (const landmark of landmarks) {
1698
+ if (landmark.scope !== scope) continue;
1699
+ const sourceDistance = Math.abs(sourceLine - landmark.sourceLine);
1700
+ if (sourceDistance === 0 || sourceDistance > LOCAL_LANDMARK_WINDOW) continue;
1701
+ const landmarkShift = landmark.targetLine - landmark.sourceLine;
1702
+ const shiftError = Math.abs(candidateShift - landmarkShift);
1703
+ const agreement = Math.max(0, 1 - shiftError / 8);
1704
+ const weight = 1 / (1 + sourceDistance / 4);
1705
+ weightedSupport += agreement * weight;
1706
+ totalWeight += weight;
1707
+ if (agreement >= MIN_LANDMARK_SUPPORT) supportingLandmarks += 1;
1708
+ }
1709
+ const support = totalWeight > 0 ? weightedSupport / totalWeight : 0;
1710
+ return {
1711
+ candidate,
1712
+ globalScore: candidate.score * 0.5 + support * 0.45 + (candidate.exact ? 0.05 : 0),
1713
+ support,
1714
+ supportingLandmarks
1715
+ };
1716
+ }
1717
+ function deduplicateCandidates2(candidates) {
1718
+ const unique = /* @__PURE__ */ new Map();
1719
+ for (const candidate of candidates) {
1720
+ const key = [
1721
+ candidate.line,
1722
+ candidate.endLine,
1723
+ candidate.startColumn ?? "",
1724
+ candidate.endColumn ?? ""
1725
+ ].join(":");
1726
+ const existing = unique.get(key);
1727
+ if (!existing || candidate.score > existing.score) {
1728
+ unique.set(key, candidate);
1729
+ }
1730
+ }
1731
+ return [...unique.values()].sort(
1732
+ (left, right) => right.score - left.score || left.line - right.line
1733
+ );
1734
+ }
1735
+
663
1736
  // src/lib/validate-core.ts
664
1737
  var import_ajv = __toESM(require("ajv"), 1);
665
1738
  var import_ajv_formats = __toESM(require("ajv-formats"), 1);
@@ -1039,6 +2112,10 @@ var reanchorDocumentLines2 = reanchorDocumentLines;
1039
2112
  var reanchorDocumentText2 = reanchorDocumentText;
1040
2113
  var resolveAnchor2 = resolveAnchor;
1041
2114
  var toReanchorLines2 = toReanchorLines;
2115
+ var createAnchorContextIndex2 = createAnchorContextIndex;
2116
+ var reconcileCommentAnchors2 = reconcileCommentAnchors;
2117
+ var createRevisionProjection2 = createRevisionProjection;
2118
+ var calibrateAnchorEvidence2 = calibrateAnchorEvidence;
1042
2119
  var validateDocument2 = validateDocument;
1043
2120
  var formatAuthor2 = formatAuthor;
1044
2121
  var parseAuthor2 = parseAuthor;