@mrsf/cli 0.4.3 → 0.5.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.
@@ -0,0 +1,887 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/browser.ts
31
+ var browser_exports = {};
32
+ __export(browser_exports, {
33
+ DEFAULT_THRESHOLD: () => DEFAULT_THRESHOLD2,
34
+ HIGH_THRESHOLD: () => HIGH_THRESHOLD2,
35
+ applyReanchorResults: () => applyReanchorResults2,
36
+ combinedScore: () => combinedScore2,
37
+ exactMatch: () => exactMatch2,
38
+ formatAuthor: () => formatAuthor2,
39
+ fuzzySearch: () => fuzzySearch2,
40
+ levenshteinScore: () => levenshteinScore2,
41
+ mrsfSchema: () => mrsfSchema,
42
+ newCommentId: () => newCommentId2,
43
+ normalizedMatch: () => normalizedMatch2,
44
+ parseAuthor: () => parseAuthor2,
45
+ reanchorComment: () => reanchorComment2,
46
+ reanchorDocumentLines: () => reanchorDocumentLines2,
47
+ reanchorDocumentText: () => reanchorDocumentText2,
48
+ resolveAnchor: () => resolveAnchor2,
49
+ toReanchorLines: () => toReanchorLines2,
50
+ tokenLcsScore: () => tokenLcsScore2,
51
+ validateDocument: () => validateDocument2
52
+ });
53
+ module.exports = __toCommonJS(browser_exports);
54
+
55
+ // src/lib/fuzzy.ts
56
+ var import_fastest_levenshtein = require("fastest-levenshtein");
57
+ function exactMatch(lines, needle) {
58
+ if (!needle) return [];
59
+ const results = [];
60
+ const needleLines = needle.split("\n");
61
+ const needleLineCount = needleLines.length;
62
+ for (let startLine = 1; startLine <= lines.length - needleLineCount; startLine++) {
63
+ const windowLines = lines.slice(startLine, startLine + needleLineCount);
64
+ const windowText = windowLines.join("\n");
65
+ if (needleLineCount === 1) {
66
+ let col = 0;
67
+ const line = windowLines[0];
68
+ while (col < line.length) {
69
+ const idx = line.indexOf(needle, col);
70
+ if (idx === -1) break;
71
+ results.push({
72
+ text: needle,
73
+ line: startLine,
74
+ endLine: startLine,
75
+ startColumn: idx,
76
+ endColumn: idx + needle.length,
77
+ score: 1
78
+ });
79
+ col = idx + 1;
80
+ }
81
+ } else {
82
+ const idx = windowText.indexOf(needle);
83
+ if (idx !== -1) {
84
+ const beforeMatch = windowText.slice(0, idx);
85
+ const linesBeforeEnd = beforeMatch.split("\n");
86
+ const startCol = linesBeforeEnd[linesBeforeEnd.length - 1].length;
87
+ const afterMatch = needle.split("\n");
88
+ const endCol = afterMatch[afterMatch.length - 1].length;
89
+ if (startCol === 0 || linesBeforeEnd.length === 1) {
90
+ results.push({
91
+ text: needle,
92
+ line: startLine + linesBeforeEnd.length - 1,
93
+ endLine: startLine + linesBeforeEnd.length - 1 + afterMatch.length - 1,
94
+ startColumn: startCol,
95
+ endColumn: endCol,
96
+ score: 1
97
+ });
98
+ }
99
+ }
100
+ }
101
+ }
102
+ return results;
103
+ }
104
+ function normalize(text) {
105
+ return text.replace(/\s+/g, " ").trim();
106
+ }
107
+ function normalizedMatch(lines, needle) {
108
+ const normNeedle = normalize(needle);
109
+ if (!normNeedle) return [];
110
+ const results = [];
111
+ const needleLineEstimate = needle.split("\n").length;
112
+ const minWindow = Math.max(1, needleLineEstimate - 1);
113
+ const maxWindow = Math.min(lines.length - 1, needleLineEstimate + 2);
114
+ for (let winSize = minWindow; winSize <= maxWindow; winSize++) {
115
+ for (let startLine = 1; startLine + winSize - 1 < lines.length; startLine++) {
116
+ const windowLines = lines.slice(startLine, startLine + winSize);
117
+ const windowText = windowLines.join("\n");
118
+ const normWindow = normalize(windowText);
119
+ if (normWindow.includes(normNeedle)) {
120
+ results.push({
121
+ text: windowText,
122
+ line: startLine,
123
+ endLine: startLine + winSize - 1,
124
+ startColumn: 0,
125
+ endColumn: windowLines[windowLines.length - 1].length,
126
+ score: 0.95
127
+ });
128
+ }
129
+ }
130
+ }
131
+ return deduplicateCandidates(results);
132
+ }
133
+ function tokenize(text) {
134
+ return text.split(/\s+/).filter((t) => t.length > 0);
135
+ }
136
+ function lcsLength(a, b) {
137
+ const m = a.length;
138
+ const n = b.length;
139
+ let prev = new Array(n + 1).fill(0);
140
+ let curr = new Array(n + 1).fill(0);
141
+ for (let i = 1; i <= m; i++) {
142
+ for (let j = 1; j <= n; j++) {
143
+ if (a[i - 1] === b[j - 1]) {
144
+ curr[j] = prev[j - 1] + 1;
145
+ } else {
146
+ curr[j] = Math.max(prev[j], curr[j - 1]);
147
+ }
148
+ }
149
+ [prev, curr] = [curr, prev];
150
+ curr.fill(0);
151
+ }
152
+ return prev[n];
153
+ }
154
+ function tokenLcsScore(a, b) {
155
+ const tokA = tokenize(a);
156
+ const tokB = tokenize(b);
157
+ if (tokA.length === 0 && tokB.length === 0) return 1;
158
+ if (tokA.length === 0 || tokB.length === 0) return 0;
159
+ const lcs = lcsLength(tokA, tokB);
160
+ return lcs / Math.max(tokA.length, tokB.length);
161
+ }
162
+ function levenshteinScore(a, b) {
163
+ if (a.length === 0 && b.length === 0) return 1;
164
+ const maxLen = Math.max(a.length, b.length);
165
+ if (maxLen === 0) return 1;
166
+ const dist = (0, import_fastest_levenshtein.distance)(a, b);
167
+ return 1 - dist / maxLen;
168
+ }
169
+ function combinedScore(needle, candidate) {
170
+ const tScore = tokenLcsScore(needle, candidate);
171
+ let lScore;
172
+ if (needle.length < 500 && candidate.length < 500) {
173
+ lScore = levenshteinScore(needle, candidate);
174
+ } else {
175
+ lScore = tScore;
176
+ }
177
+ return tScore * 0.6 + lScore * 0.4;
178
+ }
179
+ function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
180
+ if (!needle) return [];
181
+ const needleLines = needle.split("\n");
182
+ const needleLineCount = needleLines.length;
183
+ const candidates = [];
184
+ const minWindow = Math.max(1, Math.floor(needleLineCount * 0.7));
185
+ const maxWindow = Math.min(
186
+ lines.length - 1,
187
+ Math.ceil(needleLineCount * 1.3) + 1
188
+ );
189
+ for (let winSize = minWindow; winSize <= maxWindow; winSize++) {
190
+ for (let startLine = 1; startLine + winSize - 1 < lines.length; startLine++) {
191
+ const windowLines = lines.slice(startLine, startLine + winSize);
192
+ const windowText = windowLines.join("\n");
193
+ const score = combinedScore(needle, windowText);
194
+ if (score >= threshold) {
195
+ candidates.push({
196
+ text: windowText,
197
+ line: startLine,
198
+ endLine: startLine + winSize - 1,
199
+ startColumn: 0,
200
+ endColumn: windowLines[windowLines.length - 1].length,
201
+ score
202
+ });
203
+ }
204
+ }
205
+ }
206
+ if (needleLineCount === 1 && needle.length < 200) {
207
+ for (let lineNum = 1; lineNum < lines.length; lineNum++) {
208
+ const line = lines[lineNum];
209
+ if (!line) continue;
210
+ const winLen = needle.length;
211
+ const minWinLen = Math.max(3, Math.floor(winLen * 0.7));
212
+ const maxWinLen = Math.min(line.length, Math.ceil(winLen * 1.3));
213
+ for (let len = minWinLen; len <= maxWinLen; len++) {
214
+ for (let col = 0; col + len <= line.length; col++) {
215
+ const sub = line.substring(col, col + len);
216
+ const score = combinedScore(needle, sub);
217
+ if (score >= threshold) {
218
+ candidates.push({
219
+ text: sub,
220
+ line: lineNum,
221
+ endLine: lineNum,
222
+ startColumn: col,
223
+ endColumn: col + len,
224
+ score
225
+ });
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
231
+ let deduped = deduplicateCandidates(candidates);
232
+ if (hintLine != null) {
233
+ deduped = deduped.map((c) => {
234
+ const dist = Math.abs(c.line - hintLine);
235
+ const proximityBonus = 0.1 * Math.max(0, 1 - dist / 50);
236
+ return { ...c, score: Math.min(1, c.score + proximityBonus) };
237
+ });
238
+ }
239
+ return deduped.sort((a, b) => b.score - a.score);
240
+ }
241
+ function deduplicateCandidates(candidates) {
242
+ const seen = /* @__PURE__ */ new Map();
243
+ for (const c of candidates) {
244
+ const key = `${c.line}:${c.startColumn}:${c.endLine}:${c.endColumn}`;
245
+ const existing = seen.get(key);
246
+ if (!existing || c.score > existing.score) {
247
+ seen.set(key, c);
248
+ }
249
+ }
250
+ return Array.from(seen.values());
251
+ }
252
+
253
+ // src/lib/identity.ts
254
+ var import_uuid = require("uuid");
255
+ function formatAuthor(name, handle) {
256
+ const trimmedName = name.trim();
257
+ const trimmedHandle = handle?.trim();
258
+ return trimmedHandle ? `${trimmedName} (${trimmedHandle})` : trimmedName;
259
+ }
260
+ function parseAuthor(author) {
261
+ const trimmed = author.trim();
262
+ const match = /^(.*?)\s*\(([^()]*)\)\s*$/.exec(trimmed);
263
+ if (!match) return { name: trimmed };
264
+ const name = match[1].trim();
265
+ const handle = match[2].trim();
266
+ return handle ? { name, handle } : { name };
267
+ }
268
+ function newCommentId() {
269
+ return (0, import_uuid.v4)();
270
+ }
271
+
272
+ // src/lib/reanchor-core.ts
273
+ var HIGH_THRESHOLD = 0.8;
274
+ var DEFAULT_THRESHOLD = 0.6;
275
+ function toReanchorLines(documentText) {
276
+ return ["", ...documentText.replace(/\r\n/g, "\n").split("\n")];
277
+ }
278
+ function reanchorComment(comment, documentLines, opts = {}) {
279
+ const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
280
+ const commentId = comment.id;
281
+ const selectedText = comment.selected_text;
282
+ if (!selectedText && comment.line == null) {
283
+ return {
284
+ commentId,
285
+ status: "anchored",
286
+ score: 1,
287
+ reason: "Document-level comment (no anchor needed)."
288
+ };
289
+ }
290
+ if (comment.line != null && opts.diffHunks?.length) {
291
+ const { shift, modified } = getLineShift(opts.diffHunks, comment.line);
292
+ if (!selectedText) {
293
+ const shiftedLine = comment.line + shift;
294
+ const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
295
+ const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : void 0;
296
+ return {
297
+ commentId,
298
+ status: shift === 0 ? "anchored" : "shifted",
299
+ score: 1,
300
+ newLine: shiftedLine,
301
+ newEndLine: shiftedEndLine,
302
+ reason: shift === 0 ? "Line-only comment unchanged (diff confirms position)." : `Line-only comment shifted by ${shift > 0 ? "+" : ""}${shift} line(s) via diff.`
303
+ };
304
+ }
305
+ if (!modified) {
306
+ const shiftedLine = comment.line + shift;
307
+ const lineSpan = comment.end_line != null ? comment.end_line - comment.line : 0;
308
+ const shiftedEndLine = comment.end_line != null ? shiftedLine + lineSpan : void 0;
309
+ const textAtShifted = extractText(
310
+ documentLines,
311
+ shiftedLine,
312
+ shiftedEndLine,
313
+ comment.start_column,
314
+ comment.end_column
315
+ );
316
+ if (textAtShifted === selectedText) {
317
+ return {
318
+ commentId,
319
+ status: shift === 0 ? "anchored" : "shifted",
320
+ score: 1,
321
+ newLine: shiftedLine,
322
+ newEndLine: shiftedEndLine,
323
+ reason: shift === 0 ? "Diff confirms text unchanged at original position." : `Diff shifted by ${shift > 0 ? "+" : ""}${shift} line(s).`
324
+ };
325
+ }
326
+ }
327
+ }
328
+ if (selectedText) {
329
+ const exactCandidates = exactMatch(documentLines, selectedText);
330
+ if (exactCandidates.length === 1) {
331
+ const candidate = exactCandidates[0];
332
+ return {
333
+ commentId,
334
+ status: "anchored",
335
+ score: 1,
336
+ newLine: candidate.line,
337
+ newEndLine: candidate.endLine,
338
+ newStartColumn: candidate.startColumn,
339
+ newEndColumn: candidate.endColumn,
340
+ reason: "Exact text match (unique)."
341
+ };
342
+ }
343
+ if (exactCandidates.length > 1 && comment.line != null) {
344
+ const best = closestToLine(exactCandidates, comment.line);
345
+ return {
346
+ commentId,
347
+ status: "anchored",
348
+ score: 1,
349
+ newLine: best.line,
350
+ newEndLine: best.endLine,
351
+ newStartColumn: best.startColumn,
352
+ newEndColumn: best.endColumn,
353
+ reason: `Exact text match (${exactCandidates.length} occurrences; chose nearest to original line ${comment.line}).`
354
+ };
355
+ }
356
+ const normCandidates = normalizedMatch(documentLines, selectedText);
357
+ if (normCandidates.length === 1) {
358
+ const candidate = normCandidates[0];
359
+ return {
360
+ commentId,
361
+ status: "fuzzy",
362
+ score: candidate.score,
363
+ newLine: candidate.line,
364
+ newEndLine: candidate.endLine,
365
+ newStartColumn: candidate.startColumn,
366
+ newEndColumn: candidate.endColumn,
367
+ anchoredText: candidate.text,
368
+ previousSelectedText: selectedText,
369
+ reason: "Normalized whitespace match."
370
+ };
371
+ }
372
+ const fuzzyCandidates = fuzzySearch(
373
+ documentLines,
374
+ selectedText,
375
+ HIGH_THRESHOLD,
376
+ comment.line
377
+ );
378
+ if (fuzzyCandidates.length === 1 || fuzzyCandidates.length > 0 && fuzzyCandidates[0].score >= HIGH_THRESHOLD) {
379
+ const best = fuzzyCandidates.length === 1 ? fuzzyCandidates[0] : closestToLine(fuzzyCandidates, comment.line ?? 1);
380
+ return {
381
+ commentId,
382
+ status: "fuzzy",
383
+ score: best.score,
384
+ newLine: best.line,
385
+ newEndLine: best.endLine,
386
+ newStartColumn: best.startColumn,
387
+ newEndColumn: best.endColumn,
388
+ anchoredText: best.text,
389
+ previousSelectedText: selectedText,
390
+ reason: `High-confidence fuzzy match (score ${best.score.toFixed(3)}).`
391
+ };
392
+ }
393
+ }
394
+ if (comment.line != null) {
395
+ const lineIdx = comment.line;
396
+ if (lineIdx > 0 && lineIdx < documentLines.length) {
397
+ const qualifier = opts.commitIsStale ? " (commit is stale \u2014 line may have shifted)" : "";
398
+ if (selectedText) {
399
+ const lineText = documentLines[lineIdx];
400
+ const candidates = fuzzySearch(["", lineText], selectedText, DEFAULT_THRESHOLD);
401
+ if (candidates.length > 0) {
402
+ return {
403
+ commentId,
404
+ status: "fuzzy",
405
+ score: candidates[0].score,
406
+ newLine: comment.line,
407
+ newEndLine: comment.end_line,
408
+ anchoredText: candidates[0].text,
409
+ previousSelectedText: selectedText,
410
+ reason: `Line-fallback with fuzzy text match (score ${candidates[0].score.toFixed(3)})${qualifier}.`
411
+ };
412
+ }
413
+ }
414
+ const isLineOnly = !selectedText;
415
+ return {
416
+ commentId,
417
+ status: isLineOnly ? "anchored" : opts.commitIsStale ? "ambiguous" : "anchored",
418
+ score: isLineOnly ? 1 : opts.commitIsStale ? 0.5 : 0.8,
419
+ newLine: comment.line,
420
+ newEndLine: comment.end_line,
421
+ reason: isLineOnly ? "Line-only comment (no selected_text to verify)." : `Line/column fallback${qualifier}.`
422
+ };
423
+ }
424
+ }
425
+ if (selectedText) {
426
+ const lowCandidates = fuzzySearch(
427
+ documentLines,
428
+ selectedText,
429
+ threshold,
430
+ comment.line
431
+ );
432
+ if (lowCandidates.length === 1) {
433
+ const candidate = lowCandidates[0];
434
+ return {
435
+ commentId,
436
+ status: "fuzzy",
437
+ score: candidate.score,
438
+ newLine: candidate.line,
439
+ newEndLine: candidate.endLine,
440
+ newStartColumn: candidate.startColumn,
441
+ newEndColumn: candidate.endColumn,
442
+ anchoredText: candidate.text,
443
+ previousSelectedText: selectedText,
444
+ reason: `Low-threshold fuzzy match (score ${candidate.score.toFixed(3)}).`
445
+ };
446
+ }
447
+ if (lowCandidates.length > 1) {
448
+ const best = lowCandidates[0];
449
+ return {
450
+ commentId,
451
+ status: "ambiguous",
452
+ score: best.score,
453
+ newLine: best.line,
454
+ newEndLine: best.endLine,
455
+ reason: `Ambiguous: ${lowCandidates.length} fuzzy matches (best score ${best.score.toFixed(3)}).`
456
+ };
457
+ }
458
+ }
459
+ return {
460
+ commentId,
461
+ status: "orphaned",
462
+ score: 0,
463
+ reason: "No match found. Comment is orphaned."
464
+ };
465
+ }
466
+ function reanchorDocumentLines(doc, documentLines, opts = {}) {
467
+ return doc.comments.map((comment) => reanchorComment(comment, documentLines, opts));
468
+ }
469
+ function reanchorDocumentText(doc, documentText, opts = {}) {
470
+ return reanchorDocumentLines(doc, toReanchorLines(documentText), opts);
471
+ }
472
+ function resolveAnchor(comment, documentText, opts = {}) {
473
+ const normalizedText = documentText.replace(/\r\n/g, "\n");
474
+ const documentLines = toReanchorLines(documentText);
475
+ const result = reanchorComment(comment, documentLines, opts);
476
+ if (result.status === "orphaned") {
477
+ return {
478
+ status: "orphaned",
479
+ score: result.score,
480
+ reason: result.reason
481
+ };
482
+ }
483
+ const line = result.newLine ?? comment.line;
484
+ if (line == null) {
485
+ return {
486
+ status: result.status,
487
+ score: result.score,
488
+ reason: result.reason
489
+ };
490
+ }
491
+ const rawLines = normalizedText.split("\n");
492
+ const lineStarts = computeLineStarts(rawLines);
493
+ const endLine = result.newEndLine ?? comment.end_line ?? line;
494
+ const startColumn = result.newStartColumn ?? comment.start_column ?? 0;
495
+ const endColumn = result.newEndColumn ?? comment.end_column;
496
+ const from = offsetFor(lineStarts, rawLines, line, startColumn);
497
+ const selectedText = comment.selected_text?.replace(/\r\n/g, "\n");
498
+ const to = endColumn != null ? offsetFor(lineStarts, rawLines, endLine, endColumn) : selectedText ? from + selectedText.length : offsetFor(lineStarts, rawLines, endLine, rawLines[endLine - 1]?.length ?? 0);
499
+ return {
500
+ status: result.status,
501
+ score: result.score,
502
+ from,
503
+ to,
504
+ line,
505
+ endLine,
506
+ startColumn,
507
+ endColumn: endColumn ?? columnForOffset(lineStarts, rawLines, endLine, to),
508
+ reason: result.reason
509
+ };
510
+ }
511
+ function computeLineStarts(lines) {
512
+ const starts = [0];
513
+ let offset = 0;
514
+ for (const line of lines) {
515
+ starts.push(offset);
516
+ offset += line.length + 1;
517
+ }
518
+ return starts;
519
+ }
520
+ function offsetFor(lineStarts, lines, line, column) {
521
+ const start = lineStarts[line] ?? 0;
522
+ const maxColumn = lines[line - 1]?.length ?? 0;
523
+ return start + Math.max(0, Math.min(column, maxColumn));
524
+ }
525
+ function columnForOffset(lineStarts, lines, line, offset) {
526
+ const start = lineStarts[line] ?? 0;
527
+ const maxColumn = lines[line - 1]?.length ?? 0;
528
+ return Math.max(0, Math.min(offset - start, maxColumn));
529
+ }
530
+ function applyReanchorResults(doc, results, opts = {}) {
531
+ let changed = 0;
532
+ const resultMap = new Map(results.map((result) => [result.commentId, result]));
533
+ for (const comment of doc.comments) {
534
+ const result = resultMap.get(comment.id);
535
+ if (!result) continue;
536
+ let isChanged = false;
537
+ if (result.newLine != null && result.newLine !== comment.line) {
538
+ comment.line = result.newLine;
539
+ isChanged = true;
540
+ }
541
+ if (result.newEndLine != null && result.newEndLine !== comment.end_line) {
542
+ comment.end_line = result.newEndLine;
543
+ isChanged = true;
544
+ }
545
+ if (result.newStartColumn != null && result.newStartColumn !== comment.start_column) {
546
+ comment.start_column = result.newStartColumn;
547
+ isChanged = true;
548
+ }
549
+ if (result.newEndColumn != null && result.newEndColumn !== comment.end_column) {
550
+ comment.end_column = result.newEndColumn;
551
+ isChanged = true;
552
+ }
553
+ if (result.anchoredText != null && result.anchoredText !== comment.selected_text) {
554
+ if (opts.updateText) {
555
+ comment.selected_text = result.anchoredText;
556
+ delete comment.anchored_text;
557
+ } else {
558
+ comment.anchored_text = result.anchoredText;
559
+ }
560
+ isChanged = true;
561
+ } else if (result.anchoredText != null && result.anchoredText === comment.selected_text) {
562
+ if (comment.anchored_text) {
563
+ delete comment.anchored_text;
564
+ isChanged = true;
565
+ }
566
+ }
567
+ if (isChanged || result.status !== "anchored") {
568
+ comment.x_reanchor_status = result.status;
569
+ comment.x_reanchor_score = result.score;
570
+ }
571
+ if (opts.force && opts.headCommit && (result.status === "anchored" || result.status === "shifted") && result.score >= HIGH_THRESHOLD) {
572
+ comment.commit = opts.headCommit;
573
+ delete comment.x_reanchor_status;
574
+ delete comment.x_reanchor_score;
575
+ if (comment.anchored_text && comment.anchored_text === comment.selected_text) {
576
+ delete comment.anchored_text;
577
+ }
578
+ isChanged = true;
579
+ }
580
+ if (isChanged) {
581
+ changed += 1;
582
+ }
583
+ }
584
+ return changed;
585
+ }
586
+ function extractText(lines, line, endLine, startColumn, endColumn) {
587
+ const startIdx = line;
588
+ const endIdx = endLine ?? line;
589
+ if (startIdx < 1 || endIdx >= lines.length) return null;
590
+ if (startIdx === endIdx) {
591
+ const text = lines[startIdx];
592
+ if (startColumn != null && endColumn != null) {
593
+ return text.slice(startColumn, endColumn);
594
+ }
595
+ return text;
596
+ }
597
+ const result = [];
598
+ for (let index = startIdx; index <= endIdx; index += 1) {
599
+ let currentLine = lines[index];
600
+ if (index === startIdx && startColumn != null) currentLine = currentLine.slice(startColumn);
601
+ if (index === endIdx && endColumn != null) currentLine = currentLine.slice(0, endColumn);
602
+ result.push(currentLine);
603
+ }
604
+ return result.join("\n");
605
+ }
606
+ function closestToLine(candidates, targetLine) {
607
+ return candidates.reduce(
608
+ (best, candidate) => Math.abs(candidate.line - targetLine) < Math.abs(best.line - targetLine) ? candidate : best
609
+ );
610
+ }
611
+ function getLineShift(diffHunks, line) {
612
+ let shift = 0;
613
+ let modified = false;
614
+ for (const hunk of diffHunks) {
615
+ const oldStart = hunk.oldStart;
616
+ const oldEnd = hunk.oldStart + Math.max(hunk.oldCount, 1) - 1;
617
+ if (line >= oldStart && line <= oldEnd && hunk.oldCount > 0) {
618
+ modified = true;
619
+ }
620
+ if (line > oldEnd || hunk.oldCount === 0 && line >= oldStart) {
621
+ shift += hunk.newCount - hunk.oldCount;
622
+ }
623
+ }
624
+ return { shift, modified };
625
+ }
626
+
627
+ // src/lib/validate-core.ts
628
+ var import_ajv = __toESM(require("ajv"), 1);
629
+ var import_ajv_formats = __toESM(require("ajv-formats"), 1);
630
+
631
+ // mrsf.schema.json
632
+ var mrsf_schema_default = {
633
+ $schema: "https://json-schema.org/draft/2020-12/schema",
634
+ $id: "https://github.com/wictorwilen/MRSF/raw/main/mrsf.schema.json",
635
+ title: "Markdown Review Sidecar Format (MRSF) v1.0",
636
+ description: "Schema for MRSF review sidecar files. See MRSF-v1.0.md for the full specification.",
637
+ type: "object",
638
+ required: ["mrsf_version", "document", "comments"],
639
+ additionalProperties: true,
640
+ properties: {
641
+ mrsf_version: {
642
+ type: "string",
643
+ pattern: "^1\\.\\d+$",
644
+ description: "MRSF format version. MUST be a supported major.minor version (e.g., 1.0)."
645
+ },
646
+ document: {
647
+ type: "string",
648
+ description: "Relative path to the Markdown document being reviewed."
649
+ },
650
+ comments: {
651
+ type: "array",
652
+ items: {
653
+ type: "object",
654
+ required: ["id", "author", "timestamp", "text", "resolved"],
655
+ additionalProperties: true,
656
+ properties: {
657
+ id: {
658
+ type: "string",
659
+ description: "Globally unique, opaque, collision-resistant identifier for the comment."
660
+ },
661
+ author: {
662
+ type: "string",
663
+ description: "Creator of the comment. SHOULD follow the convention 'Display Name (identifier)'."
664
+ },
665
+ timestamp: {
666
+ type: "string",
667
+ format: "date-time",
668
+ description: "ISO 8601 / RFC 3339 timestamp of comment creation; SHOULD include timezone offset."
669
+ },
670
+ text: {
671
+ type: "string",
672
+ maxLength: 16384,
673
+ description: "The content of the review comment. MUST be plain text."
674
+ },
675
+ resolved: {
676
+ type: "boolean",
677
+ description: "Whether the comment has been resolved."
678
+ },
679
+ commit: {
680
+ type: "string",
681
+ description: "Git commit hash associated with the comment. SHOULD be the full (long) SHA."
682
+ },
683
+ type: {
684
+ type: "string",
685
+ description: "Categorization of the comment. Recommended values listed in examples.",
686
+ examples: [
687
+ "suggestion",
688
+ "issue",
689
+ "question",
690
+ "accuracy",
691
+ "style",
692
+ "clarity"
693
+ ]
694
+ },
695
+ severity: {
696
+ type: "string",
697
+ description: "Importance level of the comment.",
698
+ enum: ["low", "medium", "high"]
699
+ },
700
+ reply_to: {
701
+ type: "string",
702
+ description: "ID of another comment in the same file that this comment replies to."
703
+ },
704
+ line: {
705
+ type: "integer",
706
+ minimum: 1,
707
+ description: "Starting line number (1-based) in the target document."
708
+ },
709
+ end_line: {
710
+ type: "integer",
711
+ minimum: 1,
712
+ description: "Ending line number (inclusive, 1-based). MUST be >= line."
713
+ },
714
+ start_column: {
715
+ type: "integer",
716
+ minimum: 0,
717
+ description: "Starting column index (0-based) within the starting line."
718
+ },
719
+ end_column: {
720
+ type: "integer",
721
+ minimum: 0,
722
+ description: "Ending column index. MUST be >= start_column when on the same line."
723
+ },
724
+ selected_text: {
725
+ type: "string",
726
+ maxLength: 4096,
727
+ description: "Exact text selected by the reviewer. SHOULD NOT be modified by re-anchoring tools. SHOULD NOT exceed 4096 characters."
728
+ },
729
+ anchored_text: {
730
+ type: "string",
731
+ maxLength: 4096,
732
+ 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."
733
+ },
734
+ selected_text_hash: {
735
+ type: "string",
736
+ pattern: "^[a-f0-9]{64}$",
737
+ 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."
738
+ }
739
+ }
740
+ }
741
+ }
742
+ }
743
+ };
744
+
745
+ // src/lib/schema.ts
746
+ var mrsfSchema = mrsf_schema_default;
747
+
748
+ // src/lib/validate-core.ts
749
+ var Ajv = import_ajv.default.default ?? import_ajv.default;
750
+ var addFormats = import_ajv_formats.default.default ?? import_ajv_formats.default;
751
+ function validateDocument(doc, schema = mrsfSchema) {
752
+ const errors = [];
753
+ const warnings = [];
754
+ validateSchema(doc, schema, errors);
755
+ validateCrossFields(doc, errors, warnings);
756
+ return {
757
+ valid: errors.length === 0,
758
+ errors,
759
+ warnings
760
+ };
761
+ }
762
+ function validateSchema(doc, rawSchema, errors) {
763
+ const { $schema, ...schema } = rawSchema;
764
+ void $schema;
765
+ const ajv = new Ajv({ allErrors: true, strict: false });
766
+ addFormats(ajv);
767
+ const ajvValidate = ajv.compile(schema);
768
+ const schemaValid = ajvValidate(doc);
769
+ if (!schemaValid && ajvValidate.errors) {
770
+ for (const err of ajvValidate.errors) {
771
+ errors.push({
772
+ severity: "error",
773
+ code: "schema-violation",
774
+ message: `${err.instancePath || "/"}: ${err.message ?? "schema error"}`,
775
+ path: err.instancePath || "/"
776
+ });
777
+ }
778
+ }
779
+ }
780
+ function validateCrossFields(doc, errors, warnings, hash) {
781
+ if (!Array.isArray(doc.comments)) return;
782
+ const ids = /* @__PURE__ */ new Set();
783
+ const allIds = doc.comments.map((x) => x.id);
784
+ for (let i = 0; i < doc.comments.length; i++) {
785
+ const c = doc.comments[i];
786
+ const prefix = `/comments/${i}`;
787
+ if (c.id) {
788
+ if (ids.has(c.id)) {
789
+ errors.push({
790
+ severity: "error",
791
+ code: "duplicate-id",
792
+ message: `Duplicate comment id "${c.id}"`,
793
+ path: `${prefix}/id`,
794
+ commentId: c.id
795
+ });
796
+ }
797
+ ids.add(c.id);
798
+ }
799
+ if (c.line != null && c.end_line != null && c.end_line < c.line) {
800
+ errors.push({
801
+ severity: "error",
802
+ code: "end-line-before-line",
803
+ message: `end_line (${c.end_line}) must be \u2265 line (${c.line})`,
804
+ path: `${prefix}/end_line`,
805
+ commentId: c.id
806
+ });
807
+ }
808
+ if (c.start_column != null && c.end_column != null && (c.line == null || c.end_line == null || c.line === c.end_line) && c.end_column < c.start_column) {
809
+ errors.push({
810
+ severity: "error",
811
+ code: "end-column-before-start-column",
812
+ message: `end_column (${c.end_column}) must be \u2265 start_column (${c.start_column}) on the same line`,
813
+ path: `${prefix}/end_column`,
814
+ commentId: c.id
815
+ });
816
+ }
817
+ if (c.selected_text && c.selected_text.length > 4096) {
818
+ errors.push({
819
+ severity: "error",
820
+ code: "selected-text-too-long",
821
+ message: `selected_text exceeds 4096 characters (${c.selected_text.length})`,
822
+ path: `${prefix}/selected_text`,
823
+ commentId: c.id
824
+ });
825
+ }
826
+ if (c.text && c.text.length > 16384) {
827
+ warnings.push({
828
+ severity: "warning",
829
+ code: "text-too-long",
830
+ message: `text exceeds recommended 16384 characters (${c.text.length})`,
831
+ path: `${prefix}/text`,
832
+ commentId: c.id
833
+ });
834
+ }
835
+ if (hash && c.selected_text && c.selected_text_hash) {
836
+ const expected = hash(c.selected_text);
837
+ if (c.selected_text_hash !== expected) {
838
+ warnings.push({
839
+ severity: "warning",
840
+ code: "hash-mismatch",
841
+ message: `selected_text_hash mismatch (expected ${expected.slice(0, 12)}\u2026, got ${c.selected_text_hash.slice(0, 12)}\u2026)`,
842
+ path: `${prefix}/selected_text_hash`,
843
+ commentId: c.id
844
+ });
845
+ }
846
+ }
847
+ if (c.reply_to && !ids.has(c.reply_to) && !allIds.includes(c.reply_to)) {
848
+ warnings.push({
849
+ severity: "warning",
850
+ code: "unresolved-reply-to",
851
+ message: `reply_to "${c.reply_to}" does not resolve to any comment id in this file`,
852
+ path: `${prefix}/reply_to`,
853
+ commentId: c.id
854
+ });
855
+ }
856
+ if (c.line != null && !c.selected_text) {
857
+ warnings.push({
858
+ severity: "warning",
859
+ code: "missing-selected-text",
860
+ message: "Comment has line anchors but no selected_text \u2014 anchoring will be fragile across edits",
861
+ path: `${prefix}/selected_text`,
862
+ commentId: c.id
863
+ });
864
+ }
865
+ }
866
+ }
867
+
868
+ // src/browser.ts
869
+ var combinedScore2 = combinedScore;
870
+ var exactMatch2 = exactMatch;
871
+ var fuzzySearch2 = fuzzySearch;
872
+ var levenshteinScore2 = levenshteinScore;
873
+ var normalizedMatch2 = normalizedMatch;
874
+ var tokenLcsScore2 = tokenLcsScore;
875
+ var applyReanchorResults2 = applyReanchorResults;
876
+ var DEFAULT_THRESHOLD2 = DEFAULT_THRESHOLD;
877
+ var HIGH_THRESHOLD2 = HIGH_THRESHOLD;
878
+ var reanchorComment2 = reanchorComment;
879
+ var reanchorDocumentLines2 = reanchorDocumentLines;
880
+ var reanchorDocumentText2 = reanchorDocumentText;
881
+ var resolveAnchor2 = resolveAnchor;
882
+ var toReanchorLines2 = toReanchorLines;
883
+ var validateDocument2 = validateDocument;
884
+ var formatAuthor2 = formatAuthor;
885
+ var parseAuthor2 = parseAuthor;
886
+ var newCommentId2 = newCommentId;
887
+ //# sourceMappingURL=browser.cjs.map