@peaske7/readit 0.1.7 → 0.2.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 (118) hide show
  1. package/README.md +0 -3
  2. package/biome.json +1 -1
  3. package/bun.lock +43 -185
  4. package/docs/perf-baseline.md +75 -0
  5. package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
  6. package/e2e/perf/add-comment.spec.ts +118 -0
  7. package/e2e/perf/fixtures/generate.ts +331 -0
  8. package/e2e/perf/initial-load.spec.ts +49 -0
  9. package/e2e/perf/perf.setup.ts +23 -0
  10. package/e2e/perf/perf.teardown.ts +9 -0
  11. package/e2e/perf/scroll.spec.ts +39 -0
  12. package/e2e/perf/tab-switch.spec.ts +69 -0
  13. package/e2e/perf/text-selection.spec.ts +119 -0
  14. package/e2e/perf/utils/metrics.ts +286 -0
  15. package/e2e/perf/utils/perf-cli.ts +86 -0
  16. package/package.json +9 -18
  17. package/playwright.config.ts +12 -0
  18. package/src/App.tsx +133 -178
  19. package/src/{cli/index.ts → cli.ts} +211 -107
  20. package/src/components/ActionsMenu.tsx +6 -27
  21. package/src/components/DocumentViewer/DocumentViewer.tsx +78 -105
  22. package/src/components/DocumentViewer/MermaidDiagram.tsx +6 -7
  23. package/src/components/Header.tsx +9 -20
  24. package/src/components/InlineEditor.tsx +5 -5
  25. package/src/components/MarginNote.tsx +71 -93
  26. package/src/components/MarginNotes.tsx +7 -34
  27. package/src/components/RawModal.tsx +9 -8
  28. package/src/components/ReanchorConfirm.tsx +2 -2
  29. package/src/components/SettingsModal.tsx +11 -89
  30. package/src/components/TabBar.tsx +4 -4
  31. package/src/components/TableOfContents.tsx +5 -5
  32. package/src/components/comments/CommentInput.tsx +7 -35
  33. package/src/components/comments/CommentListItem.tsx +9 -11
  34. package/src/components/comments/CommentManager.tsx +53 -37
  35. package/src/components/comments/CommentNav.tsx +14 -14
  36. package/src/components/ui/ActionLink.tsx +14 -18
  37. package/src/components/ui/Button.tsx +42 -43
  38. package/src/components/ui/Dialog.tsx +73 -113
  39. package/src/components/ui/DropdownMenu.tsx +113 -69
  40. package/src/components/ui/Text.tsx +30 -37
  41. package/src/contexts/CommentContext.tsx +75 -106
  42. package/src/contexts/LocaleContext.tsx +45 -4
  43. package/src/contexts/PositionsContext.tsx +16 -0
  44. package/src/contexts/SettingsContext.tsx +133 -0
  45. package/src/hooks/useClickOutside.ts +0 -4
  46. package/src/hooks/useCommentNavigation.ts +6 -29
  47. package/src/hooks/useComments.ts +6 -18
  48. package/src/hooks/useDocument.ts +35 -34
  49. package/src/hooks/useHeadings.test.ts +8 -50
  50. package/src/hooks/useHeadings.ts +5 -88
  51. package/src/hooks/useScrollSpy.ts +10 -14
  52. package/src/hooks/useTextSelection.ts +1 -38
  53. package/src/lib/__fixtures__/bench-data.ts +1 -41
  54. package/src/lib/anchor.bench.ts +57 -67
  55. package/src/lib/anchor.test.ts +5 -1
  56. package/src/lib/anchor.ts +13 -93
  57. package/src/lib/comment-storage.test.ts +4 -4
  58. package/src/lib/comment-storage.ts +2 -46
  59. package/src/lib/export.ts +7 -13
  60. package/src/lib/highlight/core.test.ts +1 -1
  61. package/src/lib/highlight/dom.ts +5 -68
  62. package/src/lib/highlight/highlighter.ts +102 -262
  63. package/src/lib/highlight/resolver.ts +112 -0
  64. package/src/lib/highlight/types.ts +0 -35
  65. package/src/lib/highlight/worker.ts +45 -0
  66. package/src/lib/i18n/en.ts +1 -50
  67. package/src/lib/i18n/ja.ts +1 -50
  68. package/src/lib/i18n/types.ts +1 -49
  69. package/src/lib/margin-layout.ts +5 -27
  70. package/src/lib/positions.ts +150 -0
  71. package/src/lib/utils.ts +2 -19
  72. package/src/schema.ts +81 -0
  73. package/src/{server/index.ts → server.ts} +111 -81
  74. package/src/{store/index.ts → store.ts} +14 -46
  75. package/vite.config.ts +8 -0
  76. package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
  77. package/src/components/DocumentViewer/InlineCode.tsx +0 -60
  78. package/src/components/DocumentViewer/index.ts +0 -1
  79. package/src/components/FloatingTOC.tsx +0 -61
  80. package/src/components/ShortcutCapture.tsx +0 -48
  81. package/src/components/ShortcutList.tsx +0 -198
  82. package/src/components/comments/CommentMinimap.tsx +0 -62
  83. package/src/components/ui/ActionBar.tsx +0 -16
  84. package/src/components/ui/SeparatorDot.tsx +0 -9
  85. package/src/contexts/LayoutContext.tsx +0 -88
  86. package/src/hooks/useClipboard.ts +0 -82
  87. package/src/hooks/useEditorScheme.ts +0 -51
  88. package/src/hooks/useFontPreference.ts +0 -59
  89. package/src/hooks/useKeybindings.ts +0 -108
  90. package/src/hooks/useKeyboardShortcuts.ts +0 -63
  91. package/src/hooks/useLayoutMode.ts +0 -44
  92. package/src/hooks/useLocalePreference.ts +0 -42
  93. package/src/hooks/useReanchorMode.ts +0 -33
  94. package/src/hooks/useScrollMetrics.ts +0 -56
  95. package/src/hooks/useThemePreference.ts +0 -66
  96. package/src/lib/comment-storage.bench.ts +0 -63
  97. package/src/lib/context.bench.ts +0 -41
  98. package/src/lib/context.test.ts +0 -224
  99. package/src/lib/context.ts +0 -193
  100. package/src/lib/editor-links.ts +0 -59
  101. package/src/lib/export.bench.ts +0 -35
  102. package/src/lib/highlight/colors.ts +0 -37
  103. package/src/lib/highlight/core.ts +0 -54
  104. package/src/lib/highlight/index.ts +0 -23
  105. package/src/lib/highlight/script-builder.ts +0 -485
  106. package/src/lib/html-processor.test.tsx +0 -170
  107. package/src/lib/html-processor.tsx +0 -95
  108. package/src/lib/i18n/completeness.test.ts +0 -51
  109. package/src/lib/i18n/translations.test.ts +0 -39
  110. package/src/lib/layout-constants.ts +0 -12
  111. package/src/lib/margin-layout.bench.ts +0 -28
  112. package/src/lib/scroll.test.ts +0 -118
  113. package/src/lib/scroll.ts +0 -47
  114. package/src/lib/shortcut-registry.test.ts +0 -173
  115. package/src/lib/shortcut-registry.ts +0 -209
  116. package/src/lib/utils.test.ts +0 -110
  117. package/src/store/index.test.ts +0 -242
  118. package/src/types/index.ts +0 -127
@@ -1,112 +1,102 @@
1
1
  import { bench, describe } from "vitest";
2
- import { LARGE_DOC } from "./__fixtures__/bench-data";
2
+ import {
3
+ COMMENTS_1,
4
+ COMMENTS_10,
5
+ COMMENTS_50,
6
+ LARGE_DOC,
7
+ MEDIUM_DOC,
8
+ } from "./__fixtures__/bench-data";
3
9
  import {
4
10
  findAnchor,
5
11
  findAnchorFuzzy,
6
12
  findAnchorNormalized,
7
13
  findAnchorWithFallback,
8
- levenshteinDistance,
9
14
  } from "./anchor";
10
15
 
11
- // Text known to exist at specific lines in LARGE_DOC
12
- const EXACT_TEXT_L149 =
13
- "The conclusion of section 8 summarizes the key findings";
14
- const EXACT_TEXT_L250 = "- Item 1 in section 14";
15
- const WHITESPACE_TEXT = "Item 1 in\n section 14";
16
- const FUZZY_TEXT = "- Item 1 in section 1x"; // 1-char typo
17
-
18
- describe("levenshteinDistance", () => {
19
- const str20 = "hello world testing!";
20
-
21
- bench("identical strings (20 chars)", () => {
22
- levenshteinDistance(str20, str20);
23
- });
16
+ // --- Exact match (best case) ---
24
17
 
25
- bench("1 edit distance (20 chars)", () => {
26
- levenshteinDistance("hello world testing!", "hello world testinx!");
27
- });
28
-
29
- bench("maxDistance early exit", () => {
30
- levenshteinDistance("completely different", "nothing alike here!", 3);
31
- });
18
+ describe("findAnchor exact match", () => {
19
+ const comment = COMMENTS_10[5];
32
20
 
33
- const longA = "a".repeat(50) + "b".repeat(50);
34
- const longB = "a".repeat(50) + "c".repeat(50);
35
-
36
- bench("longer strings (100 chars)", () => {
37
- levenshteinDistance(longA, longB);
38
- });
39
- });
40
-
41
- describe("findAnchor (exact)", () => {
42
- bench("300-line doc, correct hint", () => {
21
+ bench("medium doc (150 lines)", () => {
43
22
  findAnchor({
44
- source: LARGE_DOC,
45
- selectedText: EXACT_TEXT_L149,
46
- lineHint: "L149",
23
+ source: MEDIUM_DOC,
24
+ selectedText: comment.selectedText,
25
+ lineHint: comment.lineHint ?? "L1",
47
26
  });
48
27
  });
49
28
 
50
- bench("300-line doc, wrong hint (global fallback)", () => {
29
+ bench("large doc (300 lines)", () => {
51
30
  findAnchor({
52
31
  source: LARGE_DOC,
53
- selectedText: EXACT_TEXT_L250,
54
- lineHint: "L10",
32
+ selectedText: comment.selectedText,
33
+ lineHint: comment.lineHint ?? "L1",
55
34
  });
56
35
  });
57
36
  });
58
37
 
38
+ // --- Normalized match ---
39
+
59
40
  describe("findAnchorNormalized", () => {
60
- bench("300-line doc, whitespace-collapsed match", () => {
41
+ // Create text with extra whitespace to force normalization path
42
+ const comment = COMMENTS_10[5];
43
+ const normalizedText = comment.selectedText.replace(/ /g, " ");
44
+
45
+ bench("large doc — normalized whitespace", () => {
61
46
  findAnchorNormalized({
62
47
  source: LARGE_DOC,
63
- selectedText: WHITESPACE_TEXT,
64
- lineHint: "L250",
48
+ selectedText: normalizedText,
49
+ lineHint: comment.lineHint ?? "L1",
65
50
  });
66
51
  });
67
52
  });
68
53
 
54
+ // --- Fuzzy match (worst case) ---
55
+
69
56
  describe("findAnchorFuzzy", () => {
70
- bench("300-line doc, 1-char typo with hint", () => {
71
- findAnchorFuzzy({
72
- source: LARGE_DOC,
73
- selectedText: FUZZY_TEXT,
74
- lineHint: "L250",
75
- threshold: 3,
76
- });
77
- });
57
+ // Slightly mutate text to force Levenshtein search
58
+ const comment = COMMENTS_10[5];
59
+ const mutated =
60
+ "X" + comment.selectedText.slice(1, -1) + "Z";
78
61
 
79
- bench("300-line doc, no hint (full scan)", () => {
62
+ bench("large doc fuzzy (mutated text)", () => {
80
63
  findAnchorFuzzy({
81
64
  source: LARGE_DOC,
82
- selectedText: FUZZY_TEXT,
83
- threshold: 3,
65
+ selectedText: mutated.slice(0, 50), // Keep within MAX_FUZZY_TEXT_LENGTH
66
+ lineHint: comment.lineHint ?? "L1",
84
67
  });
85
68
  });
86
69
  });
87
70
 
71
+ // --- Full fallback chain ---
72
+
88
73
  describe("findAnchorWithFallback", () => {
89
- bench("exact match (fast path)", () => {
74
+ bench("1 comment exact hit", () => {
75
+ const c = COMMENTS_1[0];
90
76
  findAnchorWithFallback({
91
77
  source: LARGE_DOC,
92
- selectedText: EXACT_TEXT_L149,
93
- lineHint: "L149",
78
+ selectedText: c.selectedText,
79
+ lineHint: c.lineHint ?? "L1",
94
80
  });
95
81
  });
96
82
 
97
- bench("normalized fallback", () => {
98
- findAnchorWithFallback({
99
- source: LARGE_DOC,
100
- selectedText: WHITESPACE_TEXT,
101
- lineHint: "L250",
102
- });
83
+ bench("10 comments — exact hits", () => {
84
+ for (const c of COMMENTS_10) {
85
+ findAnchorWithFallback({
86
+ source: LARGE_DOC,
87
+ selectedText: c.selectedText,
88
+ lineHint: c.lineHint ?? "L1",
89
+ });
90
+ }
103
91
  });
104
92
 
105
- bench("fuzzy fallback", () => {
106
- findAnchorWithFallback({
107
- source: LARGE_DOC,
108
- selectedText: FUZZY_TEXT,
109
- lineHint: "L250",
110
- });
93
+ bench("50 comments — exact hits", () => {
94
+ for (const c of COMMENTS_50) {
95
+ findAnchorWithFallback({
96
+ source: LARGE_DOC,
97
+ selectedText: c.selectedText,
98
+ lineHint: c.lineHint ?? "L1",
99
+ });
100
+ }
111
101
  });
112
102
  });
@@ -135,8 +135,12 @@ describe("parseLineHint", () => {
135
135
  });
136
136
 
137
137
  it("parses line range hint", () => {
138
+ expect(parseLineHint("L42-L45")).toEqual({ start: 42, end: 45 });
139
+ expect(parseLineHint("L10-L20")).toEqual({ start: 10, end: 20 });
140
+ });
141
+
142
+ it("parses legacy format without L prefix on end line", () => {
138
143
  expect(parseLineHint("L42-45")).toEqual({ start: 42, end: 45 });
139
- expect(parseLineHint("L10-20")).toEqual({ start: 10, end: 20 });
140
144
  });
141
145
 
142
146
  it("returns default for invalid hint", () => {
package/src/lib/anchor.ts CHANGED
@@ -1,16 +1,11 @@
1
- import { type Anchor, AnchorConfidences } from "../types";
1
+ import { type Anchor, AnchorConfidences } from "../schema";
2
2
  import { getLineNumber } from "./comment-storage";
3
3
 
4
- // Anchor matching configuration
5
- const DEFAULT_SEARCH_WINDOW = 500; // chars before/after line hint for exact match
6
- const DEFAULT_FUZZY_THRESHOLD = 5; // max Levenshtein distance for fuzzy match
7
- const MAX_FUZZY_TEXT_LENGTH = 200; // skip fuzzy matching for texts longer than this
8
- const FUZZY_SEARCH_WINDOW = 2000; // larger window for fuzzy search near line hint
4
+ const DEFAULT_SEARCH_WINDOW = 500;
5
+ const DEFAULT_FUZZY_THRESHOLD = 5;
6
+ const MAX_FUZZY_TEXT_LENGTH = 200;
7
+ const FUZZY_SEARCH_WINDOW = 2000;
9
8
 
10
- /**
11
- * Common parameters for anchor finding functions.
12
- * Using object destructuring per style guide §3.5 for multiple string parameters.
13
- */
14
9
  export interface FindAnchorParams {
15
10
  source: string;
16
11
  selectedText: string;
@@ -32,27 +27,16 @@ export interface FindAnchorWithFallbackParams {
32
27
  fuzzyThreshold?: number;
33
28
  }
34
29
 
35
- /**
36
- * Normalize whitespace for comparison: collapse runs of whitespace to single space.
37
- * This allows matching text that was reformatted (line breaks, indentation changes).
38
- */
39
30
  export function normalizeWhitespace(text: string): string {
40
31
  return text.replace(/\s+/g, " ").trim();
41
32
  }
42
33
 
43
- /**
44
- * Calculate Levenshtein distance between two strings.
45
- * Uses Wagner-Fischer algorithm with O(min(m,n)) space.
46
- *
47
- * @param maxDistance Optional early exit threshold. If set, returns Infinity
48
- * when distance is guaranteed to exceed this value.
49
- */
34
+ /** Wagner-Fischer with O(min(m,n)) space. Returns Infinity when > maxDistance. */
50
35
  export function levenshteinDistance(
51
36
  a: string,
52
37
  b: string,
53
38
  maxDistance?: number,
54
39
  ): number {
55
- // Ensure a is the shorter string for space optimization
56
40
  if (a.length > b.length) {
57
41
  [a, b] = [b, a];
58
42
  }
@@ -60,20 +44,16 @@ export function levenshteinDistance(
60
44
  const m = a.length;
61
45
  const n = b.length;
62
46
 
63
- // Early termination for empty strings
64
47
  if (m === 0) return n;
65
48
  if (n === 0) return m;
66
49
 
67
- // Early exit: length difference alone exceeds threshold
68
50
  if (maxDistance !== undefined && Math.abs(m - n) > maxDistance) {
69
51
  return Number.POSITIVE_INFINITY;
70
52
  }
71
53
 
72
- // Use single row for space optimization
73
54
  let prevRow = new Array(m + 1);
74
55
  let currRow = new Array(m + 1);
75
56
 
76
- // Initialize first row
77
57
  for (let i = 0; i <= m; i++) {
78
58
  prevRow[i] = i;
79
59
  }
@@ -81,20 +61,18 @@ export function levenshteinDistance(
81
61
  for (let j = 1; j <= n; j++) {
82
62
  currRow[0] = j;
83
63
 
84
- // Track minimum value in this row for early exit
85
64
  let rowMin = currRow[0];
86
65
 
87
66
  for (let i = 1; i <= m; i++) {
88
67
  const cost = a[i - 1] === b[j - 1] ? 0 : 1;
89
68
  currRow[i] = Math.min(
90
- prevRow[i] + 1, // deletion
91
- currRow[i - 1] + 1, // insertion
92
- prevRow[i - 1] + cost, // substitution
69
+ prevRow[i] + 1,
70
+ currRow[i - 1] + 1,
71
+ prevRow[i - 1] + cost,
93
72
  );
94
73
  rowMin = Math.min(rowMin, currRow[i]);
95
74
  }
96
75
 
97
- // Early exit: minimum possible distance exceeds threshold
98
76
  if (maxDistance !== undefined && rowMin > maxDistance) {
99
77
  return Number.POSITIVE_INFINITY;
100
78
  }
@@ -105,9 +83,6 @@ export function levenshteinDistance(
105
83
  return prevRow[m];
106
84
  }
107
85
 
108
- /**
109
- * Get character offset for the start of a line number (1-indexed).
110
- */
111
86
  export function getLineOffset(content: string, lineNumber: number): number {
112
87
  if (lineNumber <= 1) return 0;
113
88
 
@@ -125,15 +100,12 @@ export function getLineOffset(content: string, lineNumber: number): number {
125
100
  return content.length;
126
101
  }
127
102
 
128
- /**
129
- * Parse line hint string to get line number(s).
130
- * Supports "L42" and "L42-45" formats.
131
- */
103
+ /** Supports "L42", "L42-L55", and legacy "L42-45" format. */
132
104
  export function parseLineHint(lineHint: string): {
133
105
  start: number;
134
106
  end: number;
135
107
  } {
136
- const match = lineHint.match(/^L(\d+)(?:-(\d+))?$/);
108
+ const match = lineHint.match(/^L(\d+)(?:-L?(\d+))?$/);
137
109
  if (!match) {
138
110
  return { start: 1, end: 1 };
139
111
  }
@@ -143,10 +115,6 @@ export function parseLineHint(lineHint: string): {
143
115
  return { start, end };
144
116
  }
145
117
 
146
- /**
147
- * Find anchor position for selected text in source content.
148
- * Uses line hint for fast lookup, falls back to global search.
149
- */
150
118
  export function findAnchor({
151
119
  source,
152
120
  selectedText,
@@ -158,7 +126,6 @@ export function findAnchor({
158
126
  }
159
127
  const { start: hintLine } = parseLineHint(lineHint);
160
128
 
161
- // Fast path: search near line hint
162
129
  const lineOffset = getLineOffset(source, hintLine);
163
130
  const windowStart = Math.max(0, lineOffset - searchWindow);
164
131
  const windowEnd = Math.min(source.length, lineOffset + searchWindow);
@@ -176,7 +143,6 @@ export function findAnchor({
176
143
  };
177
144
  }
178
145
 
179
- // Fallback: global search
180
146
  const globalIndex = source.indexOf(selectedText);
181
147
  if (globalIndex !== -1) {
182
148
  return {
@@ -190,10 +156,6 @@ export function findAnchor({
190
156
  return undefined;
191
157
  }
192
158
 
193
- /**
194
- * Build a position map from normalized string positions back to original positions.
195
- * Returns array where normalizedToOriginal[i] = original position for normalized char i.
196
- */
197
159
  function buildNormalizedPositionMap(text: string): {
198
160
  normalized: string;
199
161
  toOriginal: number[];
@@ -208,7 +170,6 @@ function buildNormalizedPositionMap(text: string): {
208
170
 
209
171
  if (isSpace) {
210
172
  if (!inWhitespace && normalized.length > 0) {
211
- // First whitespace after content - emit single space
212
173
  normalized += " ";
213
174
  toOriginal.push(i);
214
175
  }
@@ -220,7 +181,6 @@ function buildNormalizedPositionMap(text: string): {
220
181
  }
221
182
  }
222
183
 
223
- // Trim trailing space
224
184
  if (normalized.endsWith(" ")) {
225
185
  normalized = normalized.slice(0, -1);
226
186
  toOriginal.pop();
@@ -229,16 +189,6 @@ function buildNormalizedPositionMap(text: string): {
229
189
  return { normalized, toOriginal };
230
190
  }
231
191
 
232
- /**
233
- * Find anchor using whitespace-normalized matching.
234
- * Useful when document was reformatted but content is unchanged.
235
- * Returns "normalized" confidence level.
236
- *
237
- * Algorithm:
238
- * 1. Normalize source and build position map
239
- * 2. Find normalized text in normalized source (fast substring search)
240
- * 3. Map positions back to original source
241
- */
242
192
  export function findAnchorNormalized({
243
193
  source,
244
194
  selectedText,
@@ -254,31 +204,25 @@ export function findAnchorNormalized({
254
204
  return undefined;
255
205
  }
256
206
 
257
- // Skip if text has no collapsible whitespace (exact match would have worked)
258
207
  if (normalizedText === selectedText) {
259
208
  return undefined;
260
209
  }
261
210
  const { start: hintLine } = parseLineHint(lineHint);
262
211
  const lineOffset = getLineOffset(source, hintLine);
263
212
 
264
- // Define search window
265
213
  const windowStart = Math.max(0, lineOffset - searchWindow);
266
214
  const windowEnd = Math.min(source.length, lineOffset + searchWindow);
267
215
  const window = source.slice(windowStart, windowEnd);
268
216
 
269
- // Build normalized version with position mapping
270
217
  const { normalized: normalizedWindow, toOriginal } =
271
218
  buildNormalizedPositionMap(window);
272
219
 
273
- // Fast substring search on normalized text
274
220
  const normalizedIndex = normalizedWindow.indexOf(normalizedText);
275
221
  if (normalizedIndex !== -1) {
276
- // Map back to original positions
277
222
  const originalStart = windowStart + toOriginal[normalizedIndex];
278
223
  const endNormIndex = normalizedIndex + normalizedText.length - 1;
279
- // Find original end: scan forward from mapped position to include trailing whitespace
280
224
  let originalEnd = windowStart + toOriginal[endNormIndex] + 1;
281
- // Extend to include any trailing whitespace that was collapsed
225
+ // Extend past trailing whitespace that was collapsed during normalization
282
226
  while (originalEnd < source.length && /\s/.test(source[originalEnd])) {
283
227
  originalEnd++;
284
228
  }
@@ -291,7 +235,6 @@ export function findAnchorNormalized({
291
235
  };
292
236
  }
293
237
 
294
- // Global fallback (outside hint window)
295
238
  const { normalized: fullNormalized, toOriginal: fullToOriginal } =
296
239
  buildNormalizedPositionMap(source);
297
240
  const globalIndex = fullNormalized.indexOf(normalizedText);
@@ -314,10 +257,6 @@ export function findAnchorNormalized({
314
257
  return undefined;
315
258
  }
316
259
 
317
- /**
318
- * Find anchor using fuzzy matching with Levenshtein distance.
319
- * Scans the source for substrings similar to the selected text.
320
- */
321
260
  export function findAnchorFuzzy({
322
261
  source,
323
262
  selectedText,
@@ -330,7 +269,6 @@ export function findAnchorFuzzy({
330
269
 
331
270
  const textLen = selectedText.length;
332
271
 
333
- // For very long texts, skip fuzzy matching (too expensive)
334
272
  if (textLen > MAX_FUZZY_TEXT_LENGTH) {
335
273
  return undefined;
336
274
  }
@@ -338,26 +276,22 @@ export function findAnchorFuzzy({
338
276
  let bestMatch: Anchor | undefined;
339
277
  let bestDistance = threshold + 1;
340
278
 
341
- // Determine search range based on line hint
342
279
  let searchStart = 0;
343
280
  let searchEnd = source.length;
344
281
 
345
282
  if (lineHint) {
346
283
  const { start: hintLine } = parseLineHint(lineHint);
347
284
  const lineOffset = getLineOffset(source, hintLine);
348
- // Search in a larger window for fuzzy matching
349
285
  searchStart = Math.max(0, lineOffset - FUZZY_SEARCH_WINDOW);
350
286
  searchEnd = Math.min(source.length, lineOffset + FUZZY_SEARCH_WINDOW);
351
287
  }
352
288
 
353
- // Slide window of similar length through the search range
354
289
  const minLen = Math.max(1, textLen - threshold);
355
290
  const maxLen = textLen + threshold;
356
291
 
357
292
  for (let len = minLen; len <= maxLen; len++) {
358
293
  for (let i = searchStart; i <= searchEnd - len; i++) {
359
294
  const candidate = source.slice(i, i + len);
360
- // Use early exit: only compute if distance could improve on current best
361
295
  const distance = levenshteinDistance(
362
296
  selectedText,
363
297
  candidate,
@@ -374,7 +308,6 @@ export function findAnchorFuzzy({
374
308
  distance,
375
309
  };
376
310
 
377
- // Early exit if we found an exact match
378
311
  if (distance === 0) {
379
312
  return bestMatch;
380
313
  }
@@ -385,27 +318,18 @@ export function findAnchorFuzzy({
385
318
  return bestMatch;
386
319
  }
387
320
 
388
- /**
389
- * Find anchor with fallback chain: exact → normalized → fuzzy.
390
- *
391
- * Matching strategies in order of preference:
392
- * 1. Exact: Fast substring match (O(n))
393
- * 2. Normalized: Whitespace-collapsed match for reformatted text (O(n))
394
- * 3. Fuzzy: Levenshtein distance for small edits (O(n × m × threshold))
395
- */
321
+ /** Fallback chain: exact → normalized → fuzzy. */
396
322
  export function findAnchorWithFallback({
397
323
  source,
398
324
  selectedText,
399
325
  lineHint,
400
326
  fuzzyThreshold,
401
327
  }: FindAnchorWithFallbackParams): Anchor | undefined {
402
- // Try exact match first (fastest)
403
328
  const exactMatch = findAnchor({ source, selectedText, lineHint });
404
329
  if (exactMatch) {
405
330
  return exactMatch;
406
331
  }
407
332
 
408
- // Try normalized match (handles reformatting)
409
333
  const normalizedMatch = findAnchorNormalized({
410
334
  source,
411
335
  selectedText,
@@ -415,7 +339,6 @@ export function findAnchorWithFallback({
415
339
  return normalizedMatch;
416
340
  }
417
341
 
418
- // Fall back to fuzzy matching (handles small edits)
419
342
  return findAnchorFuzzy({
420
343
  source,
421
344
  selectedText,
@@ -424,9 +347,6 @@ export function findAnchorWithFallback({
424
347
  });
425
348
  }
426
349
 
427
- /**
428
- * Find the closest match when multiple occurrences exist.
429
- */
430
350
  export function findClosestOccurrence({
431
351
  source,
432
352
  selectedText,
@@ -1,6 +1,6 @@
1
1
  import type * as os from "node:os";
2
2
  import { describe, expect, it, vi } from "vitest";
3
- import type { CommentFile } from "../types";
3
+ import type { CommentFile } from "../schema";
4
4
  import { COMMENT_FILE_LARGE } from "./__fixtures__/bench-data";
5
5
  import {
6
6
  computeHash,
@@ -124,7 +124,7 @@ describe("getLineHint", () => {
124
124
 
125
125
  it("returns range hint for multiple lines", () => {
126
126
  const content = "line one\nline two\nline three";
127
- expect(getLineHint(content, 0, 20)).toBe("L1-3");
127
+ expect(getLineHint(content, 0, 20)).toBe("L1-L3");
128
128
  });
129
129
  });
130
130
 
@@ -363,7 +363,7 @@ describe("serializeComments", () => {
363
363
  selectedText: "line one\nline two",
364
364
  comment: "Comment",
365
365
  createdAt: "2024-12-24T10:30:00Z",
366
- lineHint: "L42-43",
366
+ lineHint: "L42-L43",
367
367
  startOffset: 100,
368
368
  endOffset: 120,
369
369
  },
@@ -439,7 +439,7 @@ describe("serializeComments", () => {
439
439
  selectedText: "another\nmultiline\nselection",
440
440
  comment: "Another comment with\n\nmultiple paragraphs.",
441
441
  createdAt: "2024-12-24T11:00:00Z",
442
- lineHint: "L50-52",
442
+ lineHint: "L50-L52",
443
443
  startOffset: 200,
444
444
  endOffset: 230,
445
445
  },