@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/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,
@@ -42,11 +45,16 @@ __export(browser_exports, {
42
45
  newCommentId: () => newCommentId2,
43
46
  normalizedMatch: () => normalizedMatch2,
44
47
  parseAuthor: () => parseAuthor2,
48
+ parseSidecarContent: () => parseSidecarContent2,
49
+ parseSidecarContentLenient: () => parseSidecarContentLenient2,
45
50
  reanchorComment: () => reanchorComment2,
46
51
  reanchorDocumentLines: () => reanchorDocumentLines2,
47
52
  reanchorDocumentText: () => reanchorDocumentText2,
53
+ reconcileCommentAnchors: () => reconcileCommentAnchors2,
48
54
  resolveAnchor: () => resolveAnchor2,
55
+ toJson: () => toJson2,
49
56
  toReanchorLines: () => toReanchorLines2,
57
+ toYaml: () => toYaml2,
50
58
  tokenLcsScore: () => tokenLcsScore2,
51
59
  validateDocument: () => validateDocument2
52
60
  });
@@ -54,6 +62,16 @@ module.exports = __toCommonJS(browser_exports);
54
62
 
55
63
  // src/lib/fuzzy.ts
56
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
+ }
57
75
  function exactMatch(lines, needle) {
58
76
  if (!needle) return [];
59
77
  const results = [];
@@ -176,22 +194,48 @@ function combinedScore(needle, candidate) {
176
194
  }
177
195
  return tScore * 0.6 + lScore * 0.4;
178
196
  }
179
- function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
180
- 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
+ }
181
214
  const needleLines = needle.split("\n");
182
215
  const needleLineCount = needleLines.length;
183
216
  const candidates = [];
217
+ const minimumThreshold = Math.min(...uniqueThresholds);
218
+ const candidateLines = retrieveCandidateLines(index, needle, hintLine);
184
219
  const minWindow = Math.max(1, Math.floor(needleLineCount * 0.7));
185
220
  const maxWindow = Math.min(
186
221
  lines.length - 1,
187
222
  Math.ceil(needleLineCount * 1.3) + 1
188
223
  );
189
224
  for (let winSize = minWindow; winSize <= maxWindow; winSize++) {
190
- 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) {
191
235
  const windowLines = lines.slice(startLine, startLine + winSize);
192
236
  const windowText = windowLines.join("\n");
193
237
  const score = combinedScore(needle, windowText);
194
- if (score >= threshold) {
238
+ if (score >= minimumThreshold) {
195
239
  candidates.push({
196
240
  text: windowText,
197
241
  line: startLine,
@@ -204,17 +248,18 @@ function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
204
248
  }
205
249
  }
206
250
  if (needleLineCount === 1 && needle.length < 200) {
207
- for (let lineNum = 1; lineNum < lines.length; lineNum++) {
251
+ for (const lineNum of candidateLines) {
208
252
  const line = lines[lineNum];
209
253
  if (!line) continue;
210
254
  const winLen = needle.length;
211
255
  const minWinLen = Math.max(3, Math.floor(winLen * 0.7));
212
256
  const maxWinLen = Math.min(line.length, Math.ceil(winLen * 1.3));
213
- for (let len = minWinLen; len <= maxWinLen; len++) {
257
+ const lengths = evenlySpacedIntegers(minWinLen, maxWinLen, 7);
258
+ for (const len of lengths) {
214
259
  for (let col = 0; col + len <= line.length; col++) {
215
260
  const sub = line.substring(col, col + len);
216
261
  const score = combinedScore(needle, sub);
217
- if (score >= threshold) {
262
+ if (score >= minimumThreshold) {
218
263
  candidates.push({
219
264
  text: sub,
220
265
  line: lineNum,
@@ -228,15 +273,83 @@ function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
228
273
  }
229
274
  }
230
275
  }
231
- 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
+ }
232
302
  if (hintLine != null) {
233
- deduped = deduped.map((c) => {
234
- const dist = Math.abs(c.line - hintLine);
235
- const proximityBonus = 0.1 * Math.max(0, 1 - dist / 50);
236
- return { ...c, score: Math.min(1, c.score + proximityBonus) };
237
- });
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);
312
+ }
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(""));
238
340
  }
239
- return deduped.sort((a, b) => b.score - a.score);
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);
240
353
  }
241
354
  function deduplicateCandidates(candidates) {
242
355
  const seen = /* @__PURE__ */ new Map();
@@ -249,6 +362,15 @@ function deduplicateCandidates(candidates) {
249
362
  }
250
363
  return Array.from(seen.values());
251
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
+ }
252
374
 
253
375
  // src/lib/identity.ts
254
376
  var import_uuid = require("uuid");
@@ -269,16 +391,801 @@ function newCommentId() {
269
391
  return (0, import_uuid.v4)();
270
392
  }
271
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
+
272
1171
  // src/lib/reanchor-core.ts
273
1172
  var HIGH_THRESHOLD = 0.8;
274
1173
  var DEFAULT_THRESHOLD = 0.6;
1174
+ var DEFAULT_PROXIMITY_WINDOW = 5;
275
1175
  function toReanchorLines(documentText) {
276
1176
  return ["", ...documentText.replace(/\r\n/g, "\n").split("\n")];
277
1177
  }
278
1178
  function reanchorComment(comment, documentLines, opts = {}) {
279
1179
  const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
1180
+ const proximityWindow = opts.proximityWindow ?? DEFAULT_PROXIMITY_WINDOW;
280
1181
  const commentId = comment.id;
281
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
+ };
282
1189
  if (!selectedText && comment.line == null) {
283
1190
  return {
284
1191
  commentId,
@@ -306,7 +1213,7 @@ function reanchorComment(comment, documentLines, opts = {}) {
306
1213
  const shiftedLine = comment.line + shift;
307
1214
  const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
308
1215
  const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : void 0;
309
- const textAtShifted = extractText(
1216
+ const textAtShifted = extractText3(
310
1217
  documentLines,
311
1218
  shiftedLine,
312
1219
  shiftedEndLine,
@@ -325,32 +1232,79 @@ function reanchorComment(comment, documentLines, opts = {}) {
325
1232
  }
326
1233
  }
327
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
+ }
328
1258
  if (selectedText) {
329
1259
  const exactCandidates = exactMatch(documentLines, selectedText);
330
- if (exactCandidates.length === 1) {
331
- const candidate = exactCandidates[0];
1260
+ if (exactCandidates.length > 1 && comment.line == null) {
332
1261
  return {
333
1262
  commentId,
334
- status: "anchored",
1263
+ status: "ambiguous",
335
1264
  score: 1,
336
- newLine: candidate.line,
337
- newEndLine: candidate.endLine,
338
- newStartColumn: candidate.startColumn,
339
- newEndColumn: candidate.endColumn,
340
- reason: "Exact text match (unique)."
1265
+ reason: `Ambiguous: ${exactCandidates.length} exact matches and no position or source context to disambiguate them.`
341
1266
  };
342
1267
  }
343
- if (exactCandidates.length > 1 && comment.line != null) {
344
- const best = closestToLine(exactCandidates, comment.line);
1268
+ let chosen;
1269
+ let chosenReason = "";
1270
+ if (exactCandidates.length === 1) {
1271
+ chosen = exactCandidates[0];
1272
+ chosenReason = "Exact text match (unique).";
1273
+ } else if (exactCandidates.length > 1 && comment.line != null) {
1274
+ chosen = closestToLine(exactCandidates, comment.line);
1275
+ chosenReason = `Exact text match (${exactCandidates.length} occurrences; chose nearest to original line ${comment.line}).`;
1276
+ }
1277
+ if (chosen) {
1278
+ if (isImplausibleExactRelocation(comment, chosen, documentLines, proximityWindow)) {
1279
+ const textAtOrigin = extractText3(
1280
+ documentLines,
1281
+ comment.line,
1282
+ comment.end_line,
1283
+ comment.start_column,
1284
+ comment.end_column
1285
+ );
1286
+ return {
1287
+ commentId,
1288
+ status: "fuzzy",
1289
+ score: 0.5,
1290
+ newLine: comment.line,
1291
+ newEndLine: comment.end_line,
1292
+ newStartColumn: comment.start_column,
1293
+ newEndColumn: comment.end_column,
1294
+ anchoredText: textAtOrigin ?? void 0,
1295
+ previousSelectedText: selectedText,
1296
+ 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.`
1297
+ };
1298
+ }
345
1299
  return {
346
1300
  commentId,
347
1301
  status: "anchored",
348
1302
  score: 1,
349
- newLine: best.line,
350
- newEndLine: best.endLine,
351
- newStartColumn: best.startColumn,
352
- newEndColumn: best.endColumn,
353
- reason: `Exact text match (${exactCandidates.length} occurrences; chose nearest to original line ${comment.line}).`
1303
+ newLine: chosen.line,
1304
+ newEndLine: chosen.endLine,
1305
+ newStartColumn: chosen.startColumn,
1306
+ newEndColumn: chosen.endColumn,
1307
+ reason: chosenReason
354
1308
  };
355
1309
  }
356
1310
  const normCandidates = normalizedMatch(documentLines, selectedText);
@@ -369,12 +1323,14 @@ function reanchorComment(comment, documentLines, opts = {}) {
369
1323
  reason: "Normalized whitespace match."
370
1324
  };
371
1325
  }
372
- const fuzzyCandidates = fuzzySearch(
1326
+ fuzzyCandidateSets = fuzzySearchThresholds(
373
1327
  documentLines,
374
1328
  selectedText,
375
- HIGH_THRESHOLD,
376
- comment.line
1329
+ [HIGH_THRESHOLD, threshold],
1330
+ comment.line,
1331
+ getFuzzySearchIndex()
377
1332
  );
1333
+ const fuzzyCandidates = fuzzyCandidateSets.get(HIGH_THRESHOLD) ?? [];
378
1334
  if (fuzzyCandidates.length === 1 || fuzzyCandidates.length > 0 && fuzzyCandidates[0].score >= HIGH_THRESHOLD) {
379
1335
  const best = fuzzyCandidates.length === 1 ? fuzzyCandidates[0] : closestToLine(fuzzyCandidates, comment.line ?? 1);
380
1336
  return {
@@ -423,11 +1379,12 @@ function reanchorComment(comment, documentLines, opts = {}) {
423
1379
  }
424
1380
  }
425
1381
  if (selectedText) {
426
- const lowCandidates = fuzzySearch(
1382
+ const lowCandidates = fuzzyCandidateSets?.get(threshold) ?? fuzzySearch(
427
1383
  documentLines,
428
1384
  selectedText,
429
1385
  threshold,
430
- comment.line
1386
+ comment.line,
1387
+ getFuzzySearchIndex()
431
1388
  );
432
1389
  if (lowCandidates.length === 1) {
433
1390
  const candidate = lowCandidates[0];
@@ -464,7 +1421,14 @@ function reanchorComment(comment, documentLines, opts = {}) {
464
1421
  };
465
1422
  }
466
1423
  function reanchorDocumentLines(doc, documentLines, opts = {}) {
467
- 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
+ );
468
1432
  }
469
1433
  function reanchorDocumentText(doc, documentText, opts = {}) {
470
1434
  return reanchorDocumentLines(doc, toReanchorLines(documentText), opts);
@@ -583,7 +1547,7 @@ function applyReanchorResults(doc, results, opts = {}) {
583
1547
  }
584
1548
  return changed;
585
1549
  }
586
- function extractText(lines, line, endLine, startColumn, endColumn) {
1550
+ function extractText3(lines, line, endLine, startColumn, endColumn) {
587
1551
  const startIdx = line;
588
1552
  const endIdx = endLine ?? line;
589
1553
  if (startIdx < 1 || endIdx >= lines.length) return null;
@@ -608,6 +1572,20 @@ function closestToLine(candidates, targetLine) {
608
1572
  (best, candidate) => Math.abs(candidate.line - targetLine) < Math.abs(best.line - targetLine) ? candidate : best
609
1573
  );
610
1574
  }
1575
+ function isImplausibleExactRelocation(comment, candidate, lines, proximityWindow) {
1576
+ if (comment.line == null) return false;
1577
+ if (comment.line <= 0 || comment.line >= lines.length) return false;
1578
+ if (Math.abs(candidate.line - comment.line) <= proximityWindow) return false;
1579
+ const textAtOrigin = extractText3(
1580
+ lines,
1581
+ comment.line,
1582
+ comment.end_line,
1583
+ comment.start_column,
1584
+ comment.end_column
1585
+ );
1586
+ if (textAtOrigin === comment.selected_text) return false;
1587
+ return true;
1588
+ }
611
1589
  function getLineShift(diffHunks, line) {
612
1590
  let shift = 0;
613
1591
  let modified = false;
@@ -624,6 +1602,137 @@ function getLineShift(diffHunks, line) {
624
1602
  return { shift, modified };
625
1603
  }
626
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
+
627
1736
  // src/lib/validate-core.ts
628
1737
  var import_ajv = __toESM(require("ajv"), 1);
629
1738
  var import_ajv_formats = __toESM(require("ajv-formats"), 1);
@@ -865,7 +1974,130 @@ function validateCrossFields(doc, errors, warnings, hash) {
865
1974
  }
866
1975
  }
867
1976
 
1977
+ // src/lib/serialize.ts
1978
+ var import_js_yaml = __toESM(require("js-yaml"), 1);
1979
+ var import_yaml = require("yaml");
1980
+ function parseSidecarContent(content, filenameHint) {
1981
+ const trimmed = content.trim();
1982
+ let parsed;
1983
+ const isJson = trimmed.startsWith("{") || filenameHint && filenameHint.endsWith(".review.json");
1984
+ if (isJson) {
1985
+ try {
1986
+ parsed = JSON.parse(trimmed);
1987
+ } catch (e) {
1988
+ throw new Error(`Failed to parse JSON: ${e.message}`);
1989
+ }
1990
+ } else {
1991
+ try {
1992
+ parsed = import_js_yaml.default.load(trimmed, { schema: import_js_yaml.default.JSON_SCHEMA });
1993
+ } catch (e) {
1994
+ throw new Error(`Failed to parse YAML: ${e.message}`);
1995
+ }
1996
+ }
1997
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1998
+ throw new Error("MRSF sidecar must be a YAML/JSON object");
1999
+ }
2000
+ return parsed;
2001
+ }
2002
+ function parseSidecarContentLenient(content, filenameHint) {
2003
+ const trimmed = content.trim();
2004
+ if (!trimmed) {
2005
+ return { doc: null, error: "File is empty" };
2006
+ }
2007
+ const isJson = trimmed.startsWith("{") || filenameHint && filenameHint.endsWith(".review.json");
2008
+ let parsed;
2009
+ try {
2010
+ parsed = isJson ? JSON.parse(trimmed) : import_js_yaml.default.load(trimmed, { schema: import_js_yaml.default.JSON_SCHEMA });
2011
+ } catch (e) {
2012
+ if (!isJson) {
2013
+ return salvageYaml(trimmed);
2014
+ }
2015
+ return { doc: null, error: `Failed to parse JSON: ${e.message}` };
2016
+ }
2017
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2018
+ return { doc: null, error: "MRSF sidecar must be a YAML/JSON object" };
2019
+ }
2020
+ const obj = parsed;
2021
+ const doc = {
2022
+ mrsf_version: typeof obj.mrsf_version === "string" ? obj.mrsf_version : "1.0",
2023
+ document: typeof obj.document === "string" ? obj.document : "unknown",
2024
+ comments: []
2025
+ };
2026
+ if (!Array.isArray(obj.comments)) {
2027
+ return {
2028
+ doc,
2029
+ error: "comments field is not an array \u2014 file may be corrupted"
2030
+ };
2031
+ }
2032
+ const good = [];
2033
+ const bad = [];
2034
+ for (let i = 0; i < obj.comments.length; i++) {
2035
+ const c = obj.comments[i];
2036
+ if (c && typeof c === "object" && !Array.isArray(c) && typeof c.id === "string") {
2037
+ good.push(c);
2038
+ } else {
2039
+ bad.push(i);
2040
+ }
2041
+ }
2042
+ doc.comments = good;
2043
+ if (bad.length > 0) {
2044
+ return {
2045
+ doc,
2046
+ error: `${bad.length} comment(s) at indices [${bad.join(", ")}] were malformed and skipped`,
2047
+ partialComments: good
2048
+ };
2049
+ }
2050
+ return { doc };
2051
+ }
2052
+ function salvageYaml(content) {
2053
+ const salvaged = [];
2054
+ let mrsf_version = "1.0";
2055
+ let document = "unknown";
2056
+ const versionMatch = content.match(/^mrsf_version:\s*["']?([^"'\n]+)/m);
2057
+ if (versionMatch) mrsf_version = versionMatch[1].trim();
2058
+ const docMatch = content.match(/^document:\s*["']?([^"'\n]+)/m);
2059
+ if (docMatch) document = docMatch[1].trim();
2060
+ const blocks = content.split(/(?=^ - id:\s)/m);
2061
+ for (const block of blocks) {
2062
+ const trimmed = block.trim();
2063
+ if (!trimmed.startsWith("- id:")) continue;
2064
+ try {
2065
+ const parsed = import_js_yaml.default.load(trimmed, { schema: import_js_yaml.default.JSON_SCHEMA });
2066
+ if (Array.isArray(parsed) && parsed.length > 0) {
2067
+ const c = parsed[0];
2068
+ if (c && typeof c === "object" && typeof c.id === "string") {
2069
+ salvaged.push(c);
2070
+ }
2071
+ } else if (parsed && typeof parsed === "object" && typeof parsed.id === "string") {
2072
+ salvaged.push(parsed);
2073
+ }
2074
+ } catch {
2075
+ }
2076
+ }
2077
+ const doc = {
2078
+ mrsf_version,
2079
+ document,
2080
+ comments: salvaged
2081
+ };
2082
+ return {
2083
+ doc: salvaged.length > 0 ? doc : null,
2084
+ error: `YAML parse failed. Salvaged ${salvaged.length} comment(s) from raw content.`,
2085
+ partialComments: salvaged.length > 0 ? salvaged : void 0
2086
+ };
2087
+ }
2088
+ function toYaml(doc) {
2089
+ const yamlDoc = new import_yaml.Document(doc);
2090
+ return yamlDoc.toString({ lineWidth: 0 });
2091
+ }
2092
+ function toJson(doc) {
2093
+ return JSON.stringify(doc, null, 2) + "\n";
2094
+ }
2095
+
868
2096
  // src/browser.ts
2097
+ var parseSidecarContent2 = parseSidecarContent;
2098
+ var parseSidecarContentLenient2 = parseSidecarContentLenient;
2099
+ var toYaml2 = toYaml;
2100
+ var toJson2 = toJson;
869
2101
  var combinedScore2 = combinedScore;
870
2102
  var exactMatch2 = exactMatch;
871
2103
  var fuzzySearch2 = fuzzySearch;
@@ -880,6 +2112,10 @@ var reanchorDocumentLines2 = reanchorDocumentLines;
880
2112
  var reanchorDocumentText2 = reanchorDocumentText;
881
2113
  var resolveAnchor2 = resolveAnchor;
882
2114
  var toReanchorLines2 = toReanchorLines;
2115
+ var createAnchorContextIndex2 = createAnchorContextIndex;
2116
+ var reconcileCommentAnchors2 = reconcileCommentAnchors;
2117
+ var createRevisionProjection2 = createRevisionProjection;
2118
+ var calibrateAnchorEvidence2 = calibrateAnchorEvidence;
883
2119
  var validateDocument2 = validateDocument;
884
2120
  var formatAuthor2 = formatAuthor;
885
2121
  var parseAuthor2 = parseAuthor;