@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/index.cjs
CHANGED
|
@@ -31,20 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
));
|
|
32
32
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
33
|
|
|
34
|
-
// src/lib/
|
|
35
|
-
var parser_exports = {};
|
|
36
|
-
__export(parser_exports, {
|
|
37
|
-
parseSidecar: () => parseSidecar,
|
|
38
|
-
parseSidecarContent: () => parseSidecarContent,
|
|
39
|
-
parseSidecarContentLenient: () => parseSidecarContentLenient,
|
|
40
|
-
parseSidecarLenient: () => parseSidecarLenient,
|
|
41
|
-
readDocumentLines: () => readDocumentLines
|
|
42
|
-
});
|
|
43
|
-
async function parseSidecar(filePath) {
|
|
44
|
-
const abs = import_node_path3.default.resolve(filePath);
|
|
45
|
-
const content = await (0, import_promises2.readFile)(abs, "utf-8");
|
|
46
|
-
return parseSidecarContent(content, abs);
|
|
47
|
-
}
|
|
34
|
+
// src/lib/serialize.ts
|
|
48
35
|
function parseSidecarContent(content, filenameHint) {
|
|
49
36
|
const trimmed = content.trim();
|
|
50
37
|
let parsed;
|
|
@@ -67,16 +54,6 @@ function parseSidecarContent(content, filenameHint) {
|
|
|
67
54
|
}
|
|
68
55
|
return parsed;
|
|
69
56
|
}
|
|
70
|
-
async function parseSidecarLenient(filePath) {
|
|
71
|
-
const abs = import_node_path3.default.resolve(filePath);
|
|
72
|
-
let content;
|
|
73
|
-
try {
|
|
74
|
-
content = await (0, import_promises2.readFile)(abs, "utf-8");
|
|
75
|
-
} catch (e) {
|
|
76
|
-
return { doc: null, error: `Cannot read file: ${e.message}` };
|
|
77
|
-
}
|
|
78
|
-
return parseSidecarContentLenient(content, abs);
|
|
79
|
-
}
|
|
80
57
|
function parseSidecarContentLenient(content, filenameHint) {
|
|
81
58
|
const trimmed = content.trim();
|
|
82
59
|
if (!trimmed) {
|
|
@@ -163,18 +140,59 @@ function salvageYaml(content) {
|
|
|
163
140
|
partialComments: salvaged.length > 0 ? salvaged : void 0
|
|
164
141
|
};
|
|
165
142
|
}
|
|
143
|
+
function toYaml(doc) {
|
|
144
|
+
const yamlDoc = new import_yaml.Document(doc);
|
|
145
|
+
return yamlDoc.toString({ lineWidth: 0 });
|
|
146
|
+
}
|
|
147
|
+
function toJson(doc) {
|
|
148
|
+
return JSON.stringify(doc, null, 2) + "\n";
|
|
149
|
+
}
|
|
150
|
+
var import_js_yaml2, import_yaml;
|
|
151
|
+
var init_serialize = __esm({
|
|
152
|
+
"src/lib/serialize.ts"() {
|
|
153
|
+
"use strict";
|
|
154
|
+
import_js_yaml2 = __toESM(require("js-yaml"), 1);
|
|
155
|
+
import_yaml = require("yaml");
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// src/lib/parser.ts
|
|
160
|
+
var parser_exports = {};
|
|
161
|
+
__export(parser_exports, {
|
|
162
|
+
parseSidecar: () => parseSidecar,
|
|
163
|
+
parseSidecarContent: () => parseSidecarContent,
|
|
164
|
+
parseSidecarContentLenient: () => parseSidecarContentLenient,
|
|
165
|
+
parseSidecarLenient: () => parseSidecarLenient,
|
|
166
|
+
readDocumentLines: () => readDocumentLines
|
|
167
|
+
});
|
|
168
|
+
async function parseSidecar(filePath) {
|
|
169
|
+
const abs = import_node_path3.default.resolve(filePath);
|
|
170
|
+
const content = await (0, import_promises2.readFile)(abs, "utf-8");
|
|
171
|
+
return parseSidecarContent(content, abs);
|
|
172
|
+
}
|
|
173
|
+
async function parseSidecarLenient(filePath) {
|
|
174
|
+
const abs = import_node_path3.default.resolve(filePath);
|
|
175
|
+
let content;
|
|
176
|
+
try {
|
|
177
|
+
content = await (0, import_promises2.readFile)(abs, "utf-8");
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return { doc: null, error: `Cannot read file: ${e.message}` };
|
|
180
|
+
}
|
|
181
|
+
return parseSidecarContentLenient(content, abs);
|
|
182
|
+
}
|
|
166
183
|
async function readDocumentLines(filePath) {
|
|
167
184
|
const content = await (0, import_promises2.readFile)(import_node_path3.default.resolve(filePath), "utf-8");
|
|
168
185
|
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
169
186
|
return ["", ...lines];
|
|
170
187
|
}
|
|
171
|
-
var import_promises2, import_node_path3
|
|
188
|
+
var import_promises2, import_node_path3;
|
|
172
189
|
var init_parser = __esm({
|
|
173
190
|
"src/lib/parser.ts"() {
|
|
174
191
|
"use strict";
|
|
175
192
|
import_promises2 = require("node:fs/promises");
|
|
176
193
|
import_node_path3 = __toESM(require("node:path"), 1);
|
|
177
|
-
|
|
194
|
+
init_serialize();
|
|
195
|
+
init_serialize();
|
|
178
196
|
}
|
|
179
197
|
});
|
|
180
198
|
|
|
@@ -185,8 +203,11 @@ __export(index_exports, {
|
|
|
185
203
|
HIGH_THRESHOLD: () => HIGH_THRESHOLD2,
|
|
186
204
|
addComment: () => addComment2,
|
|
187
205
|
applyReanchorResults: () => applyReanchorResults2,
|
|
206
|
+
calibrateAnchorEvidence: () => calibrateAnchorEvidence2,
|
|
188
207
|
combinedScore: () => combinedScore2,
|
|
189
208
|
computeHash: () => computeHash2,
|
|
209
|
+
createAnchorContextIndex: () => createAnchorContextIndex2,
|
|
210
|
+
createRevisionProjection: () => createRevisionProjection2,
|
|
190
211
|
detectRenames: () => detectRenames2,
|
|
191
212
|
discoverAllSidecars: () => discoverAllSidecars2,
|
|
192
213
|
discoverSidecar: () => discoverSidecar3,
|
|
@@ -199,7 +220,7 @@ __export(index_exports, {
|
|
|
199
220
|
fuzzySearch: () => fuzzySearch2,
|
|
200
221
|
getCurrentCommit: () => getCurrentCommit2,
|
|
201
222
|
getDiff: () => getDiff2,
|
|
202
|
-
getFileAtCommit: () =>
|
|
223
|
+
getFileAtCommit: () => getFileAtCommit2,
|
|
203
224
|
getLineShift: () => getLineShift4,
|
|
204
225
|
getStagedFiles: () => getStagedFiles2,
|
|
205
226
|
getThreads: () => getThreads2,
|
|
@@ -223,9 +244,11 @@ __export(index_exports, {
|
|
|
223
244
|
reanchorDocumentLines: () => reanchorDocumentLines2,
|
|
224
245
|
reanchorDocumentText: () => reanchorDocumentText2,
|
|
225
246
|
reanchorFile: () => reanchorFile2,
|
|
247
|
+
reconcileCommentAnchors: () => reconcileCommentAnchors2,
|
|
226
248
|
removeComment: () => removeComment2,
|
|
227
249
|
resolveAnchor: () => resolveAnchor2,
|
|
228
250
|
resolveComment: () => resolveComment2,
|
|
251
|
+
resolveCommit: () => resolveCommit2,
|
|
229
252
|
resolveSidecarPaths: () => resolveSidecarPaths2,
|
|
230
253
|
sidecarToDocument: () => sidecarToDocument2,
|
|
231
254
|
summarize: () => summarize2,
|
|
@@ -358,7 +381,9 @@ var import_promises3 = require("node:fs/promises");
|
|
|
358
381
|
var import_node_fs2 = require("node:fs");
|
|
359
382
|
var import_node_crypto = require("node:crypto");
|
|
360
383
|
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
361
|
-
var
|
|
384
|
+
var import_yaml2 = require("yaml");
|
|
385
|
+
init_serialize();
|
|
386
|
+
init_serialize();
|
|
362
387
|
var writeQueue = /* @__PURE__ */ new Map();
|
|
363
388
|
function enqueueWrite(abs, fn) {
|
|
364
389
|
const prev = writeQueue.get(abs) ?? Promise.resolve();
|
|
@@ -558,13 +583,6 @@ function deepEqual(a, b) {
|
|
|
558
583
|
if (typeof a === "number" && typeof b === "number") return a === b;
|
|
559
584
|
return false;
|
|
560
585
|
}
|
|
561
|
-
function toYaml(doc) {
|
|
562
|
-
const yamlDoc = new import_yaml.Document(doc);
|
|
563
|
-
return yamlDoc.toString({ lineWidth: 0 });
|
|
564
|
-
}
|
|
565
|
-
function toJson(doc) {
|
|
566
|
-
return JSON.stringify(doc, null, 2) + "\n";
|
|
567
|
-
}
|
|
568
586
|
async function writeSidecar(filePath, doc) {
|
|
569
587
|
const abs = import_node_path4.default.resolve(filePath);
|
|
570
588
|
return enqueueWrite(abs, () => writeSidecarInternal(abs, doc));
|
|
@@ -593,14 +611,14 @@ async function writeSidecarInternal(abs, doc) {
|
|
|
593
611
|
}
|
|
594
612
|
let tokens;
|
|
595
613
|
try {
|
|
596
|
-
tokens = [...new
|
|
614
|
+
tokens = [...new import_yaml2.Parser().parse(raw)];
|
|
597
615
|
} catch {
|
|
598
616
|
await atomicWriteFile(abs, toYaml(doc));
|
|
599
617
|
return;
|
|
600
618
|
}
|
|
601
619
|
let currentDoc = null;
|
|
602
620
|
try {
|
|
603
|
-
currentDoc = (0,
|
|
621
|
+
currentDoc = (0, import_yaml2.parse)(raw);
|
|
604
622
|
} catch {
|
|
605
623
|
}
|
|
606
624
|
if (currentDoc && !Array.isArray(currentDoc.comments)) {
|
|
@@ -687,7 +705,7 @@ async function writeSidecarInternal(abs, doc) {
|
|
|
687
705
|
}
|
|
688
706
|
}
|
|
689
707
|
}
|
|
690
|
-
const result = tokens.map((t) =>
|
|
708
|
+
const result = tokens.map((t) => import_yaml2.CST.stringify(t)).join("");
|
|
691
709
|
await atomicWriteFile(abs, result);
|
|
692
710
|
}
|
|
693
711
|
|
|
@@ -995,6 +1013,16 @@ async function validateFile(filePath, options = {}) {
|
|
|
995
1013
|
|
|
996
1014
|
// src/lib/fuzzy.ts
|
|
997
1015
|
var import_fastest_levenshtein = require("fastest-levenshtein");
|
|
1016
|
+
var MAX_FUZZY_CANDIDATE_LINES = 64;
|
|
1017
|
+
function createFuzzySearchIndex(lines) {
|
|
1018
|
+
const tokenPostings = /* @__PURE__ */ new Map();
|
|
1019
|
+
const trigramPostings = /* @__PURE__ */ new Map();
|
|
1020
|
+
for (let line = 1; line < lines.length; line += 1) {
|
|
1021
|
+
addPostingSignals(tokenPostings, lexicalTokens(lines[line]), line);
|
|
1022
|
+
addPostingSignals(trigramPostings, characterTrigrams(lines[line]), line);
|
|
1023
|
+
}
|
|
1024
|
+
return { lines, tokenPostings, trigramPostings };
|
|
1025
|
+
}
|
|
998
1026
|
function exactMatch(lines, needle) {
|
|
999
1027
|
if (!needle) return [];
|
|
1000
1028
|
const results = [];
|
|
@@ -1117,22 +1145,48 @@ function combinedScore(needle, candidate) {
|
|
|
1117
1145
|
}
|
|
1118
1146
|
return tScore * 0.6 + lScore * 0.4;
|
|
1119
1147
|
}
|
|
1120
|
-
function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
|
|
1121
|
-
|
|
1148
|
+
function fuzzySearch(lines, needle, threshold = 0.6, hintLine, index) {
|
|
1149
|
+
return fuzzySearchThresholds(
|
|
1150
|
+
lines,
|
|
1151
|
+
needle,
|
|
1152
|
+
[threshold],
|
|
1153
|
+
hintLine,
|
|
1154
|
+
index
|
|
1155
|
+
).get(threshold) ?? [];
|
|
1156
|
+
}
|
|
1157
|
+
function fuzzySearchThresholds(lines, needle, thresholds, hintLine, index = createFuzzySearchIndex(lines)) {
|
|
1158
|
+
const uniqueThresholds = [...new Set(thresholds)];
|
|
1159
|
+
const results = /* @__PURE__ */ new Map();
|
|
1160
|
+
if (uniqueThresholds.length === 0) return results;
|
|
1161
|
+
if (!needle) {
|
|
1162
|
+
for (const threshold of uniqueThresholds) results.set(threshold, []);
|
|
1163
|
+
return results;
|
|
1164
|
+
}
|
|
1122
1165
|
const needleLines = needle.split("\n");
|
|
1123
1166
|
const needleLineCount = needleLines.length;
|
|
1124
1167
|
const candidates = [];
|
|
1168
|
+
const minimumThreshold = Math.min(...uniqueThresholds);
|
|
1169
|
+
const candidateLines = retrieveCandidateLines(index, needle, hintLine);
|
|
1125
1170
|
const minWindow = Math.max(1, Math.floor(needleLineCount * 0.7));
|
|
1126
1171
|
const maxWindow = Math.min(
|
|
1127
1172
|
lines.length - 1,
|
|
1128
1173
|
Math.ceil(needleLineCount * 1.3) + 1
|
|
1129
1174
|
);
|
|
1130
1175
|
for (let winSize = minWindow; winSize <= maxWindow; winSize++) {
|
|
1131
|
-
|
|
1176
|
+
const startLines = /* @__PURE__ */ new Set();
|
|
1177
|
+
for (const candidateLine of candidateLines) {
|
|
1178
|
+
for (let offset = 0; offset < winSize; offset += 1) {
|
|
1179
|
+
const startLine = candidateLine - offset;
|
|
1180
|
+
if (startLine >= 1 && startLine + winSize - 1 < lines.length) {
|
|
1181
|
+
startLines.add(startLine);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
for (const startLine of startLines) {
|
|
1132
1186
|
const windowLines = lines.slice(startLine, startLine + winSize);
|
|
1133
1187
|
const windowText = windowLines.join("\n");
|
|
1134
1188
|
const score = combinedScore(needle, windowText);
|
|
1135
|
-
if (score >=
|
|
1189
|
+
if (score >= minimumThreshold) {
|
|
1136
1190
|
candidates.push({
|
|
1137
1191
|
text: windowText,
|
|
1138
1192
|
line: startLine,
|
|
@@ -1145,17 +1199,18 @@ function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
|
|
|
1145
1199
|
}
|
|
1146
1200
|
}
|
|
1147
1201
|
if (needleLineCount === 1 && needle.length < 200) {
|
|
1148
|
-
for (
|
|
1202
|
+
for (const lineNum of candidateLines) {
|
|
1149
1203
|
const line = lines[lineNum];
|
|
1150
1204
|
if (!line) continue;
|
|
1151
1205
|
const winLen = needle.length;
|
|
1152
1206
|
const minWinLen = Math.max(3, Math.floor(winLen * 0.7));
|
|
1153
1207
|
const maxWinLen = Math.min(line.length, Math.ceil(winLen * 1.3));
|
|
1154
|
-
|
|
1208
|
+
const lengths = evenlySpacedIntegers(minWinLen, maxWinLen, 7);
|
|
1209
|
+
for (const len of lengths) {
|
|
1155
1210
|
for (let col = 0; col + len <= line.length; col++) {
|
|
1156
1211
|
const sub = line.substring(col, col + len);
|
|
1157
1212
|
const score = combinedScore(needle, sub);
|
|
1158
|
-
if (score >=
|
|
1213
|
+
if (score >= minimumThreshold) {
|
|
1159
1214
|
candidates.push({
|
|
1160
1215
|
text: sub,
|
|
1161
1216
|
line: lineNum,
|
|
@@ -1169,15 +1224,83 @@ function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
|
|
|
1169
1224
|
}
|
|
1170
1225
|
}
|
|
1171
1226
|
}
|
|
1172
|
-
|
|
1227
|
+
const deduped = deduplicateCandidates(candidates);
|
|
1228
|
+
const scored = deduped.map((candidate) => ({
|
|
1229
|
+
baseScore: candidate.score,
|
|
1230
|
+
candidate: applyProximityBonus(candidate, hintLine)
|
|
1231
|
+
}));
|
|
1232
|
+
for (const threshold of uniqueThresholds) {
|
|
1233
|
+
results.set(
|
|
1234
|
+
threshold,
|
|
1235
|
+
scored.filter((item) => item.baseScore >= threshold).map((item) => item.candidate).sort((left, right) => right.score - left.score)
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
return results;
|
|
1239
|
+
}
|
|
1240
|
+
function retrieveCandidateLines(index, needle, hintLine) {
|
|
1241
|
+
const votes = /* @__PURE__ */ new Map();
|
|
1242
|
+
const lineCount = Math.max(0, index.lines.length - 1);
|
|
1243
|
+
const signals = [
|
|
1244
|
+
...postingSignals(index.tokenPostings, lexicalTokens(needle), 2),
|
|
1245
|
+
...postingSignals(index.trigramPostings, characterTrigrams(needle), 1)
|
|
1246
|
+
].sort((left, right) => left.postings.length - right.postings.length).slice(0, 16);
|
|
1247
|
+
for (const signal of signals) {
|
|
1248
|
+
const rarity = Math.log1p(lineCount / signal.postings.length);
|
|
1249
|
+
for (const line of signal.postings) {
|
|
1250
|
+
votes.set(line, (votes.get(line) ?? 0) + signal.weight * rarity);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1173
1253
|
if (hintLine != null) {
|
|
1174
|
-
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1254
|
+
for (let offset = -4; offset <= 4; offset += 1) {
|
|
1255
|
+
const line = hintLine + offset;
|
|
1256
|
+
if (line >= 1 && line <= lineCount) {
|
|
1257
|
+
votes.set(line, (votes.get(line) ?? 0) + 0.25);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (votes.size === 0) {
|
|
1262
|
+
return Array.from({ length: lineCount }, (_, index2) => index2 + 1);
|
|
1263
|
+
}
|
|
1264
|
+
return [...votes.entries()].sort(
|
|
1265
|
+
(left, right) => right[1] - left[1] || distanceFromHint(left[0], hintLine) - distanceFromHint(right[0], hintLine) || left[0] - right[0]
|
|
1266
|
+
).slice(0, MAX_FUZZY_CANDIDATE_LINES).map(([line]) => line);
|
|
1267
|
+
}
|
|
1268
|
+
function postingSignals(postings, values, weight) {
|
|
1269
|
+
return [...new Set(values)].map((value) => ({ postings: postings.get(value) ?? [], weight })).filter((signal) => signal.postings.length > 0);
|
|
1270
|
+
}
|
|
1271
|
+
function addPostingSignals(postings, values, line) {
|
|
1272
|
+
for (const value of new Set(values)) {
|
|
1273
|
+
const lines = postings.get(value);
|
|
1274
|
+
if (lines) {
|
|
1275
|
+
lines.push(line);
|
|
1276
|
+
} else {
|
|
1277
|
+
postings.set(value, [line]);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
function lexicalTokens(text) {
|
|
1282
|
+
return text.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? [];
|
|
1283
|
+
}
|
|
1284
|
+
function characterTrigrams(text) {
|
|
1285
|
+
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1286
|
+
const characters = [...normalized];
|
|
1287
|
+
if (characters.length < 3) return normalized ? [normalized] : [];
|
|
1288
|
+
const trigrams = [];
|
|
1289
|
+
for (let index = 0; index <= characters.length - 3; index += 1) {
|
|
1290
|
+
trigrams.push(characters.slice(index, index + 3).join(""));
|
|
1179
1291
|
}
|
|
1180
|
-
return
|
|
1292
|
+
return trigrams;
|
|
1293
|
+
}
|
|
1294
|
+
function evenlySpacedIntegers(minimum, maximum, count) {
|
|
1295
|
+
if (maximum <= minimum) return [minimum];
|
|
1296
|
+
const values = /* @__PURE__ */ new Set();
|
|
1297
|
+
for (let index = 0; index < count; index += 1) {
|
|
1298
|
+
values.add(Math.round(minimum + (maximum - minimum) * index / (count - 1)));
|
|
1299
|
+
}
|
|
1300
|
+
return [...values];
|
|
1301
|
+
}
|
|
1302
|
+
function distanceFromHint(line, hintLine) {
|
|
1303
|
+
return hintLine == null ? 0 : Math.abs(line - hintLine);
|
|
1181
1304
|
}
|
|
1182
1305
|
function deduplicateCandidates(candidates) {
|
|
1183
1306
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -1190,6 +1313,15 @@ function deduplicateCandidates(candidates) {
|
|
|
1190
1313
|
}
|
|
1191
1314
|
return Array.from(seen.values());
|
|
1192
1315
|
}
|
|
1316
|
+
function applyProximityBonus(candidate, hintLine) {
|
|
1317
|
+
if (hintLine == null) return candidate;
|
|
1318
|
+
const distance = Math.abs(candidate.line - hintLine);
|
|
1319
|
+
const proximityBonus = 0.1 * Math.max(0, 1 - distance / 50);
|
|
1320
|
+
return {
|
|
1321
|
+
...candidate,
|
|
1322
|
+
score: Math.min(1, candidate.score + proximityBonus)
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1193
1325
|
|
|
1194
1326
|
// src/lib/git.ts
|
|
1195
1327
|
var import_node_child_process = require("node:child_process");
|
|
@@ -1233,6 +1365,19 @@ async function getCurrentCommit(repoRoot) {
|
|
|
1233
1365
|
return null;
|
|
1234
1366
|
}
|
|
1235
1367
|
}
|
|
1368
|
+
async function resolveCommit(revision, repoRoot) {
|
|
1369
|
+
if (!await isGitAvailable()) return null;
|
|
1370
|
+
try {
|
|
1371
|
+
const { stdout } = await execFile(
|
|
1372
|
+
"git",
|
|
1373
|
+
["rev-parse", "--verify", `${revision}^{commit}`],
|
|
1374
|
+
{ cwd: repoRoot, timeout: GIT_TIMEOUT }
|
|
1375
|
+
);
|
|
1376
|
+
return stdout.trim();
|
|
1377
|
+
} catch {
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1236
1381
|
async function isStale(commentCommit, repoRoot) {
|
|
1237
1382
|
const head = await getCurrentCommit(repoRoot);
|
|
1238
1383
|
if (!head) return false;
|
|
@@ -1346,16 +1491,801 @@ async function detectRenames(fromCommit, toCommit, repoRoot) {
|
|
|
1346
1491
|
}
|
|
1347
1492
|
}
|
|
1348
1493
|
|
|
1494
|
+
// src/lib/revision-projection.ts
|
|
1495
|
+
var CONTEXT_RADIUS = 8;
|
|
1496
|
+
function createRevisionProjection(sourceLines, targetLines) {
|
|
1497
|
+
const sourceOccurrences = collectLineOccurrences(sourceLines);
|
|
1498
|
+
const targetOccurrences = collectLineOccurrences(targetLines);
|
|
1499
|
+
const lineMap = /* @__PURE__ */ new Map();
|
|
1500
|
+
for (const [text, sourceLineNumbers] of sourceOccurrences) {
|
|
1501
|
+
const targetLineNumbers = targetOccurrences.get(text);
|
|
1502
|
+
if (sourceLineNumbers.length === 1 && targetLineNumbers?.length === 1) {
|
|
1503
|
+
lineMap.set(sourceLineNumbers[0], targetLineNumbers[0]);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return { sourceLines, targetLines, lineMap };
|
|
1507
|
+
}
|
|
1508
|
+
function projectCommentAnchor(comment, projection, threshold) {
|
|
1509
|
+
if (comment.line == null || !comment.selected_text) return void 0;
|
|
1510
|
+
const sourceText = extractText(
|
|
1511
|
+
projection.sourceLines,
|
|
1512
|
+
comment.line,
|
|
1513
|
+
comment.end_line,
|
|
1514
|
+
comment.start_column,
|
|
1515
|
+
comment.end_column
|
|
1516
|
+
);
|
|
1517
|
+
if (sourceText !== comment.selected_text) return void 0;
|
|
1518
|
+
const exactCandidates = exactMatch(
|
|
1519
|
+
projection.targetLines,
|
|
1520
|
+
comment.selected_text
|
|
1521
|
+
);
|
|
1522
|
+
if (exactCandidates.length === 1) {
|
|
1523
|
+
const candidate = exactCandidates[0];
|
|
1524
|
+
const contextSupport = countIndependentExactSupport(
|
|
1525
|
+
comment,
|
|
1526
|
+
candidate.line,
|
|
1527
|
+
projection
|
|
1528
|
+
);
|
|
1529
|
+
if (contextSupport >= 2) {
|
|
1530
|
+
return {
|
|
1531
|
+
line: candidate.line,
|
|
1532
|
+
endLine: candidate.endLine,
|
|
1533
|
+
startColumn: candidate.startColumn,
|
|
1534
|
+
endColumn: candidate.endColumn,
|
|
1535
|
+
text: candidate.text,
|
|
1536
|
+
score: 1,
|
|
1537
|
+
exact: true,
|
|
1538
|
+
contextSupport,
|
|
1539
|
+
contextMargin: 1,
|
|
1540
|
+
reason: "Source revision and neighboring line evidence confirm exact relocation."
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
if (exactCandidates.length > 0) return void 0;
|
|
1545
|
+
const projected = projectLineFromContext(comment, projection);
|
|
1546
|
+
if (projected == null) return void 0;
|
|
1547
|
+
const projectedLine = projected.line;
|
|
1548
|
+
const lineSpan = (comment.end_line ?? comment.line) - comment.line;
|
|
1549
|
+
const projectedEndLine = projectedLine + lineSpan;
|
|
1550
|
+
if (projectedLine < 1 || projectedEndLine >= projection.targetLines.length) {
|
|
1551
|
+
return void 0;
|
|
1552
|
+
}
|
|
1553
|
+
const columns = projectColumns(comment, projection, projectedLine);
|
|
1554
|
+
const targetText = extractText(
|
|
1555
|
+
projection.targetLines,
|
|
1556
|
+
projectedLine,
|
|
1557
|
+
projectedEndLine,
|
|
1558
|
+
columns.startColumn,
|
|
1559
|
+
columns.endColumn
|
|
1560
|
+
);
|
|
1561
|
+
if (targetText == null) return void 0;
|
|
1562
|
+
const score = Math.min(
|
|
1563
|
+
1,
|
|
1564
|
+
combinedScore(comment.selected_text, targetText) + 0.1
|
|
1565
|
+
);
|
|
1566
|
+
if (score < threshold) return void 0;
|
|
1567
|
+
return {
|
|
1568
|
+
line: projectedLine,
|
|
1569
|
+
endLine: projectedEndLine,
|
|
1570
|
+
startColumn: columns.startColumn,
|
|
1571
|
+
endColumn: columns.endColumn,
|
|
1572
|
+
text: targetText,
|
|
1573
|
+
score,
|
|
1574
|
+
exact: targetText === comment.selected_text,
|
|
1575
|
+
contextSupport: projected.support,
|
|
1576
|
+
contextMargin: projected.margin,
|
|
1577
|
+
reason: "Source revision context projects the edited anchor range."
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function collectLineOccurrences(lines) {
|
|
1581
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
1582
|
+
for (let line = 1; line < lines.length; line += 1) {
|
|
1583
|
+
const text = lines[line];
|
|
1584
|
+
if (!text.trim()) continue;
|
|
1585
|
+
const existing = occurrences.get(text);
|
|
1586
|
+
if (existing) {
|
|
1587
|
+
existing.push(line);
|
|
1588
|
+
} else {
|
|
1589
|
+
occurrences.set(text, [line]);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return occurrences;
|
|
1593
|
+
}
|
|
1594
|
+
function countIndependentExactSupport(comment, candidateLine, projection) {
|
|
1595
|
+
const sourceEndLine = comment.end_line ?? comment.line;
|
|
1596
|
+
const expectedShift = candidateLine - comment.line;
|
|
1597
|
+
if (comment.start_column != null || comment.end_column != null) {
|
|
1598
|
+
const mappedContainerLine = projection.lineMap.get(comment.line);
|
|
1599
|
+
if (mappedContainerLine === candidateLine) return 2;
|
|
1600
|
+
}
|
|
1601
|
+
let support = 0;
|
|
1602
|
+
for (let distance = 1; distance <= CONTEXT_RADIUS; distance += 1) {
|
|
1603
|
+
for (const sourceLine of [
|
|
1604
|
+
comment.line - distance,
|
|
1605
|
+
sourceEndLine + distance
|
|
1606
|
+
]) {
|
|
1607
|
+
if (sourceLine < 1 || sourceLine >= projection.sourceLines.length) {
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
const targetLine = projection.lineMap.get(sourceLine);
|
|
1611
|
+
if (targetLine != null && targetLine - sourceLine === expectedShift) {
|
|
1612
|
+
support += 1;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return support;
|
|
1617
|
+
}
|
|
1618
|
+
function projectLineFromContext(comment, projection) {
|
|
1619
|
+
const sourceLine = comment.line;
|
|
1620
|
+
const sourceEndLine = comment.end_line ?? sourceLine;
|
|
1621
|
+
const votes = /* @__PURE__ */ new Map();
|
|
1622
|
+
for (let distance = 1; distance <= CONTEXT_RADIUS; distance += 1) {
|
|
1623
|
+
for (const contextLine of [
|
|
1624
|
+
sourceLine - distance,
|
|
1625
|
+
sourceEndLine + distance
|
|
1626
|
+
]) {
|
|
1627
|
+
if (contextLine < 1 || contextLine >= projection.sourceLines.length) {
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
const targetLine = projection.lineMap.get(contextLine);
|
|
1631
|
+
if (targetLine == null) continue;
|
|
1632
|
+
const shift = targetLine - contextLine;
|
|
1633
|
+
const vote = votes.get(shift);
|
|
1634
|
+
if (vote) {
|
|
1635
|
+
vote.count += 1;
|
|
1636
|
+
vote.nearest = Math.min(vote.nearest, distance);
|
|
1637
|
+
} else {
|
|
1638
|
+
votes.set(shift, { count: 1, nearest: distance });
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const ranked = [...votes.entries()].sort(
|
|
1643
|
+
(left, right) => right[1].count - left[1].count || left[1].nearest - right[1].nearest || Math.abs(left[0]) - Math.abs(right[0])
|
|
1644
|
+
);
|
|
1645
|
+
const best = ranked[0];
|
|
1646
|
+
if (!best) return void 0;
|
|
1647
|
+
if (best[1].count < 2 && best[1].nearest > 3) return void 0;
|
|
1648
|
+
if (ranked[1] && ranked[1][1].count === best[1].count && ranked[1][1].nearest === best[1].nearest) {
|
|
1649
|
+
return void 0;
|
|
1650
|
+
}
|
|
1651
|
+
const runnerUpCount = ranked[1]?.[1].count ?? 0;
|
|
1652
|
+
return {
|
|
1653
|
+
line: sourceLine + best[0],
|
|
1654
|
+
support: best[1].count,
|
|
1655
|
+
margin: (best[1].count - runnerUpCount) / best[1].count
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
function projectColumns(comment, projection, targetLine) {
|
|
1659
|
+
if (comment.line == null || comment.end_line != null && comment.end_line !== comment.line || comment.start_column == null || comment.end_column == null) {
|
|
1660
|
+
return {
|
|
1661
|
+
startColumn: comment.start_column,
|
|
1662
|
+
endColumn: comment.end_column
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
const sourceLine = projection.sourceLines[comment.line];
|
|
1666
|
+
const targetLineText = projection.targetLines[targetLine];
|
|
1667
|
+
const prefix = sourceLine.slice(0, comment.start_column);
|
|
1668
|
+
const suffix = sourceLine.slice(comment.end_column);
|
|
1669
|
+
const startColumn = targetLineText.startsWith(prefix) ? prefix.length : Math.min(comment.start_column, targetLineText.length);
|
|
1670
|
+
const endColumn = targetLineText.endsWith(suffix) ? targetLineText.length - suffix.length : Math.min(
|
|
1671
|
+
targetLineText.length,
|
|
1672
|
+
startColumn + (comment.end_column - comment.start_column)
|
|
1673
|
+
);
|
|
1674
|
+
return { startColumn, endColumn };
|
|
1675
|
+
}
|
|
1676
|
+
function extractText(lines, line, endLine, startColumn, endColumn) {
|
|
1677
|
+
const finalLine = endLine ?? line;
|
|
1678
|
+
if (line < 1 || finalLine >= lines.length) return null;
|
|
1679
|
+
if (line === finalLine) {
|
|
1680
|
+
const text = lines[line];
|
|
1681
|
+
return startColumn != null && endColumn != null ? text.slice(startColumn, endColumn) : text;
|
|
1682
|
+
}
|
|
1683
|
+
const result = [];
|
|
1684
|
+
for (let current = line; current <= finalLine; current += 1) {
|
|
1685
|
+
let text = lines[current];
|
|
1686
|
+
if (current === line && startColumn != null) text = text.slice(startColumn);
|
|
1687
|
+
if (current === finalLine && endColumn != null) text = text.slice(0, endColumn);
|
|
1688
|
+
result.push(text);
|
|
1689
|
+
}
|
|
1690
|
+
return result.join("\n");
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// src/lib/anchor-context.ts
|
|
1694
|
+
var MATCH_THRESHOLD = 0.35;
|
|
1695
|
+
var AMBIGUITY_MARGIN = 0.03;
|
|
1696
|
+
var MAX_CONTEXT_CANDIDATE_BLOCKS = 64;
|
|
1697
|
+
function createAnchorContextIndex(sourceLines, targetLines) {
|
|
1698
|
+
return {
|
|
1699
|
+
source: createDocumentBlockIndex(sourceLines),
|
|
1700
|
+
target: createDocumentBlockIndex(targetLines)
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
function resolveContextAnchor(comment, index) {
|
|
1704
|
+
if (comment.line == null || !comment.selected_text) return void 0;
|
|
1705
|
+
const sourceBlockIndex = index.source.lineToBlock.get(comment.line);
|
|
1706
|
+
if (sourceBlockIndex == null) return void 0;
|
|
1707
|
+
const sourceBlock = index.source.blocks[sourceBlockIndex];
|
|
1708
|
+
const sourceText = extractText2(
|
|
1709
|
+
index.source.lines,
|
|
1710
|
+
comment.line,
|
|
1711
|
+
comment.end_line,
|
|
1712
|
+
comment.start_column,
|
|
1713
|
+
comment.end_column
|
|
1714
|
+
);
|
|
1715
|
+
if (sourceText !== comment.selected_text) return void 0;
|
|
1716
|
+
const textAtCurrentPosition = extractText2(
|
|
1717
|
+
index.target.lines,
|
|
1718
|
+
comment.line,
|
|
1719
|
+
comment.end_line,
|
|
1720
|
+
comment.start_column,
|
|
1721
|
+
comment.end_column
|
|
1722
|
+
);
|
|
1723
|
+
if (textAtCurrentPosition === comment.selected_text) {
|
|
1724
|
+
return {
|
|
1725
|
+
status: "anchored",
|
|
1726
|
+
score: 1,
|
|
1727
|
+
line: comment.line,
|
|
1728
|
+
endLine: comment.end_line ?? comment.line,
|
|
1729
|
+
startColumn: comment.start_column,
|
|
1730
|
+
endColumn: comment.end_column,
|
|
1731
|
+
text: comment.selected_text,
|
|
1732
|
+
candidateMargin: 1,
|
|
1733
|
+
reason: "Source-verified anchor remains exact at its stored position."
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
const candidates = findContextAnchorCandidates(comment, index);
|
|
1737
|
+
const best = candidates[0];
|
|
1738
|
+
if (!best) {
|
|
1739
|
+
if (index.target.lines.slice(1).join("\n").includes(comment.selected_text)) {
|
|
1740
|
+
return void 0;
|
|
1741
|
+
}
|
|
1742
|
+
return {
|
|
1743
|
+
status: "orphaned",
|
|
1744
|
+
score: 0,
|
|
1745
|
+
candidateMargin: 1,
|
|
1746
|
+
reason: "Source block has no plausible structural or contextual match."
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
if (candidates[1] && best.score - candidates[1].score < AMBIGUITY_MARGIN) {
|
|
1750
|
+
return {
|
|
1751
|
+
status: "ambiguous",
|
|
1752
|
+
score: best.score,
|
|
1753
|
+
line: best.line,
|
|
1754
|
+
endLine: best.endLine,
|
|
1755
|
+
candidateMargin: best.score - candidates[1].score,
|
|
1756
|
+
reason: `Structural candidates are too close (${best.score.toFixed(3)} vs ${candidates[1].score.toFixed(3)}).`
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
const repeatedExactText = best.exact && countOccurrences(
|
|
1760
|
+
index.target.lines.slice(1).join("\n"),
|
|
1761
|
+
comment.selected_text
|
|
1762
|
+
) > 1;
|
|
1763
|
+
return {
|
|
1764
|
+
status: best.exact && !repeatedExactText ? "anchored" : "fuzzy",
|
|
1765
|
+
score: best.exact && !repeatedExactText ? 1 : best.score,
|
|
1766
|
+
line: best.line,
|
|
1767
|
+
endLine: best.endLine,
|
|
1768
|
+
startColumn: best.startColumn,
|
|
1769
|
+
endColumn: best.endColumn,
|
|
1770
|
+
text: best.text,
|
|
1771
|
+
candidateMargin: candidates[1] ? best.score - candidates[1].score : 1,
|
|
1772
|
+
reason: best.exact && !repeatedExactText ? "Markdown structure and bidirectional context disambiguate the exact anchor." : repeatedExactText ? "Markdown context selects one repeated exact anchor tentatively." : "Markdown structure and bidirectional context locate the edited anchor."
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
function findContextAnchorCandidates(comment, index) {
|
|
1776
|
+
if (comment.line == null || !comment.selected_text) return [];
|
|
1777
|
+
const sourceBlockIndex = index.source.lineToBlock.get(comment.line);
|
|
1778
|
+
if (sourceBlockIndex == null) return [];
|
|
1779
|
+
const sourceBlock = index.source.blocks[sourceBlockIndex];
|
|
1780
|
+
const sourceText = extractText2(
|
|
1781
|
+
index.source.lines,
|
|
1782
|
+
comment.line,
|
|
1783
|
+
comment.end_line,
|
|
1784
|
+
comment.start_column,
|
|
1785
|
+
comment.end_column
|
|
1786
|
+
);
|
|
1787
|
+
if (sourceText !== comment.selected_text) return [];
|
|
1788
|
+
return createCandidateWindows(
|
|
1789
|
+
sourceBlock,
|
|
1790
|
+
sourceBlockIndex,
|
|
1791
|
+
comment.line,
|
|
1792
|
+
index
|
|
1793
|
+
).map((candidate) => ({
|
|
1794
|
+
...candidate,
|
|
1795
|
+
score: scoreCandidate(
|
|
1796
|
+
sourceBlock,
|
|
1797
|
+
sourceBlockIndex,
|
|
1798
|
+
candidate,
|
|
1799
|
+
index,
|
|
1800
|
+
comment.line
|
|
1801
|
+
)
|
|
1802
|
+
})).filter((candidate) => candidate.score >= MATCH_THRESHOLD).sort(
|
|
1803
|
+
(left, right) => right.score - left.score || Math.abs(left.startLine - comment.line) - Math.abs(right.startLine - comment.line)
|
|
1804
|
+
).map((candidate) => {
|
|
1805
|
+
const range = resolveCandidateRange(
|
|
1806
|
+
comment,
|
|
1807
|
+
sourceBlock,
|
|
1808
|
+
candidate,
|
|
1809
|
+
index.target
|
|
1810
|
+
);
|
|
1811
|
+
return {
|
|
1812
|
+
score: candidate.score,
|
|
1813
|
+
line: range.line,
|
|
1814
|
+
endLine: range.endLine,
|
|
1815
|
+
startColumn: range.startColumn,
|
|
1816
|
+
endColumn: range.endColumn,
|
|
1817
|
+
text: range.text,
|
|
1818
|
+
exact: range.text === comment.selected_text
|
|
1819
|
+
};
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
function getAnchorContextScope(comment, index) {
|
|
1823
|
+
if (comment.line == null) return void 0;
|
|
1824
|
+
const blockIndex = index.source.lineToBlock.get(comment.line);
|
|
1825
|
+
if (blockIndex == null) return void 0;
|
|
1826
|
+
return index.source.blocks[blockIndex].headingPath.join("");
|
|
1827
|
+
}
|
|
1828
|
+
function createDocumentBlockIndex(lines) {
|
|
1829
|
+
const blocks = [];
|
|
1830
|
+
const lineToBlock = /* @__PURE__ */ new Map();
|
|
1831
|
+
const headings = [];
|
|
1832
|
+
let line = 1;
|
|
1833
|
+
while (line < lines.length) {
|
|
1834
|
+
if (!lines[line].trim()) {
|
|
1835
|
+
line += 1;
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
const startLine = line;
|
|
1839
|
+
const marker = classifyLine(lines[line]);
|
|
1840
|
+
if (marker.type === "heading") {
|
|
1841
|
+
while (headings.length > 0 && headings[headings.length - 1].level >= marker.headingLevel) {
|
|
1842
|
+
headings.pop();
|
|
1843
|
+
}
|
|
1844
|
+
const block = makeBlock(
|
|
1845
|
+
lines,
|
|
1846
|
+
startLine,
|
|
1847
|
+
startLine,
|
|
1848
|
+
"heading",
|
|
1849
|
+
headings.map((heading) => heading.title)
|
|
1850
|
+
);
|
|
1851
|
+
blocks.push(block);
|
|
1852
|
+
headings.push({ level: marker.headingLevel, title: marker.headingTitle });
|
|
1853
|
+
line += 1;
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
if (marker.type === "code") {
|
|
1857
|
+
line += 1;
|
|
1858
|
+
while (line < lines.length && !lines[line].trimStart().startsWith("```")) {
|
|
1859
|
+
line += 1;
|
|
1860
|
+
}
|
|
1861
|
+
if (line < lines.length) line += 1;
|
|
1862
|
+
} else {
|
|
1863
|
+
line += 1;
|
|
1864
|
+
while (line < lines.length && lines[line].trim() && continuesBlock(marker.type, lines[line])) {
|
|
1865
|
+
line += 1;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
blocks.push(makeBlock(
|
|
1869
|
+
lines,
|
|
1870
|
+
startLine,
|
|
1871
|
+
line - 1,
|
|
1872
|
+
marker.type,
|
|
1873
|
+
headings.map((heading) => heading.title)
|
|
1874
|
+
));
|
|
1875
|
+
}
|
|
1876
|
+
for (const [blockIndex, block] of blocks.entries()) {
|
|
1877
|
+
for (let blockLine = block.startLine; blockLine <= block.endLine; blockLine += 1) {
|
|
1878
|
+
lineToBlock.set(blockLine, blockIndex);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return {
|
|
1882
|
+
lines,
|
|
1883
|
+
blocks,
|
|
1884
|
+
lineToBlock,
|
|
1885
|
+
tokenPostings: createTokenPostings(blocks)
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
function makeBlock(lines, startLine, endLine, type, headingPath) {
|
|
1889
|
+
return {
|
|
1890
|
+
startLine,
|
|
1891
|
+
endLine,
|
|
1892
|
+
type,
|
|
1893
|
+
text: lines.slice(startLine, endLine + 1).join("\n"),
|
|
1894
|
+
headingPath
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
function classifyLine(line) {
|
|
1898
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
1899
|
+
if (heading) {
|
|
1900
|
+
return {
|
|
1901
|
+
type: "heading",
|
|
1902
|
+
headingLevel: heading[1].length,
|
|
1903
|
+
headingTitle: heading[2].trim()
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
if (line.trimStart().startsWith("```")) {
|
|
1907
|
+
return { type: "code", headingLevel: 0, headingTitle: "" };
|
|
1908
|
+
}
|
|
1909
|
+
if (/^\s*(?:[-*+]|\d+\.)\s+/.test(line)) {
|
|
1910
|
+
return { type: "list", headingLevel: 0, headingTitle: "" };
|
|
1911
|
+
}
|
|
1912
|
+
if (/^\s*\|/.test(line)) {
|
|
1913
|
+
return { type: "table", headingLevel: 0, headingTitle: "" };
|
|
1914
|
+
}
|
|
1915
|
+
if (/^\s*>/.test(line)) {
|
|
1916
|
+
return { type: "blockquote", headingLevel: 0, headingTitle: "" };
|
|
1917
|
+
}
|
|
1918
|
+
return { type: "paragraph", headingLevel: 0, headingTitle: "" };
|
|
1919
|
+
}
|
|
1920
|
+
function continuesBlock(type, line) {
|
|
1921
|
+
if (type === "list") return /^\s*(?:[-*+]|\d+\.)\s+/.test(line);
|
|
1922
|
+
if (type === "table") return /^\s*\|/.test(line);
|
|
1923
|
+
if (type === "blockquote") return /^\s*>/.test(line);
|
|
1924
|
+
if (type === "paragraph") {
|
|
1925
|
+
const next = classifyLine(line);
|
|
1926
|
+
return next.type === "paragraph";
|
|
1927
|
+
}
|
|
1928
|
+
return false;
|
|
1929
|
+
}
|
|
1930
|
+
function createCandidateWindows(sourceBlock, sourceBlockIndex, originalLine, index) {
|
|
1931
|
+
const target = index.target;
|
|
1932
|
+
const candidates = [];
|
|
1933
|
+
for (const start of retrieveCandidateBlocks(
|
|
1934
|
+
sourceBlock,
|
|
1935
|
+
sourceBlockIndex,
|
|
1936
|
+
originalLine,
|
|
1937
|
+
index
|
|
1938
|
+
)) {
|
|
1939
|
+
const block = target.blocks[start];
|
|
1940
|
+
candidates.push(toCandidateWindow(target, start, start));
|
|
1941
|
+
if (sourceBlock.type === "paragraph" && block.type === "paragraph" && target.blocks[start + 1]?.type === "paragraph" && samePath(block.headingPath, target.blocks[start + 1].headingPath)) {
|
|
1942
|
+
candidates.push(toCandidateWindow(target, start, start + 1));
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
return candidates;
|
|
1946
|
+
}
|
|
1947
|
+
function createTokenPostings(blocks) {
|
|
1948
|
+
const postings = /* @__PURE__ */ new Map();
|
|
1949
|
+
for (const [blockIndex, block] of blocks.entries()) {
|
|
1950
|
+
for (const token of new Set(tokenize2(block.text))) {
|
|
1951
|
+
const blocksForToken = postings.get(token);
|
|
1952
|
+
if (blocksForToken) {
|
|
1953
|
+
blocksForToken.push(blockIndex);
|
|
1954
|
+
} else {
|
|
1955
|
+
postings.set(token, [blockIndex]);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
return postings;
|
|
1960
|
+
}
|
|
1961
|
+
function retrieveCandidateBlocks(sourceBlock, sourceBlockIndex, originalLine, index) {
|
|
1962
|
+
const votes = /* @__PURE__ */ new Map();
|
|
1963
|
+
const targetCount = index.target.blocks.length;
|
|
1964
|
+
const addEvidence = (text, targetOffset, weight) => {
|
|
1965
|
+
if (!text) return;
|
|
1966
|
+
const rankedTokens = [...new Set(tokenize2(text))].map((token) => ({
|
|
1967
|
+
token,
|
|
1968
|
+
postings: index.target.tokenPostings.get(token) ?? []
|
|
1969
|
+
})).filter((item) => item.postings.length > 0).sort((left, right) => left.postings.length - right.postings.length).slice(0, 12);
|
|
1970
|
+
for (const { postings } of rankedTokens) {
|
|
1971
|
+
const rarity = Math.log1p(targetCount / postings.length);
|
|
1972
|
+
for (const posting of postings) {
|
|
1973
|
+
const candidate = posting + targetOffset;
|
|
1974
|
+
if (candidate >= 0 && candidate < targetCount) {
|
|
1975
|
+
votes.set(candidate, (votes.get(candidate) ?? 0) + weight * rarity);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
addEvidence(sourceBlock.text, 0, 1);
|
|
1981
|
+
addEvidence(index.source.blocks[sourceBlockIndex - 1]?.text, 1, 0.7);
|
|
1982
|
+
addEvidence(index.source.blocks[sourceBlockIndex + 1]?.text, -1, 0.7);
|
|
1983
|
+
const nearbyBlock = closestBlockToLine(index.target.blocks, originalLine);
|
|
1984
|
+
if (nearbyBlock != null) {
|
|
1985
|
+
for (let offset = -2; offset <= 2; offset += 1) {
|
|
1986
|
+
const candidate = nearbyBlock + offset;
|
|
1987
|
+
if (candidate >= 0 && candidate < targetCount) {
|
|
1988
|
+
votes.set(candidate, (votes.get(candidate) ?? 0) + 0.25);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return [...votes.entries()].sort(
|
|
1993
|
+
(left, right) => right[1] - left[1] || Math.abs(index.target.blocks[left[0]].startLine - originalLine) - Math.abs(index.target.blocks[right[0]].startLine - originalLine) || left[0] - right[0]
|
|
1994
|
+
).slice(0, MAX_CONTEXT_CANDIDATE_BLOCKS).map(([blockIndex]) => blockIndex);
|
|
1995
|
+
}
|
|
1996
|
+
function closestBlockToLine(blocks, line) {
|
|
1997
|
+
if (blocks.length === 0) return void 0;
|
|
1998
|
+
let low = 0;
|
|
1999
|
+
let high = blocks.length - 1;
|
|
2000
|
+
while (low <= high) {
|
|
2001
|
+
const middle = Math.floor((low + high) / 2);
|
|
2002
|
+
const block = blocks[middle];
|
|
2003
|
+
if (line < block.startLine) {
|
|
2004
|
+
high = middle - 1;
|
|
2005
|
+
} else if (line > block.endLine) {
|
|
2006
|
+
low = middle + 1;
|
|
2007
|
+
} else {
|
|
2008
|
+
return middle;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
if (low >= blocks.length) return blocks.length - 1;
|
|
2012
|
+
if (high < 0) return 0;
|
|
2013
|
+
return Math.abs(blocks[low].startLine - line) < Math.abs(blocks[high].endLine - line) ? low : high;
|
|
2014
|
+
}
|
|
2015
|
+
function toCandidateWindow(target, startBlock, endBlock) {
|
|
2016
|
+
const first = target.blocks[startBlock];
|
|
2017
|
+
const last = target.blocks[endBlock];
|
|
2018
|
+
return {
|
|
2019
|
+
startBlock,
|
|
2020
|
+
endBlock,
|
|
2021
|
+
startLine: first.startLine,
|
|
2022
|
+
endLine: last.endLine,
|
|
2023
|
+
type: first.type,
|
|
2024
|
+
text: target.lines.slice(first.startLine, last.endLine + 1).join("\n"),
|
|
2025
|
+
headingPath: first.headingPath,
|
|
2026
|
+
score: 0
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
function scoreCandidate(sourceBlock, sourceBlockIndex, candidate, index, originalLine) {
|
|
2030
|
+
const content = textSimilarity(sourceBlock.text, candidate.text);
|
|
2031
|
+
const type = sourceBlock.type === candidate.type ? 1 : 0;
|
|
2032
|
+
const heading = pathSimilarity(sourceBlock.headingPath, candidate.headingPath);
|
|
2033
|
+
const previous = neighborSimilarity(
|
|
2034
|
+
index.source.blocks[sourceBlockIndex - 1],
|
|
2035
|
+
index.target.blocks[candidate.startBlock - 1]
|
|
2036
|
+
);
|
|
2037
|
+
const next = neighborSimilarity(
|
|
2038
|
+
index.source.blocks[sourceBlockIndex + 1],
|
|
2039
|
+
index.target.blocks[candidate.endBlock + 1]
|
|
2040
|
+
);
|
|
2041
|
+
const proximity = Math.max(
|
|
2042
|
+
0,
|
|
2043
|
+
1 - Math.abs(candidate.startLine - originalLine) / 100
|
|
2044
|
+
);
|
|
2045
|
+
return Math.min(
|
|
2046
|
+
1,
|
|
2047
|
+
content * 0.45 + type * 0.1 + heading * 0.1 + previous * 0.15 + next * 0.15 + proximity * 0.05
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
function neighborSimilarity(source, target) {
|
|
2051
|
+
if (!source || !target) return 0;
|
|
2052
|
+
return textSimilarity(source.text, target.text);
|
|
2053
|
+
}
|
|
2054
|
+
function textSimilarity(left, right) {
|
|
2055
|
+
return Math.max(
|
|
2056
|
+
tokenDice(left, right),
|
|
2057
|
+
combinedScore(normalizeText(left), normalizeText(right))
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
function pathSimilarity(left, right) {
|
|
2061
|
+
if (left.length === 0 || right.length === 0) return 0;
|
|
2062
|
+
return tokenDice(left.join(" "), right.join(" "));
|
|
2063
|
+
}
|
|
2064
|
+
function tokenDice(left, right) {
|
|
2065
|
+
const leftTokens = tokenize2(left);
|
|
2066
|
+
const rightTokens = tokenize2(right);
|
|
2067
|
+
if (leftTokens.length === 0 || rightTokens.length === 0) return 0;
|
|
2068
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
2069
|
+
for (const token of rightTokens) {
|
|
2070
|
+
remaining.set(token, (remaining.get(token) ?? 0) + 1);
|
|
2071
|
+
}
|
|
2072
|
+
let overlap = 0;
|
|
2073
|
+
for (const token of leftTokens) {
|
|
2074
|
+
const count = remaining.get(token) ?? 0;
|
|
2075
|
+
if (count > 0) {
|
|
2076
|
+
overlap += 1;
|
|
2077
|
+
remaining.set(token, count - 1);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
return 2 * overlap / (leftTokens.length + rightTokens.length);
|
|
2081
|
+
}
|
|
2082
|
+
function tokenize2(text) {
|
|
2083
|
+
return normalizeText(text).match(/[\p{L}\p{N}_-]+/gu) ?? [];
|
|
2084
|
+
}
|
|
2085
|
+
function normalizeText(text) {
|
|
2086
|
+
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2087
|
+
}
|
|
2088
|
+
function samePath(left, right) {
|
|
2089
|
+
return left.length === right.length && left.every((item, index) => item === right[index]);
|
|
2090
|
+
}
|
|
2091
|
+
function resolveCandidateRange(comment, sourceBlock, candidate, target) {
|
|
2092
|
+
const exactIndex = candidate.text.indexOf(comment.selected_text);
|
|
2093
|
+
const selectedIsWholeBlock = comment.line === sourceBlock.startLine && (comment.end_line ?? comment.line) === sourceBlock.endLine && comment.start_column == null && comment.end_column == null && comment.selected_text === sourceBlock.text;
|
|
2094
|
+
const sourceOccurrenceIsUnique = countOccurrences(sourceBlock.text, comment.selected_text) === 1;
|
|
2095
|
+
if (exactIndex >= 0 && (selectedIsWholeBlock || sourceOccurrenceIsUnique)) {
|
|
2096
|
+
return exactRange(
|
|
2097
|
+
candidate.startLine,
|
|
2098
|
+
candidate.text,
|
|
2099
|
+
comment.selected_text,
|
|
2100
|
+
exactIndex
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
if (selectedIsWholeBlock) {
|
|
2104
|
+
return {
|
|
2105
|
+
line: candidate.startLine,
|
|
2106
|
+
endLine: candidate.endLine,
|
|
2107
|
+
text: candidate.text
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
const relativeLine = comment.line - sourceBlock.startLine;
|
|
2111
|
+
const line = Math.min(candidate.endLine, candidate.startLine + relativeLine);
|
|
2112
|
+
const lineSpan = (comment.end_line ?? comment.line) - comment.line;
|
|
2113
|
+
const endLine = Math.min(candidate.endLine, line + lineSpan);
|
|
2114
|
+
const startColumn = comment.start_column;
|
|
2115
|
+
const endColumn = comment.end_column;
|
|
2116
|
+
return {
|
|
2117
|
+
line,
|
|
2118
|
+
endLine,
|
|
2119
|
+
startColumn,
|
|
2120
|
+
endColumn,
|
|
2121
|
+
text: extractText2(
|
|
2122
|
+
target.lines,
|
|
2123
|
+
line,
|
|
2124
|
+
endLine,
|
|
2125
|
+
startColumn,
|
|
2126
|
+
endColumn
|
|
2127
|
+
) ?? ""
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
function countOccurrences(text, needle) {
|
|
2131
|
+
if (!needle) return 0;
|
|
2132
|
+
let count = 0;
|
|
2133
|
+
let offset = 0;
|
|
2134
|
+
while (offset <= text.length - needle.length) {
|
|
2135
|
+
const index = text.indexOf(needle, offset);
|
|
2136
|
+
if (index < 0) break;
|
|
2137
|
+
count += 1;
|
|
2138
|
+
offset = index + 1;
|
|
2139
|
+
}
|
|
2140
|
+
return count;
|
|
2141
|
+
}
|
|
2142
|
+
function exactRange(startLine, candidateText, selectedText, index) {
|
|
2143
|
+
const before = candidateText.slice(0, index).split("\n");
|
|
2144
|
+
const selectedLines = selectedText.split("\n");
|
|
2145
|
+
const line = startLine + before.length - 1;
|
|
2146
|
+
const startColumn = before.at(-1)?.length ?? 0;
|
|
2147
|
+
const finalSelectedLineLength = selectedLines.at(-1)?.length ?? 0;
|
|
2148
|
+
return {
|
|
2149
|
+
line,
|
|
2150
|
+
endLine: line + selectedLines.length - 1,
|
|
2151
|
+
startColumn,
|
|
2152
|
+
endColumn: selectedLines.length === 1 ? startColumn + finalSelectedLineLength : finalSelectedLineLength,
|
|
2153
|
+
text: selectedText
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
function extractText2(lines, line, endLine, startColumn, endColumn) {
|
|
2157
|
+
const finalLine = endLine ?? line;
|
|
2158
|
+
if (line < 1 || finalLine >= lines.length) return null;
|
|
2159
|
+
if (line === finalLine) {
|
|
2160
|
+
const text = lines[line];
|
|
2161
|
+
return startColumn != null && endColumn != null ? text.slice(startColumn, endColumn) : text;
|
|
2162
|
+
}
|
|
2163
|
+
const result = [];
|
|
2164
|
+
for (let current = line; current <= finalLine; current += 1) {
|
|
2165
|
+
let text = lines[current];
|
|
2166
|
+
if (current === line && startColumn != null) text = text.slice(startColumn);
|
|
2167
|
+
if (current === finalLine && endColumn != null) text = text.slice(0, endColumn);
|
|
2168
|
+
result.push(text);
|
|
2169
|
+
}
|
|
2170
|
+
return result.join("\n");
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// src/lib/confidence-calibration.ts
|
|
2174
|
+
function calibrateAnchorEvidence(commentId, selectedText, projected, contextual) {
|
|
2175
|
+
if (projected?.exact) {
|
|
2176
|
+
return {
|
|
2177
|
+
band: "certain",
|
|
2178
|
+
result: projectedResult(commentId, selectedText, projected)
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
if (contextual?.status === "anchored") {
|
|
2182
|
+
return {
|
|
2183
|
+
band: "certain",
|
|
2184
|
+
result: contextualResult(commentId, selectedText, contextual)
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
if (projected && contextual) {
|
|
2188
|
+
if (sameRange(projected, contextual)) {
|
|
2189
|
+
const result = contextualResult(commentId, selectedText, contextual);
|
|
2190
|
+
result.status = "fuzzy";
|
|
2191
|
+
result.score = combineIndependentScores(projected.score, contextual.score);
|
|
2192
|
+
result.reason = `Probable anchor: revision projection and Markdown context agree (confidence ${result.score.toFixed(3)}).`;
|
|
2193
|
+
return { band: "probable", result };
|
|
2194
|
+
}
|
|
2195
|
+
if (contextual.status === "orphaned" && !isStrongProjection(projected)) {
|
|
2196
|
+
return {
|
|
2197
|
+
band: "orphaned",
|
|
2198
|
+
result: contextualResult(commentId, selectedText, contextual)
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
if (contextual.status === "fuzzy" && contextual.candidateMargin >= 0.15 && contextual.score >= projected.score + 0.1) {
|
|
2202
|
+
return {
|
|
2203
|
+
band: "probable",
|
|
2204
|
+
result: contextualResult(commentId, selectedText, contextual)
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
return {
|
|
2208
|
+
band: "ambiguous",
|
|
2209
|
+
result: {
|
|
2210
|
+
commentId,
|
|
2211
|
+
status: "ambiguous",
|
|
2212
|
+
score: Math.max(projected.score, contextual.score),
|
|
2213
|
+
reason: `Ambiguous evidence: revision projection points to line ${projected.line}, while Markdown context points to ${contextual.line ?? "no location"}.`
|
|
2214
|
+
}
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
if (contextual) {
|
|
2218
|
+
const band = contextual.status === "orphaned" ? "orphaned" : contextual.status === "ambiguous" ? "ambiguous" : "probable";
|
|
2219
|
+
return {
|
|
2220
|
+
band,
|
|
2221
|
+
result: contextualResult(commentId, selectedText, contextual)
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
if (projected) {
|
|
2225
|
+
if (!isStrongProjection(projected)) return void 0;
|
|
2226
|
+
return {
|
|
2227
|
+
band: "probable",
|
|
2228
|
+
result: projectedResult(commentId, selectedText, projected)
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
return void 0;
|
|
2232
|
+
}
|
|
2233
|
+
function isStrongProjection(projected) {
|
|
2234
|
+
return projected.contextSupport >= 3 || projected.contextSupport >= 2 && projected.contextMargin >= 0.5;
|
|
2235
|
+
}
|
|
2236
|
+
function sameRange(projected, contextual) {
|
|
2237
|
+
return projected.line === contextual.line && projected.endLine === contextual.endLine;
|
|
2238
|
+
}
|
|
2239
|
+
function combineIndependentScores(left, right) {
|
|
2240
|
+
return Math.min(0.95, (left + right) / 2 + 0.05);
|
|
2241
|
+
}
|
|
2242
|
+
function projectedResult(commentId, selectedText, projected) {
|
|
2243
|
+
return {
|
|
2244
|
+
commentId,
|
|
2245
|
+
status: projected.exact ? "anchored" : "fuzzy",
|
|
2246
|
+
score: projected.score,
|
|
2247
|
+
newLine: projected.line,
|
|
2248
|
+
newEndLine: projected.endLine,
|
|
2249
|
+
newStartColumn: projected.startColumn,
|
|
2250
|
+
newEndColumn: projected.endColumn,
|
|
2251
|
+
anchoredText: projected.exact ? void 0 : projected.text,
|
|
2252
|
+
previousSelectedText: projected.exact ? void 0 : selectedText,
|
|
2253
|
+
reason: projected.reason
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
function contextualResult(commentId, selectedText, contextual) {
|
|
2257
|
+
return {
|
|
2258
|
+
commentId,
|
|
2259
|
+
status: contextual.status,
|
|
2260
|
+
score: contextual.score,
|
|
2261
|
+
newLine: contextual.line,
|
|
2262
|
+
newEndLine: contextual.endLine,
|
|
2263
|
+
newStartColumn: contextual.startColumn,
|
|
2264
|
+
newEndColumn: contextual.endColumn,
|
|
2265
|
+
anchoredText: contextual.status === "fuzzy" ? contextual.text : void 0,
|
|
2266
|
+
previousSelectedText: contextual.status === "fuzzy" ? selectedText : void 0,
|
|
2267
|
+
reason: contextual.reason
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
|
|
1349
2271
|
// src/lib/reanchor-core.ts
|
|
1350
2272
|
var HIGH_THRESHOLD = 0.8;
|
|
1351
2273
|
var DEFAULT_THRESHOLD = 0.6;
|
|
2274
|
+
var DEFAULT_PROXIMITY_WINDOW = 5;
|
|
1352
2275
|
function toReanchorLines(documentText) {
|
|
1353
2276
|
return ["", ...documentText.replace(/\r\n/g, "\n").split("\n")];
|
|
1354
2277
|
}
|
|
1355
2278
|
function reanchorComment(comment, documentLines, opts = {}) {
|
|
1356
2279
|
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
2280
|
+
const proximityWindow = opts.proximityWindow ?? DEFAULT_PROXIMITY_WINDOW;
|
|
1357
2281
|
const commentId = comment.id;
|
|
1358
2282
|
const selectedText = comment.selected_text;
|
|
2283
|
+
let fuzzyCandidateSets;
|
|
2284
|
+
let fuzzySearchIndex = opts.fuzzySearchIndex;
|
|
2285
|
+
const getFuzzySearchIndex = () => {
|
|
2286
|
+
fuzzySearchIndex ??= opts.getFuzzySearchIndex?.() ?? createFuzzySearchIndex(documentLines);
|
|
2287
|
+
return fuzzySearchIndex;
|
|
2288
|
+
};
|
|
1359
2289
|
if (!selectedText && comment.line == null) {
|
|
1360
2290
|
return {
|
|
1361
2291
|
commentId,
|
|
@@ -1383,7 +2313,7 @@ function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
1383
2313
|
const shiftedLine = comment.line + shift;
|
|
1384
2314
|
const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
|
|
1385
2315
|
const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : void 0;
|
|
1386
|
-
const textAtShifted =
|
|
2316
|
+
const textAtShifted = extractText3(
|
|
1387
2317
|
documentLines,
|
|
1388
2318
|
shiftedLine,
|
|
1389
2319
|
shiftedEndLine,
|
|
@@ -1402,32 +2332,79 @@ function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
1402
2332
|
}
|
|
1403
2333
|
}
|
|
1404
2334
|
}
|
|
2335
|
+
const projected = selectedText && opts.revisionProjection ? projectCommentAnchor(
|
|
2336
|
+
comment,
|
|
2337
|
+
opts.revisionProjection,
|
|
2338
|
+
threshold
|
|
2339
|
+
) : void 0;
|
|
2340
|
+
if (selectedText && projected?.exact) {
|
|
2341
|
+
const exactCalibration = calibrateAnchorEvidence(
|
|
2342
|
+
commentId,
|
|
2343
|
+
selectedText,
|
|
2344
|
+
projected
|
|
2345
|
+
);
|
|
2346
|
+
if (exactCalibration) return exactCalibration.result;
|
|
2347
|
+
}
|
|
2348
|
+
const contextual = selectedText && opts.anchorContext ? resolveContextAnchor(comment, opts.anchorContext) : void 0;
|
|
2349
|
+
if (selectedText && (projected || contextual)) {
|
|
2350
|
+
const calibrated = calibrateAnchorEvidence(
|
|
2351
|
+
commentId,
|
|
2352
|
+
selectedText,
|
|
2353
|
+
projected,
|
|
2354
|
+
contextual
|
|
2355
|
+
);
|
|
2356
|
+
if (calibrated) return calibrated.result;
|
|
2357
|
+
}
|
|
1405
2358
|
if (selectedText) {
|
|
1406
2359
|
const exactCandidates = exactMatch(documentLines, selectedText);
|
|
1407
|
-
if (exactCandidates.length
|
|
1408
|
-
const candidate = exactCandidates[0];
|
|
2360
|
+
if (exactCandidates.length > 1 && comment.line == null) {
|
|
1409
2361
|
return {
|
|
1410
2362
|
commentId,
|
|
1411
|
-
status: "
|
|
2363
|
+
status: "ambiguous",
|
|
1412
2364
|
score: 1,
|
|
1413
|
-
|
|
1414
|
-
newEndLine: candidate.endLine,
|
|
1415
|
-
newStartColumn: candidate.startColumn,
|
|
1416
|
-
newEndColumn: candidate.endColumn,
|
|
1417
|
-
reason: "Exact text match (unique)."
|
|
2365
|
+
reason: `Ambiguous: ${exactCandidates.length} exact matches and no position or source context to disambiguate them.`
|
|
1418
2366
|
};
|
|
1419
2367
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
2368
|
+
let chosen;
|
|
2369
|
+
let chosenReason = "";
|
|
2370
|
+
if (exactCandidates.length === 1) {
|
|
2371
|
+
chosen = exactCandidates[0];
|
|
2372
|
+
chosenReason = "Exact text match (unique).";
|
|
2373
|
+
} else if (exactCandidates.length > 1 && comment.line != null) {
|
|
2374
|
+
chosen = closestToLine(exactCandidates, comment.line);
|
|
2375
|
+
chosenReason = `Exact text match (${exactCandidates.length} occurrences; chose nearest to original line ${comment.line}).`;
|
|
2376
|
+
}
|
|
2377
|
+
if (chosen) {
|
|
2378
|
+
if (isImplausibleExactRelocation(comment, chosen, documentLines, proximityWindow)) {
|
|
2379
|
+
const textAtOrigin = extractText3(
|
|
2380
|
+
documentLines,
|
|
2381
|
+
comment.line,
|
|
2382
|
+
comment.end_line,
|
|
2383
|
+
comment.start_column,
|
|
2384
|
+
comment.end_column
|
|
2385
|
+
);
|
|
2386
|
+
return {
|
|
2387
|
+
commentId,
|
|
2388
|
+
status: "fuzzy",
|
|
2389
|
+
score: 0.5,
|
|
2390
|
+
newLine: comment.line,
|
|
2391
|
+
newEndLine: comment.end_line,
|
|
2392
|
+
newStartColumn: comment.start_column,
|
|
2393
|
+
newEndColumn: comment.end_column,
|
|
2394
|
+
anchoredText: textAtOrigin ?? void 0,
|
|
2395
|
+
previousSelectedText: selectedText,
|
|
2396
|
+
reason: `Lone exact match at line ${chosen.line} is beyond the proximity window (\xB1${proximityWindow}) of original line ${comment.line} and the text at the original position changed; kept at original position, needs re-anchoring.`
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
1422
2399
|
return {
|
|
1423
2400
|
commentId,
|
|
1424
2401
|
status: "anchored",
|
|
1425
2402
|
score: 1,
|
|
1426
|
-
newLine:
|
|
1427
|
-
newEndLine:
|
|
1428
|
-
newStartColumn:
|
|
1429
|
-
newEndColumn:
|
|
1430
|
-
reason:
|
|
2403
|
+
newLine: chosen.line,
|
|
2404
|
+
newEndLine: chosen.endLine,
|
|
2405
|
+
newStartColumn: chosen.startColumn,
|
|
2406
|
+
newEndColumn: chosen.endColumn,
|
|
2407
|
+
reason: chosenReason
|
|
1431
2408
|
};
|
|
1432
2409
|
}
|
|
1433
2410
|
const normCandidates = normalizedMatch(documentLines, selectedText);
|
|
@@ -1446,12 +2423,14 @@ function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
1446
2423
|
reason: "Normalized whitespace match."
|
|
1447
2424
|
};
|
|
1448
2425
|
}
|
|
1449
|
-
|
|
2426
|
+
fuzzyCandidateSets = fuzzySearchThresholds(
|
|
1450
2427
|
documentLines,
|
|
1451
2428
|
selectedText,
|
|
1452
|
-
HIGH_THRESHOLD,
|
|
1453
|
-
comment.line
|
|
2429
|
+
[HIGH_THRESHOLD, threshold],
|
|
2430
|
+
comment.line,
|
|
2431
|
+
getFuzzySearchIndex()
|
|
1454
2432
|
);
|
|
2433
|
+
const fuzzyCandidates = fuzzyCandidateSets.get(HIGH_THRESHOLD) ?? [];
|
|
1455
2434
|
if (fuzzyCandidates.length === 1 || fuzzyCandidates.length > 0 && fuzzyCandidates[0].score >= HIGH_THRESHOLD) {
|
|
1456
2435
|
const best = fuzzyCandidates.length === 1 ? fuzzyCandidates[0] : closestToLine(fuzzyCandidates, comment.line ?? 1);
|
|
1457
2436
|
return {
|
|
@@ -1500,11 +2479,12 @@ function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
1500
2479
|
}
|
|
1501
2480
|
}
|
|
1502
2481
|
if (selectedText) {
|
|
1503
|
-
const lowCandidates = fuzzySearch(
|
|
2482
|
+
const lowCandidates = fuzzyCandidateSets?.get(threshold) ?? fuzzySearch(
|
|
1504
2483
|
documentLines,
|
|
1505
2484
|
selectedText,
|
|
1506
2485
|
threshold,
|
|
1507
|
-
comment.line
|
|
2486
|
+
comment.line,
|
|
2487
|
+
getFuzzySearchIndex()
|
|
1508
2488
|
);
|
|
1509
2489
|
if (lowCandidates.length === 1) {
|
|
1510
2490
|
const candidate = lowCandidates[0];
|
|
@@ -1541,7 +2521,14 @@ function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
1541
2521
|
};
|
|
1542
2522
|
}
|
|
1543
2523
|
function reanchorDocumentLines(doc, documentLines, opts = {}) {
|
|
1544
|
-
|
|
2524
|
+
let fuzzySearchIndex;
|
|
2525
|
+
const getFuzzySearchIndex = () => {
|
|
2526
|
+
fuzzySearchIndex ??= createFuzzySearchIndex(documentLines);
|
|
2527
|
+
return fuzzySearchIndex;
|
|
2528
|
+
};
|
|
2529
|
+
return doc.comments.map(
|
|
2530
|
+
(comment) => reanchorComment(comment, documentLines, { ...opts, getFuzzySearchIndex })
|
|
2531
|
+
);
|
|
1545
2532
|
}
|
|
1546
2533
|
function reanchorDocumentText(doc, documentText, opts = {}) {
|
|
1547
2534
|
return reanchorDocumentLines(doc, toReanchorLines(documentText), opts);
|
|
@@ -1660,7 +2647,7 @@ function applyReanchorResults(doc, results, opts = {}) {
|
|
|
1660
2647
|
}
|
|
1661
2648
|
return changed;
|
|
1662
2649
|
}
|
|
1663
|
-
function
|
|
2650
|
+
function extractText3(lines, line, endLine, startColumn, endColumn) {
|
|
1664
2651
|
const startIdx = line;
|
|
1665
2652
|
const endIdx = endLine ?? line;
|
|
1666
2653
|
if (startIdx < 1 || endIdx >= lines.length) return null;
|
|
@@ -1685,6 +2672,20 @@ function closestToLine(candidates, targetLine) {
|
|
|
1685
2672
|
(best, candidate) => Math.abs(candidate.line - targetLine) < Math.abs(best.line - targetLine) ? candidate : best
|
|
1686
2673
|
);
|
|
1687
2674
|
}
|
|
2675
|
+
function isImplausibleExactRelocation(comment, candidate, lines, proximityWindow) {
|
|
2676
|
+
if (comment.line == null) return false;
|
|
2677
|
+
if (comment.line <= 0 || comment.line >= lines.length) return false;
|
|
2678
|
+
if (Math.abs(candidate.line - comment.line) <= proximityWindow) return false;
|
|
2679
|
+
const textAtOrigin = extractText3(
|
|
2680
|
+
lines,
|
|
2681
|
+
comment.line,
|
|
2682
|
+
comment.end_line,
|
|
2683
|
+
comment.start_column,
|
|
2684
|
+
comment.end_column
|
|
2685
|
+
);
|
|
2686
|
+
if (textAtOrigin === comment.selected_text) return false;
|
|
2687
|
+
return true;
|
|
2688
|
+
}
|
|
1688
2689
|
function getLineShift2(diffHunks, line) {
|
|
1689
2690
|
let shift = 0;
|
|
1690
2691
|
let modified = false;
|
|
@@ -1701,6 +2702,137 @@ function getLineShift2(diffHunks, line) {
|
|
|
1701
2702
|
return { shift, modified };
|
|
1702
2703
|
}
|
|
1703
2704
|
|
|
2705
|
+
// src/lib/global-reconciliation.ts
|
|
2706
|
+
var LOCAL_LANDMARK_WINDOW = 30;
|
|
2707
|
+
var MIN_LANDMARK_SUPPORT = 0.65;
|
|
2708
|
+
var MIN_SUPPORTING_LANDMARKS = 2;
|
|
2709
|
+
var MIN_GLOBAL_MARGIN = 0.08;
|
|
2710
|
+
var MAX_RECONCILIATION_ROUNDS = 4;
|
|
2711
|
+
function reconcileCommentAnchors(comments, results, anchorContext) {
|
|
2712
|
+
const reconciled = results.map((result) => ({ ...result }));
|
|
2713
|
+
if (comments.length < MIN_SUPPORTING_LANDMARKS + 1 || !reconciled.some((result) => result.status === "ambiguous")) {
|
|
2714
|
+
return reconciled;
|
|
2715
|
+
}
|
|
2716
|
+
const resultIndexes = new Map(
|
|
2717
|
+
reconciled.map((result, index) => [result.commentId, index])
|
|
2718
|
+
);
|
|
2719
|
+
const landmarks = collectLandmarks(comments, reconciled, anchorContext);
|
|
2720
|
+
const landmarkCounts = /* @__PURE__ */ new Map();
|
|
2721
|
+
for (const landmark of landmarks) {
|
|
2722
|
+
landmarkCounts.set(
|
|
2723
|
+
landmark.scope,
|
|
2724
|
+
(landmarkCounts.get(landmark.scope) ?? 0) + 1
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
for (let round = 0; round < MAX_RECONCILIATION_ROUNDS; round += 1) {
|
|
2728
|
+
let changed = false;
|
|
2729
|
+
for (const comment of comments) {
|
|
2730
|
+
if (comment.line == null || !comment.selected_text) continue;
|
|
2731
|
+
const scope = getAnchorContextScope(comment, anchorContext);
|
|
2732
|
+
if (scope == null) continue;
|
|
2733
|
+
if ((landmarkCounts.get(scope) ?? 0) < MIN_SUPPORTING_LANDMARKS) {
|
|
2734
|
+
continue;
|
|
2735
|
+
}
|
|
2736
|
+
const resultIndex = resultIndexes.get(comment.id);
|
|
2737
|
+
if (resultIndex == null) continue;
|
|
2738
|
+
const result = reconciled[resultIndex];
|
|
2739
|
+
if (result.status !== "ambiguous") continue;
|
|
2740
|
+
const candidates = deduplicateCandidates2(
|
|
2741
|
+
findContextAnchorCandidates(comment, anchorContext)
|
|
2742
|
+
).slice(0, 8);
|
|
2743
|
+
if (candidates.length < 2) continue;
|
|
2744
|
+
const ranked = candidates.map(
|
|
2745
|
+
(candidate) => scoreWithLandmarks(
|
|
2746
|
+
comment.line,
|
|
2747
|
+
scope,
|
|
2748
|
+
candidate,
|
|
2749
|
+
landmarks
|
|
2750
|
+
)
|
|
2751
|
+
).sort(
|
|
2752
|
+
(left, right) => right.globalScore - left.globalScore || right.candidate.score - left.candidate.score || left.candidate.line - right.candidate.line
|
|
2753
|
+
);
|
|
2754
|
+
const best = ranked[0];
|
|
2755
|
+
const second = ranked[1];
|
|
2756
|
+
if (!best.candidate.exact || best.supportingLandmarks < MIN_SUPPORTING_LANDMARKS || best.support < MIN_LANDMARK_SUPPORT || best.globalScore - second.globalScore < MIN_GLOBAL_MARGIN) {
|
|
2757
|
+
continue;
|
|
2758
|
+
}
|
|
2759
|
+
reconciled[resultIndex] = {
|
|
2760
|
+
commentId: comment.id,
|
|
2761
|
+
status: "anchored",
|
|
2762
|
+
score: Math.min(0.99, 0.8 + best.support * 0.19),
|
|
2763
|
+
newLine: best.candidate.line,
|
|
2764
|
+
newEndLine: best.candidate.endLine,
|
|
2765
|
+
newStartColumn: best.candidate.startColumn,
|
|
2766
|
+
newEndColumn: best.candidate.endColumn,
|
|
2767
|
+
reason: `Global reconciliation selected this candidate from ${best.supportingLandmarks} nearby landmark(s) (support ${best.support.toFixed(3)}, margin ${(best.globalScore - second.globalScore).toFixed(3)}).`
|
|
2768
|
+
};
|
|
2769
|
+
changed = true;
|
|
2770
|
+
landmarks.push({
|
|
2771
|
+
sourceLine: comment.line,
|
|
2772
|
+
targetLine: best.candidate.line,
|
|
2773
|
+
scope
|
|
2774
|
+
});
|
|
2775
|
+
landmarkCounts.set(scope, (landmarkCounts.get(scope) ?? 0) + 1);
|
|
2776
|
+
}
|
|
2777
|
+
if (!changed) break;
|
|
2778
|
+
}
|
|
2779
|
+
return reconciled;
|
|
2780
|
+
}
|
|
2781
|
+
function collectLandmarks(comments, results, anchorContext) {
|
|
2782
|
+
const resultMap = new Map(results.map((result) => [result.commentId, result]));
|
|
2783
|
+
return comments.flatMap((comment) => {
|
|
2784
|
+
const result = resultMap.get(comment.id);
|
|
2785
|
+
if (comment.line == null || result?.newLine == null || result.status !== "anchored" && result.status !== "shifted" || result.score < 0.99) {
|
|
2786
|
+
return [];
|
|
2787
|
+
}
|
|
2788
|
+
const scope = getAnchorContextScope(comment, anchorContext);
|
|
2789
|
+
return scope == null ? [] : [{ sourceLine: comment.line, targetLine: result.newLine, scope }];
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
function scoreWithLandmarks(sourceLine, scope, candidate, landmarks) {
|
|
2793
|
+
let weightedSupport = 0;
|
|
2794
|
+
let totalWeight = 0;
|
|
2795
|
+
let supportingLandmarks = 0;
|
|
2796
|
+
const candidateShift = candidate.line - sourceLine;
|
|
2797
|
+
for (const landmark of landmarks) {
|
|
2798
|
+
if (landmark.scope !== scope) continue;
|
|
2799
|
+
const sourceDistance = Math.abs(sourceLine - landmark.sourceLine);
|
|
2800
|
+
if (sourceDistance === 0 || sourceDistance > LOCAL_LANDMARK_WINDOW) continue;
|
|
2801
|
+
const landmarkShift = landmark.targetLine - landmark.sourceLine;
|
|
2802
|
+
const shiftError = Math.abs(candidateShift - landmarkShift);
|
|
2803
|
+
const agreement = Math.max(0, 1 - shiftError / 8);
|
|
2804
|
+
const weight = 1 / (1 + sourceDistance / 4);
|
|
2805
|
+
weightedSupport += agreement * weight;
|
|
2806
|
+
totalWeight += weight;
|
|
2807
|
+
if (agreement >= MIN_LANDMARK_SUPPORT) supportingLandmarks += 1;
|
|
2808
|
+
}
|
|
2809
|
+
const support = totalWeight > 0 ? weightedSupport / totalWeight : 0;
|
|
2810
|
+
return {
|
|
2811
|
+
candidate,
|
|
2812
|
+
globalScore: candidate.score * 0.5 + support * 0.45 + (candidate.exact ? 0.05 : 0),
|
|
2813
|
+
support,
|
|
2814
|
+
supportingLandmarks
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
function deduplicateCandidates2(candidates) {
|
|
2818
|
+
const unique = /* @__PURE__ */ new Map();
|
|
2819
|
+
for (const candidate of candidates) {
|
|
2820
|
+
const key = [
|
|
2821
|
+
candidate.line,
|
|
2822
|
+
candidate.endLine,
|
|
2823
|
+
candidate.startColumn ?? "",
|
|
2824
|
+
candidate.endColumn ?? ""
|
|
2825
|
+
].join(":");
|
|
2826
|
+
const existing = unique.get(key);
|
|
2827
|
+
if (!existing || candidate.score > existing.score) {
|
|
2828
|
+
unique.set(key, candidate);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
return [...unique.values()].sort(
|
|
2832
|
+
(left, right) => right.score - left.score || left.line - right.line
|
|
2833
|
+
);
|
|
2834
|
+
}
|
|
2835
|
+
|
|
1704
2836
|
// src/lib/reanchor.ts
|
|
1705
2837
|
init_parser();
|
|
1706
2838
|
init_parser();
|
|
@@ -1708,35 +2840,115 @@ var import_node_path6 = __toESM(require("node:path"), 1);
|
|
|
1708
2840
|
async function reanchorDocument(doc, documentLines, opts = {}) {
|
|
1709
2841
|
const results = [];
|
|
1710
2842
|
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
1711
|
-
|
|
1712
|
-
let
|
|
2843
|
+
const proximityWindow = opts.proximityWindow;
|
|
2844
|
+
let fuzzySearchIndex;
|
|
2845
|
+
const getFuzzySearchIndex = () => {
|
|
2846
|
+
fuzzySearchIndex ??= createFuzzySearchIndex(documentLines);
|
|
2847
|
+
return fuzzySearchIndex;
|
|
2848
|
+
};
|
|
1713
2849
|
if (!opts.noGit && await isGitAvailable()) {
|
|
1714
2850
|
const repoRoot = opts.repoRoot ?? await findRepoRoot(opts.cwd);
|
|
1715
2851
|
if (repoRoot && opts.documentPath) {
|
|
1716
2852
|
const relPath = import_node_path6.default.relative(repoRoot, opts.documentPath);
|
|
1717
2853
|
const head = await getCurrentCommit(repoRoot);
|
|
1718
2854
|
const globalFrom = opts.fromCommit;
|
|
2855
|
+
const diffCache = /* @__PURE__ */ new Map();
|
|
2856
|
+
const projectionCache = /* @__PURE__ */ new Map();
|
|
2857
|
+
const contextCache = /* @__PURE__ */ new Map();
|
|
2858
|
+
const canonicalCommitCache = /* @__PURE__ */ new Map();
|
|
2859
|
+
const reconciliationGroups = /* @__PURE__ */ new Map();
|
|
1719
2860
|
for (const comment of doc.comments) {
|
|
1720
|
-
const
|
|
1721
|
-
if (
|
|
1722
|
-
|
|
1723
|
-
const
|
|
2861
|
+
const rawCommentCommit = globalFrom ?? comment.commit;
|
|
2862
|
+
if (rawCommentCommit && head) {
|
|
2863
|
+
const cachedCommit = canonicalCommitCache.get(rawCommentCommit);
|
|
2864
|
+
const commentCommit = cachedCommit ?? await resolveCommit(rawCommentCommit, repoRoot) ?? rawCommentCommit;
|
|
2865
|
+
if (!cachedCommit) {
|
|
2866
|
+
canonicalCommitCache.set(rawCommentCommit, commentCommit);
|
|
2867
|
+
}
|
|
2868
|
+
if (commentCommit === head) {
|
|
2869
|
+
results.push(
|
|
2870
|
+
reanchorComment(comment, documentLines, {
|
|
2871
|
+
threshold,
|
|
2872
|
+
commitIsStale: false,
|
|
2873
|
+
proximityWindow,
|
|
2874
|
+
getFuzzySearchIndex
|
|
2875
|
+
})
|
|
2876
|
+
);
|
|
2877
|
+
continue;
|
|
2878
|
+
}
|
|
2879
|
+
let hunks = diffCache.get(commentCommit);
|
|
2880
|
+
if (!hunks) {
|
|
2881
|
+
hunks = await getDiff(commentCommit, head, relPath, repoRoot);
|
|
2882
|
+
diffCache.set(commentCommit, hunks);
|
|
2883
|
+
}
|
|
2884
|
+
let revisionProjection = projectionCache.get(commentCommit);
|
|
2885
|
+
let anchorContext = contextCache.get(commentCommit);
|
|
2886
|
+
if (!projectionCache.has(commentCommit)) {
|
|
2887
|
+
const sourceText = await getFileAtCommit(
|
|
2888
|
+
commentCommit,
|
|
2889
|
+
relPath,
|
|
2890
|
+
repoRoot
|
|
2891
|
+
);
|
|
2892
|
+
revisionProjection = sourceText == null ? void 0 : createRevisionProjection(
|
|
2893
|
+
toReanchorLines(sourceText),
|
|
2894
|
+
documentLines
|
|
2895
|
+
);
|
|
2896
|
+
anchorContext = sourceText == null ? void 0 : createAnchorContextIndex(
|
|
2897
|
+
toReanchorLines(sourceText),
|
|
2898
|
+
documentLines
|
|
2899
|
+
);
|
|
2900
|
+
projectionCache.set(commentCommit, revisionProjection);
|
|
2901
|
+
contextCache.set(commentCommit, anchorContext);
|
|
2902
|
+
}
|
|
1724
2903
|
const result = reanchorComment(comment, documentLines, {
|
|
1725
2904
|
diffHunks: hunks,
|
|
1726
2905
|
threshold,
|
|
1727
|
-
commitIsStale: true
|
|
2906
|
+
commitIsStale: true,
|
|
2907
|
+
proximityWindow,
|
|
2908
|
+
revisionProjection,
|
|
2909
|
+
anchorContext,
|
|
2910
|
+
getFuzzySearchIndex
|
|
1728
2911
|
});
|
|
2912
|
+
const resultIndex = results.length;
|
|
1729
2913
|
results.push(result);
|
|
2914
|
+
if (anchorContext) {
|
|
2915
|
+
const group = reconciliationGroups.get(commentCommit);
|
|
2916
|
+
if (group) {
|
|
2917
|
+
group.comments.push(comment);
|
|
2918
|
+
group.resultIndexes.push(resultIndex);
|
|
2919
|
+
} else {
|
|
2920
|
+
reconciliationGroups.set(commentCommit, {
|
|
2921
|
+
comments: [comment],
|
|
2922
|
+
resultIndexes: [resultIndex],
|
|
2923
|
+
anchorContext
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
1730
2927
|
continue;
|
|
1731
2928
|
}
|
|
1732
2929
|
results.push(
|
|
1733
|
-
reanchorComment(comment, documentLines, {
|
|
2930
|
+
reanchorComment(comment, documentLines, {
|
|
2931
|
+
threshold,
|
|
2932
|
+
commitIsStale: false,
|
|
2933
|
+
proximityWindow,
|
|
2934
|
+
getFuzzySearchIndex
|
|
2935
|
+
})
|
|
1734
2936
|
);
|
|
1735
2937
|
}
|
|
2938
|
+
for (const group of reconciliationGroups.values()) {
|
|
2939
|
+
const reconciled = reconcileCommentAnchors(
|
|
2940
|
+
group.comments,
|
|
2941
|
+
group.resultIndexes.map((index) => results[index]),
|
|
2942
|
+
group.anchorContext
|
|
2943
|
+
);
|
|
2944
|
+
for (const [offset, resultIndex] of group.resultIndexes.entries()) {
|
|
2945
|
+
results[resultIndex] = reconciled[offset];
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
1736
2948
|
return results;
|
|
1737
2949
|
}
|
|
1738
2950
|
}
|
|
1739
|
-
return reanchorDocumentLines(doc, documentLines, { threshold });
|
|
2951
|
+
return reanchorDocumentLines(doc, documentLines, { threshold, proximityWindow });
|
|
1740
2952
|
}
|
|
1741
2953
|
async function reanchorFile(sidecarPath, opts = {}) {
|
|
1742
2954
|
const doc = await parseSidecar(sidecarPath);
|
|
@@ -2044,10 +3256,11 @@ var combinedScore2 = combinedScore;
|
|
|
2044
3256
|
var isGitAvailable2 = isGitAvailable;
|
|
2045
3257
|
var findRepoRoot3 = findRepoRoot;
|
|
2046
3258
|
var getCurrentCommit2 = getCurrentCommit;
|
|
3259
|
+
var resolveCommit2 = resolveCommit;
|
|
2047
3260
|
var isStale2 = isStale;
|
|
2048
3261
|
var getDiff2 = getDiff;
|
|
2049
3262
|
var getLineShift4 = getLineShift;
|
|
2050
|
-
var
|
|
3263
|
+
var getFileAtCommit2 = getFileAtCommit;
|
|
2051
3264
|
var getStagedFiles2 = getStagedFiles;
|
|
2052
3265
|
var detectRenames2 = detectRenames;
|
|
2053
3266
|
var parseDiffHunks3 = parseDiffHunks;
|
|
@@ -2061,6 +3274,10 @@ var resolveAnchor2 = resolveAnchor;
|
|
|
2061
3274
|
var reanchorDocument2 = reanchorDocument;
|
|
2062
3275
|
var applyReanchorResults2 = applyReanchorResults;
|
|
2063
3276
|
var reanchorFile2 = reanchorFile;
|
|
3277
|
+
var createAnchorContextIndex2 = createAnchorContextIndex;
|
|
3278
|
+
var reconcileCommentAnchors2 = reconcileCommentAnchors;
|
|
3279
|
+
var createRevisionProjection2 = createRevisionProjection;
|
|
3280
|
+
var calibrateAnchorEvidence2 = calibrateAnchorEvidence;
|
|
2064
3281
|
var addComment2 = addComment;
|
|
2065
3282
|
var editComment2 = editComment;
|
|
2066
3283
|
var normalizeCommentExtensions2 = normalizeCommentExtensions;
|
|
@@ -2080,8 +3297,11 @@ var newCommentId2 = newCommentId;
|
|
|
2080
3297
|
HIGH_THRESHOLD,
|
|
2081
3298
|
addComment,
|
|
2082
3299
|
applyReanchorResults,
|
|
3300
|
+
calibrateAnchorEvidence,
|
|
2083
3301
|
combinedScore,
|
|
2084
3302
|
computeHash,
|
|
3303
|
+
createAnchorContextIndex,
|
|
3304
|
+
createRevisionProjection,
|
|
2085
3305
|
detectRenames,
|
|
2086
3306
|
discoverAllSidecars,
|
|
2087
3307
|
discoverSidecar,
|
|
@@ -2118,9 +3338,11 @@ var newCommentId2 = newCommentId;
|
|
|
2118
3338
|
reanchorDocumentLines,
|
|
2119
3339
|
reanchorDocumentText,
|
|
2120
3340
|
reanchorFile,
|
|
3341
|
+
reconcileCommentAnchors,
|
|
2121
3342
|
removeComment,
|
|
2122
3343
|
resolveAnchor,
|
|
2123
3344
|
resolveComment,
|
|
3345
|
+
resolveCommit,
|
|
2124
3346
|
resolveSidecarPaths,
|
|
2125
3347
|
sidecarToDocument,
|
|
2126
3348
|
summarize,
|