@mrsf/cli 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -9
- package/dist/browser.cjs +1101 -24
- package/dist/browser.cjs.map +4 -4
- package/dist/browser.d.ts +11 -0
- package/dist/browser.js +8 -0
- package/dist/commands/add.js +12 -4
- package/dist/evaluation/generate-reanchor-cases.d.ts +10 -0
- package/dist/evaluation/generate-reanchor-cases.js +301 -0
- package/dist/evaluation/reanchor-eval.d.ts +111 -0
- package/dist/evaluation/reanchor-eval.js +266 -0
- package/dist/evaluation/reanchor-gates.d.ts +29 -0
- package/dist/evaluation/reanchor-gates.js +47 -0
- package/dist/evaluation/run-reanchor-eval.d.ts +3 -0
- package/dist/evaluation/run-reanchor-eval.js +103 -0
- package/dist/evaluation/run-reanchor-generator.d.ts +3 -0
- package/dist/evaluation/run-reanchor-generator.js +63 -0
- package/dist/evaluation/run-reanchor-profile.d.ts +3 -0
- package/dist/evaluation/run-reanchor-profile.js +129 -0
- package/dist/index.cjs +1226 -34
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/dist/lib/anchor-context.d.ts +46 -0
- package/dist/lib/anchor-context.js +474 -0
- package/dist/lib/confidence-calibration.d.ts +16 -0
- package/dist/lib/confidence-calibration.js +120 -0
- package/dist/lib/fuzzy.d.ts +14 -1
- package/dist/lib/fuzzy.js +140 -19
- package/dist/lib/git.d.ts +9 -0
- package/dist/lib/git.js +30 -0
- package/dist/lib/global-reconciliation.d.ts +11 -0
- package/dist/lib/global-reconciliation.js +145 -0
- package/dist/lib/reanchor-core.d.ts +7 -0
- package/dist/lib/reanchor-core.js +46 -4
- package/dist/lib/reanchor.js +82 -11
- package/dist/lib/revision-projection.d.ts +21 -0
- package/dist/lib/revision-projection.js +203 -0
- package/package.json +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,10 @@ import * as validator from "./lib/validator.js";
|
|
|
12
12
|
import * as fuzzy from "./lib/fuzzy.js";
|
|
13
13
|
import * as git from "./lib/git.js";
|
|
14
14
|
import * as reanchor from "./lib/reanchor.js";
|
|
15
|
+
import * as anchorContext from "./lib/anchor-context.js";
|
|
16
|
+
import * as globalReconciliation from "./lib/global-reconciliation.js";
|
|
17
|
+
import * as revisionProjection from "./lib/revision-projection.js";
|
|
18
|
+
import * as confidenceCalibration from "./lib/confidence-calibration.js";
|
|
15
19
|
import * as comments from "./lib/comments.js";
|
|
16
20
|
import * as identity from "./lib/identity.js";
|
|
17
21
|
import * as validateCore from "./lib/validate-core.js";
|
|
@@ -44,7 +48,9 @@ export declare const fuzzySearch: typeof fuzzy.fuzzySearch;
|
|
|
44
48
|
export declare const combinedScore: typeof fuzzy.combinedScore;
|
|
45
49
|
export declare const isGitAvailable: typeof git.isGitAvailable;
|
|
46
50
|
export declare const findRepoRoot: typeof git.findRepoRoot;
|
|
51
|
+
export declare const getGitUserName: typeof git.getGitUserName;
|
|
47
52
|
export declare const getCurrentCommit: typeof git.getCurrentCommit;
|
|
53
|
+
export declare const resolveCommit: typeof git.resolveCommit;
|
|
48
54
|
export declare const isStale: typeof git.isStale;
|
|
49
55
|
export declare const getDiff: typeof git.getDiff;
|
|
50
56
|
export declare const getLineShift: typeof git.getLineShift;
|
|
@@ -62,6 +68,13 @@ export declare const resolveAnchor: typeof reanchor.resolveAnchor;
|
|
|
62
68
|
export declare const reanchorDocument: typeof reanchor.reanchorDocument;
|
|
63
69
|
export declare const applyReanchorResults: typeof reanchor.applyReanchorResults;
|
|
64
70
|
export declare const reanchorFile: typeof reanchor.reanchorFile;
|
|
71
|
+
export declare const createAnchorContextIndex: typeof anchorContext.createAnchorContextIndex;
|
|
72
|
+
export declare const reconcileCommentAnchors: typeof globalReconciliation.reconcileCommentAnchors;
|
|
73
|
+
export declare const createRevisionProjection: typeof revisionProjection.createRevisionProjection;
|
|
74
|
+
export declare const calibrateAnchorEvidence: typeof confidenceCalibration.calibrateAnchorEvidence;
|
|
75
|
+
export type { AnchorContextIndex, ContextAnchorCandidate, ContextAnchorResolution, } from "./lib/anchor-context.js";
|
|
76
|
+
export type { ProjectedAnchor, RevisionProjectionIndex, } from "./lib/revision-projection.js";
|
|
77
|
+
export type { CalibratedAnchor, ConfidenceBand, } from "./lib/confidence-calibration.js";
|
|
65
78
|
export declare const addComment: typeof comments.addComment;
|
|
66
79
|
export declare const editComment: typeof comments.editComment;
|
|
67
80
|
export declare const normalizeCommentExtensions: typeof comments.normalizeCommentExtensions;
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,10 @@ import * as validator from "./lib/validator.js";
|
|
|
12
12
|
import * as fuzzy from "./lib/fuzzy.js";
|
|
13
13
|
import * as git from "./lib/git.js";
|
|
14
14
|
import * as reanchor from "./lib/reanchor.js";
|
|
15
|
+
import * as anchorContext from "./lib/anchor-context.js";
|
|
16
|
+
import * as globalReconciliation from "./lib/global-reconciliation.js";
|
|
17
|
+
import * as revisionProjection from "./lib/revision-projection.js";
|
|
18
|
+
import * as confidenceCalibration from "./lib/confidence-calibration.js";
|
|
15
19
|
import * as comments from "./lib/comments.js";
|
|
16
20
|
import * as identity from "./lib/identity.js";
|
|
17
21
|
import * as validateCore from "./lib/validate-core.js";
|
|
@@ -49,7 +53,9 @@ export const combinedScore = fuzzy.combinedScore;
|
|
|
49
53
|
// Git
|
|
50
54
|
export const isGitAvailable = git.isGitAvailable;
|
|
51
55
|
export const findRepoRoot = git.findRepoRoot;
|
|
56
|
+
export const getGitUserName = git.getGitUserName;
|
|
52
57
|
export const getCurrentCommit = git.getCurrentCommit;
|
|
58
|
+
export const resolveCommit = git.resolveCommit;
|
|
53
59
|
export const isStale = git.isStale;
|
|
54
60
|
export const getDiff = git.getDiff;
|
|
55
61
|
export const getLineShift = git.getLineShift;
|
|
@@ -68,6 +74,10 @@ export const resolveAnchor = reanchor.resolveAnchor;
|
|
|
68
74
|
export const reanchorDocument = reanchor.reanchorDocument;
|
|
69
75
|
export const applyReanchorResults = reanchor.applyReanchorResults;
|
|
70
76
|
export const reanchorFile = reanchor.reanchorFile;
|
|
77
|
+
export const createAnchorContextIndex = anchorContext.createAnchorContextIndex;
|
|
78
|
+
export const reconcileCommentAnchors = globalReconciliation.reconcileCommentAnchors;
|
|
79
|
+
export const createRevisionProjection = revisionProjection.createRevisionProjection;
|
|
80
|
+
export const calibrateAnchorEvidence = confidenceCalibration.calibrateAnchorEvidence;
|
|
71
81
|
// Comments
|
|
72
82
|
export const addComment = comments.addComment;
|
|
73
83
|
export const editComment = comments.editComment;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Comment, ReanchorStatus } from "./types.js";
|
|
2
|
+
export declare const MAX_CONTEXT_CANDIDATE_BLOCKS = 64;
|
|
3
|
+
type BlockType = "heading" | "code" | "list" | "table" | "blockquote" | "paragraph";
|
|
4
|
+
interface MarkdownBlock {
|
|
5
|
+
startLine: number;
|
|
6
|
+
endLine: number;
|
|
7
|
+
type: BlockType;
|
|
8
|
+
text: string;
|
|
9
|
+
headingPath: string[];
|
|
10
|
+
}
|
|
11
|
+
interface DocumentBlockIndex {
|
|
12
|
+
lines: string[];
|
|
13
|
+
blocks: MarkdownBlock[];
|
|
14
|
+
lineToBlock: Map<number, number>;
|
|
15
|
+
tokenPostings: Map<string, number[]>;
|
|
16
|
+
}
|
|
17
|
+
export interface AnchorContextIndex {
|
|
18
|
+
source: DocumentBlockIndex;
|
|
19
|
+
target: DocumentBlockIndex;
|
|
20
|
+
}
|
|
21
|
+
export interface ContextAnchorResolution {
|
|
22
|
+
status: Extract<ReanchorStatus, "anchored" | "fuzzy" | "ambiguous" | "orphaned">;
|
|
23
|
+
score: number;
|
|
24
|
+
line?: number;
|
|
25
|
+
endLine?: number;
|
|
26
|
+
startColumn?: number;
|
|
27
|
+
endColumn?: number;
|
|
28
|
+
text?: string;
|
|
29
|
+
candidateMargin: number;
|
|
30
|
+
reason: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ContextAnchorCandidate {
|
|
33
|
+
score: number;
|
|
34
|
+
line: number;
|
|
35
|
+
endLine: number;
|
|
36
|
+
startColumn?: number;
|
|
37
|
+
endColumn?: number;
|
|
38
|
+
text: string;
|
|
39
|
+
exact: boolean;
|
|
40
|
+
}
|
|
41
|
+
export declare function createAnchorContextIndex(sourceLines: string[], targetLines: string[]): AnchorContextIndex;
|
|
42
|
+
export declare function resolveContextAnchor(comment: Comment, index: AnchorContextIndex): ContextAnchorResolution | undefined;
|
|
43
|
+
export declare function findContextAnchorCandidates(comment: Comment, index: AnchorContextIndex): ContextAnchorCandidate[];
|
|
44
|
+
export declare function getAnchorContextScope(comment: Comment, index: AnchorContextIndex): string | undefined;
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=anchor-context.d.ts.map
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { combinedScore } from "./fuzzy.js";
|
|
2
|
+
const MATCH_THRESHOLD = 0.35;
|
|
3
|
+
const AMBIGUITY_MARGIN = 0.03;
|
|
4
|
+
export const MAX_CONTEXT_CANDIDATE_BLOCKS = 64;
|
|
5
|
+
export function createAnchorContextIndex(sourceLines, targetLines) {
|
|
6
|
+
return {
|
|
7
|
+
source: createDocumentBlockIndex(sourceLines),
|
|
8
|
+
target: createDocumentBlockIndex(targetLines),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function resolveContextAnchor(comment, index) {
|
|
12
|
+
if (comment.line == null || !comment.selected_text)
|
|
13
|
+
return undefined;
|
|
14
|
+
const sourceBlockIndex = index.source.lineToBlock.get(comment.line);
|
|
15
|
+
if (sourceBlockIndex == null)
|
|
16
|
+
return undefined;
|
|
17
|
+
const sourceBlock = index.source.blocks[sourceBlockIndex];
|
|
18
|
+
const sourceText = extractText(index.source.lines, comment.line, comment.end_line, comment.start_column, comment.end_column);
|
|
19
|
+
if (sourceText !== comment.selected_text)
|
|
20
|
+
return undefined;
|
|
21
|
+
const textAtCurrentPosition = extractText(index.target.lines, comment.line, comment.end_line, comment.start_column, comment.end_column);
|
|
22
|
+
if (textAtCurrentPosition === comment.selected_text) {
|
|
23
|
+
return {
|
|
24
|
+
status: "anchored",
|
|
25
|
+
score: 1,
|
|
26
|
+
line: comment.line,
|
|
27
|
+
endLine: comment.end_line ?? comment.line,
|
|
28
|
+
startColumn: comment.start_column,
|
|
29
|
+
endColumn: comment.end_column,
|
|
30
|
+
text: comment.selected_text,
|
|
31
|
+
candidateMargin: 1,
|
|
32
|
+
reason: "Source-verified anchor remains exact at its stored position.",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const candidates = findContextAnchorCandidates(comment, index);
|
|
36
|
+
const best = candidates[0];
|
|
37
|
+
if (!best) {
|
|
38
|
+
if (index.target.lines.slice(1).join("\n").includes(comment.selected_text)) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
status: "orphaned",
|
|
43
|
+
score: 0,
|
|
44
|
+
candidateMargin: 1,
|
|
45
|
+
reason: "Source block has no plausible structural or contextual match.",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (candidates[1] && best.score - candidates[1].score < AMBIGUITY_MARGIN) {
|
|
49
|
+
return {
|
|
50
|
+
status: "ambiguous",
|
|
51
|
+
score: best.score,
|
|
52
|
+
line: best.line,
|
|
53
|
+
endLine: best.endLine,
|
|
54
|
+
candidateMargin: best.score - candidates[1].score,
|
|
55
|
+
reason: `Structural candidates are too close (${best.score.toFixed(3)} vs `
|
|
56
|
+
+ `${candidates[1].score.toFixed(3)}).`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const repeatedExactText = best.exact
|
|
60
|
+
&& countOccurrences(index.target.lines.slice(1).join("\n"), comment.selected_text) > 1;
|
|
61
|
+
return {
|
|
62
|
+
status: best.exact && !repeatedExactText ? "anchored" : "fuzzy",
|
|
63
|
+
score: best.exact && !repeatedExactText ? 1 : best.score,
|
|
64
|
+
line: best.line,
|
|
65
|
+
endLine: best.endLine,
|
|
66
|
+
startColumn: best.startColumn,
|
|
67
|
+
endColumn: best.endColumn,
|
|
68
|
+
text: best.text,
|
|
69
|
+
candidateMargin: candidates[1] ? best.score - candidates[1].score : 1,
|
|
70
|
+
reason: best.exact && !repeatedExactText
|
|
71
|
+
? "Markdown structure and bidirectional context disambiguate the exact anchor."
|
|
72
|
+
: repeatedExactText
|
|
73
|
+
? "Markdown context selects one repeated exact anchor tentatively."
|
|
74
|
+
: "Markdown structure and bidirectional context locate the edited anchor.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function findContextAnchorCandidates(comment, index) {
|
|
78
|
+
if (comment.line == null || !comment.selected_text)
|
|
79
|
+
return [];
|
|
80
|
+
const sourceBlockIndex = index.source.lineToBlock.get(comment.line);
|
|
81
|
+
if (sourceBlockIndex == null)
|
|
82
|
+
return [];
|
|
83
|
+
const sourceBlock = index.source.blocks[sourceBlockIndex];
|
|
84
|
+
const sourceText = extractText(index.source.lines, comment.line, comment.end_line, comment.start_column, comment.end_column);
|
|
85
|
+
if (sourceText !== comment.selected_text)
|
|
86
|
+
return [];
|
|
87
|
+
return createCandidateWindows(sourceBlock, sourceBlockIndex, comment.line, index)
|
|
88
|
+
.map((candidate) => ({
|
|
89
|
+
...candidate,
|
|
90
|
+
score: scoreCandidate(sourceBlock, sourceBlockIndex, candidate, index, comment.line),
|
|
91
|
+
}))
|
|
92
|
+
.filter((candidate) => candidate.score >= MATCH_THRESHOLD)
|
|
93
|
+
.sort((left, right) => right.score - left.score
|
|
94
|
+
|| Math.abs(left.startLine - comment.line)
|
|
95
|
+
- Math.abs(right.startLine - comment.line))
|
|
96
|
+
.map((candidate) => {
|
|
97
|
+
const range = resolveCandidateRange(comment, sourceBlock, candidate, index.target);
|
|
98
|
+
return {
|
|
99
|
+
score: candidate.score,
|
|
100
|
+
line: range.line,
|
|
101
|
+
endLine: range.endLine,
|
|
102
|
+
startColumn: range.startColumn,
|
|
103
|
+
endColumn: range.endColumn,
|
|
104
|
+
text: range.text,
|
|
105
|
+
exact: range.text === comment.selected_text,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export function getAnchorContextScope(comment, index) {
|
|
110
|
+
if (comment.line == null)
|
|
111
|
+
return undefined;
|
|
112
|
+
const blockIndex = index.source.lineToBlock.get(comment.line);
|
|
113
|
+
if (blockIndex == null)
|
|
114
|
+
return undefined;
|
|
115
|
+
return index.source.blocks[blockIndex].headingPath.join("\u001f");
|
|
116
|
+
}
|
|
117
|
+
function createDocumentBlockIndex(lines) {
|
|
118
|
+
const blocks = [];
|
|
119
|
+
const lineToBlock = new Map();
|
|
120
|
+
const headings = [];
|
|
121
|
+
let line = 1;
|
|
122
|
+
while (line < lines.length) {
|
|
123
|
+
if (!lines[line].trim()) {
|
|
124
|
+
line += 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const startLine = line;
|
|
128
|
+
const marker = classifyLine(lines[line]);
|
|
129
|
+
if (marker.type === "heading") {
|
|
130
|
+
while (headings.length > 0
|
|
131
|
+
&& headings[headings.length - 1].level >= marker.headingLevel) {
|
|
132
|
+
headings.pop();
|
|
133
|
+
}
|
|
134
|
+
const block = makeBlock(lines, startLine, startLine, "heading", headings.map((heading) => heading.title));
|
|
135
|
+
blocks.push(block);
|
|
136
|
+
headings.push({ level: marker.headingLevel, title: marker.headingTitle });
|
|
137
|
+
line += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (marker.type === "code") {
|
|
141
|
+
line += 1;
|
|
142
|
+
while (line < lines.length && !lines[line].trimStart().startsWith("```")) {
|
|
143
|
+
line += 1;
|
|
144
|
+
}
|
|
145
|
+
if (line < lines.length)
|
|
146
|
+
line += 1;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
line += 1;
|
|
150
|
+
while (line < lines.length
|
|
151
|
+
&& lines[line].trim()
|
|
152
|
+
&& continuesBlock(marker.type, lines[line])) {
|
|
153
|
+
line += 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
blocks.push(makeBlock(lines, startLine, line - 1, marker.type, headings.map((heading) => heading.title)));
|
|
157
|
+
}
|
|
158
|
+
for (const [blockIndex, block] of blocks.entries()) {
|
|
159
|
+
for (let blockLine = block.startLine; blockLine <= block.endLine; blockLine += 1) {
|
|
160
|
+
lineToBlock.set(blockLine, blockIndex);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
lines,
|
|
165
|
+
blocks,
|
|
166
|
+
lineToBlock,
|
|
167
|
+
tokenPostings: createTokenPostings(blocks),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function makeBlock(lines, startLine, endLine, type, headingPath) {
|
|
171
|
+
return {
|
|
172
|
+
startLine,
|
|
173
|
+
endLine,
|
|
174
|
+
type,
|
|
175
|
+
text: lines.slice(startLine, endLine + 1).join("\n"),
|
|
176
|
+
headingPath,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function classifyLine(line) {
|
|
180
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
181
|
+
if (heading) {
|
|
182
|
+
return {
|
|
183
|
+
type: "heading",
|
|
184
|
+
headingLevel: heading[1].length,
|
|
185
|
+
headingTitle: heading[2].trim(),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (line.trimStart().startsWith("```")) {
|
|
189
|
+
return { type: "code", headingLevel: 0, headingTitle: "" };
|
|
190
|
+
}
|
|
191
|
+
if (/^\s*(?:[-*+]|\d+\.)\s+/.test(line)) {
|
|
192
|
+
return { type: "list", headingLevel: 0, headingTitle: "" };
|
|
193
|
+
}
|
|
194
|
+
if (/^\s*\|/.test(line)) {
|
|
195
|
+
return { type: "table", headingLevel: 0, headingTitle: "" };
|
|
196
|
+
}
|
|
197
|
+
if (/^\s*>/.test(line)) {
|
|
198
|
+
return { type: "blockquote", headingLevel: 0, headingTitle: "" };
|
|
199
|
+
}
|
|
200
|
+
return { type: "paragraph", headingLevel: 0, headingTitle: "" };
|
|
201
|
+
}
|
|
202
|
+
function continuesBlock(type, line) {
|
|
203
|
+
if (type === "list")
|
|
204
|
+
return /^\s*(?:[-*+]|\d+\.)\s+/.test(line);
|
|
205
|
+
if (type === "table")
|
|
206
|
+
return /^\s*\|/.test(line);
|
|
207
|
+
if (type === "blockquote")
|
|
208
|
+
return /^\s*>/.test(line);
|
|
209
|
+
if (type === "paragraph") {
|
|
210
|
+
const next = classifyLine(line);
|
|
211
|
+
return next.type === "paragraph";
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
function createCandidateWindows(sourceBlock, sourceBlockIndex, originalLine, index) {
|
|
216
|
+
const target = index.target;
|
|
217
|
+
const candidates = [];
|
|
218
|
+
for (const start of retrieveCandidateBlocks(sourceBlock, sourceBlockIndex, originalLine, index)) {
|
|
219
|
+
const block = target.blocks[start];
|
|
220
|
+
candidates.push(toCandidateWindow(target, start, start));
|
|
221
|
+
if (sourceBlock.type === "paragraph"
|
|
222
|
+
&& block.type === "paragraph"
|
|
223
|
+
&& target.blocks[start + 1]?.type === "paragraph"
|
|
224
|
+
&& samePath(block.headingPath, target.blocks[start + 1].headingPath)) {
|
|
225
|
+
candidates.push(toCandidateWindow(target, start, start + 1));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return candidates;
|
|
229
|
+
}
|
|
230
|
+
function createTokenPostings(blocks) {
|
|
231
|
+
const postings = new Map();
|
|
232
|
+
for (const [blockIndex, block] of blocks.entries()) {
|
|
233
|
+
for (const token of new Set(tokenize(block.text))) {
|
|
234
|
+
const blocksForToken = postings.get(token);
|
|
235
|
+
if (blocksForToken) {
|
|
236
|
+
blocksForToken.push(blockIndex);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
postings.set(token, [blockIndex]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return postings;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Retrieve a small candidate pool using rare content tokens and directional
|
|
247
|
+
* neighbor evidence. Expensive similarity scoring is bounded to this pool.
|
|
248
|
+
*/
|
|
249
|
+
function retrieveCandidateBlocks(sourceBlock, sourceBlockIndex, originalLine, index) {
|
|
250
|
+
const votes = new Map();
|
|
251
|
+
const targetCount = index.target.blocks.length;
|
|
252
|
+
const addEvidence = (text, targetOffset, weight) => {
|
|
253
|
+
if (!text)
|
|
254
|
+
return;
|
|
255
|
+
const rankedTokens = [...new Set(tokenize(text))]
|
|
256
|
+
.map((token) => ({
|
|
257
|
+
token,
|
|
258
|
+
postings: index.target.tokenPostings.get(token) ?? [],
|
|
259
|
+
}))
|
|
260
|
+
.filter((item) => item.postings.length > 0)
|
|
261
|
+
.sort((left, right) => left.postings.length - right.postings.length)
|
|
262
|
+
.slice(0, 12);
|
|
263
|
+
for (const { postings } of rankedTokens) {
|
|
264
|
+
const rarity = Math.log1p(targetCount / postings.length);
|
|
265
|
+
for (const posting of postings) {
|
|
266
|
+
const candidate = posting + targetOffset;
|
|
267
|
+
if (candidate >= 0 && candidate < targetCount) {
|
|
268
|
+
votes.set(candidate, (votes.get(candidate) ?? 0) + weight * rarity);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
addEvidence(sourceBlock.text, 0, 1);
|
|
274
|
+
addEvidence(index.source.blocks[sourceBlockIndex - 1]?.text, 1, 0.7);
|
|
275
|
+
addEvidence(index.source.blocks[sourceBlockIndex + 1]?.text, -1, 0.7);
|
|
276
|
+
const nearbyBlock = closestBlockToLine(index.target.blocks, originalLine);
|
|
277
|
+
if (nearbyBlock != null) {
|
|
278
|
+
for (let offset = -2; offset <= 2; offset += 1) {
|
|
279
|
+
const candidate = nearbyBlock + offset;
|
|
280
|
+
if (candidate >= 0 && candidate < targetCount) {
|
|
281
|
+
votes.set(candidate, (votes.get(candidate) ?? 0) + 0.25);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return [...votes.entries()]
|
|
286
|
+
.sort((left, right) => right[1] - left[1]
|
|
287
|
+
|| Math.abs(index.target.blocks[left[0]].startLine - originalLine)
|
|
288
|
+
- Math.abs(index.target.blocks[right[0]].startLine - originalLine)
|
|
289
|
+
|| left[0] - right[0])
|
|
290
|
+
.slice(0, MAX_CONTEXT_CANDIDATE_BLOCKS)
|
|
291
|
+
.map(([blockIndex]) => blockIndex);
|
|
292
|
+
}
|
|
293
|
+
function closestBlockToLine(blocks, line) {
|
|
294
|
+
if (blocks.length === 0)
|
|
295
|
+
return undefined;
|
|
296
|
+
let low = 0;
|
|
297
|
+
let high = blocks.length - 1;
|
|
298
|
+
while (low <= high) {
|
|
299
|
+
const middle = Math.floor((low + high) / 2);
|
|
300
|
+
const block = blocks[middle];
|
|
301
|
+
if (line < block.startLine) {
|
|
302
|
+
high = middle - 1;
|
|
303
|
+
}
|
|
304
|
+
else if (line > block.endLine) {
|
|
305
|
+
low = middle + 1;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
return middle;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (low >= blocks.length)
|
|
312
|
+
return blocks.length - 1;
|
|
313
|
+
if (high < 0)
|
|
314
|
+
return 0;
|
|
315
|
+
return Math.abs(blocks[low].startLine - line)
|
|
316
|
+
< Math.abs(blocks[high].endLine - line)
|
|
317
|
+
? low
|
|
318
|
+
: high;
|
|
319
|
+
}
|
|
320
|
+
function toCandidateWindow(target, startBlock, endBlock) {
|
|
321
|
+
const first = target.blocks[startBlock];
|
|
322
|
+
const last = target.blocks[endBlock];
|
|
323
|
+
return {
|
|
324
|
+
startBlock,
|
|
325
|
+
endBlock,
|
|
326
|
+
startLine: first.startLine,
|
|
327
|
+
endLine: last.endLine,
|
|
328
|
+
type: first.type,
|
|
329
|
+
text: target.lines.slice(first.startLine, last.endLine + 1).join("\n"),
|
|
330
|
+
headingPath: first.headingPath,
|
|
331
|
+
score: 0,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function scoreCandidate(sourceBlock, sourceBlockIndex, candidate, index, originalLine) {
|
|
335
|
+
const content = textSimilarity(sourceBlock.text, candidate.text);
|
|
336
|
+
const type = sourceBlock.type === candidate.type ? 1 : 0;
|
|
337
|
+
const heading = pathSimilarity(sourceBlock.headingPath, candidate.headingPath);
|
|
338
|
+
const previous = neighborSimilarity(index.source.blocks[sourceBlockIndex - 1], index.target.blocks[candidate.startBlock - 1]);
|
|
339
|
+
const next = neighborSimilarity(index.source.blocks[sourceBlockIndex + 1], index.target.blocks[candidate.endBlock + 1]);
|
|
340
|
+
const proximity = Math.max(0, 1 - Math.abs(candidate.startLine - originalLine) / 100);
|
|
341
|
+
return Math.min(1, content * 0.45
|
|
342
|
+
+ type * 0.1
|
|
343
|
+
+ heading * 0.1
|
|
344
|
+
+ previous * 0.15
|
|
345
|
+
+ next * 0.15
|
|
346
|
+
+ proximity * 0.05);
|
|
347
|
+
}
|
|
348
|
+
function neighborSimilarity(source, target) {
|
|
349
|
+
if (!source || !target)
|
|
350
|
+
return 0;
|
|
351
|
+
return textSimilarity(source.text, target.text);
|
|
352
|
+
}
|
|
353
|
+
function textSimilarity(left, right) {
|
|
354
|
+
return Math.max(tokenDice(left, right), combinedScore(normalizeText(left), normalizeText(right)));
|
|
355
|
+
}
|
|
356
|
+
function pathSimilarity(left, right) {
|
|
357
|
+
if (left.length === 0 || right.length === 0)
|
|
358
|
+
return 0;
|
|
359
|
+
return tokenDice(left.join(" "), right.join(" "));
|
|
360
|
+
}
|
|
361
|
+
function tokenDice(left, right) {
|
|
362
|
+
const leftTokens = tokenize(left);
|
|
363
|
+
const rightTokens = tokenize(right);
|
|
364
|
+
if (leftTokens.length === 0 || rightTokens.length === 0)
|
|
365
|
+
return 0;
|
|
366
|
+
const remaining = new Map();
|
|
367
|
+
for (const token of rightTokens) {
|
|
368
|
+
remaining.set(token, (remaining.get(token) ?? 0) + 1);
|
|
369
|
+
}
|
|
370
|
+
let overlap = 0;
|
|
371
|
+
for (const token of leftTokens) {
|
|
372
|
+
const count = remaining.get(token) ?? 0;
|
|
373
|
+
if (count > 0) {
|
|
374
|
+
overlap += 1;
|
|
375
|
+
remaining.set(token, count - 1);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return (2 * overlap) / (leftTokens.length + rightTokens.length);
|
|
379
|
+
}
|
|
380
|
+
function tokenize(text) {
|
|
381
|
+
return normalizeText(text).match(/[\p{L}\p{N}_-]+/gu) ?? [];
|
|
382
|
+
}
|
|
383
|
+
function normalizeText(text) {
|
|
384
|
+
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
385
|
+
}
|
|
386
|
+
function samePath(left, right) {
|
|
387
|
+
return left.length === right.length
|
|
388
|
+
&& left.every((item, index) => item === right[index]);
|
|
389
|
+
}
|
|
390
|
+
function resolveCandidateRange(comment, sourceBlock, candidate, target) {
|
|
391
|
+
const exactIndex = candidate.text.indexOf(comment.selected_text);
|
|
392
|
+
const selectedIsWholeBlock = comment.line === sourceBlock.startLine
|
|
393
|
+
&& (comment.end_line ?? comment.line) === sourceBlock.endLine
|
|
394
|
+
&& comment.start_column == null
|
|
395
|
+
&& comment.end_column == null
|
|
396
|
+
&& comment.selected_text === sourceBlock.text;
|
|
397
|
+
const sourceOccurrenceIsUnique = countOccurrences(sourceBlock.text, comment.selected_text) === 1;
|
|
398
|
+
if (exactIndex >= 0 && (selectedIsWholeBlock || sourceOccurrenceIsUnique)) {
|
|
399
|
+
return exactRange(candidate.startLine, candidate.text, comment.selected_text, exactIndex);
|
|
400
|
+
}
|
|
401
|
+
if (selectedIsWholeBlock) {
|
|
402
|
+
return {
|
|
403
|
+
line: candidate.startLine,
|
|
404
|
+
endLine: candidate.endLine,
|
|
405
|
+
text: candidate.text,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const relativeLine = comment.line - sourceBlock.startLine;
|
|
409
|
+
const line = Math.min(candidate.endLine, candidate.startLine + relativeLine);
|
|
410
|
+
const lineSpan = (comment.end_line ?? comment.line)
|
|
411
|
+
- comment.line;
|
|
412
|
+
const endLine = Math.min(candidate.endLine, line + lineSpan);
|
|
413
|
+
const startColumn = comment.start_column;
|
|
414
|
+
const endColumn = comment.end_column;
|
|
415
|
+
return {
|
|
416
|
+
line,
|
|
417
|
+
endLine,
|
|
418
|
+
startColumn,
|
|
419
|
+
endColumn,
|
|
420
|
+
text: extractText(target.lines, line, endLine, startColumn, endColumn) ?? "",
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function countOccurrences(text, needle) {
|
|
424
|
+
if (!needle)
|
|
425
|
+
return 0;
|
|
426
|
+
let count = 0;
|
|
427
|
+
let offset = 0;
|
|
428
|
+
while (offset <= text.length - needle.length) {
|
|
429
|
+
const index = text.indexOf(needle, offset);
|
|
430
|
+
if (index < 0)
|
|
431
|
+
break;
|
|
432
|
+
count += 1;
|
|
433
|
+
offset = index + 1;
|
|
434
|
+
}
|
|
435
|
+
return count;
|
|
436
|
+
}
|
|
437
|
+
function exactRange(startLine, candidateText, selectedText, index) {
|
|
438
|
+
const before = candidateText.slice(0, index).split("\n");
|
|
439
|
+
const selectedLines = selectedText.split("\n");
|
|
440
|
+
const line = startLine + before.length - 1;
|
|
441
|
+
const startColumn = before.at(-1)?.length ?? 0;
|
|
442
|
+
const finalSelectedLineLength = selectedLines.at(-1)?.length ?? 0;
|
|
443
|
+
return {
|
|
444
|
+
line,
|
|
445
|
+
endLine: line + selectedLines.length - 1,
|
|
446
|
+
startColumn,
|
|
447
|
+
endColumn: selectedLines.length === 1
|
|
448
|
+
? startColumn + finalSelectedLineLength
|
|
449
|
+
: finalSelectedLineLength,
|
|
450
|
+
text: selectedText,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function extractText(lines, line, endLine, startColumn, endColumn) {
|
|
454
|
+
const finalLine = endLine ?? line;
|
|
455
|
+
if (line < 1 || finalLine >= lines.length)
|
|
456
|
+
return null;
|
|
457
|
+
if (line === finalLine) {
|
|
458
|
+
const text = lines[line];
|
|
459
|
+
return startColumn != null && endColumn != null
|
|
460
|
+
? text.slice(startColumn, endColumn)
|
|
461
|
+
: text;
|
|
462
|
+
}
|
|
463
|
+
const result = [];
|
|
464
|
+
for (let current = line; current <= finalLine; current += 1) {
|
|
465
|
+
let text = lines[current];
|
|
466
|
+
if (current === line && startColumn != null)
|
|
467
|
+
text = text.slice(startColumn);
|
|
468
|
+
if (current === finalLine && endColumn != null)
|
|
469
|
+
text = text.slice(0, endColumn);
|
|
470
|
+
result.push(text);
|
|
471
|
+
}
|
|
472
|
+
return result.join("\n");
|
|
473
|
+
}
|
|
474
|
+
//# sourceMappingURL=anchor-context.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ContextAnchorResolution } from "./anchor-context.js";
|
|
2
|
+
import type { ProjectedAnchor } from "./revision-projection.js";
|
|
3
|
+
import type { ReanchorResult } from "./types.js";
|
|
4
|
+
export type ConfidenceBand = "certain" | "probable" | "ambiguous" | "orphaned";
|
|
5
|
+
export interface CalibratedAnchor {
|
|
6
|
+
band: ConfidenceBand;
|
|
7
|
+
result: ReanchorResult;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Calibrate independent revision and structural evidence into a public result.
|
|
11
|
+
*
|
|
12
|
+
* Exact evidence is certain. Edited anchors are probable only when evidence
|
|
13
|
+
* agrees or one source has a decisive margin; conflicting evidence abstains.
|
|
14
|
+
*/
|
|
15
|
+
export declare function calibrateAnchorEvidence(commentId: string, selectedText: string, projected?: ProjectedAnchor, contextual?: ContextAnchorResolution): CalibratedAnchor | undefined;
|
|
16
|
+
//# sourceMappingURL=confidence-calibration.d.ts.map
|