@mrsf/cli 0.2.0 → 0.2.1

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.
@@ -8,7 +8,7 @@ import { readDocumentLines } from "../lib/parser.js";
8
8
  import { sidecarToDocument } from "../lib/discovery.js";
9
9
  import { resolveSidecarPaths } from "../lib/resolve-files.js";
10
10
  import { writeSidecar } from "../lib/writer.js";
11
- import { findRepoRoot, getStagedFiles } from "../lib/git.js";
11
+ import { findRepoRoot, getStagedFiles, getCurrentCommit } from "../lib/git.js";
12
12
  import { reanchorDocument, applyReanchorResults, } from "../lib/reanchor.js";
13
13
  export function registerReanchor(program) {
14
14
  program
@@ -20,6 +20,7 @@ export function registerReanchor(program) {
20
20
  .option("--no-git", "Disable git integration")
21
21
  .option("--from <commit>", "Override from-commit for all comments")
22
22
  .option("--update-text", "Also update selected_text to match current document text")
23
+ .option("-f, --force", "Firmly anchor high-confidence results (update commit to HEAD, clear audit fields)")
23
24
  .action(async (files, opts) => {
24
25
  const parentOpts = program.opts();
25
26
  const cwd = parentOpts.cwd ?? process.cwd();
@@ -70,6 +71,7 @@ export function registerReanchor(program) {
70
71
  const docPath = sidecarToDocument(sp);
71
72
  const lines = await readDocumentLines(docPath);
72
73
  const repoRoot = !noGit ? await findRepoRoot(cwd) : null;
74
+ const headCommit = repoRoot ? await getCurrentCommit(repoRoot) : undefined;
73
75
  const results = await reanchorDocument(doc, lines, {
74
76
  threshold,
75
77
  noGit,
@@ -82,6 +84,8 @@ export function registerReanchor(program) {
82
84
  if (!opts.dryRun) {
83
85
  const changed = applyReanchorResults(doc, results, {
84
86
  updateText: opts.updateText,
87
+ force: opts.force,
88
+ headCommit: headCommit ?? undefined,
85
89
  });
86
90
  totalChanged += changed;
87
91
  if (changed > 0) {
@@ -93,6 +97,8 @@ export function registerReanchor(program) {
93
97
  const clone = structuredClone(doc);
94
98
  totalChanged += applyReanchorResults(clone, results, {
95
99
  updateText: opts.updateText,
100
+ force: opts.force,
101
+ headCommit: headCommit ?? undefined,
96
102
  });
97
103
  }
98
104
  if (!quiet) {
@@ -33,6 +33,8 @@ export declare function reanchorDocument(doc: MrsfDocument, documentLines: strin
33
33
  */
34
34
  export declare function applyReanchorResults(doc: MrsfDocument, results: ReanchorResult[], opts?: {
35
35
  updateText?: boolean;
36
+ force?: boolean;
37
+ headCommit?: string;
36
38
  }): number;
37
39
  /**
38
40
  * High-level re-anchor for a single sidecar file path.
@@ -45,8 +45,24 @@ export function reanchorComment(comment, documentLines, opts = {}) {
45
45
  // -----------------------------------------------------------------------
46
46
  // Step 0: Diff-based shift
47
47
  // -----------------------------------------------------------------------
48
- if (selectedText && comment.line != null && opts.diffHunks?.length) {
48
+ if (comment.line != null && opts.diffHunks?.length) {
49
49
  const { shift, modified } = getLineShift(opts.diffHunks, comment.line);
50
+ // For line-only comments (no selected_text), apply the diff shift directly
51
+ if (!selectedText) {
52
+ const shiftedLine = comment.line + shift;
53
+ const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
54
+ const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : undefined;
55
+ return {
56
+ commentId,
57
+ status: shift === 0 ? "anchored" : "shifted",
58
+ score: 1.0,
59
+ newLine: shiftedLine,
60
+ newEndLine: shiftedEndLine,
61
+ reason: shift === 0
62
+ ? "Line-only comment unchanged (diff confirms position)."
63
+ : `Line-only comment shifted by ${shift > 0 ? "+" : ""}${shift} line(s) via diff.`,
64
+ };
65
+ }
50
66
  if (!modified) {
51
67
  const shiftedLine = comment.line + shift;
52
68
  const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
@@ -167,13 +183,18 @@ export function reanchorComment(comment, documentLines, opts = {}) {
167
183
  }
168
184
  }
169
185
  // Pure line fallback
186
+ // If there's no selected_text, the comment is purely line-anchored.
187
+ // A commit change alone doesn't make it ambiguous — we accept it as-is.
188
+ const isLineOnly = !selectedText;
170
189
  return {
171
190
  commentId,
172
- status: opts.commitIsStale ? "ambiguous" : "anchored",
173
- score: opts.commitIsStale ? 0.5 : 0.8,
191
+ status: isLineOnly ? "anchored" : (opts.commitIsStale ? "ambiguous" : "anchored"),
192
+ score: isLineOnly ? 1.0 : (opts.commitIsStale ? 0.5 : 0.8),
174
193
  newLine: comment.line,
175
194
  newEndLine: comment.end_line,
176
- reason: `Line/column fallback${qualifier}.`,
195
+ reason: isLineOnly
196
+ ? "Line-only comment (no selected_text to verify)."
197
+ : `Line/column fallback${qualifier}.`,
177
198
  };
178
199
  }
179
200
  }
@@ -320,6 +341,22 @@ export function applyReanchorResults(doc, results, opts = {}) {
320
341
  comment.x_reanchor_status = result.status;
321
342
  comment.x_reanchor_score = result.score;
322
343
  }
344
+ // --force: firmly anchor high-confidence results
345
+ // Update commit to HEAD and clear audit fields so the comment
346
+ // won't be re-evaluated on the next reanchor run.
347
+ if (opts.force &&
348
+ opts.headCommit &&
349
+ (result.status === "anchored" || result.status === "shifted") &&
350
+ result.score >= HIGH_THRESHOLD) {
351
+ comment.commit = opts.headCommit;
352
+ delete comment.x_reanchor_status;
353
+ delete comment.x_reanchor_score;
354
+ // Clear stale anchored_text since anchor is now confirmed
355
+ if (comment.anchored_text && comment.anchored_text === comment.selected_text) {
356
+ delete comment.anchored_text;
357
+ }
358
+ isChanged = true;
359
+ }
323
360
  if (isChanged)
324
361
  changed++;
325
362
  }
@@ -333,6 +370,7 @@ export async function reanchorFile(sidecarPath, opts = {}) {
333
370
  const docPath = sidecarToDocument(sidecarPath);
334
371
  const documentLines = await readDocumentLines(docPath);
335
372
  const repoRoot = !opts.noGit ? await findRepoRoot(opts.cwd) : null;
373
+ const headCommit = repoRoot ? await getCurrentCommit(repoRoot) : undefined;
336
374
  const results = await reanchorDocument(doc, documentLines, {
337
375
  ...opts,
338
376
  documentPath: docPath,
@@ -341,7 +379,11 @@ export async function reanchorFile(sidecarPath, opts = {}) {
341
379
  let changed = 0;
342
380
  let written = false;
343
381
  if (!opts.dryRun) {
344
- changed = applyReanchorResults(doc, results, { updateText: opts.updateText });
382
+ changed = applyReanchorResults(doc, results, {
383
+ updateText: opts.updateText,
384
+ force: opts.force,
385
+ headCommit: headCommit ?? undefined,
386
+ });
345
387
  if (changed > 0 || opts.autoUpdate) {
346
388
  await writeSidecar(sidecarPath, doc);
347
389
  written = true;
@@ -143,6 +143,8 @@ export interface ReanchorOptions extends BaseOptions {
143
143
  fromCommit?: string;
144
144
  /** Update selected_text to match anchored_text (opt-in per §6.2). */
145
145
  updateText?: boolean;
146
+ /** Force-anchor: update commit to HEAD and clear audit fields for high-confidence results. */
147
+ force?: boolean;
146
148
  }
147
149
  export interface ValidateOptions extends BaseOptions {
148
150
  /** Treat warnings as errors. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrsf/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI and library for the Markdown Review Sidecar Format (MRSF)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",