@amsterdamdatalabs/enact-docs-search 0.1.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 (47) hide show
  1. package/NOTICE +50 -0
  2. package/README.md +35 -0
  3. package/dist/adapter/repo-walker.d.ts +22 -0
  4. package/dist/adapter/repo-walker.d.ts.map +1 -0
  5. package/dist/adapter/repo-walker.js +96 -0
  6. package/dist/adapter/repo-walker.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +310 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/embeddings/embedding-service.d.ts +12 -0
  12. package/dist/embeddings/embedding-service.d.ts.map +1 -0
  13. package/dist/embeddings/embedding-service.js +67 -0
  14. package/dist/embeddings/embedding-service.js.map +1 -0
  15. package/dist/indexing/chunker.d.ts +52 -0
  16. package/dist/indexing/chunker.d.ts.map +1 -0
  17. package/dist/indexing/chunker.js +196 -0
  18. package/dist/indexing/chunker.js.map +1 -0
  19. package/dist/indexing/indexing-service.d.ts +29 -0
  20. package/dist/indexing/indexing-service.d.ts.map +1 -0
  21. package/dist/indexing/indexing-service.js +151 -0
  22. package/dist/indexing/indexing-service.js.map +1 -0
  23. package/dist/search/search-service.d.ts +53 -0
  24. package/dist/search/search-service.d.ts.map +1 -0
  25. package/dist/search/search-service.js +139 -0
  26. package/dist/search/search-service.js.map +1 -0
  27. package/dist/search/snippet.d.ts +65 -0
  28. package/dist/search/snippet.d.ts.map +1 -0
  29. package/dist/search/snippet.js +352 -0
  30. package/dist/search/snippet.js.map +1 -0
  31. package/dist/storage/document-store.d.ts +72 -0
  32. package/dist/storage/document-store.d.ts.map +1 -0
  33. package/dist/storage/document-store.js +134 -0
  34. package/dist/storage/document-store.js.map +1 -0
  35. package/dist/storage/persistence.d.ts +55 -0
  36. package/dist/storage/persistence.d.ts.map +1 -0
  37. package/dist/storage/persistence.js +132 -0
  38. package/dist/storage/persistence.js.map +1 -0
  39. package/dist/storage/vector-store.d.ts +63 -0
  40. package/dist/storage/vector-store.d.ts.map +1 -0
  41. package/dist/storage/vector-store.js +172 -0
  42. package/dist/storage/vector-store.js.map +1 -0
  43. package/dist/types.d.ts +12 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +24 -0
  46. package/dist/types.js.map +1 -0
  47. package/package.json +40 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-service.js","sourceRoot":"","sources":["../../src/search/search-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Extract meaningful query terms for match/highlight purposes: lowercase,
3
+ * whitespace-split, drop short tokens (<3 chars) and stopwords. This is
4
+ * intentionally independent of whatever tokenization MiniSearch does
5
+ * internally for indexing/ranking — this list only drives snippet
6
+ * selection and highlighting, so it must not include stopwords regardless
7
+ * of what MiniSearch itself does under the hood.
8
+ */
9
+ export declare function extractQueryTerms(query: string): string[];
10
+ /** How matched terms should be marked in a generated snippet:
11
+ * - "ansi": wrap with real ANSI bold escapes (\x1b[1m...\x1b[22m) — only
12
+ * correct when writing to an interactive terminal.
13
+ * - "plain": no markup at all. Used for --json output (a JSON consumer
14
+ * wants raw data, not a display marker) and for non-TTY text output
15
+ * (piped/redirected — a human doing that is likely about to grep/process
16
+ * the output programmatically, so escape codes would just be noise, and
17
+ * literal `**` clutter is exactly the bug being fixed). */
18
+ export type HighlightMode = "ansi" | "plain";
19
+ /** Context needed to resolve a chunk-local match position against the real,
20
+ * full source file rather than just the chunk's own text (fix #4 in the
21
+ * module doc-comment). `chunkStartLine` is the chunk's 1-based start line
22
+ * within the full file — already tracked per-chunk as
23
+ * `IndexedDocument.startLine`. */
24
+ export interface FileContext {
25
+ fullContent: string;
26
+ chunkStartLine: number;
27
+ }
28
+ /**
29
+ * Build a human-readable snippet around the first occurrence of any of
30
+ * `terms` in `content`, then highlight those terms with word-boundary-safe
31
+ * bolding. Table rows are detected and handled specially so a match inside
32
+ * a markdown table doesn't produce a raw pipe-fragment slice.
33
+ *
34
+ * `fileContext`, when supplied, lets the rest of snippet assembly (most
35
+ * importantly table-header lookback) operate against the REAL file instead
36
+ * of just the chunk's own text — a chunk boundary can put a table's header
37
+ * outside the chunk that contains the matched row, even though the header
38
+ * is genuinely only a few lines away in the real file. The match itself is
39
+ * still located within `content` (the chunk actually matched by the
40
+ * search index); only the window used to build the surrounding snippet is
41
+ * widened to the full file, by converting the chunk-local match position
42
+ * to a file-absolute one via `chunkStartLine`.
43
+ */
44
+ export declare function generateSnippet(content: string, terms: string[], highlightMode?: HighlightMode, fileContext?: FileContext): string;
45
+ /** Minimal shape `buildSnippet` needs from an indexed chunk. */
46
+ export interface SnippetDoc {
47
+ filePath: string;
48
+ content: string;
49
+ /** 1-based line number where this chunk starts in the source file. */
50
+ startLine: number;
51
+ }
52
+ /**
53
+ * Convenience wrapper around `generateSnippet` for the common call-site
54
+ * shape: an indexed chunk plus the repo root it lives under. Reads the full
55
+ * source file from disk so snippet generation can see the real document,
56
+ * not just the chunk (fix #4). Result counts per query are small (~10), so
57
+ * a handful of extra file reads at query time is the right tradeoff over
58
+ * silently degraded snippets.
59
+ *
60
+ * If `repoRoot` is omitted, or the file can't be read (moved/deleted since
61
+ * indexing, permission error, etc.), falls back to chunk-only snippet
62
+ * generation rather than failing the whole query.
63
+ */
64
+ export declare function buildSnippet(doc: SnippetDoc, terms: string[], highlightMode: HighlightMode, repoRoot?: string): Promise<string>;
65
+ //# sourceMappingURL=snippet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snippet.d.ts","sourceRoot":"","sources":["../../src/search/snippet.ts"],"names":[],"mappings":"AAwDA;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAKzD;AAMD;;;;;;;8DAO8D;AAC9D,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC;AAoO7C;;;;kCAIkC;AAClC,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EAAE,EACf,aAAa,GAAE,aAAuB,EACtC,WAAW,CAAC,EAAE,WAAW,GACxB,MAAM,CA4BR;AAED,gEAAgE;AAChE,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,MAAM,EAAE,EACf,aAAa,EAAE,aAAa,EAC5B,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAWjB"}
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Shared query-term extraction and snippet/highlight logic used by both
3
+ * DocumentStore (BM25/keyword results) and SearchService (vector/hybrid
4
+ * results). Extracted so the two code paths can't drift and produce two
5
+ * different (and differently broken) renderings of the same kind of bug.
6
+ *
7
+ * Fixes several related readability bugs surfaced by real queries against
8
+ * this repo's own docs:
9
+ *
10
+ * 1. Highlighting used a bare substring match, so short common words like
11
+ * "and" matched inside unrelated words ("H**and**le", "S**and**box").
12
+ * Fixed by requiring a word boundary on whichever side of the term is
13
+ * a word character.
14
+ * 2. Stopwords ("and", "to", ...) were treated as real query terms, so
15
+ * they got matched/highlighted everywhere. Fixed by filtering a small
16
+ * stopword list out of the extracted term list used for matching and
17
+ * highlighting (MiniSearch's own internal tokenization for indexing/
18
+ * ranking is untouched — this only affects the terms list used for
19
+ * snippet selection and highlighting).
20
+ * 3. Snippets were raw character slices with no regard for word/sentence
21
+ * boundaries or markdown table structure, so a match inside a table
22
+ * row produced meaningless pipe-delimited fragments. Fixed by
23
+ * expanding to word boundaries, preferring sentence boundaries within
24
+ * a small extra budget, and special-casing table rows.
25
+ * 4. Table-header pairing (`findTableHeader`) only ever saw a single
26
+ * CHUNK's text (`IndexedDocument.content`), not the full source file.
27
+ * When a matched table row happened to sit near the start of its
28
+ * chunk, the real header — genuinely just a handful of lines up in the
29
+ * actual document — fell outside the chunk entirely, so the lookback
30
+ * silently failed and every such row degraded to the raw `[table row]`
31
+ * fallback even though a perfectly good header existed in the file.
32
+ * Fixed by giving snippet generation an optional view of the FULL file
33
+ * content plus the chunk's file-absolute start line, so the header
34
+ * lookback (and the rest of snippet assembly, for consistency) walks
35
+ * the real document rather than just the chunk. See `buildSnippet`.
36
+ * 5. Pre-existing markdown bold already present in the SOURCE content
37
+ * (e.g. `**Schedule**` in a table cell) printed as literal `**`
38
+ * clutter — `highlightTerms` only ever wrapped query-MATCHED terms,
39
+ * never touching bold that was already there. Fixed by a universal
40
+ * final pass (`applyMarkdownBold`) that runs LAST, after highlighting
41
+ * and table formatting, and converts every remaining `**...**` pair in
42
+ * the assembled snippet to real ANSI bold (TTY) or strips it entirely
43
+ * (JSON / non-TTY).
44
+ */
45
+ import { readFile } from "node:fs/promises";
46
+ import { join } from "node:path";
47
+ /** Small, deliberately conservative English stopword list — short, very
48
+ * common function words only. Not an aggressive/exhaustive list; the goal
49
+ * is just to stop them from being treated as meaningful match terms. */
50
+ const STOPWORDS = new Set([
51
+ "a", "an", "and", "the", "to", "of", "in", "on", "for", "is", "are",
52
+ "was", "were", "be", "been", "this", "that", "with", "as", "at", "by",
53
+ "from", "or",
54
+ ]);
55
+ /**
56
+ * Extract meaningful query terms for match/highlight purposes: lowercase,
57
+ * whitespace-split, drop short tokens (<3 chars) and stopwords. This is
58
+ * intentionally independent of whatever tokenization MiniSearch does
59
+ * internally for indexing/ranking — this list only drives snippet
60
+ * selection and highlighting, so it must not include stopwords regardless
61
+ * of what MiniSearch itself does under the hood.
62
+ */
63
+ export function extractQueryTerms(query) {
64
+ return query
65
+ .toLowerCase()
66
+ .split(/\s+/)
67
+ .filter((t) => t.length >= 3 && !STOPWORDS.has(t));
68
+ }
69
+ function escapeRegex(str) {
70
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
71
+ }
72
+ const ANSI_BOLD_ON = "\x1b[1m";
73
+ const ANSI_BOLD_OFF = "\x1b[22m";
74
+ /** Universal final pass: convert every remaining `**...**` pair in a fully
75
+ * assembled snippet — whether it's markdown bold that was already present
76
+ * in the source content, or bold left untouched by `highlightTerms` (which
77
+ * only ever wraps query-MATCHED terms) — according to the same TTY/non-TTY
78
+ * rule as everything else. Must run LAST, after highlighting and table
79
+ * formatting, so nothing downstream re-introduces or misses a `**` pair. */
80
+ function applyMarkdownBold(text, mode) {
81
+ const paired = mode === "ansi"
82
+ ? text.replace(/\*\*(.+?)\*\*/g, `${ANSI_BOLD_ON}$1${ANSI_BOLD_OFF}`)
83
+ : text.replace(/\*\*(.+?)\*\*/g, "$1");
84
+ // A snippet window can cut through a bold span, leaving one lone `**`
85
+ // marker with no partner (see e.g. the 220-char prose window truncating
86
+ // mid-`**word**`). The paired pass above only touches complete pairs, so
87
+ // strip whatever's left over rather than leave literal asterisks in
88
+ // either mode — there's no meaningful bolding left to apply to it.
89
+ return paired.replace(/\*\*/g, "");
90
+ }
91
+ /** Highlight `terms` inside `text`, requiring a word boundary on whichever
92
+ * side of a term is itself a word character (so "and" won't match inside
93
+ * "handle", but a term like "project_id" that starts/ends with `_` still
94
+ * matches sensibly). In "plain" mode this is a no-op (no markers added). */
95
+ function highlightTerms(text, terms, mode) {
96
+ if (mode !== "ansi")
97
+ return text;
98
+ let result = text;
99
+ for (const term of terms) {
100
+ if (!term)
101
+ continue;
102
+ const escaped = escapeRegex(term);
103
+ const leadingBoundary = /^\w/.test(term) ? "\\b" : "";
104
+ const trailingBoundary = /\w$/.test(term) ? "\\b" : "";
105
+ const regex = new RegExp(`${leadingBoundary}(${escaped})${trailingBoundary}`, "gi");
106
+ result = result.replace(regex, `${ANSI_BOLD_ON}$1${ANSI_BOLD_OFF}`);
107
+ }
108
+ return result;
109
+ }
110
+ const SNIPPET_LENGTH = 220;
111
+ const SENTENCE_EXPAND_BUDGET = 80;
112
+ function findFirstMatchIndex(content, terms) {
113
+ let firstMatch = -1;
114
+ const lower = content.toLowerCase();
115
+ for (const term of terms) {
116
+ const index = lower.indexOf(term.toLowerCase());
117
+ if (index !== -1 && (firstMatch === -1 || index < firstMatch))
118
+ firstMatch = index;
119
+ }
120
+ return firstMatch;
121
+ }
122
+ function lineBounds(content, pos) {
123
+ const lineStart = content.lastIndexOf("\n", Math.max(0, pos - 1)) + 1;
124
+ let lineEnd = content.indexOf("\n", pos);
125
+ if (lineEnd === -1)
126
+ lineEnd = content.length;
127
+ return { lineStart, lineEnd };
128
+ }
129
+ /** A line "is a table row" if it has 2+ pipe characters — enough to
130
+ * produce multiple cells when split, which is what makes a naive
131
+ * character-slice truncation unreadable. */
132
+ function isTableLine(line) {
133
+ return (line.match(/\|/g)?.length ?? 0) >= 2;
134
+ }
135
+ function splitTableRow(line) {
136
+ let l = line.trim();
137
+ if (l.startsWith("|"))
138
+ l = l.slice(1);
139
+ if (l.endsWith("|"))
140
+ l = l.slice(0, -1);
141
+ return l.split("|").map((c) => c.trim());
142
+ }
143
+ const TABLE_SEPARATOR_RE = /^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?$/;
144
+ /** Walk upward from a table row looking for the `|---|---|` separator
145
+ * line, and return the header row immediately above it, if found within
146
+ * a reasonable number of lines (bounded so we don't walk the whole file). */
147
+ function findTableHeader(content, lineStart) {
148
+ const before = content.slice(0, Math.max(0, lineStart - 1)).split("\n");
149
+ const scanFrom = before.length - 1;
150
+ for (let i = scanFrom; i >= 0 && i >= scanFrom - 30; i--) {
151
+ const l = before[i].trim();
152
+ if (TABLE_SEPARATOR_RE.test(l)) {
153
+ const headerLine = before[i - 1];
154
+ return headerLine ? splitTableRow(headerLine) : null;
155
+ }
156
+ }
157
+ return null;
158
+ }
159
+ /** Look a bounded number of lines before/after a table block for a
160
+ * non-table, non-heading, non-empty line to use as a fallback snippet.
161
+ * Rejects lines that are just leftover table punctuation — a lone "|"
162
+ * left over at a chunk boundary (which can have too few pipes to trip
163
+ * `isTableLine`), a run of "-"/":" characters, etc. — via both a
164
+ * minimum-word-content check AND a blanket "no pipe at all" check, since
165
+ * a chunk boundary can split a table row so only its last fragment (e.g.
166
+ * `enabled. |`) lands in this chunk, and that fragment alone has just one
167
+ * pipe but is exactly as meaningless as a full broken row. */
168
+ function looksLikeRealProse(line) {
169
+ return /[A-Za-z]{3,}/.test(line) && !line.includes("|");
170
+ }
171
+ function findNearestProseLine(content, lineStart, lineEnd) {
172
+ const before = content.slice(0, lineStart).split("\n");
173
+ for (let i = before.length - 1; i >= 0 && i >= before.length - 15; i--) {
174
+ const l = before[i].trim();
175
+ if (l && looksLikeRealProse(l) && !TABLE_SEPARATOR_RE.test(l) && !/^#{1,6}\s/.test(l)) {
176
+ return l;
177
+ }
178
+ }
179
+ const after = content.slice(lineEnd).split("\n");
180
+ for (let i = 0; i < after.length && i < 15; i++) {
181
+ const l = after[i].trim();
182
+ if (l && looksLikeRealProse(l) && !TABLE_SEPARATOR_RE.test(l) && !/^#{1,6}\s/.test(l)) {
183
+ return l;
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+ function tableSnippet(content, lineStart, lineEnd) {
189
+ const line = content.slice(lineStart, lineEnd);
190
+ const cells = splitTableRow(line);
191
+ const header = findTableHeader(content, lineStart);
192
+ if (header && header.length === cells.length) {
193
+ const paired = cells
194
+ .map((cell, i) => {
195
+ const h = (header[i] ?? "").trim();
196
+ return h ? `${h}: ${cell}` : cell;
197
+ })
198
+ .filter((p) => p.trim() !== "");
199
+ if (paired.length > 0)
200
+ return paired.join(" | ");
201
+ }
202
+ const prose = findNearestProseLine(content, lineStart, lineEnd);
203
+ if (prose)
204
+ return prose.length > 200 ? prose.slice(0, 200) + "..." : prose;
205
+ // Last resort: label it plainly rather than presenting broken fragments
206
+ // as if they were prose, and collapse repeated pipe padding.
207
+ const compact = line.trim().replace(/\s*\|\s*/g, " | ").replace(/^\s*\|\s*/, "").trim();
208
+ return `[table row] ${compact}`;
209
+ }
210
+ /** Move `start` left until the character immediately before it is
211
+ * whitespace (or we hit the beginning of the string) — never cuts a word
212
+ * in half. */
213
+ function expandStartToWordBoundary(content, start) {
214
+ let s = start;
215
+ while (s > 0 && !/\s/.test(content[s - 1]))
216
+ s--;
217
+ return s;
218
+ }
219
+ /** Move `end` right until the character at it is whitespace (or we hit
220
+ * the end of the string) — never cuts a word in half. */
221
+ function expandEndToWordBoundary(content, end) {
222
+ let e = end;
223
+ while (e < content.length && !/\s/.test(content[e]))
224
+ e++;
225
+ return e;
226
+ }
227
+ /** Try to extend `end` forward, within `budget` extra characters, to the
228
+ * next sentence-terminating punctuation followed by whitespace/EOF. */
229
+ function extendToSentenceEnd(content, end, budget) {
230
+ const zone = content.slice(end, Math.min(content.length, end + budget));
231
+ const match = zone.match(/[.!?](\s|$)/);
232
+ if (match && match.index !== undefined)
233
+ return end + match.index + 1;
234
+ return end;
235
+ }
236
+ /** Try to pull `start` backward, within `budget` extra characters, to just
237
+ * after the nearest preceding sentence-terminating punctuation. */
238
+ function extendToSentenceStart(content, start, budget) {
239
+ const zoneStart = Math.max(0, start - budget);
240
+ const zone = content.slice(zoneStart, start);
241
+ const matches = [...zone.matchAll(/[.!?]\s+/g)];
242
+ if (matches.length > 0) {
243
+ const last = matches[matches.length - 1];
244
+ return zoneStart + last.index + last[0].length;
245
+ }
246
+ return start;
247
+ }
248
+ function proseSnippet(content, matchPos) {
249
+ let start = Math.max(0, matchPos - Math.floor(SNIPPET_LENGTH / 3));
250
+ let end = Math.min(content.length, start + SNIPPET_LENGTH);
251
+ start = expandStartToWordBoundary(content, start);
252
+ end = expandEndToWordBoundary(content, end);
253
+ end = extendToSentenceEnd(content, end, SENTENCE_EXPAND_BUDGET);
254
+ start = extendToSentenceStart(content, start, SENTENCE_EXPAND_BUDGET);
255
+ let snippet = content.slice(start, end).replace(/\s+/g, " ").trim();
256
+ if (start > 0)
257
+ snippet = "..." + snippet;
258
+ if (end < content.length)
259
+ snippet = snippet + "...";
260
+ return snippet;
261
+ }
262
+ /** 0-based index of the line containing character offset `pos` within
263
+ * `content` (i.e. how many newlines precede `pos`). */
264
+ function lineIndexAt(content, pos) {
265
+ let count = 0;
266
+ const bound = Math.min(pos, content.length);
267
+ for (let i = 0; i < bound; i++) {
268
+ if (content[i] === "\n")
269
+ count++;
270
+ }
271
+ return count;
272
+ }
273
+ /** Character offset where 0-based line `lineIndex` starts within `content`. */
274
+ function offsetOfLineIndex(content, lineIndex) {
275
+ if (lineIndex <= 0)
276
+ return 0;
277
+ let seen = 0;
278
+ for (let i = 0; i < content.length; i++) {
279
+ if (content[i] === "\n") {
280
+ seen++;
281
+ if (seen === lineIndex)
282
+ return i + 1;
283
+ }
284
+ }
285
+ return content.length;
286
+ }
287
+ /**
288
+ * Build a human-readable snippet around the first occurrence of any of
289
+ * `terms` in `content`, then highlight those terms with word-boundary-safe
290
+ * bolding. Table rows are detected and handled specially so a match inside
291
+ * a markdown table doesn't produce a raw pipe-fragment slice.
292
+ *
293
+ * `fileContext`, when supplied, lets the rest of snippet assembly (most
294
+ * importantly table-header lookback) operate against the REAL file instead
295
+ * of just the chunk's own text — a chunk boundary can put a table's header
296
+ * outside the chunk that contains the matched row, even though the header
297
+ * is genuinely only a few lines away in the real file. The match itself is
298
+ * still located within `content` (the chunk actually matched by the
299
+ * search index); only the window used to build the surrounding snippet is
300
+ * widened to the full file, by converting the chunk-local match position
301
+ * to a file-absolute one via `chunkStartLine`.
302
+ */
303
+ export function generateSnippet(content, terms, highlightMode = "plain", fileContext) {
304
+ const firstMatch = findFirstMatchIndex(content, terms);
305
+ const matchPosInChunk = firstMatch === -1 ? 0 : firstMatch;
306
+ let workingContent = content;
307
+ let matchPos = matchPosInChunk;
308
+ if (fileContext) {
309
+ const chunkLineIndex = lineIndexAt(content, matchPosInChunk);
310
+ const columnInLine = matchPosInChunk - lineBounds(content, matchPosInChunk).lineStart;
311
+ const fileLineIndex = fileContext.chunkStartLine - 1 + chunkLineIndex;
312
+ const fileLineOffset = offsetOfLineIndex(fileContext.fullContent, fileLineIndex);
313
+ workingContent = fileContext.fullContent;
314
+ matchPos = Math.min(fileLineOffset + columnInLine, workingContent.length);
315
+ }
316
+ const { lineStart, lineEnd } = lineBounds(workingContent, matchPos);
317
+ const line = workingContent.slice(lineStart, lineEnd);
318
+ const snippet = isTableLine(line)
319
+ ? tableSnippet(workingContent, lineStart, lineEnd)
320
+ : proseSnippet(workingContent, matchPos);
321
+ const highlighted = highlightTerms(snippet, terms, highlightMode);
322
+ // Universal final pass — must run LAST, after highlighting and table
323
+ // formatting, so it catches pre-existing source markdown bold as well as
324
+ // anything table formatting left untouched (fix #5).
325
+ return applyMarkdownBold(highlighted, highlightMode);
326
+ }
327
+ /**
328
+ * Convenience wrapper around `generateSnippet` for the common call-site
329
+ * shape: an indexed chunk plus the repo root it lives under. Reads the full
330
+ * source file from disk so snippet generation can see the real document,
331
+ * not just the chunk (fix #4). Result counts per query are small (~10), so
332
+ * a handful of extra file reads at query time is the right tradeoff over
333
+ * silently degraded snippets.
334
+ *
335
+ * If `repoRoot` is omitted, or the file can't be read (moved/deleted since
336
+ * indexing, permission error, etc.), falls back to chunk-only snippet
337
+ * generation rather than failing the whole query.
338
+ */
339
+ export async function buildSnippet(doc, terms, highlightMode, repoRoot) {
340
+ let fileContext;
341
+ if (repoRoot) {
342
+ try {
343
+ const fullContent = await readFile(join(repoRoot, doc.filePath), "utf-8");
344
+ fileContext = { fullContent, chunkStartLine: doc.startLine };
345
+ }
346
+ catch {
347
+ fileContext = undefined;
348
+ }
349
+ }
350
+ return generateSnippet(doc.content, terms, highlightMode, fileContext);
351
+ }
352
+ //# sourceMappingURL=snippet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snippet.js","sourceRoot":"","sources":["../../src/search/snippet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;wEAEwE;AACxE,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK;IACnE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;IACrE,MAAM,EAAE,IAAI;CACb,CAAC,CAAC;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,OAAO,KAAK;SACT,WAAW,EAAE;SACb,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AACvD,CAAC;AAED,SAAS,WAAW,CAAC,GAAW;IAC9B,OAAO,GAAG,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACpD,CAAC;AAYD,MAAM,YAAY,GAAG,SAAS,CAAC;AAC/B,MAAM,aAAa,GAAG,UAAU,CAAC;AAEjC;;;;;4EAK4E;AAC5E,SAAS,iBAAiB,CAAC,IAAY,EAAE,IAAmB;IAC1D,MAAM,MAAM,GACV,IAAI,KAAK,MAAM;QACb,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,YAAY,KAAK,aAAa,EAAE,CAAC;QACrE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;IAC3C,sEAAsE;IACtE,wEAAwE;IACxE,yEAAyE;IACzE,oEAAoE;IACpE,mEAAmE;IACnE,OAAO,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AACrC,CAAC;AAED;;;4EAG4E;AAC5E,SAAS,cAAc,CAAC,IAAY,EAAE,KAAe,EAAE,IAAmB;IACxE,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,eAAe,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACtD,MAAM,gBAAgB,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACvD,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,GAAG,eAAe,IAAI,OAAO,IAAI,gBAAgB,EAAE,EAAE,IAAI,CAAC,CAAC;QACpF,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,YAAY,KAAK,aAAa,EAAE,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAElC,SAAS,mBAAmB,CAAC,OAAe,EAAE,KAAe;IAC3D,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC;IACpB,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IACpC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QAChD,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,KAAK,CAAC,CAAC,IAAI,KAAK,GAAG,UAAU,CAAC;YAAE,UAAU,GAAG,KAAK,CAAC;IACpF,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,UAAU,CAAC,OAAe,EAAE,GAAW;IAC9C,MAAM,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACtE,IAAI,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACzC,IAAI,OAAO,KAAK,CAAC,CAAC;QAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAC7C,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;AAChC,CAAC;AAED;;4CAE4C;AAC5C,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IACjC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IACpB,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACtC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,kBAAkB,GAAG,6CAA6C,CAAC;AAEzE;;6EAE6E;AAC7E,SAAS,eAAe,CAAC,OAAe,EAAE,SAAiB;IACzD,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACxE,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;IACnC,KAAK,IAAI,CAAC,GAAG,QAAQ,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QACzD,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/B,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACjC,OAAO,UAAU,CAAC,CAAC,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACvD,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;8DAQ8D;AAC9D,SAAS,kBAAkB,CAAC,IAAY;IACtC,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,oBAAoB,CAAC,OAAe,EAAE,SAAiB,EAAE,OAAe;IAC/E,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACvD,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QACvE,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACtF,OAAO,CAAC,CAAC;QACX,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAChD,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACtF,OAAO,CAAC,CAAC;QACX,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,YAAY,CAAC,OAAe,EAAE,SAAiB,EAAE,OAAe;IACvE,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAEnD,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,KAAK;aACjB,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;YACf,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YACnC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACpC,CAAC,CAAC;aACD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAClC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,KAAK,GAAG,oBAAoB,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IAChE,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;IAE3E,wEAAwE;IACxE,6DAA6D;IAC7D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxF,OAAO,eAAe,OAAO,EAAE,CAAC;AAClC,CAAC;AAED;;cAEc;AACd,SAAS,yBAAyB,CAAC,OAAe,EAAE,KAAa;IAC/D,IAAI,CAAC,GAAG,KAAK,CAAC;IACd,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC;QAAE,CAAC,EAAE,CAAC;IACjD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;yDACyD;AACzD,SAAS,uBAAuB,CAAC,OAAe,EAAE,GAAW;IAC3D,IAAI,CAAC,GAAG,GAAG,CAAC;IACZ,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC;QAAE,CAAC,EAAE,CAAC;IAC1D,OAAO,CAAC,CAAC;AACX,CAAC;AAED;uEACuE;AACvE,SAAS,mBAAmB,CAAC,OAAe,EAAE,GAAW,EAAE,MAAc;IACvE,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC;IACxE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;IACxC,IAAI,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,GAAG,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;IACrE,OAAO,GAAG,CAAC;AACb,CAAC;AAED;mEACmE;AACnE,SAAS,qBAAqB,CAAC,OAAe,EAAE,KAAa,EAAE,MAAc;IAC3E,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC;IAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC;IAChD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;QAC1C,OAAO,SAAS,GAAG,IAAI,CAAC,KAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAClD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,YAAY,CAAC,OAAe,EAAE,QAAgB;IACrD,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC;IACnE,IAAI,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,GAAG,cAAc,CAAC,CAAC;IAE3D,KAAK,GAAG,yBAAyB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAClD,GAAG,GAAG,uBAAuB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAE5C,GAAG,GAAG,mBAAmB,CAAC,OAAO,EAAE,GAAG,EAAE,sBAAsB,CAAC,CAAC;IAChE,KAAK,GAAG,qBAAqB,CAAC,OAAO,EAAE,KAAK,EAAE,sBAAsB,CAAC,CAAC;IAEtE,IAAI,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACpE,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IACzC,IAAI,GAAG,GAAG,OAAO,CAAC,MAAM;QAAE,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;IACpD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;uDACuD;AACvD,SAAS,WAAW,CAAC,OAAe,EAAE,GAAW;IAC/C,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI;YAAE,KAAK,EAAE,CAAC;IACnC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,+EAA+E;AAC/E,SAAS,iBAAiB,CAAC,OAAe,EAAE,SAAiB;IAC3D,IAAI,SAAS,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAC7B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACxB,IAAI,EAAE,CAAC;YACP,IAAI,IAAI,KAAK,SAAS;gBAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC,MAAM,CAAC;AACxB,CAAC;AAYD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAC7B,OAAe,EACf,KAAe,EACf,gBAA+B,OAAO,EACtC,WAAyB;IAEzB,MAAM,UAAU,GAAG,mBAAmB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACvD,MAAM,eAAe,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;IAE3D,IAAI,cAAc,GAAG,OAAO,CAAC;IAC7B,IAAI,QAAQ,GAAG,eAAe,CAAC;IAE/B,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,cAAc,GAAG,WAAW,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;QAC7D,MAAM,YAAY,GAAG,eAAe,GAAG,UAAU,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,SAAS,CAAC;QACtF,MAAM,aAAa,GAAG,WAAW,CAAC,cAAc,GAAG,CAAC,GAAG,cAAc,CAAC;QACtE,MAAM,cAAc,GAAG,iBAAiB,CAAC,WAAW,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;QACjF,cAAc,GAAG,WAAW,CAAC,WAAW,CAAC;QACzC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,GAAG,YAAY,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IAC5E,CAAC;IAED,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,UAAU,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;IACpE,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAEtD,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC;QAC/B,CAAC,CAAC,YAAY,CAAC,cAAc,EAAE,SAAS,EAAE,OAAO,CAAC;QAClD,CAAC,CAAC,YAAY,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;IAE3C,MAAM,WAAW,GAAG,cAAc,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC;IAClE,qEAAqE;IACrE,yEAAyE;IACzE,qDAAqD;IACrD,OAAO,iBAAiB,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;AACvD,CAAC;AAUD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAe,EACf,KAAe,EACf,aAA4B,EAC5B,QAAiB;IAEjB,IAAI,WAAoC,CAAC;IACzC,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,CAAC;YACH,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;YAC1E,WAAW,GAAG,EAAE,WAAW,EAAE,cAAc,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACP,WAAW,GAAG,SAAS,CAAC;QAC1B,CAAC;IACH,CAAC;IACD,OAAO,eAAe,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC;AACzE,CAAC"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Adapted from obsidian-qmd (MIT): https://github.com/thirteen37/obsidian-qmd
3
+ * Source: src/storage/DocumentStore.ts — the original had no Obsidian
4
+ * dependency at all (pure MiniSearch wrapper). Adjustments: added
5
+ * `startLine` to the indexed schema for path:line output, added a `scope`
6
+ * pre-filter to `search()`, and dropped the Obsidian-flavoured
7
+ * `pluginBasePath` option that was already unused upstream.
8
+ *
9
+ * DocumentStore provides full-text search using MiniSearch.
10
+ * Implements proper BM25 ranking with inverted index.
11
+ */
12
+ import MiniSearch from "minisearch";
13
+ import type { Scope } from "../types.js";
14
+ import type { HighlightMode } from "../search/snippet.js";
15
+ export interface IndexedDocument {
16
+ /** Unique identifier for this document/chunk */
17
+ id: string;
18
+ /** Path to the source file, relative to the repo root */
19
+ filePath: string;
20
+ /** The text content */
21
+ content: string;
22
+ /** Chunk index within the file (0 for single-chunk files) */
23
+ chunkIndex: number;
24
+ /** Last modification time of the source file (epoch ms) */
25
+ mtimeMs: number;
26
+ /** 1-based line number where this chunk starts in the source file */
27
+ startLine: number;
28
+ /** Heading hierarchy at the start of this chunk */
29
+ headingBreadcrumb?: string[];
30
+ }
31
+ export interface SearchResult {
32
+ id: string;
33
+ filePath: string;
34
+ chunkIndex: number;
35
+ startLine: number;
36
+ score: number;
37
+ snippet: string;
38
+ headingBreadcrumb?: string[];
39
+ content: string;
40
+ }
41
+ export interface SearchOptions {
42
+ limit?: number;
43
+ scope?: Scope;
44
+ /** How matched terms should be marked in generated snippets. Defaults to
45
+ * "plain" (no markup) when omitted. */
46
+ highlight?: HighlightMode;
47
+ /** Repo root used to read each result's full source file from disk for
48
+ * snippet generation (so e.g. table-header pairing can see the real
49
+ * document, not just the matched chunk). When omitted, snippets fall
50
+ * back to chunk-only generation. */
51
+ repoRoot?: string;
52
+ }
53
+ export interface ExportedDocumentStore {
54
+ documents: IndexedDocument[];
55
+ index: ReturnType<MiniSearch<IndexedDocument>["toJSON"]>;
56
+ }
57
+ export declare class DocumentStore {
58
+ private documents;
59
+ private searchIndex;
60
+ private ready;
61
+ initialize(existingData?: ExportedDocumentStore): Promise<void>;
62
+ isReady(): boolean;
63
+ indexDocuments(docs: IndexedDocument[]): Promise<void>;
64
+ getDocument(id: string): Promise<IndexedDocument | null>;
65
+ deleteByFilePath(filePath: string): Promise<void>;
66
+ /** Search for documents using MiniSearch with BM25 scoring. */
67
+ search(query: string, options?: SearchOptions): Promise<SearchResult[]>;
68
+ getDocumentCount(): Promise<number>;
69
+ getAllFilePaths(): Promise<string[]>;
70
+ export(): ExportedDocumentStore;
71
+ }
72
+ //# sourceMappingURL=document-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-store.d.ts","sourceRoot":"","sources":["../../src/storage/document-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,gDAAgD;IAChD,EAAE,EAAE,MAAM,CAAC;IACX,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,KAAK,CAAC;IACd;2CACuC;IACvC,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B;;;wCAGoC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;CAC1D;AAYD,qBAAa,aAAa;IACxB,OAAO,CAAC,SAAS,CAA2C;IAC5D,OAAO,CAAC,WAAW,CAA4C;IAC/D,OAAO,CAAC,KAAK,CAAS;IAEhB,UAAU,CAAC,YAAY,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBrE,OAAO,IAAI,OAAO;IAIZ,cAAc,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBtD,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAIxD,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBvD,+DAA+D;IACzD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IA0C3E,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC;IAInC,eAAe,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAQ1C,MAAM,IAAI,qBAAqB;CAOhC"}