@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.
- package/README.md +12 -9
- package/dist/browser.cjs +1101 -24
- package/dist/browser.cjs.map +4 -4
- package/dist/browser.d.ts +11 -0
- package/dist/browser.js +8 -0
- package/dist/commands/add.js +12 -4
- package/dist/evaluation/generate-reanchor-cases.d.ts +10 -0
- package/dist/evaluation/generate-reanchor-cases.js +301 -0
- package/dist/evaluation/reanchor-eval.d.ts +111 -0
- package/dist/evaluation/reanchor-eval.js +266 -0
- package/dist/evaluation/reanchor-gates.d.ts +29 -0
- package/dist/evaluation/reanchor-gates.js +47 -0
- package/dist/evaluation/run-reanchor-eval.d.ts +3 -0
- package/dist/evaluation/run-reanchor-eval.js +103 -0
- package/dist/evaluation/run-reanchor-generator.d.ts +3 -0
- package/dist/evaluation/run-reanchor-generator.js +63 -0
- package/dist/evaluation/run-reanchor-profile.d.ts +3 -0
- package/dist/evaluation/run-reanchor-profile.js +129 -0
- package/dist/index.cjs +1226 -34
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/dist/lib/anchor-context.d.ts +46 -0
- package/dist/lib/anchor-context.js +474 -0
- package/dist/lib/confidence-calibration.d.ts +16 -0
- package/dist/lib/confidence-calibration.js +120 -0
- package/dist/lib/fuzzy.d.ts +14 -1
- package/dist/lib/fuzzy.js +140 -19
- package/dist/lib/git.d.ts +9 -0
- package/dist/lib/git.js +30 -0
- package/dist/lib/global-reconciliation.d.ts +11 -0
- package/dist/lib/global-reconciliation.js +145 -0
- package/dist/lib/reanchor-core.d.ts +7 -0
- package/dist/lib/reanchor-core.js +46 -4
- package/dist/lib/reanchor.js +82 -11
- package/dist/lib/revision-projection.d.ts +21 -0
- package/dist/lib/revision-projection.js +203 -0
- 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
|
-
|
|
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
|
-
|
|
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 >=
|
|
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 (
|
|
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
|
-
|
|
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 >=
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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;
|