@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,95 @@
1
+ /**
2
+ * mrsf list — List comments in sidecar files.
3
+ */
4
+ import chalk from "chalk";
5
+ import { sidecarToDocument } from "../lib/discovery.js";
6
+ import { parseSidecar } from "../lib/parser.js";
7
+ import { resolveSidecarPaths } from "../lib/resolve-files.js";
8
+ import { filterComments, summarize } from "../lib/comments.js";
9
+ export function registerList(program) {
10
+ program
11
+ .command("list [files...]")
12
+ .description("List comments across sidecar files")
13
+ .option("--open", "Show only open (unresolved) comments")
14
+ .option("--resolved", "Show only resolved comments")
15
+ .option("--orphaned", "Show only orphaned comments")
16
+ .option("--author <name>", "Filter by author")
17
+ .option("--type <type>", "Filter by type")
18
+ .option("--severity <level>", "Filter by severity")
19
+ .option("--summary", "Show a summary instead of individual comments")
20
+ .option("--json", "Output as JSON")
21
+ .action(async (files, opts) => {
22
+ const parentOpts = program.opts();
23
+ const cwd = parentOpts.cwd ?? process.cwd();
24
+ const sidecarPaths = await resolveSidecarPaths(files, cwd);
25
+ if (sidecarPaths.length === 0) {
26
+ console.log("No sidecar files found.");
27
+ return;
28
+ }
29
+ const filter = {
30
+ open: opts.open,
31
+ resolved: opts.resolved,
32
+ orphaned: opts.orphaned,
33
+ author: opts.author,
34
+ type: opts.type,
35
+ severity: opts.severity,
36
+ };
37
+ const allComments = [];
38
+ for (const sp of sidecarPaths) {
39
+ try {
40
+ const doc = await parseSidecar(sp);
41
+ const filtered = filterComments(doc.comments, filter);
42
+ const docPath = sidecarToDocument(sp);
43
+ for (const c of filtered) {
44
+ allComments.push({
45
+ sidecar: sp,
46
+ document: docPath,
47
+ comment: c,
48
+ });
49
+ }
50
+ }
51
+ catch (err) {
52
+ const msg = err instanceof Error ? err.message : String(err);
53
+ console.error(chalk.red(`✗ ${sp}: ${msg}`));
54
+ }
55
+ }
56
+ if (opts.json) {
57
+ if (opts.summary) {
58
+ console.log(JSON.stringify(summarize(allComments.map((a) => a.comment)), null, 2));
59
+ }
60
+ else {
61
+ console.log(JSON.stringify(allComments, null, 2));
62
+ }
63
+ return;
64
+ }
65
+ if (opts.summary) {
66
+ const s = summarize(allComments.map((a) => a.comment));
67
+ console.log(`Total: ${s.total} Open: ${s.open} Resolved: ${s.resolved} Orphaned: ${s.orphaned} Threads: ${s.threads}`);
68
+ if (Object.keys(s.byType).length > 0) {
69
+ console.log(`By type: ${Object.entries(s.byType).map(([k, v]) => `${k}(${v})`).join(", ")}`);
70
+ }
71
+ if (Object.keys(s.bySeverity).length > 0) {
72
+ console.log(`By severity: ${Object.entries(s.bySeverity).map(([k, v]) => `${k}(${v})`).join(", ")}`);
73
+ }
74
+ return;
75
+ }
76
+ if (allComments.length === 0) {
77
+ console.log("No matching comments.");
78
+ return;
79
+ }
80
+ for (const { document, comment: c } of allComments) {
81
+ const status = c.resolved
82
+ ? chalk.dim("[resolved]")
83
+ : chalk.green("[open]");
84
+ const loc = c.line ? `:${c.line}` : "";
85
+ const sev = c.severity ? chalk.yellow(` [${c.severity}]`) : "";
86
+ const typ = c.type ? chalk.cyan(` (${c.type})`) : "";
87
+ console.log(`${status} ${chalk.bold(c.id)} ${document}${loc}${sev}${typ}`);
88
+ console.log(` ${c.author}: ${c.text}`);
89
+ if (c.reply_to)
90
+ console.log(chalk.dim(` ↳ reply to ${c.reply_to}`));
91
+ }
92
+ console.log(`\n${allComments.length} comment(s).`);
93
+ });
94
+ }
95
+ //# sourceMappingURL=list.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * mrsf reanchor — Re-anchor comments after document changes.
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerReanchor(program: Command): void;
6
+ //# sourceMappingURL=reanchor.d.ts.map
@@ -0,0 +1,127 @@
1
+ /**
2
+ * mrsf reanchor — Re-anchor comments after document changes.
3
+ */
4
+ import chalk from "chalk";
5
+ import path from "node:path";
6
+ import { parseSidecar } from "../lib/parser.js";
7
+ import { readDocumentLines } from "../lib/parser.js";
8
+ import { sidecarToDocument } from "../lib/discovery.js";
9
+ import { resolveSidecarPaths } from "../lib/resolve-files.js";
10
+ import { writeSidecar } from "../lib/writer.js";
11
+ import { findRepoRoot, getStagedFiles } from "../lib/git.js";
12
+ import { reanchorDocument, applyReanchorResults, } from "../lib/reanchor.js";
13
+ export function registerReanchor(program) {
14
+ program
15
+ .command("reanchor [files...]")
16
+ .description("Re-anchor comments to current document content")
17
+ .option("-n, --dry-run", "Report without modifying files")
18
+ .option("-t, --threshold <n>", "Fuzzy threshold 0.0–1.0", "0.6")
19
+ .option("--staged", "Only process sidecars for staged files")
20
+ .option("--no-git", "Disable git integration")
21
+ .option("--from <commit>", "Override from-commit for all comments")
22
+ .option("--update-text", "Also update selected_text to match current document text")
23
+ .action(async (files, opts) => {
24
+ const parentOpts = program.opts();
25
+ const cwd = parentOpts.cwd ?? process.cwd();
26
+ const quiet = parentOpts.quiet ?? false;
27
+ const threshold = parseFloat(opts.threshold);
28
+ const noGit = !opts.git;
29
+ let sidecarPaths;
30
+ if (opts.staged && !noGit) {
31
+ // Only process sidecars whose documents are staged
32
+ const repoRoot = await findRepoRoot(cwd);
33
+ if (!repoRoot) {
34
+ console.error(chalk.red("Not in a git repository."));
35
+ process.exit(1);
36
+ }
37
+ const staged = await getStagedFiles(repoRoot, "*.md");
38
+ sidecarPaths = [];
39
+ for (const md of staged) {
40
+ const absPath = path.resolve(repoRoot, md + ".review.yaml");
41
+ try {
42
+ await parseSidecar(absPath);
43
+ sidecarPaths.push(absPath);
44
+ }
45
+ catch {
46
+ // No sidecar for this staged file, skip
47
+ }
48
+ }
49
+ }
50
+ else {
51
+ sidecarPaths = await resolveSidecarPaths(files, cwd);
52
+ }
53
+ if (sidecarPaths.length === 0) {
54
+ if (!quiet)
55
+ console.log("No sidecar files found.");
56
+ return;
57
+ }
58
+ let totalChanged = 0;
59
+ let totalOrphaned = 0;
60
+ for (const sp of sidecarPaths) {
61
+ try {
62
+ const doc = await parseSidecar(sp);
63
+ const docPath = sidecarToDocument(sp);
64
+ const lines = await readDocumentLines(docPath);
65
+ const repoRoot = !noGit ? await findRepoRoot(cwd) : null;
66
+ const results = await reanchorDocument(doc, lines, {
67
+ threshold,
68
+ noGit,
69
+ fromCommit: opts.from,
70
+ documentPath: docPath,
71
+ repoRoot: repoRoot ?? undefined,
72
+ });
73
+ const orphaned = results.filter((r) => r.status === "orphaned").length;
74
+ totalOrphaned += orphaned;
75
+ if (!opts.dryRun) {
76
+ const changed = applyReanchorResults(doc, results, {
77
+ updateText: opts.updateText,
78
+ });
79
+ totalChanged += changed;
80
+ if (changed > 0) {
81
+ await writeSidecar(sp, doc);
82
+ }
83
+ }
84
+ if (!quiet) {
85
+ printResults(sp, results, opts.dryRun);
86
+ }
87
+ }
88
+ catch (err) {
89
+ const msg = err instanceof Error ? err.message : String(err);
90
+ console.error(chalk.red(`✗ ${sp}: ${msg}`));
91
+ }
92
+ }
93
+ if (!quiet) {
94
+ console.log();
95
+ const label = opts.dryRun ? "would change" : "changed";
96
+ console.log(`${totalChanged} comment(s) ${label}, ${totalOrphaned} orphaned.`);
97
+ }
98
+ if (totalOrphaned > 0)
99
+ process.exit(1);
100
+ });
101
+ }
102
+ function printResults(sidecarPath, results, dryRun) {
103
+ const prefix = dryRun ? "[dry-run] " : "";
104
+ console.log(`${prefix}${sidecarPath}:`);
105
+ for (const r of results) {
106
+ const icon = statusIcon(r.status);
107
+ const line = r.newLine != null ? `:${r.newLine}` : "";
108
+ console.log(` ${icon} ${r.commentId}${line} — ${r.reason}`);
109
+ }
110
+ }
111
+ function statusIcon(status) {
112
+ switch (status) {
113
+ case "anchored":
114
+ return chalk.green("●");
115
+ case "shifted":
116
+ return chalk.blue("↕");
117
+ case "fuzzy":
118
+ return chalk.yellow("≈");
119
+ case "ambiguous":
120
+ return chalk.magenta("?");
121
+ case "orphaned":
122
+ return chalk.red("✗");
123
+ default:
124
+ return "·";
125
+ }
126
+ }
127
+ //# sourceMappingURL=reanchor.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * mrsf rename — Update sidecar when a document is renamed.
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerRename(program: Command): void;
6
+ //# sourceMappingURL=rename.d.ts.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * mrsf rename — Update sidecar when a document is renamed.
3
+ */
4
+ import chalk from "chalk";
5
+ import path from "node:path";
6
+ import fs from "node:fs/promises";
7
+ import { discoverSidecar, findWorkspaceRoot } from "../lib/discovery.js";
8
+ import { parseSidecar } from "../lib/parser.js";
9
+ import { writeSidecar } from "../lib/writer.js";
10
+ export function registerRename(program) {
11
+ program
12
+ .command("rename <old-document> <new-document>")
13
+ .description("Update a sidecar after its document has been renamed or moved")
14
+ .action(async (oldDoc, newDoc) => {
15
+ const parentOpts = program.opts();
16
+ const cwd = parentOpts.cwd ?? process.cwd();
17
+ const quiet = parentOpts.quiet ?? false;
18
+ const oldDocPath = path.resolve(cwd, oldDoc);
19
+ const newDocPath = path.resolve(cwd, newDoc);
20
+ const root = await findWorkspaceRoot(cwd);
21
+ // Find old sidecar
22
+ const oldSidecarPath = await discoverSidecar(oldDocPath, { cwd: root ?? cwd });
23
+ let doc;
24
+ try {
25
+ doc = await parseSidecar(oldSidecarPath);
26
+ }
27
+ catch {
28
+ console.error(chalk.red(`Sidecar not found for ${oldDoc} at ${oldSidecarPath}`));
29
+ process.exit(1);
30
+ }
31
+ // Update document reference
32
+ doc.document = path.basename(newDocPath);
33
+ // Compute new sidecar path
34
+ const newSidecarPath = await discoverSidecar(newDocPath, { cwd: root ?? cwd });
35
+ // Ensure target directory exists
36
+ await fs.mkdir(path.dirname(newSidecarPath), { recursive: true });
37
+ // Write new sidecar
38
+ await writeSidecar(newSidecarPath, doc);
39
+ // Remove old sidecar if different location
40
+ if (oldSidecarPath !== newSidecarPath) {
41
+ try {
42
+ await fs.unlink(oldSidecarPath);
43
+ }
44
+ catch {
45
+ // Already gone or inaccessible
46
+ }
47
+ }
48
+ if (!quiet) {
49
+ console.log(chalk.green(`Renamed: ${oldSidecarPath} → ${newSidecarPath}`));
50
+ }
51
+ });
52
+ }
53
+ //# sourceMappingURL=rename.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * mrsf resolve — Resolve (or unresolve) a comment.
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerResolve(program: Command): void;
6
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * mrsf resolve — Resolve (or unresolve) a comment.
3
+ */
4
+ import chalk from "chalk";
5
+ import path from "node:path";
6
+ import { parseSidecar } from "../lib/parser.js";
7
+ import { writeSidecar } from "../lib/writer.js";
8
+ import { resolveComment, unresolveComment } from "../lib/comments.js";
9
+ export function registerResolve(program) {
10
+ program
11
+ .command("resolve <sidecar> <id>")
12
+ .description("Resolve a comment by ID")
13
+ .option("--cascade", "Also resolve direct replies")
14
+ .option("-u, --undo", "Unresolve instead")
15
+ .action(async (sidecar, id, opts) => {
16
+ const parentOpts = program.opts();
17
+ const cwd = parentOpts.cwd ?? process.cwd();
18
+ const quiet = parentOpts.quiet ?? false;
19
+ const sp = path.resolve(cwd, sidecar);
20
+ const doc = await parseSidecar(sp);
21
+ const ok = opts.undo
22
+ ? unresolveComment(doc, id)
23
+ : resolveComment(doc, id, opts.cascade);
24
+ if (!ok) {
25
+ console.error(chalk.red(`Comment ${id} not found in ${sidecar}.`));
26
+ process.exit(1);
27
+ }
28
+ await writeSidecar(sp, doc);
29
+ if (!quiet) {
30
+ const action = opts.undo ? "Unresolved" : "Resolved";
31
+ console.log(chalk.green(`${action} ${id} in ${sidecar}.`));
32
+ }
33
+ });
34
+ }
35
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * mrsf status — Show anchor health for all comments.
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerStatus(program: Command): void;
6
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1,156 @@
1
+ /**
2
+ * mrsf status — Show anchor health for all comments.
3
+ */
4
+ import chalk from "chalk";
5
+ import { sidecarToDocument } from "../lib/discovery.js";
6
+ import { parseSidecar, readDocumentLines } from "../lib/parser.js";
7
+ import { resolveSidecarPaths } from "../lib/resolve-files.js";
8
+ import { findRepoRoot, isStale as gitIsStale } from "../lib/git.js";
9
+ import { exactMatch } from "../lib/fuzzy.js";
10
+ export function registerStatus(program) {
11
+ program
12
+ .command("status [files...]")
13
+ .description("Show anchor health for comments")
14
+ .option("--json", "Output as JSON")
15
+ .action(async (files, opts) => {
16
+ const parentOpts = program.opts();
17
+ const cwd = parentOpts.cwd ?? process.cwd();
18
+ const quiet = parentOpts.quiet ?? false;
19
+ const sidecarPaths = await resolveSidecarPaths(files, cwd);
20
+ if (sidecarPaths.length === 0) {
21
+ if (!quiet)
22
+ console.log("No sidecar files found.");
23
+ return;
24
+ }
25
+ const repoRoot = await findRepoRoot(cwd);
26
+ const allResults = [];
27
+ for (const sp of sidecarPaths) {
28
+ try {
29
+ const doc = await parseSidecar(sp);
30
+ const docPath = sidecarToDocument(sp);
31
+ let lines;
32
+ try {
33
+ lines = await readDocumentLines(docPath);
34
+ }
35
+ catch {
36
+ // Document missing
37
+ for (const c of doc.comments) {
38
+ allResults.push({
39
+ commentId: c.id,
40
+ health: "unknown",
41
+ reason: "Document file not found.",
42
+ });
43
+ }
44
+ continue;
45
+ }
46
+ for (const c of doc.comments) {
47
+ const result = await assessHealth(c, lines, repoRoot);
48
+ allResults.push(result);
49
+ }
50
+ }
51
+ catch (err) {
52
+ const msg = err instanceof Error ? err.message : String(err);
53
+ console.error(chalk.red(`✗ ${sp}: ${msg}`));
54
+ }
55
+ }
56
+ if (opts.json) {
57
+ console.log(JSON.stringify(allResults, null, 2));
58
+ return;
59
+ }
60
+ for (const r of allResults) {
61
+ const icon = healthIcon(r.health);
62
+ console.log(`${icon} ${r.commentId}: ${r.reason}`);
63
+ }
64
+ const fresh = allResults.filter((r) => r.health === "fresh").length;
65
+ const stale = allResults.filter((r) => r.health === "stale").length;
66
+ const orphaned = allResults.filter((r) => r.health === "orphaned").length;
67
+ const unknown = allResults.filter((r) => r.health === "unknown").length;
68
+ if (!quiet) {
69
+ console.log(`\n${allResults.length} comment(s): ${fresh} fresh, ${stale} stale, ${orphaned} orphaned, ${unknown} unknown`);
70
+ }
71
+ });
72
+ }
73
+ async function assessHealth(comment, lines, repoRoot) {
74
+ // Already marked orphaned
75
+ if (comment.x_reanchor_status === "orphaned") {
76
+ return {
77
+ commentId: comment.id,
78
+ health: "orphaned",
79
+ reason: "Marked orphaned by previous re-anchor.",
80
+ };
81
+ }
82
+ // Check text match
83
+ if (comment.selected_text) {
84
+ const matches = exactMatch(lines, comment.selected_text);
85
+ if (matches.length === 0) {
86
+ // No exact match — might be stale or orphaned
87
+ if (comment.commit && repoRoot) {
88
+ const stale = await gitIsStale(comment.commit, repoRoot);
89
+ if (stale) {
90
+ return {
91
+ commentId: comment.id,
92
+ health: "stale",
93
+ reason: "Commit differs from HEAD and text not found. Run reanchor.",
94
+ };
95
+ }
96
+ }
97
+ return {
98
+ commentId: comment.id,
99
+ health: "orphaned",
100
+ reason: "Selected text not found in current document.",
101
+ };
102
+ }
103
+ // Text found — check commit freshness
104
+ if (comment.commit && repoRoot) {
105
+ const stale = await gitIsStale(comment.commit, repoRoot);
106
+ if (stale) {
107
+ return {
108
+ commentId: comment.id,
109
+ health: "stale",
110
+ reason: "Text still matches but commit is behind HEAD.",
111
+ };
112
+ }
113
+ }
114
+ return {
115
+ commentId: comment.id,
116
+ health: "fresh",
117
+ reason: "Text matches in current document.",
118
+ };
119
+ }
120
+ // No selected_text
121
+ if (!comment.commit) {
122
+ return {
123
+ commentId: comment.id,
124
+ health: "unknown",
125
+ reason: "No selected_text or commit to assess.",
126
+ };
127
+ }
128
+ if (repoRoot) {
129
+ const stale = await gitIsStale(comment.commit, repoRoot);
130
+ return {
131
+ commentId: comment.id,
132
+ health: stale ? "stale" : "fresh",
133
+ reason: stale
134
+ ? "Commit is behind HEAD (no text to verify)."
135
+ : "Commit matches HEAD.",
136
+ };
137
+ }
138
+ return {
139
+ commentId: comment.id,
140
+ health: "unknown",
141
+ reason: "Git not available for commit check.",
142
+ };
143
+ }
144
+ function healthIcon(health) {
145
+ switch (health) {
146
+ case "fresh":
147
+ return chalk.green("●");
148
+ case "stale":
149
+ return chalk.yellow("◐");
150
+ case "orphaned":
151
+ return chalk.red("✗");
152
+ case "unknown":
153
+ return chalk.dim("?");
154
+ }
155
+ }
156
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * mrsf validate — Validate one or all sidecar files.
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerValidate(program: Command): void;
6
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * mrsf validate — Validate one or all sidecar files.
3
+ */
4
+ import chalk from "chalk";
5
+ import { validateFile } from "../lib/validator.js";
6
+ import { resolveSidecarPaths } from "../lib/resolve-files.js";
7
+ export function registerValidate(program) {
8
+ program
9
+ .command("validate [files...]")
10
+ .description("Validate MRSF sidecar files against the schema and spec rules")
11
+ .option("-s, --strict", "Treat warnings as errors")
12
+ .action(async (files, opts) => {
13
+ const parentOpts = program.opts();
14
+ const cwd = parentOpts.cwd ?? process.cwd();
15
+ const quiet = parentOpts.quiet ?? false;
16
+ const sidecarPaths = await resolveSidecarPaths(files, cwd);
17
+ if (sidecarPaths.length === 0) {
18
+ if (!quiet)
19
+ console.log("No sidecar files found.");
20
+ return;
21
+ }
22
+ let hasErrors = false;
23
+ for (const sp of sidecarPaths) {
24
+ let result;
25
+ try {
26
+ result = await validateFile(sp);
27
+ }
28
+ catch (err) {
29
+ const msg = err instanceof Error ? err.message : String(err);
30
+ console.error(chalk.red(`✗ ${sp}: ${msg}`));
31
+ hasErrors = true;
32
+ continue;
33
+ }
34
+ const effectiveValid = opts.strict
35
+ ? result.valid && result.warnings.length === 0
36
+ : result.valid;
37
+ if (!effectiveValid)
38
+ hasErrors = true;
39
+ if (!quiet || !effectiveValid) {
40
+ const icon = effectiveValid ? chalk.green("✓") : chalk.red("✗");
41
+ console.log(`${icon} ${sp}`);
42
+ }
43
+ for (const e of result.errors) {
44
+ console.log(chalk.red(` error: ${e.message}${e.path ? ` (${e.path})` : ""}`));
45
+ }
46
+ for (const w of result.warnings) {
47
+ const fn = opts.strict ? chalk.red : chalk.yellow;
48
+ console.log(fn(` ${opts.strict ? "error" : "warn"}: ${w.message}${w.path ? ` (${w.path})` : ""}`));
49
+ }
50
+ }
51
+ if (hasErrors)
52
+ process.exit(1);
53
+ });
54
+ }
55
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * MRSF — Public API (library surface).
3
+ *
4
+ * Usage:
5
+ * import { validate, reanchorFile, addComment, ... } from "mrsf";
6
+ */
7
+ export type { MrsfDocument, Comment, MrsfConfig, DiagnosticSeverity, ValidationDiagnostic, ValidationResult, ReanchorStatus, ReanchorResult, FuzzyCandidate, DiffHunk, AddCommentOptions, CommentFilter, AnchorHealth, StatusResult, BaseOptions, ReanchorOptions, ValidateOptions, } from "./lib/types.js";
8
+ export { findWorkspaceRoot, loadConfig, discoverSidecar, sidecarToDocument, discoverAllSidecars, } from "./lib/discovery.js";
9
+ export { resolveSidecarPaths } from "./lib/resolve-files.js";
10
+ export { parseSidecar, parseSidecarContent, readDocumentLines, } from "./lib/parser.js";
11
+ export { computeHash, syncHash, toYaml, toJson, writeSidecar, } from "./lib/writer.js";
12
+ export { validate, validateFile } from "./lib/validator.js";
13
+ export { exactMatch, normalizedMatch, fuzzySearch, combinedScore, } from "./lib/fuzzy.js";
14
+ export { isGitAvailable, findRepoRoot, getCurrentCommit, isStale, getDiff, getLineShift, getFileAtCommit, getStagedFiles, detectRenames, parseDiffHunks, } from "./lib/git.js";
15
+ export { reanchorComment, reanchorDocument, applyReanchorResults, reanchorFile, } from "./lib/reanchor.js";
16
+ export { addComment, populateSelectedText, resolveComment, unresolveComment, removeComment, filterComments, getThreads, summarize, } from "./lib/comments.js";
17
+ export type { CommentSummary } from "./lib/comments.js";
18
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * MRSF — Public API (library surface).
3
+ *
4
+ * Usage:
5
+ * import { validate, reanchorFile, addComment, ... } from "mrsf";
6
+ */
7
+ // Discovery
8
+ export { findWorkspaceRoot, loadConfig, discoverSidecar, sidecarToDocument, discoverAllSidecars, } from "./lib/discovery.js";
9
+ // File resolution
10
+ export { resolveSidecarPaths } from "./lib/resolve-files.js";
11
+ // Parsing
12
+ export { parseSidecar, parseSidecarContent, readDocumentLines, } from "./lib/parser.js";
13
+ // Writing
14
+ export { computeHash, syncHash, toYaml, toJson, writeSidecar, } from "./lib/writer.js";
15
+ // Validation
16
+ export { validate, validateFile } from "./lib/validator.js";
17
+ // Fuzzy matching
18
+ export { exactMatch, normalizedMatch, fuzzySearch, combinedScore, } from "./lib/fuzzy.js";
19
+ // Git
20
+ export { isGitAvailable, findRepoRoot, getCurrentCommit, isStale, getDiff, getLineShift, getFileAtCommit, getStagedFiles, detectRenames, parseDiffHunks, } from "./lib/git.js";
21
+ // Re-anchoring
22
+ export { reanchorComment, reanchorDocument, applyReanchorResults, reanchorFile, } from "./lib/reanchor.js";
23
+ // Comments
24
+ export { addComment, populateSelectedText, resolveComment, unresolveComment, removeComment, filterComments, getThreads, summarize, } from "./lib/comments.js";
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * MRSF Comment Operations — CRUD for sidecar comments.
3
+ */
4
+ import type { AddCommentOptions, Comment, CommentFilter, MrsfDocument } from "./types.js";
5
+ /**
6
+ * Add a new comment to a document. Mutates doc.comments in place.
7
+ * Returns the created comment.
8
+ */
9
+ export declare function addComment(doc: MrsfDocument, opts: AddCommentOptions, repoRoot?: string): Promise<Comment>;
10
+ /**
11
+ * Populate selected_text from document lines (reads the file content region).
12
+ * Should be called after addComment if the caller provides line info but not selected_text.
13
+ */
14
+ export declare function populateSelectedText(comment: Comment, documentLines: string[]): void;
15
+ /**
16
+ * Resolve a comment by id. Per §9, resolving a parent does NOT
17
+ * automatically resolve its replies.
18
+ *
19
+ * Returns true if the comment was found and updated.
20
+ */
21
+ export declare function resolveComment(doc: MrsfDocument, commentId: string, cascade?: boolean): boolean;
22
+ /**
23
+ * Unresolve a comment by id.
24
+ */
25
+ export declare function unresolveComment(doc: MrsfDocument, commentId: string): boolean;
26
+ /**
27
+ * Remove a comment by id. Returns true if found and removed.
28
+ */
29
+ export declare function removeComment(doc: MrsfDocument, commentId: string): boolean;
30
+ /**
31
+ * Filter comments based on criteria.
32
+ */
33
+ export declare function filterComments(comments: Comment[], filter: CommentFilter): Comment[];
34
+ /**
35
+ * Get a list of threads — groups of comments by their root ID via reply_to.
36
+ * Returns a map of root comment ID → [root, ...replies in order].
37
+ */
38
+ export declare function getThreads(comments: Comment[]): Map<string, Comment[]>;
39
+ export interface CommentSummary {
40
+ total: number;
41
+ open: number;
42
+ resolved: number;
43
+ orphaned: number;
44
+ threads: number;
45
+ byType: Record<string, number>;
46
+ bySeverity: Record<string, number>;
47
+ }
48
+ /**
49
+ * Generate summary statistics for a comment list.
50
+ */
51
+ export declare function summarize(comments: Comment[]): CommentSummary;
52
+ //# sourceMappingURL=comments.d.ts.map