@mrsf/cli 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -9
- package/dist/browser.cjs +1101 -24
- package/dist/browser.cjs.map +4 -4
- package/dist/browser.d.ts +11 -0
- package/dist/browser.js +8 -0
- package/dist/commands/add.js +12 -4
- package/dist/evaluation/generate-reanchor-cases.d.ts +10 -0
- package/dist/evaluation/generate-reanchor-cases.js +301 -0
- package/dist/evaluation/reanchor-eval.d.ts +111 -0
- package/dist/evaluation/reanchor-eval.js +266 -0
- package/dist/evaluation/reanchor-gates.d.ts +29 -0
- package/dist/evaluation/reanchor-gates.js +47 -0
- package/dist/evaluation/run-reanchor-eval.d.ts +3 -0
- package/dist/evaluation/run-reanchor-eval.js +103 -0
- package/dist/evaluation/run-reanchor-generator.d.ts +3 -0
- package/dist/evaluation/run-reanchor-generator.js +63 -0
- package/dist/evaluation/run-reanchor-profile.d.ts +3 -0
- package/dist/evaluation/run-reanchor-profile.js +129 -0
- package/dist/index.cjs +1226 -34
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/dist/lib/anchor-context.d.ts +46 -0
- package/dist/lib/anchor-context.js +474 -0
- package/dist/lib/confidence-calibration.d.ts +16 -0
- package/dist/lib/confidence-calibration.js +120 -0
- package/dist/lib/fuzzy.d.ts +14 -1
- package/dist/lib/fuzzy.js +140 -19
- package/dist/lib/git.d.ts +9 -0
- package/dist/lib/git.js +30 -0
- package/dist/lib/global-reconciliation.d.ts +11 -0
- package/dist/lib/global-reconciliation.js +145 -0
- package/dist/lib/reanchor-core.d.ts +7 -0
- package/dist/lib/reanchor-core.js +46 -4
- package/dist/lib/reanchor.js +82 -11
- package/dist/lib/revision-projection.d.ts +21 -0
- package/dist/lib/revision-projection.js +203 -0
- package/package.json +5 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { exactMatch, fuzzySearch, normalizedMatch, } from "./fuzzy.js";
|
|
1
|
+
import { createFuzzySearchIndex, exactMatch, fuzzySearch, fuzzySearchThresholds, normalizedMatch, } from "./fuzzy.js";
|
|
2
|
+
import { projectCommentAnchor, } from "./revision-projection.js";
|
|
3
|
+
import { resolveContextAnchor, } from "./anchor-context.js";
|
|
4
|
+
import { calibrateAnchorEvidence } from "./confidence-calibration.js";
|
|
2
5
|
export const HIGH_THRESHOLD = 0.8;
|
|
3
6
|
export const DEFAULT_THRESHOLD = 0.6;
|
|
4
7
|
/**
|
|
@@ -18,6 +21,13 @@ export function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
18
21
|
const proximityWindow = opts.proximityWindow ?? DEFAULT_PROXIMITY_WINDOW;
|
|
19
22
|
const commentId = comment.id;
|
|
20
23
|
const selectedText = comment.selected_text;
|
|
24
|
+
let fuzzyCandidateSets;
|
|
25
|
+
let fuzzySearchIndex = opts.fuzzySearchIndex;
|
|
26
|
+
const getFuzzySearchIndex = () => {
|
|
27
|
+
fuzzySearchIndex ??= opts.getFuzzySearchIndex?.()
|
|
28
|
+
?? createFuzzySearchIndex(documentLines);
|
|
29
|
+
return fuzzySearchIndex;
|
|
30
|
+
};
|
|
21
31
|
if (!selectedText && comment.line == null) {
|
|
22
32
|
return {
|
|
23
33
|
commentId,
|
|
@@ -62,8 +72,33 @@ export function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
62
72
|
}
|
|
63
73
|
}
|
|
64
74
|
}
|
|
75
|
+
const projected = selectedText && opts.revisionProjection
|
|
76
|
+
? projectCommentAnchor(comment, opts.revisionProjection, threshold)
|
|
77
|
+
: undefined;
|
|
78
|
+
if (selectedText && projected?.exact) {
|
|
79
|
+
const exactCalibration = calibrateAnchorEvidence(commentId, selectedText, projected);
|
|
80
|
+
if (exactCalibration)
|
|
81
|
+
return exactCalibration.result;
|
|
82
|
+
}
|
|
83
|
+
const contextual = selectedText && opts.anchorContext
|
|
84
|
+
? resolveContextAnchor(comment, opts.anchorContext)
|
|
85
|
+
: undefined;
|
|
86
|
+
if (selectedText && (projected || contextual)) {
|
|
87
|
+
const calibrated = calibrateAnchorEvidence(commentId, selectedText, projected, contextual);
|
|
88
|
+
if (calibrated)
|
|
89
|
+
return calibrated.result;
|
|
90
|
+
}
|
|
65
91
|
if (selectedText) {
|
|
66
92
|
const exactCandidates = exactMatch(documentLines, selectedText);
|
|
93
|
+
if (exactCandidates.length > 1 && comment.line == null) {
|
|
94
|
+
return {
|
|
95
|
+
commentId,
|
|
96
|
+
status: "ambiguous",
|
|
97
|
+
score: 1,
|
|
98
|
+
reason: `Ambiguous: ${exactCandidates.length} exact matches and no position `
|
|
99
|
+
+ "or source context to disambiguate them.",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
67
102
|
// Pick the best exact candidate: the only one, or — when several remain —
|
|
68
103
|
// the one nearest to the original line (§7.4 step 1b).
|
|
69
104
|
let chosen;
|
|
@@ -127,7 +162,8 @@ export function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
127
162
|
reason: "Normalized whitespace match.",
|
|
128
163
|
};
|
|
129
164
|
}
|
|
130
|
-
|
|
165
|
+
fuzzyCandidateSets = fuzzySearchThresholds(documentLines, selectedText, [HIGH_THRESHOLD, threshold], comment.line, getFuzzySearchIndex());
|
|
166
|
+
const fuzzyCandidates = fuzzyCandidateSets.get(HIGH_THRESHOLD) ?? [];
|
|
131
167
|
if (fuzzyCandidates.length === 1 || (fuzzyCandidates.length > 0 && fuzzyCandidates[0].score >= HIGH_THRESHOLD)) {
|
|
132
168
|
const best = fuzzyCandidates.length === 1
|
|
133
169
|
? fuzzyCandidates[0]
|
|
@@ -182,7 +218,8 @@ export function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
182
218
|
}
|
|
183
219
|
}
|
|
184
220
|
if (selectedText) {
|
|
185
|
-
const lowCandidates =
|
|
221
|
+
const lowCandidates = fuzzyCandidateSets?.get(threshold)
|
|
222
|
+
?? fuzzySearch(documentLines, selectedText, threshold, comment.line, getFuzzySearchIndex());
|
|
186
223
|
if (lowCandidates.length === 1) {
|
|
187
224
|
const candidate = lowCandidates[0];
|
|
188
225
|
return {
|
|
@@ -218,7 +255,12 @@ export function reanchorComment(comment, documentLines, opts = {}) {
|
|
|
218
255
|
};
|
|
219
256
|
}
|
|
220
257
|
export function reanchorDocumentLines(doc, documentLines, opts = {}) {
|
|
221
|
-
|
|
258
|
+
let fuzzySearchIndex;
|
|
259
|
+
const getFuzzySearchIndex = () => {
|
|
260
|
+
fuzzySearchIndex ??= createFuzzySearchIndex(documentLines);
|
|
261
|
+
return fuzzySearchIndex;
|
|
262
|
+
};
|
|
263
|
+
return doc.comments.map((comment) => reanchorComment(comment, documentLines, { ...opts, getFuzzySearchIndex }));
|
|
222
264
|
}
|
|
223
265
|
export function reanchorDocumentText(doc, documentText, opts = {}) {
|
|
224
266
|
return reanchorDocumentLines(doc, toReanchorLines(documentText), opts);
|
package/dist/lib/reanchor.js
CHANGED
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
* Step 3 – lower-threshold fuzzy ≥ configured threshold (0.6)
|
|
12
12
|
* Step 4 – orphan
|
|
13
13
|
*/
|
|
14
|
-
import { findRepoRoot, getCurrentCommit, getDiff, isGitAvailable, } from "./git.js";
|
|
15
|
-
import { applyReanchorResults, DEFAULT_THRESHOLD, reanchorComment, reanchorDocumentLines, } from "./reanchor-core.js";
|
|
14
|
+
import { findRepoRoot, getCurrentCommit, getDiff, getFileAtCommit, isGitAvailable, resolveCommit, } from "./git.js";
|
|
15
|
+
import { applyReanchorResults, DEFAULT_THRESHOLD, reanchorComment, reanchorDocumentLines, toReanchorLines, } from "./reanchor-core.js";
|
|
16
|
+
import { createRevisionProjection, } from "./revision-projection.js";
|
|
17
|
+
import { createAnchorContextIndex, } from "./anchor-context.js";
|
|
18
|
+
import { createFuzzySearchIndex, } from "./fuzzy.js";
|
|
19
|
+
import { reconcileCommentAnchors } from "./global-reconciliation.js";
|
|
16
20
|
import { readDocumentLines } from "./parser.js";
|
|
17
21
|
import { sidecarToDocument } from "./discovery.js";
|
|
18
22
|
import { parseSidecar } from "./parser.js";
|
|
@@ -32,9 +36,11 @@ export async function reanchorDocument(doc, documentLines, opts = {}) {
|
|
|
32
36
|
const results = [];
|
|
33
37
|
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
34
38
|
const proximityWindow = opts.proximityWindow;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
let fuzzySearchIndex;
|
|
40
|
+
const getFuzzySearchIndex = () => {
|
|
41
|
+
fuzzySearchIndex ??= createFuzzySearchIndex(documentLines);
|
|
42
|
+
return fuzzySearchIndex;
|
|
43
|
+
};
|
|
38
44
|
if (!opts.noGit && (await isGitAvailable())) {
|
|
39
45
|
const repoRoot = opts.repoRoot ?? (await findRepoRoot(opts.cwd));
|
|
40
46
|
if (repoRoot && opts.documentPath) {
|
|
@@ -42,23 +48,88 @@ export async function reanchorDocument(doc, documentLines, opts = {}) {
|
|
|
42
48
|
const head = await getCurrentCommit(repoRoot);
|
|
43
49
|
// Use a shared fromCommit for all comments, or per-comment
|
|
44
50
|
const globalFrom = opts.fromCommit;
|
|
51
|
+
const diffCache = new Map();
|
|
52
|
+
const projectionCache = new Map();
|
|
53
|
+
const contextCache = new Map();
|
|
54
|
+
const canonicalCommitCache = new Map();
|
|
55
|
+
const reconciliationGroups = new Map();
|
|
45
56
|
for (const comment of doc.comments) {
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
const rawCommentCommit = globalFrom ?? comment.commit;
|
|
58
|
+
if (rawCommentCommit && head) {
|
|
59
|
+
const cachedCommit = canonicalCommitCache.get(rawCommentCommit);
|
|
60
|
+
const commentCommit = cachedCommit
|
|
61
|
+
?? await resolveCommit(rawCommentCommit, repoRoot)
|
|
62
|
+
?? rawCommentCommit;
|
|
63
|
+
if (!cachedCommit) {
|
|
64
|
+
canonicalCommitCache.set(rawCommentCommit, commentCommit);
|
|
65
|
+
}
|
|
66
|
+
if (commentCommit === head) {
|
|
67
|
+
results.push(reanchorComment(comment, documentLines, {
|
|
68
|
+
threshold,
|
|
69
|
+
commitIsStale: false,
|
|
70
|
+
proximityWindow,
|
|
71
|
+
getFuzzySearchIndex,
|
|
72
|
+
}));
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
let hunks = diffCache.get(commentCommit);
|
|
76
|
+
if (!hunks) {
|
|
77
|
+
hunks = await getDiff(commentCommit, head, relPath, repoRoot);
|
|
78
|
+
diffCache.set(commentCommit, hunks);
|
|
79
|
+
}
|
|
80
|
+
let revisionProjection = projectionCache.get(commentCommit);
|
|
81
|
+
let anchorContext = contextCache.get(commentCommit);
|
|
82
|
+
if (!projectionCache.has(commentCommit)) {
|
|
83
|
+
const sourceText = await getFileAtCommit(commentCommit, relPath, repoRoot);
|
|
84
|
+
revisionProjection = sourceText == null
|
|
85
|
+
? undefined
|
|
86
|
+
: createRevisionProjection(toReanchorLines(sourceText), documentLines);
|
|
87
|
+
anchorContext = sourceText == null
|
|
88
|
+
? undefined
|
|
89
|
+
: createAnchorContextIndex(toReanchorLines(sourceText), documentLines);
|
|
90
|
+
projectionCache.set(commentCommit, revisionProjection);
|
|
91
|
+
contextCache.set(commentCommit, anchorContext);
|
|
92
|
+
}
|
|
51
93
|
const result = reanchorComment(comment, documentLines, {
|
|
52
94
|
diffHunks: hunks,
|
|
53
95
|
threshold,
|
|
54
96
|
commitIsStale: true,
|
|
55
97
|
proximityWindow,
|
|
98
|
+
revisionProjection,
|
|
99
|
+
anchorContext,
|
|
100
|
+
getFuzzySearchIndex,
|
|
56
101
|
});
|
|
102
|
+
const resultIndex = results.length;
|
|
57
103
|
results.push(result);
|
|
104
|
+
if (anchorContext) {
|
|
105
|
+
const group = reconciliationGroups.get(commentCommit);
|
|
106
|
+
if (group) {
|
|
107
|
+
group.comments.push(comment);
|
|
108
|
+
group.resultIndexes.push(resultIndex);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
reconciliationGroups.set(commentCommit, {
|
|
112
|
+
comments: [comment],
|
|
113
|
+
resultIndexes: [resultIndex],
|
|
114
|
+
anchorContext,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
58
118
|
continue;
|
|
59
119
|
}
|
|
60
120
|
// non-stale or no commit
|
|
61
|
-
results.push(reanchorComment(comment, documentLines, {
|
|
121
|
+
results.push(reanchorComment(comment, documentLines, {
|
|
122
|
+
threshold,
|
|
123
|
+
commitIsStale: false,
|
|
124
|
+
proximityWindow,
|
|
125
|
+
getFuzzySearchIndex,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
for (const group of reconciliationGroups.values()) {
|
|
129
|
+
const reconciled = reconcileCommentAnchors(group.comments, group.resultIndexes.map((index) => results[index]), group.anchorContext);
|
|
130
|
+
for (const [offset, resultIndex] of group.resultIndexes.entries()) {
|
|
131
|
+
results[resultIndex] = reconciled[offset];
|
|
132
|
+
}
|
|
62
133
|
}
|
|
63
134
|
return results;
|
|
64
135
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Comment } from "./types.js";
|
|
2
|
+
export interface RevisionProjectionIndex {
|
|
3
|
+
sourceLines: string[];
|
|
4
|
+
targetLines: string[];
|
|
5
|
+
lineMap: Map<number, number>;
|
|
6
|
+
}
|
|
7
|
+
export interface ProjectedAnchor {
|
|
8
|
+
line: number;
|
|
9
|
+
endLine: number;
|
|
10
|
+
startColumn?: number;
|
|
11
|
+
endColumn?: number;
|
|
12
|
+
text: string;
|
|
13
|
+
score: number;
|
|
14
|
+
exact: boolean;
|
|
15
|
+
contextSupport: number;
|
|
16
|
+
contextMargin: number;
|
|
17
|
+
reason: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function createRevisionProjection(sourceLines: string[], targetLines: string[]): RevisionProjectionIndex;
|
|
20
|
+
export declare function projectCommentAnchor(comment: Comment, projection: RevisionProjectionIndex, threshold: number): ProjectedAnchor | undefined;
|
|
21
|
+
//# sourceMappingURL=revision-projection.d.ts.map
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { combinedScore, exactMatch } from "./fuzzy.js";
|
|
2
|
+
const CONTEXT_RADIUS = 8;
|
|
3
|
+
export function createRevisionProjection(sourceLines, targetLines) {
|
|
4
|
+
const sourceOccurrences = collectLineOccurrences(sourceLines);
|
|
5
|
+
const targetOccurrences = collectLineOccurrences(targetLines);
|
|
6
|
+
const lineMap = new Map();
|
|
7
|
+
for (const [text, sourceLineNumbers] of sourceOccurrences) {
|
|
8
|
+
const targetLineNumbers = targetOccurrences.get(text);
|
|
9
|
+
if (sourceLineNumbers.length === 1 && targetLineNumbers?.length === 1) {
|
|
10
|
+
lineMap.set(sourceLineNumbers[0], targetLineNumbers[0]);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return { sourceLines, targetLines, lineMap };
|
|
14
|
+
}
|
|
15
|
+
export function projectCommentAnchor(comment, projection, threshold) {
|
|
16
|
+
if (comment.line == null || !comment.selected_text)
|
|
17
|
+
return undefined;
|
|
18
|
+
const sourceText = extractText(projection.sourceLines, comment.line, comment.end_line, comment.start_column, comment.end_column);
|
|
19
|
+
if (sourceText !== comment.selected_text)
|
|
20
|
+
return undefined;
|
|
21
|
+
const exactCandidates = exactMatch(projection.targetLines, comment.selected_text);
|
|
22
|
+
if (exactCandidates.length === 1) {
|
|
23
|
+
const candidate = exactCandidates[0];
|
|
24
|
+
const contextSupport = countIndependentExactSupport(comment, candidate.line, projection);
|
|
25
|
+
if (contextSupport >= 2) {
|
|
26
|
+
return {
|
|
27
|
+
line: candidate.line,
|
|
28
|
+
endLine: candidate.endLine,
|
|
29
|
+
startColumn: candidate.startColumn,
|
|
30
|
+
endColumn: candidate.endColumn,
|
|
31
|
+
text: candidate.text,
|
|
32
|
+
score: 1,
|
|
33
|
+
exact: true,
|
|
34
|
+
contextSupport,
|
|
35
|
+
contextMargin: 1,
|
|
36
|
+
reason: "Source revision and neighboring line evidence confirm exact relocation.",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (exactCandidates.length > 0)
|
|
41
|
+
return undefined;
|
|
42
|
+
const projected = projectLineFromContext(comment, projection);
|
|
43
|
+
if (projected == null)
|
|
44
|
+
return undefined;
|
|
45
|
+
const projectedLine = projected.line;
|
|
46
|
+
const lineSpan = (comment.end_line ?? comment.line) - comment.line;
|
|
47
|
+
const projectedEndLine = projectedLine + lineSpan;
|
|
48
|
+
if (projectedLine < 1
|
|
49
|
+
|| projectedEndLine >= projection.targetLines.length) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const columns = projectColumns(comment, projection, projectedLine);
|
|
53
|
+
const targetText = extractText(projection.targetLines, projectedLine, projectedEndLine, columns.startColumn, columns.endColumn);
|
|
54
|
+
if (targetText == null)
|
|
55
|
+
return undefined;
|
|
56
|
+
const score = Math.min(1, combinedScore(comment.selected_text, targetText) + 0.1);
|
|
57
|
+
if (score < threshold)
|
|
58
|
+
return undefined;
|
|
59
|
+
return {
|
|
60
|
+
line: projectedLine,
|
|
61
|
+
endLine: projectedEndLine,
|
|
62
|
+
startColumn: columns.startColumn,
|
|
63
|
+
endColumn: columns.endColumn,
|
|
64
|
+
text: targetText,
|
|
65
|
+
score,
|
|
66
|
+
exact: targetText === comment.selected_text,
|
|
67
|
+
contextSupport: projected.support,
|
|
68
|
+
contextMargin: projected.margin,
|
|
69
|
+
reason: "Source revision context projects the edited anchor range.",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function collectLineOccurrences(lines) {
|
|
73
|
+
const occurrences = new Map();
|
|
74
|
+
for (let line = 1; line < lines.length; line += 1) {
|
|
75
|
+
const text = lines[line];
|
|
76
|
+
if (!text.trim())
|
|
77
|
+
continue;
|
|
78
|
+
const existing = occurrences.get(text);
|
|
79
|
+
if (existing) {
|
|
80
|
+
existing.push(line);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
occurrences.set(text, [line]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return occurrences;
|
|
87
|
+
}
|
|
88
|
+
function countIndependentExactSupport(comment, candidateLine, projection) {
|
|
89
|
+
const sourceEndLine = comment.end_line ?? comment.line;
|
|
90
|
+
const expectedShift = candidateLine - comment.line;
|
|
91
|
+
if (comment.start_column != null
|
|
92
|
+
|| comment.end_column != null) {
|
|
93
|
+
const mappedContainerLine = projection.lineMap.get(comment.line);
|
|
94
|
+
if (mappedContainerLine === candidateLine)
|
|
95
|
+
return 2;
|
|
96
|
+
}
|
|
97
|
+
let support = 0;
|
|
98
|
+
for (let distance = 1; distance <= CONTEXT_RADIUS; distance += 1) {
|
|
99
|
+
for (const sourceLine of [
|
|
100
|
+
comment.line - distance,
|
|
101
|
+
sourceEndLine + distance,
|
|
102
|
+
]) {
|
|
103
|
+
if (sourceLine < 1 || sourceLine >= projection.sourceLines.length) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const targetLine = projection.lineMap.get(sourceLine);
|
|
107
|
+
if (targetLine != null && targetLine - sourceLine === expectedShift) {
|
|
108
|
+
support += 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return support;
|
|
113
|
+
}
|
|
114
|
+
function projectLineFromContext(comment, projection) {
|
|
115
|
+
const sourceLine = comment.line;
|
|
116
|
+
const sourceEndLine = comment.end_line ?? sourceLine;
|
|
117
|
+
const votes = new Map();
|
|
118
|
+
for (let distance = 1; distance <= CONTEXT_RADIUS; distance += 1) {
|
|
119
|
+
for (const contextLine of [
|
|
120
|
+
sourceLine - distance,
|
|
121
|
+
sourceEndLine + distance,
|
|
122
|
+
]) {
|
|
123
|
+
if (contextLine < 1 || contextLine >= projection.sourceLines.length) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const targetLine = projection.lineMap.get(contextLine);
|
|
127
|
+
if (targetLine == null)
|
|
128
|
+
continue;
|
|
129
|
+
const shift = targetLine - contextLine;
|
|
130
|
+
const vote = votes.get(shift);
|
|
131
|
+
if (vote) {
|
|
132
|
+
vote.count += 1;
|
|
133
|
+
vote.nearest = Math.min(vote.nearest, distance);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
votes.set(shift, { count: 1, nearest: distance });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const ranked = [...votes.entries()].sort((left, right) => right[1].count - left[1].count
|
|
141
|
+
|| left[1].nearest - right[1].nearest
|
|
142
|
+
|| Math.abs(left[0]) - Math.abs(right[0]));
|
|
143
|
+
const best = ranked[0];
|
|
144
|
+
if (!best)
|
|
145
|
+
return undefined;
|
|
146
|
+
if (best[1].count < 2 && best[1].nearest > 3)
|
|
147
|
+
return undefined;
|
|
148
|
+
if (ranked[1]
|
|
149
|
+
&& ranked[1][1].count === best[1].count
|
|
150
|
+
&& ranked[1][1].nearest === best[1].nearest) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
const runnerUpCount = ranked[1]?.[1].count ?? 0;
|
|
154
|
+
return {
|
|
155
|
+
line: sourceLine + best[0],
|
|
156
|
+
support: best[1].count,
|
|
157
|
+
margin: (best[1].count - runnerUpCount) / best[1].count,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function projectColumns(comment, projection, targetLine) {
|
|
161
|
+
if (comment.line == null
|
|
162
|
+
|| (comment.end_line != null && comment.end_line !== comment.line)
|
|
163
|
+
|| comment.start_column == null
|
|
164
|
+
|| comment.end_column == null) {
|
|
165
|
+
return {
|
|
166
|
+
startColumn: comment.start_column,
|
|
167
|
+
endColumn: comment.end_column,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const sourceLine = projection.sourceLines[comment.line];
|
|
171
|
+
const targetLineText = projection.targetLines[targetLine];
|
|
172
|
+
const prefix = sourceLine.slice(0, comment.start_column);
|
|
173
|
+
const suffix = sourceLine.slice(comment.end_column);
|
|
174
|
+
const startColumn = targetLineText.startsWith(prefix)
|
|
175
|
+
? prefix.length
|
|
176
|
+
: Math.min(comment.start_column, targetLineText.length);
|
|
177
|
+
const endColumn = targetLineText.endsWith(suffix)
|
|
178
|
+
? targetLineText.length - suffix.length
|
|
179
|
+
: Math.min(targetLineText.length, startColumn + (comment.end_column - comment.start_column));
|
|
180
|
+
return { startColumn, endColumn };
|
|
181
|
+
}
|
|
182
|
+
function extractText(lines, line, endLine, startColumn, endColumn) {
|
|
183
|
+
const finalLine = endLine ?? line;
|
|
184
|
+
if (line < 1 || finalLine >= lines.length)
|
|
185
|
+
return null;
|
|
186
|
+
if (line === finalLine) {
|
|
187
|
+
const text = lines[line];
|
|
188
|
+
return startColumn != null && endColumn != null
|
|
189
|
+
? text.slice(startColumn, endColumn)
|
|
190
|
+
: text;
|
|
191
|
+
}
|
|
192
|
+
const result = [];
|
|
193
|
+
for (let current = line; current <= finalLine; current += 1) {
|
|
194
|
+
let text = lines[current];
|
|
195
|
+
if (current === line && startColumn != null)
|
|
196
|
+
text = text.slice(startColumn);
|
|
197
|
+
if (current === finalLine && endColumn != null)
|
|
198
|
+
text = text.slice(0, endColumn);
|
|
199
|
+
result.push(text);
|
|
200
|
+
}
|
|
201
|
+
return result.join("\n");
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=revision-projection.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrsf/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "CLI and library for the Markdown Review Sidecar Format (MRSF)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"test": "vitest run",
|
|
31
31
|
"test:watch": "vitest",
|
|
32
32
|
"lint": "tsc --noEmit",
|
|
33
|
+
"eval:reanchor": "tsc && node dist/evaluation/run-reanchor-eval.js",
|
|
34
|
+
"eval:reanchor:baseline": "tsc && node dist/evaluation/run-reanchor-eval.js --baseline ../evaluation/reanchor/baseline.json",
|
|
35
|
+
"eval:reanchor:generate": "tsc && node dist/evaluation/run-reanchor-generator.js",
|
|
36
|
+
"eval:reanchor:profile": "tsc && node dist/evaluation/run-reanchor-profile.js",
|
|
33
37
|
"prepublishOnly": "cp ../LICENSE . && npm run build && npm test",
|
|
34
38
|
"postpublish": "rm -f LICENSE"
|
|
35
39
|
},
|