@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.
- package/LICENSE +21 -0
- package/README.md +346 -0
- package/dist/bin.d.ts +8 -0
- package/dist/bin.js +76 -0
- package/dist/commands/add.d.ts +6 -0
- package/dist/commands/add.js +82 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +52 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.js +95 -0
- package/dist/commands/reanchor.d.ts +6 -0
- package/dist/commands/reanchor.js +127 -0
- package/dist/commands/rename.d.ts +6 -0
- package/dist/commands/rename.js +53 -0
- package/dist/commands/resolve.d.ts +6 -0
- package/dist/commands/resolve.js +35 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +156 -0
- package/dist/commands/validate.d.ts +6 -0
- package/dist/commands/validate.js +55 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +25 -0
- package/dist/lib/comments.d.ts +52 -0
- package/dist/lib/comments.js +228 -0
- package/dist/lib/discovery.d.ts +40 -0
- package/dist/lib/discovery.js +125 -0
- package/dist/lib/fuzzy.d.ts +39 -0
- package/dist/lib/fuzzy.js +277 -0
- package/dist/lib/git.d.ts +68 -0
- package/dist/lib/git.js +246 -0
- package/dist/lib/parser.d.ts +19 -0
- package/dist/lib/parser.js +56 -0
- package/dist/lib/reanchor.d.ts +45 -0
- package/dist/lib/reanchor.js +389 -0
- package/dist/lib/resolve-files.d.ts +18 -0
- package/dist/lib/resolve-files.js +42 -0
- package/dist/lib/types.d.ts +151 -0
- package/dist/lib/types.js +5 -0
- package/dist/lib/validator.d.ts +13 -0
- package/dist/lib/validator.js +177 -0
- package/dist/lib/writer.d.ts +30 -0
- package/dist/lib/writer.js +62 -0
- 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,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,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,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,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,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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|