@mrsf/cli 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/dist/browser.cjs +1273 -37
- package/dist/browser.cjs.map +4 -4
- package/dist/browser.d.ts +18 -1
- package/dist/browser.js +14 -0
- package/dist/commands/reanchor.js +3 -0
- package/dist/evaluation/generate-reanchor-cases.d.ts +10 -0
- package/dist/evaluation/generate-reanchor-cases.js +301 -0
- package/dist/evaluation/reanchor-eval.d.ts +111 -0
- package/dist/evaluation/reanchor-eval.js +266 -0
- package/dist/evaluation/reanchor-gates.d.ts +29 -0
- package/dist/evaluation/reanchor-gates.js +47 -0
- package/dist/evaluation/run-reanchor-eval.d.ts +3 -0
- package/dist/evaluation/run-reanchor-eval.js +103 -0
- package/dist/evaluation/run-reanchor-generator.d.ts +3 -0
- package/dist/evaluation/run-reanchor-generator.js +63 -0
- package/dist/evaluation/run-reanchor-profile.d.ts +3 -0
- package/dist/evaluation/run-reanchor-profile.js +129 -0
- package/dist/index.cjs +1307 -85
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -0
- package/dist/lib/anchor-context.d.ts +46 -0
- package/dist/lib/anchor-context.js +474 -0
- package/dist/lib/confidence-calibration.d.ts +16 -0
- package/dist/lib/confidence-calibration.js +120 -0
- package/dist/lib/fuzzy.d.ts +14 -1
- package/dist/lib/fuzzy.js +140 -19
- package/dist/lib/git.d.ts +2 -0
- package/dist/lib/git.js +12 -0
- package/dist/lib/global-reconciliation.d.ts +11 -0
- package/dist/lib/global-reconciliation.js +145 -0
- package/dist/lib/parser.d.ts +4 -19
- package/dist/lib/parser.js +4 -140
- package/dist/lib/reanchor-core.d.ts +20 -0
- package/dist/lib/reanchor-core.js +123 -20
- package/dist/lib/reanchor.js +85 -12
- package/dist/lib/revision-projection.d.ts +21 -0
- package/dist/lib/revision-projection.js +203 -0
- package/dist/lib/serialize.d.ts +37 -0
- package/dist/lib/serialize.js +170 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/writer.d.ts +1 -8
- package/dist/lib/writer.js +5 -14
- package/package.json +5 -1
|
@@ -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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MRSF string (de)serialization — pure, dependency-light helpers that parse and
|
|
3
|
+
* serialize MRSF sidecar content to/from strings.
|
|
4
|
+
*
|
|
5
|
+
* This module intentionally has **no Node-only imports** (no `node:fs`,
|
|
6
|
+
* `node:path`, `node:crypto`, …) so it can be consumed from browser/host
|
|
7
|
+
* adapters via the slim `@mrsf/cli/browser` entry point. Filesystem-bound
|
|
8
|
+
* helpers (read/discover/write a file) live in `parser.ts` / `writer.ts`.
|
|
9
|
+
*/
|
|
10
|
+
import type { MrsfDocument, Comment } from "./types.js";
|
|
11
|
+
/** Result from a lenient (non-throwing) parse attempt. */
|
|
12
|
+
export interface LenientParseResult {
|
|
13
|
+
/** Fully parsed document, or null if parsing failed entirely. */
|
|
14
|
+
doc: MrsfDocument | null;
|
|
15
|
+
/** If parsing failed or produced warnings, the error message. */
|
|
16
|
+
error?: string;
|
|
17
|
+
/** Comments that could be salvaged from a partially-corrupted sidecar. */
|
|
18
|
+
partialComments?: Comment[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse MRSF sidecar content from a string.
|
|
22
|
+
* Detects JSON vs YAML based on content or optional filename hint.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseSidecarContent(content: string, filenameHint?: string): MrsfDocument;
|
|
25
|
+
/**
|
|
26
|
+
* Lenient parse from string content.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseSidecarContentLenient(content: string, filenameHint?: string): LenientParseResult;
|
|
29
|
+
/**
|
|
30
|
+
* Serialize an MrsfDocument to YAML (for new files / non-round-trip use).
|
|
31
|
+
*/
|
|
32
|
+
export declare function toYaml(doc: MrsfDocument): string;
|
|
33
|
+
/**
|
|
34
|
+
* Serialize an MrsfDocument to JSON.
|
|
35
|
+
*/
|
|
36
|
+
export declare function toJson(doc: MrsfDocument): string;
|
|
37
|
+
//# sourceMappingURL=serialize.d.ts.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MRSF string (de)serialization — pure, dependency-light helpers that parse and
|
|
3
|
+
* serialize MRSF sidecar content to/from strings.
|
|
4
|
+
*
|
|
5
|
+
* This module intentionally has **no Node-only imports** (no `node:fs`,
|
|
6
|
+
* `node:path`, `node:crypto`, …) so it can be consumed from browser/host
|
|
7
|
+
* adapters via the slim `@mrsf/cli/browser` entry point. Filesystem-bound
|
|
8
|
+
* helpers (read/discover/write a file) live in `parser.ts` / `writer.ts`.
|
|
9
|
+
*/
|
|
10
|
+
import yaml from "js-yaml";
|
|
11
|
+
import { Document } from "yaml";
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Parsing (string → document)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
/**
|
|
16
|
+
* Parse MRSF sidecar content from a string.
|
|
17
|
+
* Detects JSON vs YAML based on content or optional filename hint.
|
|
18
|
+
*/
|
|
19
|
+
export function parseSidecarContent(content, filenameHint) {
|
|
20
|
+
const trimmed = content.trim();
|
|
21
|
+
let parsed;
|
|
22
|
+
// Detect JSON by content or filename
|
|
23
|
+
const isJson = trimmed.startsWith("{") ||
|
|
24
|
+
(filenameHint && filenameHint.endsWith(".review.json"));
|
|
25
|
+
if (isJson) {
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(trimmed);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
throw new Error(`Failed to parse JSON: ${e.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
try {
|
|
35
|
+
parsed = yaml.load(trimmed, { schema: yaml.JSON_SCHEMA });
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
throw new Error(`Failed to parse YAML: ${e.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
42
|
+
throw new Error("MRSF sidecar must be a YAML/JSON object");
|
|
43
|
+
}
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Lenient parse from string content.
|
|
48
|
+
*/
|
|
49
|
+
export function parseSidecarContentLenient(content, filenameHint) {
|
|
50
|
+
const trimmed = content.trim();
|
|
51
|
+
if (!trimmed) {
|
|
52
|
+
return { doc: null, error: "File is empty" };
|
|
53
|
+
}
|
|
54
|
+
const isJson = trimmed.startsWith("{") ||
|
|
55
|
+
(filenameHint && filenameHint.endsWith(".review.json"));
|
|
56
|
+
// First, try a normal parse
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = isJson ? JSON.parse(trimmed) : yaml.load(trimmed, { schema: yaml.JSON_SCHEMA });
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
// Total parse failure — try to salvage what we can from YAML
|
|
63
|
+
if (!isJson) {
|
|
64
|
+
return salvageYaml(trimmed);
|
|
65
|
+
}
|
|
66
|
+
return { doc: null, error: `Failed to parse JSON: ${e.message}` };
|
|
67
|
+
}
|
|
68
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
69
|
+
return { doc: null, error: "MRSF sidecar must be a YAML/JSON object" };
|
|
70
|
+
}
|
|
71
|
+
const obj = parsed;
|
|
72
|
+
const doc = {
|
|
73
|
+
mrsf_version: typeof obj.mrsf_version === "string" ? obj.mrsf_version : "1.0",
|
|
74
|
+
document: typeof obj.document === "string" ? obj.document : "unknown",
|
|
75
|
+
comments: [],
|
|
76
|
+
};
|
|
77
|
+
if (!Array.isArray(obj.comments)) {
|
|
78
|
+
return {
|
|
79
|
+
doc,
|
|
80
|
+
error: "comments field is not an array — file may be corrupted",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Validate each comment individually
|
|
84
|
+
const good = [];
|
|
85
|
+
const bad = [];
|
|
86
|
+
for (let i = 0; i < obj.comments.length; i++) {
|
|
87
|
+
const c = obj.comments[i];
|
|
88
|
+
if (c && typeof c === "object" && !Array.isArray(c) && typeof c.id === "string") {
|
|
89
|
+
good.push(c);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
bad.push(i);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
doc.comments = good;
|
|
96
|
+
if (bad.length > 0) {
|
|
97
|
+
return {
|
|
98
|
+
doc,
|
|
99
|
+
error: `${bad.length} comment(s) at indices [${bad.join(", ")}] were malformed and skipped`,
|
|
100
|
+
partialComments: good,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { doc };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Attempt to extract individual comment blocks from corrupted YAML by
|
|
107
|
+
* splitting on `- id:` patterns and parsing each block independently.
|
|
108
|
+
*/
|
|
109
|
+
function salvageYaml(content) {
|
|
110
|
+
const salvaged = [];
|
|
111
|
+
let mrsf_version = "1.0";
|
|
112
|
+
let document = "unknown";
|
|
113
|
+
// Try to extract top-level fields from the beginning
|
|
114
|
+
const versionMatch = content.match(/^mrsf_version:\s*["']?([^"'\n]+)/m);
|
|
115
|
+
if (versionMatch)
|
|
116
|
+
mrsf_version = versionMatch[1].trim();
|
|
117
|
+
const docMatch = content.match(/^document:\s*["']?([^"'\n]+)/m);
|
|
118
|
+
if (docMatch)
|
|
119
|
+
document = docMatch[1].trim();
|
|
120
|
+
// Split on comment block boundaries (- id: ...)
|
|
121
|
+
const blocks = content.split(/(?=^ - id:\s)/m);
|
|
122
|
+
for (const block of blocks) {
|
|
123
|
+
const trimmed = block.trim();
|
|
124
|
+
if (!trimmed.startsWith("- id:"))
|
|
125
|
+
continue;
|
|
126
|
+
// Wrap in a minimal YAML array context and try to parse
|
|
127
|
+
try {
|
|
128
|
+
const parsed = yaml.load(trimmed, { schema: yaml.JSON_SCHEMA });
|
|
129
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
130
|
+
const c = parsed[0];
|
|
131
|
+
if (c && typeof c === "object" && typeof c.id === "string") {
|
|
132
|
+
salvaged.push(c);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (parsed && typeof parsed === "object" && typeof parsed.id === "string") {
|
|
136
|
+
salvaged.push(parsed);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// This block is unparseable — skip
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const doc = {
|
|
144
|
+
mrsf_version,
|
|
145
|
+
document,
|
|
146
|
+
comments: salvaged,
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
doc: salvaged.length > 0 ? doc : null,
|
|
150
|
+
error: `YAML parse failed. Salvaged ${salvaged.length} comment(s) from raw content.`,
|
|
151
|
+
partialComments: salvaged.length > 0 ? salvaged : undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Serialization (document → string)
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
/**
|
|
158
|
+
* Serialize an MrsfDocument to YAML (for new files / non-round-trip use).
|
|
159
|
+
*/
|
|
160
|
+
export function toYaml(doc) {
|
|
161
|
+
const yamlDoc = new Document(doc);
|
|
162
|
+
return yamlDoc.toString({ lineWidth: 0 });
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Serialize an MrsfDocument to JSON.
|
|
166
|
+
*/
|
|
167
|
+
export function toJson(doc) {
|
|
168
|
+
return JSON.stringify(doc, null, 2) + "\n";
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=serialize.js.map
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -172,6 +172,13 @@ export interface ReanchorOptions extends BaseOptions {
|
|
|
172
172
|
fromCommit?: string;
|
|
173
173
|
/** Update selected_text to match anchored_text (opt-in per §6.2). */
|
|
174
174
|
updateText?: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Proximity window (in lines) for the §7.4 step 1a relocation guard.
|
|
177
|
+
* A lone exact match farther than this from the original line, when the
|
|
178
|
+
* original position's text has changed, is kept in place and flagged for
|
|
179
|
+
* re-anchoring rather than relocated with full confidence. Default 5.
|
|
180
|
+
*/
|
|
181
|
+
proximityWindow?: number;
|
|
175
182
|
/** Force-anchor: update commit to HEAD and clear audit fields for high-confidence results. */
|
|
176
183
|
force?: boolean;
|
|
177
184
|
}
|
package/dist/lib/writer.d.ts
CHANGED
|
@@ -21,14 +21,7 @@ export declare function computeHash(text: string): string;
|
|
|
21
21
|
* If selected_text is absent, removes the hash.
|
|
22
22
|
*/
|
|
23
23
|
export declare function syncHash(comment: Comment): Comment;
|
|
24
|
-
|
|
25
|
-
* Serialize an MrsfDocument to YAML (for new files / non-round-trip use).
|
|
26
|
-
*/
|
|
27
|
-
export declare function toYaml(doc: MrsfDocument): string;
|
|
28
|
-
/**
|
|
29
|
-
* Serialize an MrsfDocument to JSON.
|
|
30
|
-
*/
|
|
31
|
-
export declare function toJson(doc: MrsfDocument): string;
|
|
24
|
+
export { toYaml, toJson } from "./serialize.js";
|
|
32
25
|
/**
|
|
33
26
|
* Write an MrsfDocument to disk.
|
|
34
27
|
*
|
package/dist/lib/writer.js
CHANGED
|
@@ -14,7 +14,8 @@ import { readFile, writeFile, rename, unlink } from "node:fs/promises";
|
|
|
14
14
|
import { existsSync } from "node:fs";
|
|
15
15
|
import { createHash, randomBytes } from "node:crypto";
|
|
16
16
|
import path from "node:path";
|
|
17
|
-
import {
|
|
17
|
+
import { Parser, CST, parse as yamlParse } from "yaml";
|
|
18
|
+
import { toYaml, toJson } from "./serialize.js";
|
|
18
19
|
/* ------------------------------------------------------------------ */
|
|
19
20
|
/* Per-file write serialization */
|
|
20
21
|
/* ------------------------------------------------------------------ */
|
|
@@ -327,19 +328,9 @@ function deepEqual(a, b) {
|
|
|
327
328
|
/* ------------------------------------------------------------------ */
|
|
328
329
|
/* Public API */
|
|
329
330
|
/* ------------------------------------------------------------------ */
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
export function toYaml(doc) {
|
|
334
|
-
const yamlDoc = new Document(doc);
|
|
335
|
-
return yamlDoc.toString({ lineWidth: 0 });
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Serialize an MrsfDocument to JSON.
|
|
339
|
-
*/
|
|
340
|
-
export function toJson(doc) {
|
|
341
|
-
return JSON.stringify(doc, null, 2) + "\n";
|
|
342
|
-
}
|
|
331
|
+
// `toYaml` / `toJson` now live in the Node-free serialize module; re-exported
|
|
332
|
+
// here so existing `@mrsf/cli` import paths keep working.
|
|
333
|
+
export { toYaml, toJson } from "./serialize.js";
|
|
343
334
|
/**
|
|
344
335
|
* Write an MrsfDocument to disk.
|
|
345
336
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrsf/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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
|
},
|