@mrsf/cli 0.4.0 → 0.4.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 +20 -0
- package/dist/browser.d.ts +4 -0
- package/dist/browser.js +3 -0
- package/dist/commands/watch.js +14 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/reanchor-core.d.ts +21 -0
- package/dist/lib/reanchor-core.js +298 -0
- package/dist/lib/reanchor.d.ts +2 -18
- package/dist/lib/reanchor.js +4 -337
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -384,6 +384,26 @@ const updated = addComment(doc, lines, {
|
|
|
384
384
|
await writeSidecar("docs/api.md.review.yaml", updated);
|
|
385
385
|
```
|
|
386
386
|
|
|
387
|
+
### Browser-safe reanchoring
|
|
388
|
+
|
|
389
|
+
The default `@mrsf/cli` entrypoint is Node-oriented and includes file system and git-backed helpers such as `parseSidecar`, `readDocumentLines`, and `reanchorFile`. For browser bundles and editor integrations, use the `@mrsf/cli/browser` subpath instead.
|
|
390
|
+
|
|
391
|
+
`@mrsf/cli/browser` exports the pure text-based matching and reanchoring primitives only:
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import {
|
|
395
|
+
applyReanchorResults,
|
|
396
|
+
reanchorDocumentText,
|
|
397
|
+
} from "@mrsf/cli/browser";
|
|
398
|
+
|
|
399
|
+
const results = reanchorDocumentText(sidecar, markdownText);
|
|
400
|
+
const changed = applyReanchorResults(sidecar, results);
|
|
401
|
+
|
|
402
|
+
console.log(changed, results.map((result) => result.status));
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Use this entrypoint when you already have the document text and sidecar object in memory. It intentionally excludes Node-only helpers such as sidecar discovery, file parsing, file writing, and git-aware diff reanchoring.
|
|
406
|
+
|
|
387
407
|
## Configuration
|
|
388
408
|
|
|
389
409
|
Place a `.mrsf.yaml` at your repository root to configure sidecar discovery:
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { Comment, DiffHunk, FuzzyCandidate, MrsfDocument, ReanchorResult, ReanchorStatus, } from "./lib/types.js";
|
|
2
|
+
export { combinedScore, exactMatch, fuzzySearch, levenshteinScore, normalizedMatch, tokenLcsScore, } from "./lib/fuzzy.js";
|
|
3
|
+
export { applyReanchorResults, DEFAULT_THRESHOLD, HIGH_THRESHOLD, reanchorComment, reanchorDocumentLines, reanchorDocumentText, toReanchorLines, } from "./lib/reanchor-core.js";
|
|
4
|
+
//# sourceMappingURL=browser.d.ts.map
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { combinedScore, exactMatch, fuzzySearch, levenshteinScore, normalizedMatch, tokenLcsScore, } from "./lib/fuzzy.js";
|
|
2
|
+
export { applyReanchorResults, DEFAULT_THRESHOLD, HIGH_THRESHOLD, reanchorComment, reanchorDocumentLines, reanchorDocumentText, toReanchorLines, } from "./lib/reanchor-core.js";
|
|
3
|
+
//# sourceMappingURL=browser.js.map
|
package/dist/commands/watch.js
CHANGED
|
@@ -253,6 +253,7 @@ export function registerWatch(program) {
|
|
|
253
253
|
}
|
|
254
254
|
// ── Graceful shutdown ────────────────────────────────
|
|
255
255
|
async function shutdown() {
|
|
256
|
+
cleanupSignalHandlers();
|
|
256
257
|
// Clear pending debounce timers
|
|
257
258
|
for (const t of timers.values())
|
|
258
259
|
clearTimeout(t);
|
|
@@ -266,8 +267,19 @@ export function registerWatch(program) {
|
|
|
266
267
|
}
|
|
267
268
|
process.exit(totalErrors > 0 ? 1 : 0);
|
|
268
269
|
}
|
|
269
|
-
|
|
270
|
-
|
|
270
|
+
const onSigint = () => {
|
|
271
|
+
void shutdown();
|
|
272
|
+
};
|
|
273
|
+
const onSigterm = () => {
|
|
274
|
+
void shutdown();
|
|
275
|
+
};
|
|
276
|
+
function cleanupSignalHandlers() {
|
|
277
|
+
process.off("SIGINT", onSigint);
|
|
278
|
+
process.off("SIGTERM", onSigterm);
|
|
279
|
+
}
|
|
280
|
+
process.on("SIGINT", onSigint);
|
|
281
|
+
process.on("SIGTERM", onSigterm);
|
|
282
|
+
watcher.once("close", cleanupSignalHandlers);
|
|
271
283
|
// Keep the process alive
|
|
272
284
|
await new Promise(() => {
|
|
273
285
|
// Never resolves — the process runs until interrupted
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { computeHash, syncHash, toYaml, toJson, writeSidecar, } from "./lib/writ
|
|
|
13
13
|
export { validate, validateFile } from "./lib/validator.js";
|
|
14
14
|
export { exactMatch, normalizedMatch, fuzzySearch, combinedScore, } from "./lib/fuzzy.js";
|
|
15
15
|
export { isGitAvailable, findRepoRoot, getCurrentCommit, isStale, getDiff, getLineShift, getFileAtCommit, getStagedFiles, detectRenames, parseDiffHunks, } from "./lib/git.js";
|
|
16
|
-
export { reanchorComment, reanchorDocument, applyReanchorResults, reanchorFile, } from "./lib/reanchor.js";
|
|
16
|
+
export { DEFAULT_THRESHOLD, HIGH_THRESHOLD, reanchorComment, reanchorDocumentLines, reanchorDocumentText, toReanchorLines, reanchorDocument, applyReanchorResults, reanchorFile, } from "./lib/reanchor.js";
|
|
17
17
|
export { addComment, normalizeCommentExtensions, populateSelectedText, resolveComment, unresolveComment, removeComment, filterComments, getThreads, summarize, } from "./lib/comments.js";
|
|
18
18
|
export type { CommentSummary, RemoveCommentOptions } from "./lib/comments.js";
|
|
19
19
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ export { exactMatch, normalizedMatch, fuzzySearch, combinedScore, } from "./lib/
|
|
|
19
19
|
// Git
|
|
20
20
|
export { isGitAvailable, findRepoRoot, getCurrentCommit, isStale, getDiff, getLineShift, getFileAtCommit, getStagedFiles, detectRenames, parseDiffHunks, } from "./lib/git.js";
|
|
21
21
|
// Re-anchoring
|
|
22
|
-
export { reanchorComment, reanchorDocument, applyReanchorResults, reanchorFile, } from "./lib/reanchor.js";
|
|
22
|
+
export { DEFAULT_THRESHOLD, HIGH_THRESHOLD, reanchorComment, reanchorDocumentLines, reanchorDocumentText, toReanchorLines, reanchorDocument, applyReanchorResults, reanchorFile, } from "./lib/reanchor.js";
|
|
23
23
|
// Comments
|
|
24
24
|
export { addComment, normalizeCommentExtensions, populateSelectedText, resolveComment, unresolveComment, removeComment, filterComments, getThreads, summarize, } from "./lib/comments.js";
|
|
25
25
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Comment, DiffHunk, MrsfDocument, ReanchorResult } from "./types.js";
|
|
2
|
+
export declare const HIGH_THRESHOLD = 0.8;
|
|
3
|
+
export declare const DEFAULT_THRESHOLD = 0.6;
|
|
4
|
+
export declare function toReanchorLines(documentText: string): string[];
|
|
5
|
+
export declare function reanchorComment(comment: Comment, documentLines: string[], opts?: {
|
|
6
|
+
diffHunks?: DiffHunk[];
|
|
7
|
+
threshold?: number;
|
|
8
|
+
commitIsStale?: boolean;
|
|
9
|
+
}): ReanchorResult;
|
|
10
|
+
export declare function reanchorDocumentLines(doc: MrsfDocument, documentLines: string[], opts?: {
|
|
11
|
+
threshold?: number;
|
|
12
|
+
}): ReanchorResult[];
|
|
13
|
+
export declare function reanchorDocumentText(doc: MrsfDocument, documentText: string, opts?: {
|
|
14
|
+
threshold?: number;
|
|
15
|
+
}): ReanchorResult[];
|
|
16
|
+
export declare function applyReanchorResults(doc: MrsfDocument, results: ReanchorResult[], opts?: {
|
|
17
|
+
updateText?: boolean;
|
|
18
|
+
force?: boolean;
|
|
19
|
+
headCommit?: string;
|
|
20
|
+
}): number;
|
|
21
|
+
//# sourceMappingURL=reanchor-core.d.ts.map
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { exactMatch, fuzzySearch, normalizedMatch, } from "./fuzzy.js";
|
|
2
|
+
export const HIGH_THRESHOLD = 0.8;
|
|
3
|
+
export const DEFAULT_THRESHOLD = 0.6;
|
|
4
|
+
export function toReanchorLines(documentText) {
|
|
5
|
+
return ["", ...documentText.replace(/\r\n/g, "\n").split("\n")];
|
|
6
|
+
}
|
|
7
|
+
export function reanchorComment(comment, documentLines, opts = {}) {
|
|
8
|
+
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
9
|
+
const commentId = comment.id;
|
|
10
|
+
const selectedText = comment.selected_text;
|
|
11
|
+
if (!selectedText && comment.line == null) {
|
|
12
|
+
return {
|
|
13
|
+
commentId,
|
|
14
|
+
status: "anchored",
|
|
15
|
+
score: 1.0,
|
|
16
|
+
reason: "Document-level comment (no anchor needed).",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (comment.line != null && opts.diffHunks?.length) {
|
|
20
|
+
const { shift, modified } = getLineShift(opts.diffHunks, comment.line);
|
|
21
|
+
if (!selectedText) {
|
|
22
|
+
const shiftedLine = comment.line + shift;
|
|
23
|
+
const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
|
|
24
|
+
const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : undefined;
|
|
25
|
+
return {
|
|
26
|
+
commentId,
|
|
27
|
+
status: shift === 0 ? "anchored" : "shifted",
|
|
28
|
+
score: 1.0,
|
|
29
|
+
newLine: shiftedLine,
|
|
30
|
+
newEndLine: shiftedEndLine,
|
|
31
|
+
reason: shift === 0
|
|
32
|
+
? "Line-only comment unchanged (diff confirms position)."
|
|
33
|
+
: `Line-only comment shifted by ${shift > 0 ? "+" : ""}${shift} line(s) via diff.`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (!modified) {
|
|
37
|
+
const shiftedLine = comment.line + shift;
|
|
38
|
+
const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
|
|
39
|
+
const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : undefined;
|
|
40
|
+
const textAtShifted = extractText(documentLines, shiftedLine, shiftedEndLine, comment.start_column, comment.end_column);
|
|
41
|
+
if (textAtShifted === selectedText) {
|
|
42
|
+
return {
|
|
43
|
+
commentId,
|
|
44
|
+
status: shift === 0 ? "anchored" : "shifted",
|
|
45
|
+
score: 1.0,
|
|
46
|
+
newLine: shiftedLine,
|
|
47
|
+
newEndLine: shiftedEndLine,
|
|
48
|
+
reason: shift === 0
|
|
49
|
+
? "Diff confirms text unchanged at original position."
|
|
50
|
+
: `Diff shifted by ${shift > 0 ? "+" : ""}${shift} line(s).`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (selectedText) {
|
|
56
|
+
const exactCandidates = exactMatch(documentLines, selectedText);
|
|
57
|
+
if (exactCandidates.length === 1) {
|
|
58
|
+
const candidate = exactCandidates[0];
|
|
59
|
+
return {
|
|
60
|
+
commentId,
|
|
61
|
+
status: "anchored",
|
|
62
|
+
score: 1.0,
|
|
63
|
+
newLine: candidate.line,
|
|
64
|
+
newEndLine: candidate.endLine,
|
|
65
|
+
newStartColumn: candidate.startColumn,
|
|
66
|
+
newEndColumn: candidate.endColumn,
|
|
67
|
+
reason: "Exact text match (unique).",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (exactCandidates.length > 1 && comment.line != null) {
|
|
71
|
+
const best = closestToLine(exactCandidates, comment.line);
|
|
72
|
+
return {
|
|
73
|
+
commentId,
|
|
74
|
+
status: "anchored",
|
|
75
|
+
score: 1.0,
|
|
76
|
+
newLine: best.line,
|
|
77
|
+
newEndLine: best.endLine,
|
|
78
|
+
newStartColumn: best.startColumn,
|
|
79
|
+
newEndColumn: best.endColumn,
|
|
80
|
+
reason: `Exact text match (${exactCandidates.length} occurrences; chose nearest to original line ${comment.line}).`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const normCandidates = normalizedMatch(documentLines, selectedText);
|
|
84
|
+
if (normCandidates.length === 1) {
|
|
85
|
+
const candidate = normCandidates[0];
|
|
86
|
+
return {
|
|
87
|
+
commentId,
|
|
88
|
+
status: "fuzzy",
|
|
89
|
+
score: candidate.score,
|
|
90
|
+
newLine: candidate.line,
|
|
91
|
+
newEndLine: candidate.endLine,
|
|
92
|
+
newStartColumn: candidate.startColumn,
|
|
93
|
+
newEndColumn: candidate.endColumn,
|
|
94
|
+
anchoredText: candidate.text,
|
|
95
|
+
previousSelectedText: selectedText,
|
|
96
|
+
reason: "Normalized whitespace match.",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const fuzzyCandidates = fuzzySearch(documentLines, selectedText, HIGH_THRESHOLD, comment.line);
|
|
100
|
+
if (fuzzyCandidates.length === 1 || (fuzzyCandidates.length > 0 && fuzzyCandidates[0].score >= HIGH_THRESHOLD)) {
|
|
101
|
+
const best = fuzzyCandidates.length === 1
|
|
102
|
+
? fuzzyCandidates[0]
|
|
103
|
+
: closestToLine(fuzzyCandidates, comment.line ?? 1);
|
|
104
|
+
return {
|
|
105
|
+
commentId,
|
|
106
|
+
status: "fuzzy",
|
|
107
|
+
score: best.score,
|
|
108
|
+
newLine: best.line,
|
|
109
|
+
newEndLine: best.endLine,
|
|
110
|
+
newStartColumn: best.startColumn,
|
|
111
|
+
newEndColumn: best.endColumn,
|
|
112
|
+
anchoredText: best.text,
|
|
113
|
+
previousSelectedText: selectedText,
|
|
114
|
+
reason: `High-confidence fuzzy match (score ${best.score.toFixed(3)}).`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (comment.line != null) {
|
|
119
|
+
const lineIdx = comment.line;
|
|
120
|
+
if (lineIdx > 0 && lineIdx < documentLines.length) {
|
|
121
|
+
const qualifier = opts.commitIsStale
|
|
122
|
+
? " (commit is stale — line may have shifted)"
|
|
123
|
+
: "";
|
|
124
|
+
if (selectedText) {
|
|
125
|
+
const lineText = documentLines[lineIdx];
|
|
126
|
+
const candidates = fuzzySearch(["", lineText], selectedText, DEFAULT_THRESHOLD);
|
|
127
|
+
if (candidates.length > 0) {
|
|
128
|
+
return {
|
|
129
|
+
commentId,
|
|
130
|
+
status: "fuzzy",
|
|
131
|
+
score: candidates[0].score,
|
|
132
|
+
newLine: comment.line,
|
|
133
|
+
newEndLine: comment.end_line,
|
|
134
|
+
anchoredText: candidates[0].text,
|
|
135
|
+
previousSelectedText: selectedText,
|
|
136
|
+
reason: `Line-fallback with fuzzy text match (score ${candidates[0].score.toFixed(3)})${qualifier}.`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const isLineOnly = !selectedText;
|
|
141
|
+
return {
|
|
142
|
+
commentId,
|
|
143
|
+
status: isLineOnly ? "anchored" : (opts.commitIsStale ? "ambiguous" : "anchored"),
|
|
144
|
+
score: isLineOnly ? 1.0 : (opts.commitIsStale ? 0.5 : 0.8),
|
|
145
|
+
newLine: comment.line,
|
|
146
|
+
newEndLine: comment.end_line,
|
|
147
|
+
reason: isLineOnly
|
|
148
|
+
? "Line-only comment (no selected_text to verify)."
|
|
149
|
+
: `Line/column fallback${qualifier}.`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (selectedText) {
|
|
154
|
+
const lowCandidates = fuzzySearch(documentLines, selectedText, threshold, comment.line);
|
|
155
|
+
if (lowCandidates.length === 1) {
|
|
156
|
+
const candidate = lowCandidates[0];
|
|
157
|
+
return {
|
|
158
|
+
commentId,
|
|
159
|
+
status: "fuzzy",
|
|
160
|
+
score: candidate.score,
|
|
161
|
+
newLine: candidate.line,
|
|
162
|
+
newEndLine: candidate.endLine,
|
|
163
|
+
newStartColumn: candidate.startColumn,
|
|
164
|
+
newEndColumn: candidate.endColumn,
|
|
165
|
+
anchoredText: candidate.text,
|
|
166
|
+
previousSelectedText: selectedText,
|
|
167
|
+
reason: `Low-threshold fuzzy match (score ${candidate.score.toFixed(3)}).`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (lowCandidates.length > 1) {
|
|
171
|
+
const best = lowCandidates[0];
|
|
172
|
+
return {
|
|
173
|
+
commentId,
|
|
174
|
+
status: "ambiguous",
|
|
175
|
+
score: best.score,
|
|
176
|
+
newLine: best.line,
|
|
177
|
+
newEndLine: best.endLine,
|
|
178
|
+
reason: `Ambiguous: ${lowCandidates.length} fuzzy matches (best score ${best.score.toFixed(3)}).`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
commentId,
|
|
184
|
+
status: "orphaned",
|
|
185
|
+
score: 0,
|
|
186
|
+
reason: "No match found. Comment is orphaned.",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export function reanchorDocumentLines(doc, documentLines, opts = {}) {
|
|
190
|
+
return doc.comments.map((comment) => reanchorComment(comment, documentLines, opts));
|
|
191
|
+
}
|
|
192
|
+
export function reanchorDocumentText(doc, documentText, opts = {}) {
|
|
193
|
+
return reanchorDocumentLines(doc, toReanchorLines(documentText), opts);
|
|
194
|
+
}
|
|
195
|
+
export function applyReanchorResults(doc, results, opts = {}) {
|
|
196
|
+
let changed = 0;
|
|
197
|
+
const resultMap = new Map(results.map((result) => [result.commentId, result]));
|
|
198
|
+
for (const comment of doc.comments) {
|
|
199
|
+
const result = resultMap.get(comment.id);
|
|
200
|
+
if (!result)
|
|
201
|
+
continue;
|
|
202
|
+
let isChanged = false;
|
|
203
|
+
if (result.newLine != null && result.newLine !== comment.line) {
|
|
204
|
+
comment.line = result.newLine;
|
|
205
|
+
isChanged = true;
|
|
206
|
+
}
|
|
207
|
+
if (result.newEndLine != null && result.newEndLine !== comment.end_line) {
|
|
208
|
+
comment.end_line = result.newEndLine;
|
|
209
|
+
isChanged = true;
|
|
210
|
+
}
|
|
211
|
+
if (result.newStartColumn != null && result.newStartColumn !== comment.start_column) {
|
|
212
|
+
comment.start_column = result.newStartColumn;
|
|
213
|
+
isChanged = true;
|
|
214
|
+
}
|
|
215
|
+
if (result.newEndColumn != null && result.newEndColumn !== comment.end_column) {
|
|
216
|
+
comment.end_column = result.newEndColumn;
|
|
217
|
+
isChanged = true;
|
|
218
|
+
}
|
|
219
|
+
if (result.anchoredText != null && result.anchoredText !== comment.selected_text) {
|
|
220
|
+
if (opts.updateText) {
|
|
221
|
+
comment.selected_text = result.anchoredText;
|
|
222
|
+
delete comment.anchored_text;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
comment.anchored_text = result.anchoredText;
|
|
226
|
+
}
|
|
227
|
+
isChanged = true;
|
|
228
|
+
}
|
|
229
|
+
else if (result.anchoredText != null && result.anchoredText === comment.selected_text) {
|
|
230
|
+
if (comment.anchored_text) {
|
|
231
|
+
delete comment.anchored_text;
|
|
232
|
+
isChanged = true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (isChanged || result.status !== "anchored") {
|
|
236
|
+
comment.x_reanchor_status = result.status;
|
|
237
|
+
comment.x_reanchor_score = result.score;
|
|
238
|
+
}
|
|
239
|
+
if (opts.force
|
|
240
|
+
&& opts.headCommit
|
|
241
|
+
&& (result.status === "anchored" || result.status === "shifted")
|
|
242
|
+
&& result.score >= HIGH_THRESHOLD) {
|
|
243
|
+
comment.commit = opts.headCommit;
|
|
244
|
+
delete comment.x_reanchor_status;
|
|
245
|
+
delete comment.x_reanchor_score;
|
|
246
|
+
if (comment.anchored_text && comment.anchored_text === comment.selected_text) {
|
|
247
|
+
delete comment.anchored_text;
|
|
248
|
+
}
|
|
249
|
+
isChanged = true;
|
|
250
|
+
}
|
|
251
|
+
if (isChanged) {
|
|
252
|
+
changed += 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return changed;
|
|
256
|
+
}
|
|
257
|
+
function extractText(lines, line, endLine, startColumn, endColumn) {
|
|
258
|
+
const startIdx = line;
|
|
259
|
+
const endIdx = endLine ?? line;
|
|
260
|
+
if (startIdx < 1 || endIdx >= lines.length)
|
|
261
|
+
return null;
|
|
262
|
+
if (startIdx === endIdx) {
|
|
263
|
+
const text = lines[startIdx];
|
|
264
|
+
if (startColumn != null && endColumn != null) {
|
|
265
|
+
return text.slice(startColumn, endColumn);
|
|
266
|
+
}
|
|
267
|
+
return text;
|
|
268
|
+
}
|
|
269
|
+
const result = [];
|
|
270
|
+
for (let index = startIdx; index <= endIdx; index += 1) {
|
|
271
|
+
let currentLine = lines[index];
|
|
272
|
+
if (index === startIdx && startColumn != null)
|
|
273
|
+
currentLine = currentLine.slice(startColumn);
|
|
274
|
+
if (index === endIdx && endColumn != null)
|
|
275
|
+
currentLine = currentLine.slice(0, endColumn);
|
|
276
|
+
result.push(currentLine);
|
|
277
|
+
}
|
|
278
|
+
return result.join("\n");
|
|
279
|
+
}
|
|
280
|
+
function closestToLine(candidates, targetLine) {
|
|
281
|
+
return candidates.reduce((best, candidate) => Math.abs(candidate.line - targetLine) < Math.abs(best.line - targetLine) ? candidate : best);
|
|
282
|
+
}
|
|
283
|
+
function getLineShift(diffHunks, line) {
|
|
284
|
+
let shift = 0;
|
|
285
|
+
let modified = false;
|
|
286
|
+
for (const hunk of diffHunks) {
|
|
287
|
+
const oldStart = hunk.oldStart;
|
|
288
|
+
const oldEnd = hunk.oldStart + Math.max(hunk.oldCount, 1) - 1;
|
|
289
|
+
if (line >= oldStart && line <= oldEnd && hunk.oldCount > 0) {
|
|
290
|
+
modified = true;
|
|
291
|
+
}
|
|
292
|
+
if (line > oldEnd || (hunk.oldCount === 0 && line >= oldStart)) {
|
|
293
|
+
shift += hunk.newCount - hunk.oldCount;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return { shift, modified };
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=reanchor-core.js.map
|
package/dist/lib/reanchor.d.ts
CHANGED
|
@@ -11,15 +11,8 @@
|
|
|
11
11
|
* Step 3 – lower-threshold fuzzy ≥ configured threshold (0.6)
|
|
12
12
|
* Step 4 – orphan
|
|
13
13
|
*/
|
|
14
|
-
import type {
|
|
15
|
-
|
|
16
|
-
* Re-anchor a single comment against the current document lines.
|
|
17
|
-
*/
|
|
18
|
-
export declare function reanchorComment(comment: Comment, documentLines: string[], opts?: {
|
|
19
|
-
diffHunks?: DiffHunk[];
|
|
20
|
-
threshold?: number;
|
|
21
|
-
commitIsStale?: boolean;
|
|
22
|
-
}): ReanchorResult;
|
|
14
|
+
import type { MrsfDocument, ReanchorOptions, ReanchorResult } from "./types.js";
|
|
15
|
+
export { applyReanchorResults, DEFAULT_THRESHOLD, HIGH_THRESHOLD, reanchorComment, reanchorDocumentLines, reanchorDocumentText, toReanchorLines, } from "./reanchor-core.js";
|
|
23
16
|
/**
|
|
24
17
|
* Re-anchor all comments in an MRSF document.
|
|
25
18
|
*/
|
|
@@ -27,15 +20,6 @@ export declare function reanchorDocument(doc: MrsfDocument, documentLines: strin
|
|
|
27
20
|
documentPath?: string;
|
|
28
21
|
repoRoot?: string;
|
|
29
22
|
}): Promise<ReanchorResult[]>;
|
|
30
|
-
/**
|
|
31
|
-
* Apply re-anchor results to the document's comments.
|
|
32
|
-
* Mutates the document in place and returns the number of changed comments.
|
|
33
|
-
*/
|
|
34
|
-
export declare function applyReanchorResults(doc: MrsfDocument, results: ReanchorResult[], opts?: {
|
|
35
|
-
updateText?: boolean;
|
|
36
|
-
force?: boolean;
|
|
37
|
-
headCommit?: string;
|
|
38
|
-
}): number;
|
|
39
23
|
/**
|
|
40
24
|
* High-level re-anchor for a single sidecar file path.
|
|
41
25
|
*/
|
package/dist/lib/reanchor.js
CHANGED
|
@@ -11,236 +11,17 @@
|
|
|
11
11
|
* Step 3 – lower-threshold fuzzy ≥ configured threshold (0.6)
|
|
12
12
|
* Step 4 – orphan
|
|
13
13
|
*/
|
|
14
|
-
import { findRepoRoot, getCurrentCommit, getDiff,
|
|
15
|
-
import {
|
|
14
|
+
import { findRepoRoot, getCurrentCommit, getDiff, isGitAvailable, } from "./git.js";
|
|
15
|
+
import { applyReanchorResults, DEFAULT_THRESHOLD, reanchorComment, reanchorDocumentLines, } from "./reanchor-core.js";
|
|
16
16
|
import { readDocumentLines } from "./parser.js";
|
|
17
17
|
import { sidecarToDocument } from "./discovery.js";
|
|
18
18
|
import { parseSidecar } from "./parser.js";
|
|
19
19
|
import { writeSidecar } from "./writer.js";
|
|
20
20
|
import path from "node:path";
|
|
21
|
+
export { applyReanchorResults, DEFAULT_THRESHOLD, HIGH_THRESHOLD, reanchorComment, reanchorDocumentLines, reanchorDocumentText, toReanchorLines, } from "./reanchor-core.js";
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// Constants
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
|
-
const HIGH_THRESHOLD = 0.8;
|
|
25
|
-
const DEFAULT_THRESHOLD = 0.6;
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Single comment re-anchoring
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
/**
|
|
30
|
-
* Re-anchor a single comment against the current document lines.
|
|
31
|
-
*/
|
|
32
|
-
export function reanchorComment(comment, documentLines, opts = {}) {
|
|
33
|
-
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
34
|
-
const commentId = comment.id;
|
|
35
|
-
const selectedText = comment.selected_text;
|
|
36
|
-
// No selected_text and no line — nothing to anchor
|
|
37
|
-
if (!selectedText && comment.line == null) {
|
|
38
|
-
return {
|
|
39
|
-
commentId,
|
|
40
|
-
status: "anchored",
|
|
41
|
-
score: 1.0,
|
|
42
|
-
reason: "Document-level comment (no anchor needed).",
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
// -----------------------------------------------------------------------
|
|
46
|
-
// Step 0: Diff-based shift
|
|
47
|
-
// -----------------------------------------------------------------------
|
|
48
|
-
if (comment.line != null && opts.diffHunks?.length) {
|
|
49
|
-
const { shift, modified } = getLineShift(opts.diffHunks, comment.line);
|
|
50
|
-
// For line-only comments (no selected_text), apply the diff shift directly
|
|
51
|
-
if (!selectedText) {
|
|
52
|
-
const shiftedLine = comment.line + shift;
|
|
53
|
-
const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
|
|
54
|
-
const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : undefined;
|
|
55
|
-
return {
|
|
56
|
-
commentId,
|
|
57
|
-
status: shift === 0 ? "anchored" : "shifted",
|
|
58
|
-
score: 1.0,
|
|
59
|
-
newLine: shiftedLine,
|
|
60
|
-
newEndLine: shiftedEndLine,
|
|
61
|
-
reason: shift === 0
|
|
62
|
-
? "Line-only comment unchanged (diff confirms position)."
|
|
63
|
-
: `Line-only comment shifted by ${shift > 0 ? "+" : ""}${shift} line(s) via diff.`,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
if (!modified) {
|
|
67
|
-
const shiftedLine = comment.line + shift;
|
|
68
|
-
const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
|
|
69
|
-
const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : undefined;
|
|
70
|
-
// Verify the selected text still matches at the shifted position
|
|
71
|
-
const textAtShifted = extractText(documentLines, shiftedLine, shiftedEndLine, comment.start_column, comment.end_column);
|
|
72
|
-
if (textAtShifted === selectedText) {
|
|
73
|
-
return {
|
|
74
|
-
commentId,
|
|
75
|
-
status: shift === 0 ? "anchored" : "shifted",
|
|
76
|
-
score: 1.0,
|
|
77
|
-
newLine: shiftedLine,
|
|
78
|
-
newEndLine: shiftedEndLine,
|
|
79
|
-
reason: shift === 0
|
|
80
|
-
? "Diff confirms text unchanged at original position."
|
|
81
|
-
: `Diff shifted by ${shift > 0 ? "+" : ""}${shift} line(s).`,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// Fall through — line was modified or text doesn't match after shift
|
|
86
|
-
}
|
|
87
|
-
// -----------------------------------------------------------------------
|
|
88
|
-
// Step 1: Exact text match
|
|
89
|
-
// -----------------------------------------------------------------------
|
|
90
|
-
if (selectedText) {
|
|
91
|
-
const exactCandidates = exactMatch(documentLines, selectedText);
|
|
92
|
-
if (exactCandidates.length === 1) {
|
|
93
|
-
const c = exactCandidates[0];
|
|
94
|
-
return {
|
|
95
|
-
commentId,
|
|
96
|
-
status: "anchored",
|
|
97
|
-
score: 1.0,
|
|
98
|
-
newLine: c.line,
|
|
99
|
-
newEndLine: c.endLine,
|
|
100
|
-
newStartColumn: c.startColumn,
|
|
101
|
-
newEndColumn: c.endColumn,
|
|
102
|
-
reason: "Exact text match (unique).",
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
if (exactCandidates.length > 1 && comment.line != null) {
|
|
106
|
-
// Disambiguate by proximity to original line
|
|
107
|
-
const best = closestToLine(exactCandidates, comment.line);
|
|
108
|
-
return {
|
|
109
|
-
commentId,
|
|
110
|
-
status: "anchored",
|
|
111
|
-
score: 1.0,
|
|
112
|
-
newLine: best.line,
|
|
113
|
-
newEndLine: best.endLine,
|
|
114
|
-
newStartColumn: best.startColumn,
|
|
115
|
-
newEndColumn: best.endColumn,
|
|
116
|
-
reason: `Exact text match (${exactCandidates.length} occurrences; chose nearest to original line ${comment.line}).`,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
// -----------------------------------------------------------------------
|
|
120
|
-
// Step 1.5: Normalized + high-threshold fuzzy match
|
|
121
|
-
// -----------------------------------------------------------------------
|
|
122
|
-
// Try whitespace-normalized match first
|
|
123
|
-
const normCandidates = normalizedMatch(documentLines, selectedText);
|
|
124
|
-
if (normCandidates.length === 1) {
|
|
125
|
-
const c = normCandidates[0];
|
|
126
|
-
return {
|
|
127
|
-
commentId,
|
|
128
|
-
status: "fuzzy",
|
|
129
|
-
score: c.score,
|
|
130
|
-
newLine: c.line,
|
|
131
|
-
newEndLine: c.endLine,
|
|
132
|
-
newStartColumn: c.startColumn,
|
|
133
|
-
newEndColumn: c.endColumn,
|
|
134
|
-
anchoredText: c.text,
|
|
135
|
-
previousSelectedText: selectedText,
|
|
136
|
-
reason: "Normalized whitespace match.",
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
// High threshold fuzzy
|
|
140
|
-
const fuzzyCandidates = fuzzySearch(documentLines, selectedText, HIGH_THRESHOLD, comment.line);
|
|
141
|
-
if (fuzzyCandidates.length === 1 || (fuzzyCandidates.length > 0 && fuzzyCandidates[0].score >= HIGH_THRESHOLD)) {
|
|
142
|
-
const best = fuzzyCandidates.length === 1
|
|
143
|
-
? fuzzyCandidates[0]
|
|
144
|
-
: closestToLine(fuzzyCandidates, comment.line ?? 1);
|
|
145
|
-
return {
|
|
146
|
-
commentId,
|
|
147
|
-
status: "fuzzy",
|
|
148
|
-
score: best.score,
|
|
149
|
-
newLine: best.line,
|
|
150
|
-
newEndLine: best.endLine,
|
|
151
|
-
newStartColumn: best.startColumn,
|
|
152
|
-
newEndColumn: best.endColumn,
|
|
153
|
-
anchoredText: best.text,
|
|
154
|
-
previousSelectedText: selectedText,
|
|
155
|
-
reason: `High-confidence fuzzy match (score ${best.score.toFixed(3)}).`,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
// -----------------------------------------------------------------------
|
|
160
|
-
// Step 2: Line/column fallback
|
|
161
|
-
// -----------------------------------------------------------------------
|
|
162
|
-
if (comment.line != null) {
|
|
163
|
-
const lineIdx = comment.line - 1;
|
|
164
|
-
if (lineIdx >= 0 && lineIdx < documentLines.length) {
|
|
165
|
-
const qualifier = opts.commitIsStale
|
|
166
|
-
? " (commit is stale — line may have shifted)"
|
|
167
|
-
: "";
|
|
168
|
-
// If we have selected_text, check if current line text is a decent fuzzy match
|
|
169
|
-
if (selectedText) {
|
|
170
|
-
const lineText = documentLines[lineIdx];
|
|
171
|
-
const candidates = fuzzySearch([lineText], selectedText, DEFAULT_THRESHOLD);
|
|
172
|
-
if (candidates.length > 0) {
|
|
173
|
-
return {
|
|
174
|
-
commentId,
|
|
175
|
-
status: "fuzzy",
|
|
176
|
-
score: candidates[0].score,
|
|
177
|
-
newLine: comment.line,
|
|
178
|
-
newEndLine: comment.end_line,
|
|
179
|
-
anchoredText: candidates[0].text,
|
|
180
|
-
previousSelectedText: selectedText,
|
|
181
|
-
reason: `Line-fallback with fuzzy text match (score ${candidates[0].score.toFixed(3)})${qualifier}.`,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
// Pure line fallback
|
|
186
|
-
// If there's no selected_text, the comment is purely line-anchored.
|
|
187
|
-
// A commit change alone doesn't make it ambiguous — we accept it as-is.
|
|
188
|
-
const isLineOnly = !selectedText;
|
|
189
|
-
return {
|
|
190
|
-
commentId,
|
|
191
|
-
status: isLineOnly ? "anchored" : (opts.commitIsStale ? "ambiguous" : "anchored"),
|
|
192
|
-
score: isLineOnly ? 1.0 : (opts.commitIsStale ? 0.5 : 0.8),
|
|
193
|
-
newLine: comment.line,
|
|
194
|
-
newEndLine: comment.end_line,
|
|
195
|
-
reason: isLineOnly
|
|
196
|
-
? "Line-only comment (no selected_text to verify)."
|
|
197
|
-
: `Line/column fallback${qualifier}.`,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
// -----------------------------------------------------------------------
|
|
202
|
-
// Step 3: Lower-threshold fuzzy search
|
|
203
|
-
// -----------------------------------------------------------------------
|
|
204
|
-
if (selectedText) {
|
|
205
|
-
const lowCandidates = fuzzySearch(documentLines, selectedText, threshold, comment.line);
|
|
206
|
-
if (lowCandidates.length === 1) {
|
|
207
|
-
const c = lowCandidates[0];
|
|
208
|
-
return {
|
|
209
|
-
commentId,
|
|
210
|
-
status: "fuzzy",
|
|
211
|
-
score: c.score,
|
|
212
|
-
newLine: c.line,
|
|
213
|
-
newEndLine: c.endLine,
|
|
214
|
-
newStartColumn: c.startColumn,
|
|
215
|
-
newEndColumn: c.endColumn,
|
|
216
|
-
anchoredText: c.text,
|
|
217
|
-
previousSelectedText: selectedText,
|
|
218
|
-
reason: `Low-threshold fuzzy match (score ${c.score.toFixed(3)}).`,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
if (lowCandidates.length > 1) {
|
|
222
|
-
// Ambiguous — multiple low-confidence matches
|
|
223
|
-
const best = lowCandidates[0];
|
|
224
|
-
return {
|
|
225
|
-
commentId,
|
|
226
|
-
status: "ambiguous",
|
|
227
|
-
score: best.score,
|
|
228
|
-
newLine: best.line,
|
|
229
|
-
newEndLine: best.endLine,
|
|
230
|
-
reason: `Ambiguous: ${lowCandidates.length} fuzzy matches (best score ${best.score.toFixed(3)}).`,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
// -----------------------------------------------------------------------
|
|
235
|
-
// Step 4: Orphan
|
|
236
|
-
// -----------------------------------------------------------------------
|
|
237
|
-
return {
|
|
238
|
-
commentId,
|
|
239
|
-
status: "orphaned",
|
|
240
|
-
score: 0,
|
|
241
|
-
reason: "No match found. Comment is orphaned.",
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
25
|
// ---------------------------------------------------------------------------
|
|
245
26
|
// Batch re-anchoring
|
|
246
27
|
// ---------------------------------------------------------------------------
|
|
@@ -280,87 +61,7 @@ export async function reanchorDocument(doc, documentLines, opts = {}) {
|
|
|
280
61
|
return results;
|
|
281
62
|
}
|
|
282
63
|
}
|
|
283
|
-
|
|
284
|
-
for (const comment of doc.comments) {
|
|
285
|
-
results.push(reanchorComment(comment, documentLines, { threshold }));
|
|
286
|
-
}
|
|
287
|
-
return results;
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Apply re-anchor results to the document's comments.
|
|
291
|
-
* Mutates the document in place and returns the number of changed comments.
|
|
292
|
-
*/
|
|
293
|
-
export function applyReanchorResults(doc, results, opts = {}) {
|
|
294
|
-
let changed = 0;
|
|
295
|
-
const resultMap = new Map(results.map((r) => [r.commentId, r]));
|
|
296
|
-
for (const comment of doc.comments) {
|
|
297
|
-
const result = resultMap.get(comment.id);
|
|
298
|
-
if (!result)
|
|
299
|
-
continue;
|
|
300
|
-
let isChanged = false;
|
|
301
|
-
if (result.newLine != null && result.newLine !== comment.line) {
|
|
302
|
-
comment.line = result.newLine;
|
|
303
|
-
isChanged = true;
|
|
304
|
-
}
|
|
305
|
-
if (result.newEndLine != null && result.newEndLine !== comment.end_line) {
|
|
306
|
-
comment.end_line = result.newEndLine;
|
|
307
|
-
isChanged = true;
|
|
308
|
-
}
|
|
309
|
-
if (result.newStartColumn != null && result.newStartColumn !== comment.start_column) {
|
|
310
|
-
comment.start_column = result.newStartColumn;
|
|
311
|
-
isChanged = true;
|
|
312
|
-
}
|
|
313
|
-
if (result.newEndColumn != null && result.newEndColumn !== comment.end_column) {
|
|
314
|
-
comment.end_column = result.newEndColumn;
|
|
315
|
-
isChanged = true;
|
|
316
|
-
}
|
|
317
|
-
// Handle anchored_text / selected_text per §6.2 & §7.4 Step 3a.
|
|
318
|
-
// By default, selected_text is preserved (SHOULD NOT modify).
|
|
319
|
-
// anchored_text records the text currently at the resolved position.
|
|
320
|
-
if (result.anchoredText != null && result.anchoredText !== comment.selected_text) {
|
|
321
|
-
if (opts.updateText) {
|
|
322
|
-
// Opt-in: replace selected_text and clear anchored_text
|
|
323
|
-
comment.selected_text = result.anchoredText;
|
|
324
|
-
delete comment.anchored_text;
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
// Default: preserve selected_text, record current text in anchored_text
|
|
328
|
-
comment.anchored_text = result.anchoredText;
|
|
329
|
-
}
|
|
330
|
-
isChanged = true;
|
|
331
|
-
}
|
|
332
|
-
else if (result.anchoredText != null && result.anchoredText === comment.selected_text) {
|
|
333
|
-
// Text matches exactly — clear any stale anchored_text
|
|
334
|
-
if (comment.anchored_text) {
|
|
335
|
-
delete comment.anchored_text;
|
|
336
|
-
isChanged = true;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
// Audit fields (x_ extension prefix per §10)
|
|
340
|
-
if (isChanged || result.status !== "anchored") {
|
|
341
|
-
comment.x_reanchor_status = result.status;
|
|
342
|
-
comment.x_reanchor_score = result.score;
|
|
343
|
-
}
|
|
344
|
-
// --force: firmly anchor high-confidence results
|
|
345
|
-
// Update commit to HEAD and clear audit fields so the comment
|
|
346
|
-
// won't be re-evaluated on the next reanchor run.
|
|
347
|
-
if (opts.force &&
|
|
348
|
-
opts.headCommit &&
|
|
349
|
-
(result.status === "anchored" || result.status === "shifted") &&
|
|
350
|
-
result.score >= HIGH_THRESHOLD) {
|
|
351
|
-
comment.commit = opts.headCommit;
|
|
352
|
-
delete comment.x_reanchor_status;
|
|
353
|
-
delete comment.x_reanchor_score;
|
|
354
|
-
// Clear stale anchored_text since anchor is now confirmed
|
|
355
|
-
if (comment.anchored_text && comment.anchored_text === comment.selected_text) {
|
|
356
|
-
delete comment.anchored_text;
|
|
357
|
-
}
|
|
358
|
-
isChanged = true;
|
|
359
|
-
}
|
|
360
|
-
if (isChanged)
|
|
361
|
-
changed++;
|
|
362
|
-
}
|
|
363
|
-
return changed;
|
|
64
|
+
return reanchorDocumentLines(doc, documentLines, { threshold });
|
|
364
65
|
}
|
|
365
66
|
/**
|
|
366
67
|
* High-level re-anchor for a single sidecar file path.
|
|
@@ -394,38 +95,4 @@ export async function reanchorFile(sidecarPath, opts = {}) {
|
|
|
394
95
|
// ---------------------------------------------------------------------------
|
|
395
96
|
// Helpers
|
|
396
97
|
// ---------------------------------------------------------------------------
|
|
397
|
-
/**
|
|
398
|
-
* Extract text from document lines at the given position.
|
|
399
|
-
* Lines array is 1-based (index 0 is unused), so lines[line] is the content.
|
|
400
|
-
*/
|
|
401
|
-
function extractText(lines, line, endLine, startColumn, endColumn) {
|
|
402
|
-
const startIdx = line; // 1-based array: lines[1] = first line
|
|
403
|
-
const endIdx = endLine ?? line;
|
|
404
|
-
if (startIdx < 1 || endIdx >= lines.length)
|
|
405
|
-
return null;
|
|
406
|
-
if (startIdx === endIdx) {
|
|
407
|
-
const text = lines[startIdx];
|
|
408
|
-
if (startColumn != null && endColumn != null) {
|
|
409
|
-
return text.slice(startColumn, endColumn);
|
|
410
|
-
}
|
|
411
|
-
return text;
|
|
412
|
-
}
|
|
413
|
-
// Multi-line
|
|
414
|
-
const result = [];
|
|
415
|
-
for (let i = startIdx; i <= endIdx; i++) {
|
|
416
|
-
let l = lines[i];
|
|
417
|
-
if (i === startIdx && startColumn != null)
|
|
418
|
-
l = l.slice(startColumn);
|
|
419
|
-
if (i === endIdx && endColumn != null)
|
|
420
|
-
l = l.slice(0, endColumn);
|
|
421
|
-
result.push(l);
|
|
422
|
-
}
|
|
423
|
-
return result.join("\n");
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Pick the candidate closest to a hint line.
|
|
427
|
-
*/
|
|
428
|
-
function closestToLine(candidates, targetLine) {
|
|
429
|
-
return candidates.reduce((best, c) => Math.abs(c.line - targetLine) < Math.abs(best.line - targetLine) ? c : best);
|
|
430
|
-
}
|
|
431
98
|
//# sourceMappingURL=reanchor.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrsf/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "CLI and library for the Markdown Review Sidecar Format (MRSF)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"import": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./browser": {
|
|
14
|
+
"import": "./dist/browser.js",
|
|
15
|
+
"types": "./dist/browser.d.ts"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"bin": {
|
|
@@ -72,8 +76,8 @@
|
|
|
72
76
|
"@types/js-yaml": "^4.0.9",
|
|
73
77
|
"@types/node": "^22.13.10",
|
|
74
78
|
"@types/uuid": "^10.0.0",
|
|
75
|
-
"@vitest/coverage-v8": "^
|
|
79
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
76
80
|
"typescript": "^5.8.2",
|
|
77
|
-
"vitest": "^
|
|
81
|
+
"vitest": "^4.0.18"
|
|
78
82
|
}
|
|
79
83
|
}
|