@mrsf/cli 0.1.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +346 -0
  3. package/dist/bin.d.ts +8 -0
  4. package/dist/bin.js +76 -0
  5. package/dist/commands/add.d.ts +6 -0
  6. package/dist/commands/add.js +82 -0
  7. package/dist/commands/init.d.ts +6 -0
  8. package/dist/commands/init.js +52 -0
  9. package/dist/commands/list.d.ts +6 -0
  10. package/dist/commands/list.js +95 -0
  11. package/dist/commands/reanchor.d.ts +6 -0
  12. package/dist/commands/reanchor.js +127 -0
  13. package/dist/commands/rename.d.ts +6 -0
  14. package/dist/commands/rename.js +53 -0
  15. package/dist/commands/resolve.d.ts +6 -0
  16. package/dist/commands/resolve.js +35 -0
  17. package/dist/commands/status.d.ts +6 -0
  18. package/dist/commands/status.js +156 -0
  19. package/dist/commands/validate.d.ts +6 -0
  20. package/dist/commands/validate.js +55 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.js +25 -0
  23. package/dist/lib/comments.d.ts +52 -0
  24. package/dist/lib/comments.js +228 -0
  25. package/dist/lib/discovery.d.ts +40 -0
  26. package/dist/lib/discovery.js +125 -0
  27. package/dist/lib/fuzzy.d.ts +39 -0
  28. package/dist/lib/fuzzy.js +277 -0
  29. package/dist/lib/git.d.ts +68 -0
  30. package/dist/lib/git.js +246 -0
  31. package/dist/lib/parser.d.ts +19 -0
  32. package/dist/lib/parser.js +56 -0
  33. package/dist/lib/reanchor.d.ts +45 -0
  34. package/dist/lib/reanchor.js +389 -0
  35. package/dist/lib/resolve-files.d.ts +18 -0
  36. package/dist/lib/resolve-files.js +42 -0
  37. package/dist/lib/types.d.ts +151 -0
  38. package/dist/lib/types.js +5 -0
  39. package/dist/lib/validator.d.ts +13 -0
  40. package/dist/lib/validator.js +177 -0
  41. package/dist/lib/writer.d.ts +30 -0
  42. package/dist/lib/writer.js +62 -0
  43. package/package.json +70 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * MRSF Re-anchor Engine — §7.4 Anchoring Resolution Procedure.
3
+ *
4
+ * Implements a four-step algorithm to re-locate each comment's
5
+ * anchor within the current document revision:
6
+ *
7
+ * Step 0 – diff-based shift (git + commit available)
8
+ * Step 1 – exact text match
9
+ * Step 1.5– fuzzy match ≥ high threshold (0.8)
10
+ * Step 2 – line/column fallback (commit-aware staleness)
11
+ * Step 3 – lower-threshold fuzzy ≥ configured threshold (0.6)
12
+ * Step 4 – orphan
13
+ */
14
+ import type { Comment, MrsfDocument, ReanchorOptions, ReanchorResult, DiffHunk } from "./types.js";
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;
23
+ /**
24
+ * Re-anchor all comments in an MRSF document.
25
+ */
26
+ export declare function reanchorDocument(doc: MrsfDocument, documentLines: string[], opts?: ReanchorOptions & {
27
+ documentPath?: string;
28
+ repoRoot?: string;
29
+ }): 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
+ }): number;
37
+ /**
38
+ * High-level re-anchor for a single sidecar file path.
39
+ */
40
+ export declare function reanchorFile(sidecarPath: string, opts?: ReanchorOptions): Promise<{
41
+ results: ReanchorResult[];
42
+ changed: number;
43
+ written: boolean;
44
+ }>;
45
+ //# sourceMappingURL=reanchor.d.ts.map
@@ -0,0 +1,389 @@
1
+ /**
2
+ * MRSF Re-anchor Engine — §7.4 Anchoring Resolution Procedure.
3
+ *
4
+ * Implements a four-step algorithm to re-locate each comment's
5
+ * anchor within the current document revision:
6
+ *
7
+ * Step 0 – diff-based shift (git + commit available)
8
+ * Step 1 – exact text match
9
+ * Step 1.5– fuzzy match ≥ high threshold (0.8)
10
+ * Step 2 – line/column fallback (commit-aware staleness)
11
+ * Step 3 – lower-threshold fuzzy ≥ configured threshold (0.6)
12
+ * Step 4 – orphan
13
+ */
14
+ import { findRepoRoot, getCurrentCommit, getDiff, getLineShift, isGitAvailable, } from "./git.js";
15
+ import { exactMatch, fuzzySearch, normalizedMatch, } from "./fuzzy.js";
16
+ import { readDocumentLines } from "./parser.js";
17
+ import { sidecarToDocument } from "./discovery.js";
18
+ import { parseSidecar } from "./parser.js";
19
+ import { writeSidecar } from "./writer.js";
20
+ import path from "node:path";
21
+ // ---------------------------------------------------------------------------
22
+ // Constants
23
+ // ---------------------------------------------------------------------------
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 (selectedText && comment.line != null && opts.diffHunks?.length) {
49
+ const { shift, modified } = getLineShift(opts.diffHunks, comment.line);
50
+ if (!modified) {
51
+ const shiftedLine = comment.line + shift;
52
+ const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
53
+ const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : undefined;
54
+ // Verify the selected text still matches at the shifted position
55
+ const textAtShifted = extractText(documentLines, shiftedLine, shiftedEndLine, comment.start_column, comment.end_column);
56
+ if (textAtShifted === selectedText) {
57
+ return {
58
+ commentId,
59
+ status: shift === 0 ? "anchored" : "shifted",
60
+ score: 1.0,
61
+ newLine: shiftedLine,
62
+ newEndLine: shiftedEndLine,
63
+ reason: shift === 0
64
+ ? "Diff confirms text unchanged at original position."
65
+ : `Diff shifted by ${shift > 0 ? "+" : ""}${shift} line(s).`,
66
+ };
67
+ }
68
+ }
69
+ // Fall through — line was modified or text doesn't match after shift
70
+ }
71
+ // -----------------------------------------------------------------------
72
+ // Step 1: Exact text match
73
+ // -----------------------------------------------------------------------
74
+ if (selectedText) {
75
+ const exactCandidates = exactMatch(documentLines, selectedText);
76
+ if (exactCandidates.length === 1) {
77
+ const c = exactCandidates[0];
78
+ return {
79
+ commentId,
80
+ status: "anchored",
81
+ score: 1.0,
82
+ newLine: c.line,
83
+ newEndLine: c.endLine,
84
+ newStartColumn: c.startColumn,
85
+ newEndColumn: c.endColumn,
86
+ reason: "Exact text match (unique).",
87
+ };
88
+ }
89
+ if (exactCandidates.length > 1 && comment.line != null) {
90
+ // Disambiguate by proximity to original line
91
+ const best = closestToLine(exactCandidates, comment.line);
92
+ return {
93
+ commentId,
94
+ status: "anchored",
95
+ score: 1.0,
96
+ newLine: best.line,
97
+ newEndLine: best.endLine,
98
+ newStartColumn: best.startColumn,
99
+ newEndColumn: best.endColumn,
100
+ reason: `Exact text match (${exactCandidates.length} occurrences; chose nearest to original line ${comment.line}).`,
101
+ };
102
+ }
103
+ // -----------------------------------------------------------------------
104
+ // Step 1.5: Normalized + high-threshold fuzzy match
105
+ // -----------------------------------------------------------------------
106
+ // Try whitespace-normalized match first
107
+ const normCandidates = normalizedMatch(documentLines, selectedText);
108
+ if (normCandidates.length === 1) {
109
+ const c = normCandidates[0];
110
+ return {
111
+ commentId,
112
+ status: "fuzzy",
113
+ score: c.score,
114
+ newLine: c.line,
115
+ newEndLine: c.endLine,
116
+ newStartColumn: c.startColumn,
117
+ newEndColumn: c.endColumn,
118
+ anchoredText: c.text,
119
+ previousSelectedText: selectedText,
120
+ reason: "Normalized whitespace match.",
121
+ };
122
+ }
123
+ // High threshold fuzzy
124
+ const fuzzyCandidates = fuzzySearch(documentLines, selectedText, HIGH_THRESHOLD, comment.line);
125
+ if (fuzzyCandidates.length === 1 || (fuzzyCandidates.length > 0 && fuzzyCandidates[0].score >= HIGH_THRESHOLD)) {
126
+ const best = fuzzyCandidates.length === 1
127
+ ? fuzzyCandidates[0]
128
+ : closestToLine(fuzzyCandidates, comment.line ?? 1);
129
+ return {
130
+ commentId,
131
+ status: "fuzzy",
132
+ score: best.score,
133
+ newLine: best.line,
134
+ newEndLine: best.endLine,
135
+ newStartColumn: best.startColumn,
136
+ newEndColumn: best.endColumn,
137
+ anchoredText: best.text,
138
+ previousSelectedText: selectedText,
139
+ reason: `High-confidence fuzzy match (score ${best.score.toFixed(3)}).`,
140
+ };
141
+ }
142
+ }
143
+ // -----------------------------------------------------------------------
144
+ // Step 2: Line/column fallback
145
+ // -----------------------------------------------------------------------
146
+ if (comment.line != null) {
147
+ const lineIdx = comment.line - 1;
148
+ if (lineIdx >= 0 && lineIdx < documentLines.length) {
149
+ const qualifier = opts.commitIsStale
150
+ ? " (commit is stale — line may have shifted)"
151
+ : "";
152
+ // If we have selected_text, check if current line text is a decent fuzzy match
153
+ if (selectedText) {
154
+ const lineText = documentLines[lineIdx];
155
+ const candidates = fuzzySearch([lineText], selectedText, DEFAULT_THRESHOLD);
156
+ if (candidates.length > 0) {
157
+ return {
158
+ commentId,
159
+ status: "fuzzy",
160
+ score: candidates[0].score,
161
+ newLine: comment.line,
162
+ newEndLine: comment.end_line,
163
+ anchoredText: candidates[0].text,
164
+ previousSelectedText: selectedText,
165
+ reason: `Line-fallback with fuzzy text match (score ${candidates[0].score.toFixed(3)})${qualifier}.`,
166
+ };
167
+ }
168
+ }
169
+ // Pure line fallback
170
+ return {
171
+ commentId,
172
+ status: opts.commitIsStale ? "ambiguous" : "anchored",
173
+ score: opts.commitIsStale ? 0.5 : 0.8,
174
+ newLine: comment.line,
175
+ newEndLine: comment.end_line,
176
+ reason: `Line/column fallback${qualifier}.`,
177
+ };
178
+ }
179
+ }
180
+ // -----------------------------------------------------------------------
181
+ // Step 3: Lower-threshold fuzzy search
182
+ // -----------------------------------------------------------------------
183
+ if (selectedText) {
184
+ const lowCandidates = fuzzySearch(documentLines, selectedText, threshold, comment.line);
185
+ if (lowCandidates.length === 1) {
186
+ const c = lowCandidates[0];
187
+ return {
188
+ commentId,
189
+ status: "fuzzy",
190
+ score: c.score,
191
+ newLine: c.line,
192
+ newEndLine: c.endLine,
193
+ newStartColumn: c.startColumn,
194
+ newEndColumn: c.endColumn,
195
+ anchoredText: c.text,
196
+ previousSelectedText: selectedText,
197
+ reason: `Low-threshold fuzzy match (score ${c.score.toFixed(3)}).`,
198
+ };
199
+ }
200
+ if (lowCandidates.length > 1) {
201
+ // Ambiguous — multiple low-confidence matches
202
+ const best = lowCandidates[0];
203
+ return {
204
+ commentId,
205
+ status: "ambiguous",
206
+ score: best.score,
207
+ newLine: best.line,
208
+ newEndLine: best.endLine,
209
+ reason: `Ambiguous: ${lowCandidates.length} fuzzy matches (best score ${best.score.toFixed(3)}).`,
210
+ };
211
+ }
212
+ }
213
+ // -----------------------------------------------------------------------
214
+ // Step 4: Orphan
215
+ // -----------------------------------------------------------------------
216
+ return {
217
+ commentId,
218
+ status: "orphaned",
219
+ score: 0,
220
+ reason: "No match found. Comment is orphaned.",
221
+ };
222
+ }
223
+ // ---------------------------------------------------------------------------
224
+ // Batch re-anchoring
225
+ // ---------------------------------------------------------------------------
226
+ /**
227
+ * Re-anchor all comments in an MRSF document.
228
+ */
229
+ export async function reanchorDocument(doc, documentLines, opts = {}) {
230
+ const results = [];
231
+ const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
232
+ // Determine git context
233
+ let diffHunks;
234
+ let commitIsStale = false;
235
+ if (!opts.noGit && (await isGitAvailable())) {
236
+ const repoRoot = opts.repoRoot ?? (await findRepoRoot(opts.cwd));
237
+ if (repoRoot && opts.documentPath) {
238
+ const relPath = path.relative(repoRoot, opts.documentPath);
239
+ const head = await getCurrentCommit(repoRoot);
240
+ // Use a shared fromCommit for all comments, or per-comment
241
+ const globalFrom = opts.fromCommit;
242
+ for (const comment of doc.comments) {
243
+ const commentCommit = globalFrom ?? comment.commit;
244
+ if (commentCommit && head && commentCommit !== head) {
245
+ commitIsStale = true;
246
+ // Get diff for this commit range
247
+ const hunks = await getDiff(commentCommit, head, relPath, repoRoot);
248
+ const result = reanchorComment(comment, documentLines, {
249
+ diffHunks: hunks,
250
+ threshold,
251
+ commitIsStale: true,
252
+ });
253
+ results.push(result);
254
+ continue;
255
+ }
256
+ // non-stale or no commit
257
+ results.push(reanchorComment(comment, documentLines, { threshold, commitIsStale: false }));
258
+ }
259
+ return results;
260
+ }
261
+ }
262
+ // No git — pure text-based re-anchoring
263
+ for (const comment of doc.comments) {
264
+ results.push(reanchorComment(comment, documentLines, { threshold }));
265
+ }
266
+ return results;
267
+ }
268
+ /**
269
+ * Apply re-anchor results to the document's comments.
270
+ * Mutates the document in place and returns the number of changed comments.
271
+ */
272
+ export function applyReanchorResults(doc, results, opts = {}) {
273
+ let changed = 0;
274
+ const resultMap = new Map(results.map((r) => [r.commentId, r]));
275
+ for (const comment of doc.comments) {
276
+ const result = resultMap.get(comment.id);
277
+ if (!result)
278
+ continue;
279
+ let isChanged = false;
280
+ if (result.newLine != null && result.newLine !== comment.line) {
281
+ comment.line = result.newLine;
282
+ isChanged = true;
283
+ }
284
+ if (result.newEndLine != null && result.newEndLine !== comment.end_line) {
285
+ comment.end_line = result.newEndLine;
286
+ isChanged = true;
287
+ }
288
+ if (result.newStartColumn != null && result.newStartColumn !== comment.start_column) {
289
+ comment.start_column = result.newStartColumn;
290
+ isChanged = true;
291
+ }
292
+ if (result.newEndColumn != null && result.newEndColumn !== comment.end_column) {
293
+ comment.end_column = result.newEndColumn;
294
+ isChanged = true;
295
+ }
296
+ // Handle anchored_text / selected_text per §6.2 & §7.4 Step 3a.
297
+ // By default, selected_text is preserved (SHOULD NOT modify).
298
+ // anchored_text records the text currently at the resolved position.
299
+ if (result.anchoredText != null && result.anchoredText !== comment.selected_text) {
300
+ if (opts.updateText) {
301
+ // Opt-in: replace selected_text and clear anchored_text
302
+ comment.selected_text = result.anchoredText;
303
+ delete comment.anchored_text;
304
+ }
305
+ else {
306
+ // Default: preserve selected_text, record current text in anchored_text
307
+ comment.anchored_text = result.anchoredText;
308
+ }
309
+ isChanged = true;
310
+ }
311
+ else if (result.anchoredText != null && result.anchoredText === comment.selected_text) {
312
+ // Text matches exactly — clear any stale anchored_text
313
+ if (comment.anchored_text) {
314
+ delete comment.anchored_text;
315
+ isChanged = true;
316
+ }
317
+ }
318
+ // Audit fields (x_ extension prefix per §10)
319
+ if (isChanged || result.status !== "anchored") {
320
+ comment.x_reanchor_status = result.status;
321
+ comment.x_reanchor_score = result.score;
322
+ }
323
+ if (isChanged)
324
+ changed++;
325
+ }
326
+ return changed;
327
+ }
328
+ /**
329
+ * High-level re-anchor for a single sidecar file path.
330
+ */
331
+ export async function reanchorFile(sidecarPath, opts = {}) {
332
+ const doc = await parseSidecar(sidecarPath);
333
+ const docPath = sidecarToDocument(sidecarPath);
334
+ const documentLines = await readDocumentLines(docPath);
335
+ const repoRoot = !opts.noGit ? await findRepoRoot(opts.cwd) : null;
336
+ const results = await reanchorDocument(doc, documentLines, {
337
+ ...opts,
338
+ documentPath: docPath,
339
+ repoRoot: repoRoot ?? undefined,
340
+ });
341
+ let changed = 0;
342
+ let written = false;
343
+ if (!opts.dryRun) {
344
+ changed = applyReanchorResults(doc, results, { updateText: opts.updateText });
345
+ if (changed > 0 || opts.autoUpdate) {
346
+ await writeSidecar(sidecarPath, doc);
347
+ written = true;
348
+ }
349
+ }
350
+ return { results, changed, written };
351
+ }
352
+ // ---------------------------------------------------------------------------
353
+ // Helpers
354
+ // ---------------------------------------------------------------------------
355
+ /**
356
+ * Extract text from document lines at the given position.
357
+ * Lines array is 1-based (index 0 is unused), so lines[line] is the content.
358
+ */
359
+ function extractText(lines, line, endLine, startColumn, endColumn) {
360
+ const startIdx = line; // 1-based array: lines[1] = first line
361
+ const endIdx = endLine ?? line;
362
+ if (startIdx < 1 || endIdx >= lines.length)
363
+ return null;
364
+ if (startIdx === endIdx) {
365
+ const text = lines[startIdx];
366
+ if (startColumn != null && endColumn != null) {
367
+ return text.slice(startColumn, endColumn);
368
+ }
369
+ return text;
370
+ }
371
+ // Multi-line
372
+ const result = [];
373
+ for (let i = startIdx; i <= endIdx; i++) {
374
+ let l = lines[i];
375
+ if (i === startIdx && startColumn != null)
376
+ l = l.slice(startColumn);
377
+ if (i === endIdx && endColumn != null)
378
+ l = l.slice(0, endColumn);
379
+ result.push(l);
380
+ }
381
+ return result.join("\n");
382
+ }
383
+ /**
384
+ * Pick the candidate closest to a hint line.
385
+ */
386
+ function closestToLine(candidates, targetLine) {
387
+ return candidates.reduce((best, c) => Math.abs(c.line - targetLine) < Math.abs(best.line - targetLine) ? c : best);
388
+ }
389
+ //# sourceMappingURL=reanchor.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shared helper for resolving CLI file arguments.
3
+ *
4
+ * When a user passes a Markdown document path (e.g. `docs/api.md`) instead of
5
+ * a sidecar path, we auto-discover the corresponding sidecar. This avoids
6
+ * the confusing "Failed to parse YAML" error on raw Markdown files.
7
+ */
8
+ /**
9
+ * Resolve a list of CLI file arguments to sidecar paths.
10
+ *
11
+ * - If `files` is empty, discover all sidecars from the workspace root.
12
+ * - If a file already looks like a sidecar (`.review.yaml`/`.review.json`),
13
+ * resolve it to an absolute path.
14
+ * - If a file ends in `.md` (or any non-sidecar extension), treat it as a
15
+ * document path and discover its sidecar via §3.3.
16
+ */
17
+ export declare function resolveSidecarPaths(files: string[], cwd: string): Promise<string[]>;
18
+ //# sourceMappingURL=resolve-files.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Shared helper for resolving CLI file arguments.
3
+ *
4
+ * When a user passes a Markdown document path (e.g. `docs/api.md`) instead of
5
+ * a sidecar path, we auto-discover the corresponding sidecar. This avoids
6
+ * the confusing "Failed to parse YAML" error on raw Markdown files.
7
+ */
8
+ import path from "node:path";
9
+ import { discoverSidecar, findWorkspaceRoot, discoverAllSidecars } from "../lib/discovery.js";
10
+ const SIDECAR_EXTENSIONS = [".review.yaml", ".review.json"];
11
+ function isSidecarPath(file) {
12
+ return SIDECAR_EXTENSIONS.some((ext) => file.endsWith(ext));
13
+ }
14
+ /**
15
+ * Resolve a list of CLI file arguments to sidecar paths.
16
+ *
17
+ * - If `files` is empty, discover all sidecars from the workspace root.
18
+ * - If a file already looks like a sidecar (`.review.yaml`/`.review.json`),
19
+ * resolve it to an absolute path.
20
+ * - If a file ends in `.md` (or any non-sidecar extension), treat it as a
21
+ * document path and discover its sidecar via §3.3.
22
+ */
23
+ export async function resolveSidecarPaths(files, cwd) {
24
+ if (files.length === 0) {
25
+ const root = findWorkspaceRoot(cwd);
26
+ return discoverAllSidecars(root ?? cwd);
27
+ }
28
+ const resolved = [];
29
+ for (const f of files) {
30
+ const abs = path.resolve(cwd, f);
31
+ if (isSidecarPath(abs)) {
32
+ resolved.push(abs);
33
+ }
34
+ else {
35
+ // Treat as a document path → discover its sidecar
36
+ const sidecar = await discoverSidecar(abs, { cwd });
37
+ resolved.push(sidecar);
38
+ }
39
+ }
40
+ return resolved;
41
+ }
42
+ //# sourceMappingURL=resolve-files.js.map
@@ -0,0 +1,151 @@
1
+ /**
2
+ * MRSF Types — shared type definitions for the MRSF CLI and library.
3
+ */
4
+ /** A parsed MRSF sidecar document. */
5
+ export interface MrsfDocument {
6
+ mrsf_version: string;
7
+ document: string;
8
+ comments: Comment[];
9
+ /** Catch-all for unknown / x_-prefixed extension fields at the top level. */
10
+ [key: string]: unknown;
11
+ }
12
+ /** A single review comment. */
13
+ export interface Comment {
14
+ id: string;
15
+ author: string;
16
+ timestamp: string;
17
+ text: string;
18
+ resolved: boolean;
19
+ line?: number;
20
+ end_line?: number;
21
+ start_column?: number;
22
+ end_column?: number;
23
+ selected_text?: string;
24
+ selected_text_hash?: string;
25
+ anchored_text?: string;
26
+ commit?: string;
27
+ type?: string;
28
+ severity?: "low" | "medium" | "high";
29
+ reply_to?: string;
30
+ /** Catch-all for unknown / x_-prefixed extension fields. */
31
+ [key: string]: unknown;
32
+ }
33
+ export interface MrsfConfig {
34
+ sidecar_root?: string;
35
+ }
36
+ export type DiagnosticSeverity = "error" | "warning";
37
+ export interface ValidationDiagnostic {
38
+ severity: DiagnosticSeverity;
39
+ message: string;
40
+ /** Path within the document, e.g. "/comments/0/end_line". */
41
+ path?: string;
42
+ /** The comment id this diagnostic relates to, if applicable. */
43
+ commentId?: string;
44
+ }
45
+ export interface ValidationResult {
46
+ valid: boolean;
47
+ errors: ValidationDiagnostic[];
48
+ warnings: ValidationDiagnostic[];
49
+ }
50
+ export type ReanchorStatus = "anchored" | "shifted" | "fuzzy" | "ambiguous" | "orphaned";
51
+ export interface ReanchorResult {
52
+ commentId: string;
53
+ status: ReanchorStatus;
54
+ /** Confidence score 0.0–1.0; 1.0 for exact/shifted. */
55
+ score: number;
56
+ /** Updated line (if changed). */
57
+ newLine?: number;
58
+ newEndLine?: number;
59
+ newStartColumn?: number;
60
+ newEndColumn?: number;
61
+ /** Text currently at the resolved anchor position (for anchored_text field). */
62
+ anchoredText?: string;
63
+ /** Previous selected_text before update (for audit / --update-text). */
64
+ previousSelectedText?: string;
65
+ /** Human-readable explanation of the resolution. */
66
+ reason: string;
67
+ }
68
+ export interface FuzzyCandidate {
69
+ /** Matched text in the document. */
70
+ text: string;
71
+ /** 1-based start line of the match. */
72
+ line: number;
73
+ /** 1-based end line (inclusive). */
74
+ endLine: number;
75
+ /** 0-based start column on the start line. */
76
+ startColumn: number;
77
+ /** 0-based end column on the end line. */
78
+ endColumn: number;
79
+ /** Similarity score 0.0–1.0. */
80
+ score: number;
81
+ }
82
+ export interface DiffHunk {
83
+ /** Original file start line (1-based). */
84
+ oldStart: number;
85
+ /** Number of lines in the original. */
86
+ oldCount: number;
87
+ /** New file start line (1-based). */
88
+ newStart: number;
89
+ /** Number of lines in the new file. */
90
+ newCount: number;
91
+ /** Raw diff lines (prefixed with +, -, or space). */
92
+ lines: string[];
93
+ }
94
+ export interface AddCommentOptions {
95
+ text: string;
96
+ author: string;
97
+ line?: number;
98
+ end_line?: number;
99
+ start_column?: number;
100
+ end_column?: number;
101
+ type?: string;
102
+ severity?: "low" | "medium" | "high";
103
+ commit?: string;
104
+ reply_to?: string;
105
+ /** Override auto-generated id. */
106
+ id?: string;
107
+ /** Override auto-generated timestamp. */
108
+ timestamp?: string;
109
+ }
110
+ export interface CommentFilter {
111
+ open?: boolean;
112
+ resolved?: boolean;
113
+ orphaned?: boolean;
114
+ author?: string;
115
+ type?: string;
116
+ severity?: "low" | "medium" | "high";
117
+ }
118
+ export type AnchorHealth = "fresh" | "stale" | "orphaned" | "unknown";
119
+ export interface StatusResult {
120
+ commentId: string;
121
+ health: AnchorHealth;
122
+ commitAge?: string;
123
+ reason: string;
124
+ }
125
+ export interface BaseOptions {
126
+ /** Working directory (defaults to process.cwd()). */
127
+ cwd?: string;
128
+ /** Path to .mrsf.yaml config. */
129
+ configPath?: string;
130
+ }
131
+ export interface ReanchorOptions extends BaseOptions {
132
+ /** Dry run — report without modifying files. */
133
+ dryRun?: boolean;
134
+ /** Fuzzy match threshold 0.0–1.0 (default 0.6). */
135
+ threshold?: number;
136
+ /** Write updated anchors back to sidecar. */
137
+ autoUpdate?: boolean;
138
+ /** Only process sidecars for staged Markdown files. */
139
+ staged?: boolean;
140
+ /** Disable git integration. */
141
+ noGit?: boolean;
142
+ /** Override from-commit for all comments. */
143
+ fromCommit?: string;
144
+ /** Update selected_text to match anchored_text (opt-in per §6.2). */
145
+ updateText?: boolean;
146
+ }
147
+ export interface ValidateOptions extends BaseOptions {
148
+ /** Treat warnings as errors. */
149
+ strict?: boolean;
150
+ }
151
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * MRSF Types — shared type definitions for the MRSF CLI and library.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * MRSF Validator — JSON Schema + cross-field validation per §10.
3
+ */
4
+ import type { MrsfDocument, ValidationResult, ValidateOptions } from "./types.js";
5
+ /**
6
+ * Validate an MRSF document (parsed object).
7
+ */
8
+ export declare function validate(doc: MrsfDocument, options?: ValidateOptions): Promise<ValidationResult>;
9
+ /**
10
+ * Validate from a file path — convenience wrapper.
11
+ */
12
+ export declare function validateFile(filePath: string, options?: ValidateOptions): Promise<ValidationResult>;
13
+ //# sourceMappingURL=validator.d.ts.map