@mrsf/cli 0.3.0 → 0.4.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 +12 -0
- package/dist/bin.js +3 -3
- package/dist/commands/add.js +42 -0
- package/dist/commands/validate.js +2 -2
- package/dist/commands/watch.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lib/comments.d.ts +2 -1
- package/dist/lib/comments.js +44 -0
- package/dist/lib/discovery.js +1 -1
- package/dist/lib/parser.js +3 -3
- package/dist/lib/types.d.ts +6 -0
- package/dist/lib/validator.js +4 -1
- package/dist/lib/writer.js +21 -5
- package/mrsf.schema.json +113 -0
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -82,6 +82,15 @@ mrsf add docs/api.md \
|
|
|
82
82
|
--author "alice" \
|
|
83
83
|
--text "Good catch, fixed in abc123" \
|
|
84
84
|
--reply-to 3eeccbd3
|
|
85
|
+
|
|
86
|
+
# Attach tool-specific x_* metadata
|
|
87
|
+
mrsf add docs/api.md \
|
|
88
|
+
--author "bot" \
|
|
89
|
+
--text "Flagging this for follow-up" \
|
|
90
|
+
--line 27 \
|
|
91
|
+
--ext x_source=triage-bot \
|
|
92
|
+
--ext x_score=0.91 \
|
|
93
|
+
--ext 'x_labels=["needs-review","docs"]'
|
|
85
94
|
```
|
|
86
95
|
|
|
87
96
|
| Option | Description |
|
|
@@ -96,6 +105,9 @@ mrsf add docs/api.md \
|
|
|
96
105
|
| `--severity <level>` | Importance: `low`, `medium`, `high` |
|
|
97
106
|
| `--reply-to <id>` | Reply to an existing comment by ID |
|
|
98
107
|
| `--selected-text <text>` | Override auto-detected selected text |
|
|
108
|
+
| `--ext <key=value>` | Add an `x_*` extension field; repeat for multiple fields |
|
|
109
|
+
|
|
110
|
+
`--ext` values are stored as flat `x_*` fields in the sidecar. Primitive JSON literals like numbers, `true`, `false`, and `null` are parsed automatically, and objects/arrays can be passed as JSON.
|
|
99
111
|
|
|
100
112
|
---
|
|
101
113
|
|
package/dist/bin.js
CHANGED
|
@@ -49,7 +49,7 @@ function showBanner() {
|
|
|
49
49
|
${chalk.cyan.bold("╔╦╗")}${chalk.blue.bold("╦═╗")}${chalk.magenta.bold("╔═╗")}${chalk.yellow.bold("╔═╗")}
|
|
50
50
|
${chalk.cyan.bold("║║║")}${chalk.blue.bold("╠╦╝")}${chalk.magenta.bold("╚═╗")}${chalk.yellow.bold("╠╣")}
|
|
51
51
|
${chalk.cyan.bold("╩ ╩")}${chalk.blue.bold("╩╚═")}${chalk.magenta.bold("╚═╝")}${chalk.yellow.bold("╚")}
|
|
52
|
-
${chalk.dim("Markdown Review Sidecar Format")} ${chalk.dim.italic(`v${version}`)}
|
|
52
|
+
${chalk.dim("Markdown Review Sidecar Format")} ${chalk.dim.italic(`v${version} (Node.js)`)}
|
|
53
53
|
${chalk.dim.underline(repo)}
|
|
54
54
|
`;
|
|
55
55
|
process.stderr.write(banner + "\n");
|
|
@@ -59,8 +59,8 @@ showBanner();
|
|
|
59
59
|
const program = new Command();
|
|
60
60
|
program
|
|
61
61
|
.name("mrsf")
|
|
62
|
-
.description("Markdown Review Sidecar Format — CLI & toolkit")
|
|
63
|
-
.version(version)
|
|
62
|
+
.description("Markdown Review Sidecar Format — CLI & toolkit (Node.js)")
|
|
63
|
+
.version(`${version} (Node.js)`)
|
|
64
64
|
.option("--cwd <dir>", "Working directory")
|
|
65
65
|
.option("--config <path>", "Path to .mrsf.yaml")
|
|
66
66
|
.option("--no-color", "Disable colour output")
|
package/dist/commands/add.js
CHANGED
|
@@ -8,6 +8,46 @@ import { parseSidecar, readDocumentLines } from "../lib/parser.js";
|
|
|
8
8
|
import { writeSidecar } from "../lib/writer.js";
|
|
9
9
|
import { addComment, populateSelectedText } from "../lib/comments.js";
|
|
10
10
|
import { findRepoRoot } from "../lib/git.js";
|
|
11
|
+
function collectOptionValues(value, previous) {
|
|
12
|
+
previous.push(value);
|
|
13
|
+
return previous;
|
|
14
|
+
}
|
|
15
|
+
function parseExtensionValue(rawValue) {
|
|
16
|
+
const trimmed = rawValue.trim();
|
|
17
|
+
if (/^(true|false|null)$/u.test(trimmed)) {
|
|
18
|
+
return JSON.parse(trimmed);
|
|
19
|
+
}
|
|
20
|
+
if (/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/u.test(trimmed)) {
|
|
21
|
+
return JSON.parse(trimmed);
|
|
22
|
+
}
|
|
23
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(trimmed);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
throw new Error(`Invalid JSON extension value '${rawValue}': ${message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return rawValue;
|
|
33
|
+
}
|
|
34
|
+
function parseExtensionFlags(flags) {
|
|
35
|
+
if (!flags || flags.length === 0)
|
|
36
|
+
return undefined;
|
|
37
|
+
const entries = flags.map((flag) => {
|
|
38
|
+
const separatorIndex = flag.indexOf("=");
|
|
39
|
+
if (separatorIndex <= 0) {
|
|
40
|
+
throw new Error(`Invalid --ext value '${flag}'. Expected key=value.`);
|
|
41
|
+
}
|
|
42
|
+
const key = flag.slice(0, separatorIndex).trim();
|
|
43
|
+
const rawValue = flag.slice(separatorIndex + 1);
|
|
44
|
+
if (!key) {
|
|
45
|
+
throw new Error(`Invalid --ext value '${flag}'. Expected a non-empty key.`);
|
|
46
|
+
}
|
|
47
|
+
return [key, parseExtensionValue(rawValue)];
|
|
48
|
+
});
|
|
49
|
+
return Object.fromEntries(entries);
|
|
50
|
+
}
|
|
11
51
|
export function registerAdd(program) {
|
|
12
52
|
program
|
|
13
53
|
.command("add <document>")
|
|
@@ -22,6 +62,7 @@ export function registerAdd(program) {
|
|
|
22
62
|
.option("--severity <level>", "Severity: low | medium | high")
|
|
23
63
|
.option("--reply-to <id>", "Reply to an existing comment")
|
|
24
64
|
.option("--selected-text <text>", "Selected text to attach")
|
|
65
|
+
.option("--ext <key=value>", "Comment extension field keyed by x_* (repeatable)", collectOptionValues, [])
|
|
25
66
|
.action(async (document, opts) => {
|
|
26
67
|
const parentOpts = program.opts();
|
|
27
68
|
const cwd = parentOpts.cwd ?? process.cwd();
|
|
@@ -58,6 +99,7 @@ export function registerAdd(program) {
|
|
|
58
99
|
type: opts.type,
|
|
59
100
|
severity: opts.severity,
|
|
60
101
|
reply_to: opts.replyTo,
|
|
102
|
+
extensions: parseExtensionFlags(opts.ext),
|
|
61
103
|
}, repoRoot ?? undefined);
|
|
62
104
|
// Auto-populate selected_text from the document if not provided
|
|
63
105
|
if (opts.selectedText) {
|
|
@@ -42,11 +42,11 @@ export function registerValidate(program) {
|
|
|
42
42
|
console.log(`${icon} ${sp}`);
|
|
43
43
|
}
|
|
44
44
|
for (const e of result.errors) {
|
|
45
|
-
console.log(chalk.red(`
|
|
45
|
+
console.log(chalk.red(` ERROR: ${e.message}${e.path ? ` (${e.path})` : ""}`));
|
|
46
46
|
}
|
|
47
47
|
for (const w of result.warnings) {
|
|
48
48
|
const fn = opts.strict ? chalk.red : chalk.yellow;
|
|
49
|
-
console.log(fn(` ${opts.strict ? "
|
|
49
|
+
console.log(fn(` ${opts.strict ? "ERROR" : "WARN"}: ${w.message}${w.path ? ` (${w.path})` : ""}`));
|
|
50
50
|
}
|
|
51
51
|
if (verbose) {
|
|
52
52
|
console.log(chalk.dim(` ${result.errors.length} error(s), ${result.warnings.length} warning(s)`));
|
package/dist/commands/watch.js
CHANGED
|
@@ -66,11 +66,11 @@ function printValidateResults(label, result, strict) {
|
|
|
66
66
|
const icon = effectiveValid ? chalk.green("✓") : chalk.red("✗");
|
|
67
67
|
console.log(` ${chalk.cyan("validate")} ${icon} ${label}`);
|
|
68
68
|
for (const e of result.errors) {
|
|
69
|
-
console.log(chalk.red(`
|
|
69
|
+
console.log(chalk.red(` ERROR: ${e.message}${e.path ? ` (${e.path})` : ""}`));
|
|
70
70
|
}
|
|
71
71
|
for (const w of result.warnings) {
|
|
72
72
|
const fn = strict ? chalk.red : chalk.yellow;
|
|
73
|
-
const tag = strict ? "
|
|
73
|
+
const tag = strict ? "ERROR" : "WARN";
|
|
74
74
|
console.log(fn(` ${tag}: ${w.message}${w.path ? ` (${w.path})` : ""}`));
|
|
75
75
|
}
|
|
76
76
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Usage:
|
|
5
5
|
* import { validate, reanchorFile, addComment, ... } from "mrsf";
|
|
6
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";
|
|
7
|
+
export type { MrsfDocument, Comment, CommentExtensions, CommentExtensionValue, MrsfConfig, DiagnosticSeverity, ValidationDiagnostic, ValidationResult, ReanchorStatus, ReanchorResult, FuzzyCandidate, DiffHunk, AddCommentOptions, CommentFilter, AnchorHealth, StatusResult, BaseOptions, ReanchorOptions, ValidateOptions, } from "./lib/types.js";
|
|
8
8
|
export { findWorkspaceRoot, loadConfig, discoverSidecar, sidecarToDocument, discoverAllSidecars, } from "./lib/discovery.js";
|
|
9
9
|
export { resolveSidecarPaths } from "./lib/resolve-files.js";
|
|
10
10
|
export { parseSidecar, parseSidecarContent, parseSidecarLenient, parseSidecarContentLenient, readDocumentLines, } from "./lib/parser.js";
|
|
@@ -14,6 +14,6 @@ export { validate, validateFile } from "./lib/validator.js";
|
|
|
14
14
|
export { exactMatch, normalizedMatch, fuzzySearch, combinedScore, } from "./lib/fuzzy.js";
|
|
15
15
|
export { isGitAvailable, findRepoRoot, getCurrentCommit, isStale, getDiff, getLineShift, getFileAtCommit, getStagedFiles, detectRenames, parseDiffHunks, } from "./lib/git.js";
|
|
16
16
|
export { reanchorComment, reanchorDocument, applyReanchorResults, reanchorFile, } from "./lib/reanchor.js";
|
|
17
|
-
export { addComment, populateSelectedText, resolveComment, unresolveComment, removeComment, filterComments, getThreads, summarize, } from "./lib/comments.js";
|
|
17
|
+
export { addComment, normalizeCommentExtensions, populateSelectedText, resolveComment, unresolveComment, removeComment, filterComments, getThreads, summarize, } from "./lib/comments.js";
|
|
18
18
|
export type { CommentSummary, RemoveCommentOptions } from "./lib/comments.js";
|
|
19
19
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -21,5 +21,5 @@ export { isGitAvailable, findRepoRoot, getCurrentCommit, isStale, getDiff, getLi
|
|
|
21
21
|
// Re-anchoring
|
|
22
22
|
export { reanchorComment, reanchorDocument, applyReanchorResults, reanchorFile, } from "./lib/reanchor.js";
|
|
23
23
|
// Comments
|
|
24
|
-
export { addComment, populateSelectedText, resolveComment, unresolveComment, removeComment, filterComments, getThreads, summarize, } from "./lib/comments.js";
|
|
24
|
+
export { addComment, normalizeCommentExtensions, populateSelectedText, resolveComment, unresolveComment, removeComment, filterComments, getThreads, summarize, } from "./lib/comments.js";
|
|
25
25
|
//# sourceMappingURL=index.js.map
|
package/dist/lib/comments.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MRSF Comment Operations — CRUD for sidecar comments.
|
|
3
3
|
*/
|
|
4
|
-
import type { AddCommentOptions, Comment, CommentFilter, MrsfDocument } from "./types.js";
|
|
4
|
+
import type { AddCommentOptions, Comment, CommentFilter, CommentExtensions, MrsfDocument } from "./types.js";
|
|
5
|
+
export declare function normalizeCommentExtensions(extensions?: Record<string, unknown>): CommentExtensions;
|
|
5
6
|
/**
|
|
6
7
|
* Add a new comment to a document. Mutates doc.comments in place.
|
|
7
8
|
* Returns the created comment.
|
package/dist/lib/comments.js
CHANGED
|
@@ -7,6 +7,49 @@ import { getCurrentCommit, isGitAvailable } from "./git.js";
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// Add
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
if (value === null || typeof value !== "object")
|
|
12
|
+
return false;
|
|
13
|
+
const prototype = Object.getPrototypeOf(value);
|
|
14
|
+
return prototype === Object.prototype || prototype === null;
|
|
15
|
+
}
|
|
16
|
+
function isCommentExtensionValue(value) {
|
|
17
|
+
if (value == null)
|
|
18
|
+
return true;
|
|
19
|
+
switch (typeof value) {
|
|
20
|
+
case "boolean":
|
|
21
|
+
case "string":
|
|
22
|
+
return true;
|
|
23
|
+
case "number":
|
|
24
|
+
return Number.isFinite(value);
|
|
25
|
+
case "object": {
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
return value.every((item) => isCommentExtensionValue(item));
|
|
28
|
+
}
|
|
29
|
+
if (!isPlainObject(value)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return Object.values(value).every((item) => isCommentExtensionValue(item));
|
|
33
|
+
}
|
|
34
|
+
default:
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function normalizeCommentExtensions(extensions) {
|
|
39
|
+
if (!extensions)
|
|
40
|
+
return {};
|
|
41
|
+
const normalizedEntries = [];
|
|
42
|
+
for (const [key, value] of Object.entries(extensions)) {
|
|
43
|
+
if (!key.startsWith("x_")) {
|
|
44
|
+
throw new Error(`Comment extension key '${key}' must start with 'x_'.`);
|
|
45
|
+
}
|
|
46
|
+
if (!isCommentExtensionValue(value)) {
|
|
47
|
+
throw new Error(`Comment extension '${key}' must be JSON-serializable (null, boolean, finite number, string, array, or plain object).`);
|
|
48
|
+
}
|
|
49
|
+
normalizedEntries.push([key, value]);
|
|
50
|
+
}
|
|
51
|
+
return Object.fromEntries(normalizedEntries);
|
|
52
|
+
}
|
|
10
53
|
/**
|
|
11
54
|
* Add a new comment to a document. Mutates doc.comments in place.
|
|
12
55
|
* Returns the created comment.
|
|
@@ -43,6 +86,7 @@ export async function addComment(doc, opts, repoRoot) {
|
|
|
43
86
|
comment.reply_to = opts.reply_to;
|
|
44
87
|
if (commit)
|
|
45
88
|
comment.commit = commit;
|
|
89
|
+
Object.assign(comment, normalizeCommentExtensions(opts.extensions));
|
|
46
90
|
doc.comments.push(comment);
|
|
47
91
|
return comment;
|
|
48
92
|
}
|
package/dist/lib/discovery.js
CHANGED
|
@@ -38,7 +38,7 @@ export async function loadConfig(workspaceRoot, configPath) {
|
|
|
38
38
|
if (!existsSync(cfgPath))
|
|
39
39
|
return null;
|
|
40
40
|
const raw = await readFile(cfgPath, "utf-8");
|
|
41
|
-
const parsed = yaml.load(raw);
|
|
41
|
+
const parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
|
|
42
42
|
if (!parsed || typeof parsed !== "object")
|
|
43
43
|
return null;
|
|
44
44
|
const config = {};
|
package/dist/lib/parser.js
CHANGED
|
@@ -32,7 +32,7 @@ export function parseSidecarContent(content, filenameHint) {
|
|
|
32
32
|
}
|
|
33
33
|
else {
|
|
34
34
|
try {
|
|
35
|
-
parsed = yaml.load(trimmed);
|
|
35
|
+
parsed = yaml.load(trimmed, { schema: yaml.JSON_SCHEMA });
|
|
36
36
|
}
|
|
37
37
|
catch (e) {
|
|
38
38
|
throw new Error(`Failed to parse YAML: ${e.message}`);
|
|
@@ -74,7 +74,7 @@ export function parseSidecarContentLenient(content, filenameHint) {
|
|
|
74
74
|
// First, try a normal parse
|
|
75
75
|
let parsed;
|
|
76
76
|
try {
|
|
77
|
-
parsed = isJson ? JSON.parse(trimmed) : yaml.load(trimmed);
|
|
77
|
+
parsed = isJson ? JSON.parse(trimmed) : yaml.load(trimmed, { schema: yaml.JSON_SCHEMA });
|
|
78
78
|
}
|
|
79
79
|
catch (e) {
|
|
80
80
|
// Total parse failure — try to salvage what we can from YAML
|
|
@@ -143,7 +143,7 @@ function salvageYaml(content) {
|
|
|
143
143
|
continue;
|
|
144
144
|
// Wrap in a minimal YAML array context and try to parse
|
|
145
145
|
try {
|
|
146
|
-
const parsed = yaml.load(trimmed);
|
|
146
|
+
const parsed = yaml.load(trimmed, { schema: yaml.JSON_SCHEMA });
|
|
147
147
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
148
148
|
const c = parsed[0];
|
|
149
149
|
if (c && typeof c === "object" && typeof c.id === "string") {
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -30,6 +30,10 @@ export interface Comment {
|
|
|
30
30
|
/** Catch-all for unknown / x_-prefixed extension fields. */
|
|
31
31
|
[key: string]: unknown;
|
|
32
32
|
}
|
|
33
|
+
export type CommentExtensionValue = null | boolean | number | string | CommentExtensionValue[] | {
|
|
34
|
+
[key: string]: CommentExtensionValue;
|
|
35
|
+
};
|
|
36
|
+
export type CommentExtensions = Record<`x_${string}`, CommentExtensionValue>;
|
|
33
37
|
export interface MrsfConfig {
|
|
34
38
|
sidecar_root?: string;
|
|
35
39
|
}
|
|
@@ -106,6 +110,8 @@ export interface AddCommentOptions {
|
|
|
106
110
|
id?: string;
|
|
107
111
|
/** Override auto-generated timestamp. */
|
|
108
112
|
timestamp?: string;
|
|
113
|
+
/** Tool-specific extension fields keyed by x_*. */
|
|
114
|
+
extensions?: CommentExtensions;
|
|
109
115
|
}
|
|
110
116
|
export interface CommentFilter {
|
|
111
117
|
open?: boolean;
|
package/dist/lib/validator.js
CHANGED
|
@@ -16,8 +16,11 @@ async function loadSchema() {
|
|
|
16
16
|
if (_schemaCache)
|
|
17
17
|
return _schemaCache;
|
|
18
18
|
// The schema lives at the repo root, two levels up from dist/lib/
|
|
19
|
+
// When installed as a package, it's at the package root (cli/)
|
|
19
20
|
const candidates = [
|
|
20
|
-
path.resolve(__dirname, "
|
|
21
|
+
path.resolve(__dirname, "mrsf.schema.json"), // same dir (esbuild bundle, e.g. MCP server)
|
|
22
|
+
path.resolve(__dirname, "../../mrsf.schema.json"), // from dist/lib/ → cli/ (installed package)
|
|
23
|
+
path.resolve(__dirname, "../../../mrsf.schema.json"), // from dist/lib/ → repo root (dev)
|
|
21
24
|
path.resolve(__dirname, "../../../../mrsf.schema.json"), // fallback
|
|
22
25
|
path.resolve(process.cwd(), "mrsf.schema.json"), // cwd fallback
|
|
23
26
|
];
|
package/dist/lib/writer.js
CHANGED
|
@@ -107,6 +107,8 @@ const COMMENT_KEY_ORDER = [
|
|
|
107
107
|
* double-quoted via JSON.stringify, which is always valid YAML.
|
|
108
108
|
*/
|
|
109
109
|
function valueToSource(v) {
|
|
110
|
+
if (v instanceof Date)
|
|
111
|
+
return JSON.stringify(v.toISOString());
|
|
110
112
|
if (typeof v === "number" || typeof v === "boolean")
|
|
111
113
|
return String(v);
|
|
112
114
|
if (typeof v === "string") {
|
|
@@ -148,6 +150,16 @@ function valueToSource(v) {
|
|
|
148
150
|
function valueType(v) {
|
|
149
151
|
return valueToSource(v).startsWith('"') ? "double-quoted-scalar" : "scalar";
|
|
150
152
|
}
|
|
153
|
+
function isStructuredValue(value) {
|
|
154
|
+
if (value == null)
|
|
155
|
+
return false;
|
|
156
|
+
if (Array.isArray(value))
|
|
157
|
+
return true;
|
|
158
|
+
return typeof value === "object" && !(value instanceof Date);
|
|
159
|
+
}
|
|
160
|
+
function hasStructuredCommentExtensions(doc) {
|
|
161
|
+
return doc.comments.some((comment) => Object.entries(comment).some(([key, value]) => !COMMENT_KEY_ORDER.includes(key) && value !== undefined && isStructuredValue(value)));
|
|
162
|
+
}
|
|
151
163
|
/* ·· CST node construction ············································ */
|
|
152
164
|
const NL = "\n";
|
|
153
165
|
/**
|
|
@@ -349,17 +361,21 @@ export async function writeSidecar(filePath, doc) {
|
|
|
349
361
|
*/
|
|
350
362
|
async function writeSidecarInternal(abs, doc) {
|
|
351
363
|
const isJson = abs.endsWith(".review.json");
|
|
364
|
+
for (const comment of doc.comments)
|
|
365
|
+
syncHash(comment);
|
|
352
366
|
if (isJson) {
|
|
353
|
-
// For JSON, always
|
|
354
|
-
for (const comment of doc.comments)
|
|
355
|
-
syncHash(comment);
|
|
367
|
+
// For JSON, always write fresh
|
|
356
368
|
await atomicWriteFile(abs, toJson(doc));
|
|
357
369
|
return;
|
|
358
370
|
}
|
|
371
|
+
// The CST patcher only supports scalar map values. Fall back to full YAML
|
|
372
|
+
// serialisation when comment extensions include arrays or objects.
|
|
373
|
+
if (hasStructuredCommentExtensions(doc)) {
|
|
374
|
+
await atomicWriteFile(abs, toYaml(doc));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
359
377
|
// ── YAML round-trip path ──────────────────────────────────────────
|
|
360
378
|
if (!existsSync(abs)) {
|
|
361
|
-
for (const comment of doc.comments)
|
|
362
|
-
syncHash(comment);
|
|
363
379
|
await atomicWriteFile(abs, toYaml(doc));
|
|
364
380
|
return;
|
|
365
381
|
}
|
package/mrsf.schema.json
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
|
|
2
|
+
{
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"$id": "https://github.com/wictorwilen/MRSF/raw/main/mrsf.schema.json",
|
|
5
|
+
"title": "Markdown Review Sidecar Format (MRSF) v1.0",
|
|
6
|
+
"description": "Schema for MRSF review sidecar files. See MRSF-v1.0.md for the full specification.",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"required": ["mrsf_version", "document", "comments"],
|
|
9
|
+
"additionalProperties": true,
|
|
10
|
+
"properties": {
|
|
11
|
+
"mrsf_version": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"pattern": "^1\\.\\d+$",
|
|
14
|
+
"description": "MRSF format version. MUST be a supported major.minor version (e.g., 1.0)."
|
|
15
|
+
},
|
|
16
|
+
"document": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Relative path to the Markdown document being reviewed."
|
|
19
|
+
},
|
|
20
|
+
"comments": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"items": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"required": ["id", "author", "timestamp", "text", "resolved"],
|
|
25
|
+
"additionalProperties": true,
|
|
26
|
+
"properties": {
|
|
27
|
+
"id": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Globally unique, opaque, collision-resistant identifier for the comment."
|
|
30
|
+
},
|
|
31
|
+
"author": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Creator of the comment. SHOULD follow the convention 'Display Name (identifier)'."
|
|
34
|
+
},
|
|
35
|
+
"timestamp": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"format": "date-time",
|
|
38
|
+
"description": "ISO 8601 / RFC 3339 timestamp of comment creation; SHOULD include timezone offset."
|
|
39
|
+
},
|
|
40
|
+
"text": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"maxLength": 16384,
|
|
43
|
+
"description": "The content of the review comment. MUST be plain text."
|
|
44
|
+
},
|
|
45
|
+
"resolved": {
|
|
46
|
+
"type": "boolean",
|
|
47
|
+
"description": "Whether the comment has been resolved."
|
|
48
|
+
},
|
|
49
|
+
"commit": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "Git commit hash associated with the comment. SHOULD be the full (long) SHA."
|
|
52
|
+
},
|
|
53
|
+
"type": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "Categorization of the comment. Recommended values listed in examples.",
|
|
56
|
+
"examples": [
|
|
57
|
+
"suggestion",
|
|
58
|
+
"issue",
|
|
59
|
+
"question",
|
|
60
|
+
"accuracy",
|
|
61
|
+
"style",
|
|
62
|
+
"clarity"
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
"severity": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"description": "Importance level of the comment.",
|
|
68
|
+
"enum": ["low", "medium", "high"]
|
|
69
|
+
},
|
|
70
|
+
"reply_to": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "ID of another comment in the same file that this comment replies to."
|
|
73
|
+
},
|
|
74
|
+
"line": {
|
|
75
|
+
"type": "integer",
|
|
76
|
+
"minimum": 1,
|
|
77
|
+
"description": "Starting line number (1-based) in the target document."
|
|
78
|
+
},
|
|
79
|
+
"end_line": {
|
|
80
|
+
"type": "integer",
|
|
81
|
+
"minimum": 1,
|
|
82
|
+
"description": "Ending line number (inclusive, 1-based). MUST be >= line."
|
|
83
|
+
},
|
|
84
|
+
"start_column": {
|
|
85
|
+
"type": "integer",
|
|
86
|
+
"minimum": 0,
|
|
87
|
+
"description": "Starting column index (0-based) within the starting line."
|
|
88
|
+
},
|
|
89
|
+
"end_column": {
|
|
90
|
+
"type": "integer",
|
|
91
|
+
"minimum": 0,
|
|
92
|
+
"description": "Ending column index. MUST be >= start_column when on the same line."
|
|
93
|
+
},
|
|
94
|
+
"selected_text": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"maxLength": 4096,
|
|
97
|
+
"description": "Exact text selected by the reviewer. SHOULD NOT be modified by re-anchoring tools. SHOULD NOT exceed 4096 characters."
|
|
98
|
+
},
|
|
99
|
+
"anchored_text": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"maxLength": 4096,
|
|
102
|
+
"description": "Text currently found at the resolved anchor position. Populated by re-anchoring tools when the document text differs from selected_text. SHOULD be omitted when identical to selected_text."
|
|
103
|
+
},
|
|
104
|
+
"selected_text_hash": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
107
|
+
"description": "Hex-encoded SHA-256 hash of selected_text. Immutable after creation; used for fast exact-match detection during re-anchoring, integrity verification, and staleness checks. SHOULD NOT be modified by re-anchoring tools unless selected_text is also replaced (opt-in behaviour), in which case it MUST be recomputed."
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrsf/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI and library for the Markdown Review Sidecar Format (MRSF)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,8 +31,13 @@
|
|
|
31
31
|
"review",
|
|
32
32
|
"sidecar",
|
|
33
33
|
"mrsf",
|
|
34
|
+
"sidemark",
|
|
34
35
|
"anchoring",
|
|
35
|
-
"comments"
|
|
36
|
+
"comments",
|
|
37
|
+
"cli",
|
|
38
|
+
"yaml",
|
|
39
|
+
"validation",
|
|
40
|
+
"reanchor"
|
|
36
41
|
],
|
|
37
42
|
"license": "MIT",
|
|
38
43
|
"author": "Wictor Wilén (https://github.com/wictorwilen)",
|
|
@@ -48,6 +53,7 @@
|
|
|
48
53
|
"files": [
|
|
49
54
|
"dist/**/*.js",
|
|
50
55
|
"dist/**/*.d.ts",
|
|
56
|
+
"mrsf.schema.json",
|
|
51
57
|
"README.md",
|
|
52
58
|
"LICENSE"
|
|
53
59
|
],
|
|
@@ -66,6 +72,7 @@
|
|
|
66
72
|
"@types/js-yaml": "^4.0.9",
|
|
67
73
|
"@types/node": "^22.13.10",
|
|
68
74
|
"@types/uuid": "^10.0.0",
|
|
75
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
69
76
|
"typescript": "^5.8.2",
|
|
70
77
|
"vitest": "^3.0.9"
|
|
71
78
|
}
|