@mrsf/cli 0.5.0 → 0.7.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 (45) hide show
  1. package/README.md +10 -7
  2. package/dist/browser.cjs +1273 -37
  3. package/dist/browser.cjs.map +4 -4
  4. package/dist/browser.d.ts +18 -1
  5. package/dist/browser.js +14 -0
  6. package/dist/commands/reanchor.js +3 -0
  7. package/dist/evaluation/generate-reanchor-cases.d.ts +10 -0
  8. package/dist/evaluation/generate-reanchor-cases.js +301 -0
  9. package/dist/evaluation/reanchor-eval.d.ts +111 -0
  10. package/dist/evaluation/reanchor-eval.js +266 -0
  11. package/dist/evaluation/reanchor-gates.d.ts +29 -0
  12. package/dist/evaluation/reanchor-gates.js +47 -0
  13. package/dist/evaluation/run-reanchor-eval.d.ts +3 -0
  14. package/dist/evaluation/run-reanchor-eval.js +103 -0
  15. package/dist/evaluation/run-reanchor-generator.d.ts +3 -0
  16. package/dist/evaluation/run-reanchor-generator.js +63 -0
  17. package/dist/evaluation/run-reanchor-profile.d.ts +3 -0
  18. package/dist/evaluation/run-reanchor-profile.js +129 -0
  19. package/dist/index.cjs +1307 -85
  20. package/dist/index.cjs.map +4 -4
  21. package/dist/index.d.ts +12 -0
  22. package/dist/index.js +9 -0
  23. package/dist/lib/anchor-context.d.ts +46 -0
  24. package/dist/lib/anchor-context.js +474 -0
  25. package/dist/lib/confidence-calibration.d.ts +16 -0
  26. package/dist/lib/confidence-calibration.js +120 -0
  27. package/dist/lib/fuzzy.d.ts +14 -1
  28. package/dist/lib/fuzzy.js +140 -19
  29. package/dist/lib/git.d.ts +2 -0
  30. package/dist/lib/git.js +12 -0
  31. package/dist/lib/global-reconciliation.d.ts +11 -0
  32. package/dist/lib/global-reconciliation.js +145 -0
  33. package/dist/lib/parser.d.ts +4 -19
  34. package/dist/lib/parser.js +4 -140
  35. package/dist/lib/reanchor-core.d.ts +20 -0
  36. package/dist/lib/reanchor-core.js +123 -20
  37. package/dist/lib/reanchor.js +85 -12
  38. package/dist/lib/revision-projection.d.ts +21 -0
  39. package/dist/lib/revision-projection.js +203 -0
  40. package/dist/lib/serialize.d.ts +37 -0
  41. package/dist/lib/serialize.js +170 -0
  42. package/dist/lib/types.d.ts +7 -0
  43. package/dist/lib/writer.d.ts +1 -8
  44. package/dist/lib/writer.js +5 -14
  45. package/package.json +5 -1
@@ -16,6 +16,7 @@ export function registerReanchor(program) {
16
16
  .description("Re-anchor comments to current document content")
17
17
  .option("-n, --dry-run", "Report without modifying files")
18
18
  .option("-t, --threshold <n>", "Fuzzy threshold 0.0–1.0", "0.6")
19
+ .option("--proximity-window <n>", "Max line distance for a lone exact match to relocate with full confidence (§7.4 step 1a guard)", "5")
19
20
  .option("--staged", "Only process sidecars for staged files")
20
21
  .option("--no-git", "Disable git integration")
21
22
  .option("--from <commit>", "Override from-commit for all comments")
@@ -27,6 +28,7 @@ export function registerReanchor(program) {
27
28
  const quiet = parentOpts.quiet ?? false;
28
29
  const verbose = parentOpts.verbose ?? false;
29
30
  const threshold = parseFloat(opts.threshold);
31
+ const proximityWindow = parseInt(opts.proximityWindow, 10);
30
32
  const noGit = !opts.git;
31
33
  let sidecarPaths;
32
34
  if (opts.staged && !noGit) {
@@ -74,6 +76,7 @@ export function registerReanchor(program) {
74
76
  const headCommit = repoRoot ? await getCurrentCommit(repoRoot) : undefined;
75
77
  const results = await reanchorDocument(doc, lines, {
76
78
  threshold,
79
+ proximityWindow,
77
80
  noGit,
78
81
  fromCommit: opts.from,
79
82
  documentPath: docPath,
@@ -0,0 +1,10 @@
1
+ import type { EvaluationCase } from "./reanchor-eval.js";
2
+ export interface GenerateCasesOptions {
3
+ seed: number;
4
+ caseCount?: number;
5
+ blocksPerCase?: number;
6
+ commentsPerCase?: number;
7
+ mutationsPerCase?: number;
8
+ }
9
+ export declare function generateEvaluationCases(options: GenerateCasesOptions): EvaluationCase[];
10
+ //# sourceMappingURL=generate-reanchor-cases.d.ts.map
@@ -0,0 +1,301 @@
1
+ const MUTATION_TYPES = [
2
+ "insert-block",
3
+ "delete-block",
4
+ "move-block",
5
+ "rewrite-block",
6
+ "duplicate-block",
7
+ "swap-blocks",
8
+ "whitespace-block",
9
+ "split-block",
10
+ "merge-blocks",
11
+ "rename-heading",
12
+ ];
13
+ export function generateEvaluationCases(options) {
14
+ const caseCount = positiveInteger(options.caseCount ?? 10, "caseCount");
15
+ const blocksPerCase = positiveInteger(options.blocksPerCase ?? 12, "blocksPerCase");
16
+ const commentsPerCase = positiveInteger(options.commentsPerCase ?? 4, "commentsPerCase");
17
+ const mutationsPerCase = positiveInteger(options.mutationsPerCase ?? 5, "mutationsPerCase");
18
+ const seed = unsignedSeed(options.seed);
19
+ return Array.from({ length: caseCount }, (_, index) => generateCase({
20
+ seed: (seed + index) >>> 0,
21
+ blocksPerCase,
22
+ commentsPerCase,
23
+ mutationsPerCase,
24
+ }));
25
+ }
26
+ function generateCase(options) {
27
+ const random = new SeededRandom(options.seed);
28
+ const sourceBlocks = createSourceBlocks(options.seed, options.blocksPerCase);
29
+ const source = renderBlocks(sourceBlocks);
30
+ const trackedComments = selectComments(sourceBlocks, source, options.commentsPerCase, random);
31
+ const targetBlocks = sourceBlocks.map((block) => ({ ...block }));
32
+ const operations = [];
33
+ let nextBlockNumber = sourceBlocks.length + 1;
34
+ for (let index = 0; index < options.mutationsPerCase; index += 1) {
35
+ const preferredType = random.pick(MUTATION_TYPES);
36
+ const operation = applyMutation(preferredType, targetBlocks, trackedComments, random, () => `generated-${options.seed}-block-${nextBlockNumber++}`);
37
+ operations.push(operation);
38
+ }
39
+ const target = renderBlocks(targetBlocks);
40
+ const comments = trackedComments.map((comment) => createEvaluationComment(comment, targetBlocks, target));
41
+ const categories = [
42
+ "generated",
43
+ ...new Set(operations.map((operation) => operation.type)),
44
+ ];
45
+ return {
46
+ id: `generated-${options.seed}`,
47
+ description: `Seeded mutation case ${options.seed} with ${operations.length} operations.`,
48
+ categories,
49
+ source: { text: source.text },
50
+ target: { text: target.text },
51
+ generation: {
52
+ generator_version: 1,
53
+ seed: options.seed,
54
+ blocks_per_case: options.blocksPerCase,
55
+ comments_per_case: Math.min(options.commentsPerCase, options.blocksPerCase),
56
+ operations,
57
+ },
58
+ comments,
59
+ };
60
+ }
61
+ function createSourceBlocks(seed, count) {
62
+ return Array.from({ length: count }, (_, index) => {
63
+ const number = index + 1;
64
+ return {
65
+ id: `generated-${seed}-block-${number}`,
66
+ text: number % 4 === 1
67
+ ? `## Section ${number} for seed ${seed}`
68
+ : `Paragraph ${number} describes behavior token-${seed}-${number}.`,
69
+ };
70
+ });
71
+ }
72
+ function selectComments(blocks, rendered, requestedCount, random) {
73
+ const available = blocks.map((_, index) => index);
74
+ random.shuffle(available);
75
+ return available
76
+ .slice(0, Math.min(requestedCount, blocks.length))
77
+ .sort((left, right) => left - right)
78
+ .map((blockIndex, commentIndex) => {
79
+ const block = blocks[blockIndex];
80
+ const range = rendered.ranges.get(block.id);
81
+ if (!range)
82
+ throw new Error(`Missing source range for ${block.id}.`);
83
+ return {
84
+ id: `generated-comment-${commentIndex + 1}`,
85
+ sourceLine: range.line,
86
+ selectedText: block.text,
87
+ targetBlockId: block.id,
88
+ };
89
+ });
90
+ }
91
+ function applyMutation(preferredType, blocks, comments, random, nextBlockId) {
92
+ if (preferredType === "insert-block") {
93
+ const id = nextBlockId();
94
+ const position = random.nextInt(blocks.length + 1);
95
+ blocks.splice(position, 0, {
96
+ id,
97
+ text: `Inserted paragraph ${id} describes new material.`,
98
+ });
99
+ return { type: "insert-block", block_ids: [id] };
100
+ }
101
+ if (preferredType === "delete-block" && blocks.length > 1) {
102
+ const index = random.nextInt(blocks.length);
103
+ const [removed] = blocks.splice(index, 1);
104
+ for (const comment of comments) {
105
+ if (comment.targetBlockId === removed.id) {
106
+ delete comment.targetBlockId;
107
+ }
108
+ }
109
+ return { type: "delete-block", block_ids: [removed.id] };
110
+ }
111
+ if (preferredType === "move-block" && blocks.length > 1) {
112
+ const from = random.nextInt(blocks.length);
113
+ let to = random.nextInt(blocks.length);
114
+ if (to === from)
115
+ to = (to + 1) % blocks.length;
116
+ const [moved] = blocks.splice(from, 1);
117
+ blocks.splice(to, 0, moved);
118
+ return { type: "move-block", block_ids: [moved.id] };
119
+ }
120
+ if (preferredType === "rewrite-block") {
121
+ const block = random.pick(blocks);
122
+ block.text = rewriteFirstWord(block.text);
123
+ return { type: "rewrite-block", block_ids: [block.id] };
124
+ }
125
+ if (preferredType === "duplicate-block") {
126
+ const source = random.pick(blocks);
127
+ const duplicateId = nextBlockId();
128
+ const sourceIndex = blocks.indexOf(source);
129
+ blocks.splice(sourceIndex + 1, 0, {
130
+ id: duplicateId,
131
+ text: source.text,
132
+ });
133
+ return {
134
+ type: "duplicate-block",
135
+ block_ids: [source.id, duplicateId],
136
+ };
137
+ }
138
+ if (preferredType === "swap-blocks" && blocks.length > 1) {
139
+ const first = random.nextInt(blocks.length);
140
+ let second = random.nextInt(blocks.length);
141
+ if (first === second)
142
+ second = (second + 1) % blocks.length;
143
+ [blocks[first], blocks[second]] = [blocks[second], blocks[first]];
144
+ return {
145
+ type: "swap-blocks",
146
+ block_ids: [blocks[second].id, blocks[first].id],
147
+ };
148
+ }
149
+ if (preferredType === "whitespace-block") {
150
+ const block = random.pick(blocks);
151
+ block.text = block.text.replace(/ /g, " ");
152
+ return { type: "whitespace-block", block_ids: [block.id] };
153
+ }
154
+ if (preferredType === "split-block") {
155
+ const candidates = blocks.filter((block) => block.text.includes(" "));
156
+ const block = random.pick(candidates.length > 0 ? candidates : blocks);
157
+ const splitAt = splitPosition(block.text);
158
+ block.text = `${block.text.slice(0, splitAt)}\n\n${block.text.slice(splitAt + 1)}`;
159
+ return { type: "split-block", block_ids: [block.id] };
160
+ }
161
+ if (preferredType === "merge-blocks" && blocks.length > 1) {
162
+ const firstIndex = random.nextInt(blocks.length - 1);
163
+ const first = blocks[firstIndex];
164
+ const [second] = blocks.splice(firstIndex + 1, 1);
165
+ first.text = `${first.text} ${second.text}`;
166
+ for (const comment of comments) {
167
+ if (comment.targetBlockId === second.id) {
168
+ comment.targetBlockId = first.id;
169
+ }
170
+ }
171
+ return { type: "merge-blocks", block_ids: [first.id, second.id] };
172
+ }
173
+ if (preferredType === "rename-heading") {
174
+ const headings = blocks.filter((block) => block.text.startsWith("#"));
175
+ if (headings.length > 0) {
176
+ const heading = random.pick(headings);
177
+ heading.text = heading.text.replace("Section", "Topic");
178
+ return { type: "rename-heading", block_ids: [heading.id] };
179
+ }
180
+ }
181
+ return applyMutation("insert-block", blocks, comments, random, nextBlockId);
182
+ }
183
+ function createEvaluationComment(tracked, targetBlocks, target) {
184
+ if (!tracked.targetBlockId) {
185
+ return {
186
+ id: tracked.id,
187
+ anchor: {
188
+ line: tracked.sourceLine,
189
+ selected_text: tracked.selectedText,
190
+ },
191
+ expected: {
192
+ status: "orphaned",
193
+ rationale: "The semantic source block was deleted.",
194
+ },
195
+ };
196
+ }
197
+ const targetBlock = targetBlocks.find((block) => block.id === tracked.targetBlockId);
198
+ const blockRange = target.ranges.get(tracked.targetBlockId);
199
+ if (!targetBlock || !blockRange) {
200
+ throw new Error(`Missing target block ${tracked.targetBlockId}.`);
201
+ }
202
+ const exactIndex = targetBlock.text.indexOf(tracked.selectedText);
203
+ const exact = exactIndex >= 0;
204
+ const range = exact
205
+ ? exactRangeWithinBlock(blockRange.line, targetBlock.text, tracked.selectedText, exactIndex)
206
+ : blockRange;
207
+ return {
208
+ id: tracked.id,
209
+ anchor: {
210
+ line: tracked.sourceLine,
211
+ selected_text: tracked.selectedText,
212
+ },
213
+ expected: {
214
+ status: exact ? "anchored" : "fuzzy",
215
+ ranges: [range],
216
+ rationale: exact
217
+ ? "The original selection remains inside its semantic block."
218
+ : "The semantic block remains but its selected text changed.",
219
+ },
220
+ };
221
+ }
222
+ function exactRangeWithinBlock(blockLine, blockText, selectedText, index) {
223
+ const linesBefore = blockText.slice(0, index).split("\n").length - 1;
224
+ const selectedLines = selectedText.split("\n").length;
225
+ const line = blockLine + linesBefore;
226
+ return {
227
+ line,
228
+ end_line: line + selectedLines - 1,
229
+ };
230
+ }
231
+ function renderBlocks(blocks) {
232
+ const ranges = new Map();
233
+ let line = 1;
234
+ for (const block of blocks) {
235
+ const lineCount = block.text.split("\n").length;
236
+ ranges.set(block.id, {
237
+ line,
238
+ end_line: line + lineCount - 1,
239
+ });
240
+ line += lineCount + 1;
241
+ }
242
+ return {
243
+ text: `${blocks.map((block) => block.text).join("\n\n")}\n`,
244
+ ranges,
245
+ };
246
+ }
247
+ function rewriteFirstWord(text) {
248
+ return text.replace(/^(\W*)([\p{L}\p{N}]+)/u, "$1$2-revised");
249
+ }
250
+ function splitPosition(text) {
251
+ const midpoint = Math.floor(text.length / 2);
252
+ const after = text.indexOf(" ", midpoint);
253
+ if (after >= 0)
254
+ return after;
255
+ const before = text.lastIndexOf(" ", midpoint);
256
+ return before >= 0 ? before : midpoint;
257
+ }
258
+ function positiveInteger(value, name) {
259
+ if (!Number.isSafeInteger(value) || value < 1) {
260
+ throw new Error(`${name} must be a positive integer.`);
261
+ }
262
+ return value;
263
+ }
264
+ function unsignedSeed(value) {
265
+ if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) {
266
+ throw new Error("seed must be an integer between 0 and 4294967295.");
267
+ }
268
+ return value >>> 0;
269
+ }
270
+ class SeededRandom {
271
+ state;
272
+ constructor(seed) {
273
+ this.state = seed === 0 ? 0x9e3779b9 : seed >>> 0;
274
+ }
275
+ nextInt(maxExclusive) {
276
+ if (!Number.isSafeInteger(maxExclusive) || maxExclusive < 1) {
277
+ throw new Error("Random upper bound must be a positive integer.");
278
+ }
279
+ return Math.floor(this.next() * maxExclusive);
280
+ }
281
+ pick(values) {
282
+ if (values.length === 0)
283
+ throw new Error("Cannot pick from an empty array.");
284
+ return values[this.nextInt(values.length)];
285
+ }
286
+ shuffle(values) {
287
+ for (let index = values.length - 1; index > 0; index -= 1) {
288
+ const other = this.nextInt(index + 1);
289
+ [values[index], values[other]] = [values[other], values[index]];
290
+ }
291
+ }
292
+ next() {
293
+ let value = this.state;
294
+ value ^= value << 13;
295
+ value ^= value >>> 17;
296
+ value ^= value << 5;
297
+ this.state = value >>> 0;
298
+ return this.state / 0x100000000;
299
+ }
300
+ }
301
+ //# sourceMappingURL=generate-reanchor-cases.js.map
@@ -0,0 +1,111 @@
1
+ import type { ReanchorResult, ReanchorStatus } from "../lib/types.js";
2
+ export interface EvaluationDocument {
3
+ text?: string;
4
+ path?: string;
5
+ }
6
+ export interface EvaluationAnchor {
7
+ line?: number;
8
+ end_line?: number;
9
+ start_column?: number;
10
+ end_column?: number;
11
+ selected_text?: string;
12
+ selected_text_hash?: string;
13
+ commit?: string;
14
+ }
15
+ export interface EvaluationRange {
16
+ line: number;
17
+ end_line?: number;
18
+ start_column?: number;
19
+ end_column?: number;
20
+ }
21
+ export interface EvaluationExpectation {
22
+ status: ReanchorStatus;
23
+ ranges?: EvaluationRange[];
24
+ rationale?: string;
25
+ }
26
+ export interface EvaluationComment {
27
+ id: string;
28
+ anchor: EvaluationAnchor;
29
+ expected: EvaluationExpectation;
30
+ }
31
+ export interface EvaluationCase {
32
+ id: string;
33
+ description?: string;
34
+ categories: string[];
35
+ source: EvaluationDocument;
36
+ target: EvaluationDocument;
37
+ generation?: EvaluationGeneration;
38
+ comments: EvaluationComment[];
39
+ }
40
+ export interface EvaluationGeneration {
41
+ generator_version: 1;
42
+ seed: number;
43
+ blocks_per_case: number;
44
+ comments_per_case: number;
45
+ operations: EvaluationMutation[];
46
+ }
47
+ export interface EvaluationMutation {
48
+ type: "insert-block" | "delete-block" | "move-block" | "rewrite-block" | "duplicate-block" | "swap-blocks" | "whitespace-block" | "split-block" | "merge-blocks" | "rename-heading";
49
+ block_ids: string[];
50
+ }
51
+ export interface EvaluationCommentResult {
52
+ caseId: string;
53
+ commentId: string;
54
+ expected: EvaluationExpectation;
55
+ actual: ReanchorResult;
56
+ statusCorrect: boolean;
57
+ rangeCorrect: boolean;
58
+ passed: boolean;
59
+ durationMs: number;
60
+ }
61
+ export interface EvaluationSummary {
62
+ algorithm: string;
63
+ cases: number;
64
+ comments: number;
65
+ passed: number;
66
+ failed: number;
67
+ exactRangeMatches: number;
68
+ statusMatches: number;
69
+ incorrectConfidentRelocations: number;
70
+ durationMs: number;
71
+ timingMs: {
72
+ median: number;
73
+ p95: number;
74
+ max: number;
75
+ perComment: number;
76
+ reconciliationMax: number;
77
+ };
78
+ results: EvaluationCommentResult[];
79
+ }
80
+ export interface EvaluationBaseline {
81
+ version: 1;
82
+ algorithm: string;
83
+ cases: number;
84
+ comments: number;
85
+ metrics: {
86
+ passed: number;
87
+ failed: number;
88
+ exactRangeMatches: number;
89
+ statusMatches: number;
90
+ incorrectConfidentRelocations: number;
91
+ };
92
+ results: EvaluationBaselineResult[];
93
+ }
94
+ export interface EvaluationBaselineResult {
95
+ caseId: string;
96
+ commentId: string;
97
+ expectedStatus: ReanchorStatus;
98
+ actual: ReanchorResult;
99
+ statusCorrect: boolean;
100
+ rangeCorrect: boolean;
101
+ passed: boolean;
102
+ }
103
+ export interface LoadedEvaluationCase {
104
+ casePath: string;
105
+ value: EvaluationCase;
106
+ }
107
+ export declare function loadEvaluationCases(casesPath: string, schemaPath: string): Promise<LoadedEvaluationCase[]>;
108
+ export declare function evaluateCases(cases: LoadedEvaluationCase[]): Promise<EvaluationSummary>;
109
+ export declare function createEvaluationBaseline(summary: EvaluationSummary): EvaluationBaseline;
110
+ export declare function findBaselineDifferences(expected: EvaluationBaseline, actual: EvaluationBaseline): string[];
111
+ //# sourceMappingURL=reanchor-eval.d.ts.map