@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.
- package/README.md +10 -7
- package/dist/browser.cjs +1273 -37
- package/dist/browser.cjs.map +4 -4
- package/dist/browser.d.ts +18 -1
- package/dist/browser.js +14 -0
- package/dist/commands/reanchor.js +3 -0
- 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 +1307 -85
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -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 +2 -0
- package/dist/lib/git.js +12 -0
- package/dist/lib/global-reconciliation.d.ts +11 -0
- package/dist/lib/global-reconciliation.js +145 -0
- package/dist/lib/parser.d.ts +4 -19
- package/dist/lib/parser.js +4 -140
- package/dist/lib/reanchor-core.d.ts +20 -0
- package/dist/lib/reanchor-core.js +123 -20
- package/dist/lib/reanchor.js +85 -12
- package/dist/lib/revision-projection.d.ts +21 -0
- package/dist/lib/revision-projection.js +203 -0
- package/dist/lib/serialize.d.ts +37 -0
- package/dist/lib/serialize.js +170 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/writer.d.ts +1 -8
- package/dist/lib/writer.js +5 -14
- 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
|
-
|
|
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
|
-
|
|
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 >=
|
|
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 (
|
|
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
|
-
|
|
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 >=
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
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 =
|
|
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
|
|
331
|
-
const candidate = exactCandidates[0];
|
|
1260
|
+
if (exactCandidates.length > 1 && comment.line == null) {
|
|
332
1261
|
return {
|
|
333
1262
|
commentId,
|
|
334
|
-
status: "
|
|
1263
|
+
status: "ambiguous",
|
|
335
1264
|
score: 1,
|
|
336
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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:
|
|
350
|
-
newEndLine:
|
|
351
|
-
newStartColumn:
|
|
352
|
-
newEndColumn:
|
|
353
|
-
reason:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|