@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.
package/dist/lib/fuzzy.js CHANGED
@@ -110,37 +110,6 @@ export function normalizedMatch(lines, needle) {
110
110
  function tokenize(text) {
111
111
  return text.split(/\s+/).filter((t) => t.length > 0);
112
112
  }
113
- /**
114
- * Build a frequency map for a token array. Used for fast multiset
115
- * intersection during the fuzzySearch prefilter.
116
- */
117
- function buildTokenFreq(tokens) {
118
- const freq = new Map();
119
- for (const tok of tokens) {
120
- freq.set(tok, (freq.get(tok) ?? 0) + 1);
121
- }
122
- return freq;
123
- }
124
- /**
125
- * Multiset intersection size: sum over needle tokens of
126
- * min(needleFreq[token], windowFreq[token]). This is an upper bound on
127
- * the longest common subsequence length, so it lets us cheaply skip
128
- * windows that cannot possibly reach the score threshold.
129
- */
130
- function multisetIntersection(needleFreq, windowFreq) {
131
- let common = 0;
132
- // Iterate the smaller map for speed.
133
- const [smaller, larger] = needleFreq.size <= windowFreq.size
134
- ? [needleFreq, windowFreq]
135
- : [windowFreq, needleFreq];
136
- for (const [tok, cnt] of smaller) {
137
- const other = larger.get(tok);
138
- if (other !== undefined) {
139
- common += cnt < other ? cnt : other;
140
- }
141
- }
142
- return common;
143
- }
144
113
  /**
145
114
  * Longest Common Subsequence length of two token arrays.
146
115
  */
@@ -230,144 +199,38 @@ export function fuzzySearch(lines, needle, threshold = 0.6, hintLine) {
230
199
  const needleLines = needle.split("\n");
231
200
  const needleLineCount = needleLines.length;
232
201
  const candidates = [];
233
- // Pre-tokenize: do this once per call instead of per window. Per-line
234
- // tokenization gives identical results to tokenize(lines.join("\n")) since
235
- // tokenize splits on /\s+/ and filters empties.
236
- const lineTokens = new Array(lines.length);
237
- const lineTokenLengths = new Array(lines.length);
238
- for (let i = 0; i < lines.length; i++) {
239
- const toks = lines[i] ? tokenize(lines[i]) : [];
240
- lineTokens[i] = toks;
241
- lineTokenLengths[i] = toks.length;
242
- }
243
- const needleTokens = tokenize(needle);
244
- const needleTokenCount = needleTokens.length;
245
- const needleFreq = buildTokenFreq(needleTokens);
246
- // The combined score is 0.6 * tScore + 0.4 * lScore. Since lScore <= 1.0,
247
- // a window can only reach `threshold` if tScore >= (threshold - 0.4) / 0.6.
248
- // Use this as a hard prefilter before doing the expensive LCS / Levenshtein.
249
- const minTScore = Math.max(0, (threshold - 0.4) / 0.6);
250
202
  // Window sizes: ±30% of original line count, minimum 1
251
203
  const minWindow = Math.max(1, Math.floor(needleLineCount * 0.7));
252
204
  const maxWindow = Math.min(lines.length - 1, Math.ceil(needleLineCount * 1.3) + 1);
253
- // Track total token count per window using a rolling sum so we don't
254
- // re-walk the line-token arrays for every (winSize, startLine) pair.
255
205
  for (let winSize = minWindow; winSize <= maxWindow; winSize++) {
256
- if (winSize < 1)
257
- continue;
258
- if (winSize > lines.length - 1)
259
- continue;
260
- // Initial window token sum for startLine = 1.
261
- let runningTokenCount = 0;
262
- for (let i = 1; i < 1 + winSize && i < lines.length; i++) {
263
- runningTokenCount += lineTokenLengths[i];
264
- }
265
206
  for (let startLine = 1; startLine + winSize - 1 < lines.length; startLine++) {
266
- const endLineExclusive = startLine + winSize;
267
- const M = runningTokenCount;
268
- // Cheap skip: empty windows or empty needle can't fuzzy-match.
269
- if (needleTokenCount > 0 && M > 0) {
270
- // Build a frequency map for the window tokens. Cheaper than LCS:
271
- // O(M) instead of O(M * needleTokenCount). We allocate a fresh map
272
- // per window; tried a rolling map and the bookkeeping was a wash
273
- // with realistic windowSize but if profiling demands it can be
274
- // converted.
275
- const windowFreq = new Map();
276
- for (let li = startLine; li < endLineExclusive; li++) {
277
- const arr = lineTokens[li];
278
- for (let ti = 0; ti < arr.length; ti++) {
279
- const tok = arr[ti];
280
- windowFreq.set(tok, (windowFreq.get(tok) ?? 0) + 1);
281
- }
282
- }
283
- // Multiset intersection is an upper bound on LCS length. If we
284
- // can't reach minTScore even with a perfect LCS, skip the window.
285
- const common = multisetIntersection(needleFreq, windowFreq);
286
- const tScoreUpperBound = common / Math.max(needleTokenCount, M);
287
- if (tScoreUpperBound >= minTScore) {
288
- // Build the flat window token array only when we actually need
289
- // to compute LCS.
290
- const windowTokens = new Array(M);
291
- let wi = 0;
292
- for (let li = startLine; li < endLineExclusive; li++) {
293
- const arr = lineTokens[li];
294
- for (let ti = 0; ti < arr.length; ti++) {
295
- windowTokens[wi++] = arr[ti];
296
- }
297
- }
298
- const lcs = lcsLength(needleTokens, windowTokens);
299
- const tScore = lcs / Math.max(needleTokenCount, M);
300
- if (tScore >= minTScore) {
301
- // Defer expensive string join + Levenshtein until we know the
302
- // tScore could plausibly reach threshold.
303
- const windowLines = lines.slice(startLine, endLineExclusive);
304
- const windowText = windowLines.join("\n");
305
- let lScore;
306
- if (needle.length < 500 && windowText.length < 500) {
307
- lScore = levenshteinScore(needle, windowText);
308
- }
309
- else {
310
- lScore = tScore; // fall back to token score only
311
- }
312
- const score = tScore * 0.6 + lScore * 0.4;
313
- if (score >= threshold) {
314
- candidates.push({
315
- text: windowText,
316
- line: startLine,
317
- endLine: startLine + winSize - 1,
318
- startColumn: 0,
319
- endColumn: windowLines[windowLines.length - 1].length,
320
- score,
321
- });
322
- }
323
- }
324
- }
325
- }
326
- // Slide window by one: drop startLine token count, add endLine token count.
327
- const nextStart = startLine + 1;
328
- const nextEndExclusive = nextStart + winSize;
329
- if (nextEndExclusive <= lines.length) {
330
- runningTokenCount += lineTokenLengths[endLineExclusive] - lineTokenLengths[startLine];
207
+ const windowLines = lines.slice(startLine, startLine + winSize);
208
+ const windowText = windowLines.join("\n");
209
+ const score = combinedScore(needle, windowText);
210
+ if (score >= threshold) {
211
+ candidates.push({
212
+ text: windowText,
213
+ line: startLine,
214
+ endLine: startLine + winSize - 1,
215
+ startColumn: 0,
216
+ endColumn: windowLines[windowLines.length - 1].length,
217
+ score,
218
+ });
331
219
  }
332
220
  }
333
221
  }
334
- // For single-line needles, also try substring matching within each line.
335
- // Prefilter each line by token overlap before running the inner sliding
336
- // window — long lines without enough needle-token coverage are skipped
337
- // wholesale, since no substring of such a line can exceed its parent
338
- // line's token-overlap with the needle.
222
+ // For single-line needles, also try substring matching within each line
339
223
  if (needleLineCount === 1 && needle.length < 200) {
340
- // The minimum number of needle tokens that must appear in a line for any
341
- // substring of that line to plausibly reach minTScore. tScore =
342
- // lcs / max(needleN, subN), so lcs >= minTScore * needleN is required,
343
- // and lcs <= number of common tokens.
344
- const requiredLineCommon = Math.max(0, Math.ceil(needleTokenCount * minTScore));
345
224
  for (let lineNum = 1; lineNum < lines.length; lineNum++) {
346
225
  const line = lines[lineNum];
347
226
  if (!line)
348
227
  continue;
349
- if (needleTokenCount > 0 && requiredLineCommon > 0) {
350
- const lineFreq = buildTokenFreq(lineTokens[lineNum]);
351
- const lineCommon = multisetIntersection(needleFreq, lineFreq);
352
- if (lineCommon < requiredLineCommon)
353
- continue;
354
- }
355
- // Sliding window within line. Step by 1 by default for accuracy, but
356
- // for very long lines and long needles, we coarsen the column step to
357
- // avoid quadratic-in-line-length behaviour. The natural `len = winLen`
358
- // is always swept densely so we don't miss the most likely match.
228
+ // Sliding window within line
359
229
  const winLen = needle.length;
360
230
  const minWinLen = Math.max(3, Math.floor(winLen * 0.7));
361
231
  const maxWinLen = Math.min(line.length, Math.ceil(winLen * 1.3));
362
232
  for (let len = minWinLen; len <= maxWinLen; len++) {
363
- // Coarsen the column step proportionally to needle length once the
364
- // sub-window is no longer the natural needle length. This trades a
365
- // tiny bit of recall for an order-of-magnitude speedup on long
366
- // lines with many candidate offsets.
367
- const colStep = len === winLen
368
- ? 1
369
- : Math.max(1, Math.floor(winLen / 16));
370
- for (let col = 0; col + len <= line.length; col += colStep) {
233
+ for (let col = 0; col + len <= line.length; col++) {
371
234
  const sub = line.substring(col, col + len);
372
235
  const score = combinedScore(needle, sub);
373
236
  if (score >= threshold) {
@@ -0,0 +1,12 @@
1
+ export interface ParsedAuthor {
2
+ name: string;
3
+ handle?: string;
4
+ }
5
+ export declare function formatAuthor(name: string, handle?: string): string;
6
+ export declare function parseAuthor(author: string): ParsedAuthor;
7
+ /**
8
+ * Create a collision-resistant MRSF comment id.
9
+ * ULID is also permitted by the spec; UUIDv4 is the default for this package.
10
+ */
11
+ export declare function newCommentId(): string;
12
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ export function formatAuthor(name, handle) {
3
+ const trimmedName = name.trim();
4
+ const trimmedHandle = handle?.trim();
5
+ return trimmedHandle ? `${trimmedName} (${trimmedHandle})` : trimmedName;
6
+ }
7
+ export function parseAuthor(author) {
8
+ const trimmed = author.trim();
9
+ const match = /^(.*?)\s*\(([^()]*)\)\s*$/.exec(trimmed);
10
+ if (!match)
11
+ return { name: trimmed };
12
+ const name = match[1].trim();
13
+ const handle = match[2].trim();
14
+ return handle ? { name, handle } : { name };
15
+ }
16
+ /**
17
+ * Create a collision-resistant MRSF comment id.
18
+ * ULID is also permitted by the spec; UUIDv4 is the default for this package.
19
+ */
20
+ export function newCommentId() {
21
+ return uuidv4();
22
+ }
23
+ //# sourceMappingURL=identity.js.map
@@ -1,4 +1,4 @@
1
- import type { Comment, DiffHunk, MrsfDocument, ReanchorResult } from "./types.js";
1
+ import type { AnchorPosition, Comment, DiffHunk, MrsfDocument, ReanchorResult } from "./types.js";
2
2
  export declare const HIGH_THRESHOLD = 0.8;
3
3
  export declare const DEFAULT_THRESHOLD = 0.6;
4
4
  export declare function toReanchorLines(documentText: string): string[];
@@ -13,6 +13,9 @@ export declare function reanchorDocumentLines(doc: MrsfDocument, documentLines:
13
13
  export declare function reanchorDocumentText(doc: MrsfDocument, documentText: string, opts?: {
14
14
  threshold?: number;
15
15
  }): ReanchorResult[];
16
+ export declare function resolveAnchor(comment: Comment, documentText: string, opts?: {
17
+ threshold?: number;
18
+ }): AnchorPosition;
16
19
  export declare function applyReanchorResults(doc: MrsfDocument, results: ReanchorResult[], opts?: {
17
20
  updateText?: boolean;
18
21
  force?: boolean;
@@ -192,6 +192,68 @@ export function reanchorDocumentLines(doc, documentLines, opts = {}) {
192
192
  export function reanchorDocumentText(doc, documentText, opts = {}) {
193
193
  return reanchorDocumentLines(doc, toReanchorLines(documentText), opts);
194
194
  }
195
+ export function resolveAnchor(comment, documentText, opts = {}) {
196
+ const normalizedText = documentText.replace(/\r\n/g, "\n");
197
+ const documentLines = toReanchorLines(documentText);
198
+ const result = reanchorComment(comment, documentLines, opts);
199
+ if (result.status === "orphaned") {
200
+ return {
201
+ status: "orphaned",
202
+ score: result.score,
203
+ reason: result.reason,
204
+ };
205
+ }
206
+ const line = result.newLine ?? comment.line;
207
+ if (line == null) {
208
+ return {
209
+ status: result.status,
210
+ score: result.score,
211
+ reason: result.reason,
212
+ };
213
+ }
214
+ const rawLines = normalizedText.split("\n");
215
+ const lineStarts = computeLineStarts(rawLines);
216
+ const endLine = result.newEndLine ?? comment.end_line ?? line;
217
+ const startColumn = result.newStartColumn ?? comment.start_column ?? 0;
218
+ const endColumn = result.newEndColumn ?? comment.end_column;
219
+ const from = offsetFor(lineStarts, rawLines, line, startColumn);
220
+ const selectedText = comment.selected_text?.replace(/\r\n/g, "\n");
221
+ const to = endColumn != null
222
+ ? offsetFor(lineStarts, rawLines, endLine, endColumn)
223
+ : selectedText
224
+ ? from + selectedText.length
225
+ : offsetFor(lineStarts, rawLines, endLine, rawLines[endLine - 1]?.length ?? 0);
226
+ return {
227
+ status: result.status,
228
+ score: result.score,
229
+ from,
230
+ to,
231
+ line,
232
+ endLine,
233
+ startColumn,
234
+ endColumn: endColumn ?? columnForOffset(lineStarts, rawLines, endLine, to),
235
+ reason: result.reason,
236
+ };
237
+ }
238
+ function computeLineStarts(lines) {
239
+ const starts = [0];
240
+ let offset = 0;
241
+ for (const line of lines) {
242
+ starts.push(offset);
243
+ offset += line.length + 1;
244
+ }
245
+ return starts;
246
+ }
247
+ function offsetFor(lineStarts, lines, line, column) {
248
+ const start = lineStarts[line] ?? 0;
249
+ const maxColumn = lines[line - 1]?.length ?? 0;
250
+ return start + Math.max(0, Math.min(column, maxColumn));
251
+ }
252
+ function columnForOffset(lineStarts, lines, line, offset) {
253
+ const start = lineStarts[line] ?? 0;
254
+ const maxColumn = lines[line - 1]?.length ?? 0;
255
+ return Math.max(0, Math.min(offset - start, maxColumn));
256
+ }
195
257
  export function applyReanchorResults(doc, results, opts = {}) {
196
258
  let changed = 0;
197
259
  const resultMap = new Map(results.map((result) => [result.commentId, result]));
@@ -12,7 +12,7 @@
12
12
  * Step 4 – orphan
13
13
  */
14
14
  import type { MrsfDocument, ReanchorOptions, ReanchorResult } from "./types.js";
15
- export { applyReanchorResults, DEFAULT_THRESHOLD, HIGH_THRESHOLD, reanchorComment, reanchorDocumentLines, reanchorDocumentText, toReanchorLines, } from "./reanchor-core.js";
15
+ export { applyReanchorResults, DEFAULT_THRESHOLD, HIGH_THRESHOLD, reanchorComment, reanchorDocumentLines, reanchorDocumentText, resolveAnchor, toReanchorLines, } from "./reanchor-core.js";
16
16
  /**
17
17
  * Re-anchor all comments in an MRSF document.
18
18
  */
@@ -18,7 +18,7 @@ import { sidecarToDocument } from "./discovery.js";
18
18
  import { parseSidecar } from "./parser.js";
19
19
  import { writeSidecar } from "./writer.js";
20
20
  import path from "node:path";
21
- export { applyReanchorResults, DEFAULT_THRESHOLD, HIGH_THRESHOLD, reanchorComment, reanchorDocumentLines, reanchorDocumentText, toReanchorLines, } from "./reanchor-core.js";
21
+ export { applyReanchorResults, DEFAULT_THRESHOLD, HIGH_THRESHOLD, reanchorComment, reanchorDocumentLines, reanchorDocumentText, resolveAnchor, toReanchorLines, } from "./reanchor-core.js";
22
22
  // ---------------------------------------------------------------------------
23
23
  // Constants
24
24
  // ---------------------------------------------------------------------------
@@ -0,0 +1,106 @@
1
+ export declare const mrsfSchema: {
2
+ $schema: string;
3
+ $id: string;
4
+ title: string;
5
+ description: string;
6
+ type: string;
7
+ required: string[];
8
+ additionalProperties: boolean;
9
+ properties: {
10
+ mrsf_version: {
11
+ type: string;
12
+ pattern: string;
13
+ description: string;
14
+ };
15
+ document: {
16
+ type: string;
17
+ description: string;
18
+ };
19
+ comments: {
20
+ type: string;
21
+ items: {
22
+ type: string;
23
+ required: string[];
24
+ additionalProperties: boolean;
25
+ properties: {
26
+ id: {
27
+ type: string;
28
+ description: string;
29
+ };
30
+ author: {
31
+ type: string;
32
+ description: string;
33
+ };
34
+ timestamp: {
35
+ type: string;
36
+ format: string;
37
+ description: string;
38
+ };
39
+ text: {
40
+ type: string;
41
+ maxLength: number;
42
+ description: string;
43
+ };
44
+ resolved: {
45
+ type: string;
46
+ description: string;
47
+ };
48
+ commit: {
49
+ type: string;
50
+ description: string;
51
+ };
52
+ type: {
53
+ type: string;
54
+ description: string;
55
+ examples: string[];
56
+ };
57
+ severity: {
58
+ type: string;
59
+ description: string;
60
+ enum: string[];
61
+ };
62
+ reply_to: {
63
+ type: string;
64
+ description: string;
65
+ };
66
+ line: {
67
+ type: string;
68
+ minimum: number;
69
+ description: string;
70
+ };
71
+ end_line: {
72
+ type: string;
73
+ minimum: number;
74
+ description: string;
75
+ };
76
+ start_column: {
77
+ type: string;
78
+ minimum: number;
79
+ description: string;
80
+ };
81
+ end_column: {
82
+ type: string;
83
+ minimum: number;
84
+ description: string;
85
+ };
86
+ selected_text: {
87
+ type: string;
88
+ maxLength: number;
89
+ description: string;
90
+ };
91
+ anchored_text: {
92
+ type: string;
93
+ maxLength: number;
94
+ description: string;
95
+ };
96
+ selected_text_hash: {
97
+ type: string;
98
+ pattern: string;
99
+ description: string;
100
+ };
101
+ };
102
+ };
103
+ };
104
+ };
105
+ };
106
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1,3 @@
1
+ import mrsfSchemaJson from "../../mrsf.schema.json" with { type: "json" };
2
+ export const mrsfSchema = mrsfSchemaJson;
3
+ //# sourceMappingURL=schema.js.map
@@ -40,6 +40,7 @@ export interface MrsfConfig {
40
40
  export type DiagnosticSeverity = "error" | "warning";
41
41
  export interface ValidationDiagnostic {
42
42
  severity: DiagnosticSeverity;
43
+ code: string;
43
44
  message: string;
44
45
  /** Path within the document, e.g. "/comments/0/end_line". */
45
46
  path?: string;
@@ -69,6 +70,23 @@ export interface ReanchorResult {
69
70
  /** Human-readable explanation of the resolution. */
70
71
  reason: string;
71
72
  }
73
+ export interface AnchorPosition {
74
+ status: ReanchorStatus;
75
+ score: number;
76
+ /** Absolute 0-based character offset in normalized document text (start). */
77
+ from?: number;
78
+ /** Absolute 0-based character offset in normalized document text (end, exclusive). */
79
+ to?: number;
80
+ /** 1-based start line. */
81
+ line?: number;
82
+ /** 1-based end line. */
83
+ endLine?: number;
84
+ /** 0-based start column. */
85
+ startColumn?: number;
86
+ /** 0-based end column. */
87
+ endColumn?: number;
88
+ reason: string;
89
+ }
72
90
  export interface FuzzyCandidate {
73
91
  /** Matched text in the document. */
74
92
  text: string;
@@ -0,0 +1,6 @@
1
+ import type { MrsfDocument, ValidationDiagnostic, ValidationResult } from "./types.js";
2
+ export type HashFunction = (text: string) => string;
3
+ export declare function validateDocument(doc: MrsfDocument, schema?: object): ValidationResult;
4
+ export declare function validateSchema(doc: MrsfDocument, rawSchema: object, errors: ValidationDiagnostic[]): void;
5
+ export declare function validateCrossFields(doc: MrsfDocument, errors: ValidationDiagnostic[], warnings: ValidationDiagnostic[], hash?: HashFunction): void;
6
+ //# sourceMappingURL=validate-core.d.ts.map
@@ -0,0 +1,128 @@
1
+ import AjvModule from "ajv";
2
+ import addFormatsModule from "ajv-formats";
3
+ import { mrsfSchema } from "./schema.js";
4
+ const Ajv = AjvModule.default ?? AjvModule;
5
+ const addFormats = addFormatsModule.default ?? addFormatsModule;
6
+ export function validateDocument(doc, schema = mrsfSchema) {
7
+ const errors = [];
8
+ const warnings = [];
9
+ validateSchema(doc, schema, errors);
10
+ validateCrossFields(doc, errors, warnings);
11
+ return {
12
+ valid: errors.length === 0,
13
+ errors,
14
+ warnings,
15
+ };
16
+ }
17
+ export function validateSchema(doc, rawSchema, errors) {
18
+ const { $schema, ...schema } = rawSchema;
19
+ void $schema;
20
+ const ajv = new Ajv({ allErrors: true, strict: false });
21
+ addFormats(ajv);
22
+ const ajvValidate = ajv.compile(schema);
23
+ const schemaValid = ajvValidate(doc);
24
+ if (!schemaValid && ajvValidate.errors) {
25
+ for (const err of ajvValidate.errors) {
26
+ errors.push({
27
+ severity: "error",
28
+ code: "schema-violation",
29
+ message: `${err.instancePath || "/"}: ${err.message ?? "schema error"}`,
30
+ path: err.instancePath || "/",
31
+ });
32
+ }
33
+ }
34
+ }
35
+ export function validateCrossFields(doc, errors, warnings, hash) {
36
+ if (!Array.isArray(doc.comments))
37
+ return;
38
+ const ids = new Set();
39
+ const allIds = doc.comments.map((x) => x.id);
40
+ for (let i = 0; i < doc.comments.length; i++) {
41
+ const c = doc.comments[i];
42
+ const prefix = `/comments/${i}`;
43
+ if (c.id) {
44
+ if (ids.has(c.id)) {
45
+ errors.push({
46
+ severity: "error",
47
+ code: "duplicate-id",
48
+ message: `Duplicate comment id "${c.id}"`,
49
+ path: `${prefix}/id`,
50
+ commentId: c.id,
51
+ });
52
+ }
53
+ ids.add(c.id);
54
+ }
55
+ if (c.line != null && c.end_line != null && c.end_line < c.line) {
56
+ errors.push({
57
+ severity: "error",
58
+ code: "end-line-before-line",
59
+ message: `end_line (${c.end_line}) must be ≥ line (${c.line})`,
60
+ path: `${prefix}/end_line`,
61
+ commentId: c.id,
62
+ });
63
+ }
64
+ if (c.start_column != null &&
65
+ c.end_column != null &&
66
+ (c.line == null || c.end_line == null || c.line === c.end_line) &&
67
+ c.end_column < c.start_column) {
68
+ errors.push({
69
+ severity: "error",
70
+ code: "end-column-before-start-column",
71
+ message: `end_column (${c.end_column}) must be ≥ start_column (${c.start_column}) on the same line`,
72
+ path: `${prefix}/end_column`,
73
+ commentId: c.id,
74
+ });
75
+ }
76
+ if (c.selected_text && c.selected_text.length > 4096) {
77
+ errors.push({
78
+ severity: "error",
79
+ code: "selected-text-too-long",
80
+ message: `selected_text exceeds 4096 characters (${c.selected_text.length})`,
81
+ path: `${prefix}/selected_text`,
82
+ commentId: c.id,
83
+ });
84
+ }
85
+ if (c.text && c.text.length > 16384) {
86
+ warnings.push({
87
+ severity: "warning",
88
+ code: "text-too-long",
89
+ message: `text exceeds recommended 16384 characters (${c.text.length})`,
90
+ path: `${prefix}/text`,
91
+ commentId: c.id,
92
+ });
93
+ }
94
+ // Browser-safe callers do not get the Node SHA-256 implementation.
95
+ // The hash check runs only when a hash function is injected by the Node validator.
96
+ if (hash && c.selected_text && c.selected_text_hash) {
97
+ const expected = hash(c.selected_text);
98
+ if (c.selected_text_hash !== expected) {
99
+ warnings.push({
100
+ severity: "warning",
101
+ code: "hash-mismatch",
102
+ message: `selected_text_hash mismatch (expected ${expected.slice(0, 12)}…, got ${c.selected_text_hash.slice(0, 12)}…)`,
103
+ path: `${prefix}/selected_text_hash`,
104
+ commentId: c.id,
105
+ });
106
+ }
107
+ }
108
+ if (c.reply_to && !ids.has(c.reply_to) && !allIds.includes(c.reply_to)) {
109
+ warnings.push({
110
+ severity: "warning",
111
+ code: "unresolved-reply-to",
112
+ message: `reply_to "${c.reply_to}" does not resolve to any comment id in this file`,
113
+ path: `${prefix}/reply_to`,
114
+ commentId: c.id,
115
+ });
116
+ }
117
+ if (c.line != null && !c.selected_text) {
118
+ warnings.push({
119
+ severity: "warning",
120
+ code: "missing-selected-text",
121
+ message: "Comment has line anchors but no selected_text — anchoring will be fragile across edits",
122
+ path: `${prefix}/selected_text`,
123
+ commentId: c.id,
124
+ });
125
+ }
126
+ }
127
+ }
128
+ //# sourceMappingURL=validate-core.js.map