@zenera/rag 1.1.8 → 1.1.10

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 (42) hide show
  1. package/README.md +154 -10
  2. package/dist/command.js +2 -1
  3. package/dist/docs/assemble.d.ts +52 -0
  4. package/dist/docs/assemble.js +127 -0
  5. package/dist/docs/build.d.ts +34 -0
  6. package/dist/docs/build.js +108 -0
  7. package/dist/docs/chunk.d.ts +73 -0
  8. package/dist/docs/chunk.js +586 -0
  9. package/dist/docs/command.d.ts +3 -0
  10. package/dist/docs/command.js +529 -0
  11. package/dist/docs/files.d.ts +94 -0
  12. package/dist/docs/files.js +80 -0
  13. package/dist/docs/index.d.ts +13 -0
  14. package/dist/docs/index.js +13 -0
  15. package/dist/docs/load.d.ts +28 -0
  16. package/dist/docs/load.js +212 -0
  17. package/dist/docs/lookup.d.ts +80 -0
  18. package/dist/docs/lookup.js +147 -0
  19. package/dist/docs/parse.d.ts +95 -0
  20. package/dist/docs/parse.js +372 -0
  21. package/dist/docs/readme.d.ts +6 -0
  22. package/dist/docs/readme.js +122 -0
  23. package/dist/docs/render.d.ts +13 -0
  24. package/dist/docs/render.js +46 -0
  25. package/dist/docs/repl.d.ts +7 -0
  26. package/dist/docs/repl.js +130 -0
  27. package/dist/docs/search.d.ts +92 -0
  28. package/dist/docs/search.js +251 -0
  29. package/dist/docs/store.d.ts +55 -0
  30. package/dist/docs/store.js +171 -0
  31. package/dist/docs/tools.d.ts +10 -0
  32. package/dist/docs/tools.js +300 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +3 -0
  35. package/dist/schema/command.js +3 -0
  36. package/dist/schema/query.js +1 -0
  37. package/dist/schema/search.d.ts +2 -0
  38. package/dist/schema/search.js +18 -2
  39. package/dist/schema/store.d.ts +4 -2
  40. package/dist/schema/store.js +16 -9
  41. package/dist/schema/tools.js +21 -2
  42. package/package.json +17 -4
@@ -0,0 +1,372 @@
1
+ import remarkFrontmatter from 'remark-frontmatter';
2
+ import remarkGfm from 'remark-gfm';
3
+ import remarkParse from 'remark-parse';
4
+ import { unified } from 'unified';
5
+ /** Path segments carry their kind, so heading ancestors stay recoverable. */
6
+ const SEGMENT = {
7
+ heading: 'sec',
8
+ paragraph: 'para',
9
+ blockquote: 'quote',
10
+ list: 'list',
11
+ list_item: 'item',
12
+ table: 'table',
13
+ table_header: 'header',
14
+ table_row: 'row',
15
+ code: 'code',
16
+ frontmatter: 'fm',
17
+ html: 'html',
18
+ hr: 'hr',
19
+ };
20
+ /** The root of every path: a document is the structure everything else sits in. */
21
+ export const ROOT_SEGMENT = 'doc';
22
+ const processor = unified()
23
+ .use(remarkParse)
24
+ .use(remarkGfm)
25
+ .use(remarkFrontmatter, ['yaml', 'toml']);
26
+ // ---------------------------------------------------------------------------
27
+ /** Every ancestor of a section, the document root first, itself last. */
28
+ export function ancestry(section) {
29
+ const out = [];
30
+ for (let at = section; at; at = at.parent) {
31
+ out.unshift(at);
32
+ }
33
+ return out;
34
+ }
35
+ /** The breadcrumb, which prefixes every chunk of text under this section. */
36
+ export const headingPath = (section) => ancestry(section)
37
+ .map((s) => s.title)
38
+ .filter(Boolean)
39
+ .join(' > ');
40
+ /** The heading lines a chunk under this section must be rendered with. */
41
+ export const headingLines = (section) => ancestry(section)
42
+ .map((s) => s.line)
43
+ .filter((line) => line !== undefined);
44
+ /**
45
+ * Everything downstream indexes `lines` by number, and mdast positions refer to
46
+ * the exact string handed to the parser — so the normalization happens once,
47
+ * here, and the normalized text is the only version anything ever sees.
48
+ */
49
+ export const normalize = (text) => text.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
50
+ export function parseDocument(source, name, format) {
51
+ const text = normalize(source);
52
+ const lines = text.split('\n');
53
+ return format === 'text' ? plain(text, lines, name) : markdown(text, lines, name);
54
+ }
55
+ function markdown(text, lines, name) {
56
+ const tree = processor.parse(text);
57
+ const root = {
58
+ id: ROOT_SEGMENT,
59
+ path: ROOT_SEGMENT,
60
+ title: stripExtension(name),
61
+ level: 0,
62
+ line: undefined,
63
+ parent: undefined,
64
+ };
65
+ const walk = { lines, counters: new Map(), sections: [root], blocks: [] };
66
+ let section = root;
67
+ let title = '';
68
+ // Only ever holds the paragraph immediately before the current position,
69
+ // which is the whole of what a markdown table gets for a caption.
70
+ let lead = '';
71
+ for (const node of tree.children) {
72
+ if (node.type === 'heading') {
73
+ const heading = node;
74
+ const line = lineOf(heading);
75
+ const text = collapse(inline(heading.children));
76
+ while (section.parent && section.level >= heading.depth) {
77
+ section = section.parent;
78
+ }
79
+ section = {
80
+ id: id(walk, 'heading'),
81
+ path: '',
82
+ title: text,
83
+ level: heading.depth,
84
+ line,
85
+ parent: section,
86
+ };
87
+ section.path = `${section.parent.path}/${section.id}`;
88
+ walk.sections.push(section);
89
+ if (!title && heading.depth <= 2) {
90
+ title = text;
91
+ }
92
+ lead = '';
93
+ continue;
94
+ }
95
+ const block = blockOf(node, section, walk, lead);
96
+ if (block) {
97
+ walk.blocks.push(block);
98
+ lead = block.kind === 'paragraph' ? block.text : '';
99
+ }
100
+ }
101
+ return {
102
+ name,
103
+ title: title || stripExtension(basenameOf(name)),
104
+ format: 'markdown',
105
+ lines,
106
+ sections: walk.sections,
107
+ blocks: walk.blocks,
108
+ };
109
+ }
110
+ function blockOf(node, section, walk, lead) {
111
+ const start = lineOf(node);
112
+ const end = endLineOf(node);
113
+ switch (node.type) {
114
+ case 'paragraph':
115
+ return make('paragraph', collapse(inline(node.children)));
116
+ case 'blockquote':
117
+ return make('blockquote', collapse(blocks(node.children)));
118
+ case 'code': {
119
+ const code = node;
120
+ return make('code', code.value);
121
+ }
122
+ case 'html': {
123
+ const stripped = collapse(tags(node.value));
124
+ return stripped.length >= 3 ? make('html', stripped) : undefined;
125
+ }
126
+ case 'yaml':
127
+ return make('frontmatter', String(node.value ?? ''));
128
+ case 'thematicBreak':
129
+ return make('hr', '');
130
+ case 'list':
131
+ return list(node, section, walk, start, end);
132
+ case 'table':
133
+ return table(node, section, walk, start, end, lead);
134
+ default:
135
+ // A `+++` block. remark-frontmatter puts it in the tree; mdast's own
136
+ // node union names only the `---` one.
137
+ return node.type === 'toml'
138
+ ? make('frontmatter', String(node.value ?? ''))
139
+ : undefined;
140
+ }
141
+ function make(kind, text) {
142
+ const own = id(walk, kind);
143
+ return { id: own, path: `${section.path}/${own}`, kind, start, end, section, text };
144
+ }
145
+ }
146
+ function list(node, section, walk, start, end) {
147
+ const own = id(walk, 'list');
148
+ const path = `${section.path}/${own}`;
149
+ const ordinal = node.start ?? 1;
150
+ const items = node.children.map((child, at) => {
151
+ const item = child;
152
+ const mine = id(walk, 'list_item');
153
+ const marker = node.ordered ? `${ordinal + at}.` : '-';
154
+ // The marker is kept: enumeration and step order are meaning, and a
155
+ // numbered step reads differently from a bullet.
156
+ return {
157
+ id: mine,
158
+ path: `${path}/${mine}`,
159
+ start: lineOf(item),
160
+ end: endLineOf(item),
161
+ text: `${marker} ${collapse(blocks(item.children))}`.trim(),
162
+ };
163
+ });
164
+ return {
165
+ id: own,
166
+ path,
167
+ kind: 'list',
168
+ start,
169
+ end,
170
+ section,
171
+ text: items.map((i) => i.text).join('\n'),
172
+ items,
173
+ };
174
+ }
175
+ function table(node, section, walk, start, end, caption) {
176
+ const own = id(walk, 'table');
177
+ const path = `${section.path}/${own}`;
178
+ const [header, ...body] = node.children;
179
+ const columns = header ? cellsOf(header) : [];
180
+ const headerLine = header ? lineOf(header) : start;
181
+ const firstRow = body[0] ? lineOf(body[0]) : undefined;
182
+ // mdast has no node for the alignment row — alignment lives on the table —
183
+ // so it is the line after the header, when there is room for it.
184
+ const separatorLine = firstRow === undefined || firstRow > headerLine + 1 ? headerLine + 1 : undefined;
185
+ const rows = body.map((row) => {
186
+ const mine = id(walk, 'table_row');
187
+ const cells = cellsOf(row);
188
+ return {
189
+ id: mine,
190
+ path: `${path}/${mine}`,
191
+ line: lineOf(row),
192
+ cells,
193
+ text: rowText(columns, cells),
194
+ };
195
+ });
196
+ return {
197
+ id: own,
198
+ path,
199
+ kind: 'table',
200
+ start,
201
+ end,
202
+ section,
203
+ text: rows.map((r) => r.text).join('\n'),
204
+ table: {
205
+ columns,
206
+ headerLine,
207
+ separatorLine,
208
+ caption,
209
+ keyColumn: keyColumnOf(columns, rows),
210
+ rows,
211
+ },
212
+ };
213
+ }
214
+ /**
215
+ * A data row does not contain its own column names. Stripping the pipes leaves
216
+ * `V-200-30 3" WCC 98.2`, in which the word `Cv` appears nowhere, so a query
217
+ * naming a column could only ever match the header line and never a row — and
218
+ * hybrid search would silently become vector-only for every table in the
219
+ * corpus. Pairing each cell with its header is what fixes that, and it embeds
220
+ * better too, because `Cv: 45` is a sentence and `| 45 |` is not.
221
+ */
222
+ export function rowText(columns, cells) {
223
+ return cells
224
+ .map((cell, at) => cellText(columns[at], cell))
225
+ .filter(Boolean)
226
+ .join(' ');
227
+ }
228
+ export function cellText(column, cell) {
229
+ const value = cell.trim();
230
+ if (!value) {
231
+ return '';
232
+ }
233
+ return column ? `${column}: ${value}.` : `${value}.`;
234
+ }
235
+ /** Column 1, unless it is numeric — a slice with no name on it is unattributable. */
236
+ function keyColumnOf(columns, rows) {
237
+ const numeric = (at) => rows.length > 0 &&
238
+ rows.every((row) => {
239
+ const cell = (row.cells[at] ?? '').trim();
240
+ return cell === '' || /^[-+]?[\d.,%\s]+$/.test(cell);
241
+ });
242
+ for (let at = 0; at < columns.length; at++) {
243
+ if (!numeric(at)) {
244
+ return at;
245
+ }
246
+ }
247
+ return 0;
248
+ }
249
+ const cellsOf = (row) => row.children.map((cell) => collapse(inline(cell.children)));
250
+ // ---------------------------------------------------------------------------
251
+ // plain text
252
+ // ---------------------------------------------------------------------------
253
+ /**
254
+ * A `.txt` file is read as paragraphs and nothing else. It would be easy to run
255
+ * it through the markdown parser and get headings for free, but they would be
256
+ * invented: a line beginning with `#` in a log or a licence is not a title, and
257
+ * an index that says it is would scope searches to sections nobody wrote.
258
+ */
259
+ function plain(text, lines, name) {
260
+ const root = {
261
+ id: ROOT_SEGMENT,
262
+ path: ROOT_SEGMENT,
263
+ title: stripExtension(name),
264
+ level: 0,
265
+ line: undefined,
266
+ parent: undefined,
267
+ };
268
+ const blocks = [];
269
+ let at = 0;
270
+ let ordinal = 0;
271
+ while (at < lines.length) {
272
+ if ((lines[at] ?? '').trim() === '') {
273
+ at++;
274
+ continue;
275
+ }
276
+ const start = at;
277
+ while (at < lines.length && (lines[at] ?? '').trim() !== '') {
278
+ at++;
279
+ }
280
+ const own = `${SEGMENT.paragraph}:${++ordinal}`;
281
+ blocks.push({
282
+ id: own,
283
+ path: `${root.path}/${own}`,
284
+ kind: 'paragraph',
285
+ start: start + 1,
286
+ end: at,
287
+ section: root,
288
+ text: collapse(lines.slice(start, at).join(' ')),
289
+ });
290
+ }
291
+ const first = lines.find((line) => line.trim() !== '')?.trim() ?? '';
292
+ return {
293
+ name,
294
+ title: first.length > 0 && first.length <= 80 ? first : stripExtension(basenameOf(name)),
295
+ format: 'text',
296
+ lines,
297
+ sections: [root],
298
+ blocks,
299
+ };
300
+ }
301
+ // ---------------------------------------------------------------------------
302
+ // serialization — walking the inline tree, never a regex over the markup
303
+ // ---------------------------------------------------------------------------
304
+ function inline(nodes) {
305
+ return nodes.map(one).join('');
306
+ }
307
+ function one(node) {
308
+ switch (node.type) {
309
+ case 'text':
310
+ return node.value;
311
+ // The value, not the markup: this is the node a regex stripper eats.
312
+ case 'inlineCode':
313
+ return node.value;
314
+ case 'image':
315
+ case 'imageReference':
316
+ // Distinguishable from a sentence on purpose.
317
+ return node.alt ? `image: ${node.alt}` : '';
318
+ case 'break':
319
+ return ' ';
320
+ // Inline html carries no words worth indexing, and its angle brackets
321
+ // would only ever match a query by accident.
322
+ case 'html':
323
+ return '';
324
+ case 'footnoteReference':
325
+ return '';
326
+ default:
327
+ return 'children' in node ? inline(node.children) : '';
328
+ }
329
+ }
330
+ /** Nested block content — a blockquote's paragraphs, a list item's children. */
331
+ function blocks(nodes) {
332
+ return nodes
333
+ .map((node) => {
334
+ switch (node.type) {
335
+ case 'paragraph':
336
+ case 'heading':
337
+ return inline(node.children);
338
+ case 'code':
339
+ return node.value;
340
+ case 'blockquote':
341
+ return blocks(node.children);
342
+ case 'list':
343
+ return node.children
344
+ .map((item) => blocks(item.children))
345
+ .join(' ');
346
+ case 'table':
347
+ return node.children
348
+ .slice(1)
349
+ .map((row) => rowText(cellsOf(node.children[0]), cellsOf(row)))
350
+ .join(' ');
351
+ default:
352
+ return '';
353
+ }
354
+ })
355
+ .filter(Boolean)
356
+ .join(' ');
357
+ }
358
+ /** Soft wraps inside a paragraph are wrapping, not meaning, so they rejoin. */
359
+ export const collapse = (text) => text.replace(/\s+/g, ' ').trim();
360
+ const tags = (html) => html.replace(/<[^>]*>/g, ' ');
361
+ // ---------------------------------------------------------------------------
362
+ function id(walk, kind) {
363
+ const segment = SEGMENT[kind];
364
+ const next = (walk.counters.get(segment) ?? 0) + 1;
365
+ walk.counters.set(segment, next);
366
+ return `${segment}:${next}`;
367
+ }
368
+ const lineOf = (node) => node.position?.start.line ?? 1;
369
+ const endLineOf = (node) => node.position?.end.line ?? 1;
370
+ const basenameOf = (name) => name.split('/').pop() ?? name;
371
+ const stripExtension = (name) => name.replace(/\.[^./]+$/, '');
372
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1,6 @@
1
+ import { type Report } from '../common/progress.ts';
2
+ import type { Counts, Manifest } from './files.ts';
3
+ export type Phase = 'reading' | 'embedding' | 'writing';
4
+ export declare const PHASES: Record<Phase, string>;
5
+ export declare const DOCS_REPORT: Report<Counts, Manifest>;
6
+ //# sourceMappingURL=readme.d.ts.map
@@ -0,0 +1,122 @@
1
+ import { basename } from 'node:path';
2
+ import { INTERVAL_MS, } from "../common/progress.js";
3
+ import { duration, fields, grid, message, plural, searched } from "../common/prose.js";
4
+ export const PHASES = {
5
+ reading: 'reading the documents and cutting them into chunks',
6
+ embedding: 'embedding',
7
+ writing: 'writing the store',
8
+ };
9
+ export const DOCS_REPORT = { building, complete, failed };
10
+ function building(state) {
11
+ const rows = [
12
+ ['documents', summary(state.documents)],
13
+ ['embedding', state.embedding],
14
+ [
15
+ 'started',
16
+ `${new Date(state.started).toISOString()} (${duration(state.now - state.started)} ago)`,
17
+ ],
18
+ ['step', state.step],
19
+ ];
20
+ if (state.summary) {
21
+ rows.push(['found', counted(state.summary)]);
22
+ }
23
+ if (state.total > 0) {
24
+ const percent = Math.round((state.done / state.total) * 100);
25
+ rows.push(['embedded', `${state.done} of ${state.total} · ${percent}%`]);
26
+ }
27
+ rows.push(['updated', new Date(state.now).toISOString()]);
28
+ return [
29
+ '# Document index — being built',
30
+ '',
31
+ 'A searchable index of the documents named below, written by `zen rag docs index`.',
32
+ '**It is incomplete. Nothing should read it yet.**',
33
+ '',
34
+ ...fields(rows),
35
+ '',
36
+ `These lines are refreshed at most every ${INTERVAL_MS / 1000} seconds while the build runs, and`,
37
+ 'the whole file is replaced by a description of the index when it finishes. If it still says',
38
+ '"being built" and `.lock` names no living process, the build died part way.',
39
+ '',
40
+ ].join('\n');
41
+ }
42
+ function complete(state) {
43
+ const { manifest } = state;
44
+ return [
45
+ `# Document index — ${basename(state.dir)}`,
46
+ '',
47
+ `A searchable index of ${plural(manifest.sources.length, 'document')}, built with`,
48
+ `${manifest.embedding.ref} (${manifest.embedding.dimensions}d) in ${duration(state.ms)}.`,
49
+ 'Ask it a question and it answers with the passages that matched, quoted verbatim from',
50
+ 'the copies kept in `sources/` — line numbers included, and with a marker wherever',
51
+ 'something between two of them was left out.',
52
+ '',
53
+ '## What it covers',
54
+ '',
55
+ ...documentTable(manifest.sources),
56
+ '',
57
+ `${counted(manifest.counts)},`,
58
+ `${searched(manifest.indexes)}.`,
59
+ '',
60
+ '## Files',
61
+ '',
62
+ ...fields([
63
+ ['manifest.json', 'what this index is and what built it — read this first'],
64
+ ['outline.json', 'every heading and table, with the lines they cover'],
65
+ ['sources/', 'the documents themselves, verbatim: where the quotes come from'],
66
+ ['lance/', 'the chunks: the search text, the vectors, the filter columns'],
67
+ ]),
68
+ '',
69
+ '## Asking it something',
70
+ '',
71
+ 'From this directory:',
72
+ '',
73
+ '```',
74
+ 'zen rag docs search --dir . "what you are after"',
75
+ 'zen rag docs search --dir . --file "guides/**" --kind table "pressure rating"',
76
+ 'zen rag docs list files --dir .',
77
+ '```',
78
+ '',
79
+ `Built by ${manifest.indexer} on ${manifest.createdAt}.`,
80
+ '',
81
+ ].join('\n');
82
+ }
83
+ function failed(state) {
84
+ return [
85
+ '# Document index — failed',
86
+ '',
87
+ 'This index was not finished and what is here is incomplete. Nothing should read it;',
88
+ 'build it again with `zen rag docs index`.',
89
+ '',
90
+ ...fields([
91
+ ['documents', summary(state.documents)],
92
+ ['step', state.step],
93
+ ['reason', message(state.reason)],
94
+ ['started', new Date(state.started).toISOString()],
95
+ [
96
+ 'failed',
97
+ `${new Date().toISOString()} (after ${duration(Date.now() - state.started)})`,
98
+ ],
99
+ ]),
100
+ '',
101
+ ].join('\n');
102
+ }
103
+ // ---------------------------------------------------------------------------
104
+ const HEADERS = ['document', 'format', 'lines', 'sections', 'tables', 'chunks'];
105
+ const documentTable = (sources) => grid(HEADERS, sources.map((s) => [
106
+ s.name,
107
+ s.format,
108
+ String(s.lines),
109
+ String(s.sections),
110
+ String(s.tables),
111
+ String(s.chunks),
112
+ ]));
113
+ function counted(counts) {
114
+ return (`${plural(counts.chunks, 'chunk')} over ${plural(counts.documents, 'document')}: ` +
115
+ `${counts.lines} lines, ${counts.sections} sections, ${counts.tables} tables`);
116
+ }
117
+ /** A corpus can be thousands of files; a README listing them all is a wall. */
118
+ function summary(documents) {
119
+ const shown = documents.slice(0, 12).join(', ');
120
+ return documents.length > 12 ? `${shown}, and ${documents.length - 12} more` : shown;
121
+ }
122
+ //# sourceMappingURL=readme.js.map
@@ -0,0 +1,13 @@
1
+ import type { Assembly, Excerpt } from './assemble.ts';
2
+ import type { Match } from './search.ts';
3
+ export interface RenderOptions {
4
+ /** the line-number gutter; on unless something else is going to eat this */
5
+ numbers?: boolean;
6
+ colour?: boolean;
7
+ }
8
+ export declare function renderAssembly(assembly: Assembly, options?: RenderOptions): string;
9
+ export declare function renderExcerpt(file: Excerpt, options?: RenderOptions): string[];
10
+ /** The one-line-per-match view, for `--quiet` and for the prompt loop. */
11
+ export declare const matchRows: (matches: readonly Match[]) => string[][];
12
+ export declare const MATCH_HEADERS: readonly ["id", "kind", "lines", "score", "vec / txt", "heading"];
13
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1,46 @@
1
+ import { bold, dim } from '@zenera/cli/lib';
2
+ export function renderAssembly(assembly, options = {}) {
3
+ const out = [];
4
+ for (const file of assembly.files) {
5
+ out.push(...renderExcerpt(file, options), '');
6
+ }
7
+ if (assembly.truncated) {
8
+ out.push('(cut short by the line budget — raise it with --max-lines)');
9
+ }
10
+ return out.join('\n').trimEnd();
11
+ }
12
+ export function renderExcerpt(file, options = {}) {
13
+ const paint = options.colour === false ? (s) => s : undefined;
14
+ const strong = paint ?? bold;
15
+ const faint = paint ?? dim;
16
+ const width = String(file.lines).length;
17
+ return [
18
+ `## ${strong(file.path)} ${faint(`— ${file.shown} of ${file.lines} lines`)}`,
19
+ '',
20
+ ...file.pieces.flatMap((piece) => renderPiece(piece, width, options, faint)),
21
+ ];
22
+ }
23
+ function renderPiece(piece, width, options, faint) {
24
+ if (piece.type === 'omission') {
25
+ const named = piece.sections.length > 0 ? ` (${piece.sections.join(', ')})` : '';
26
+ return [faint(`... ${piece.count} lines omitted${named} ...`), ''];
27
+ }
28
+ const lines = piece.lines.map((line, at) => options.numbers === false
29
+ ? line
30
+ : `${faint(String(piece.start + at).padStart(width))} ${faint('|')} ${line}`);
31
+ return [...lines, ''];
32
+ }
33
+ /** The one-line-per-match view, for `--quiet` and for the prompt loop. */
34
+ export const matchRows = (matches) => matches.map((m) => [
35
+ m.id,
36
+ m.kind,
37
+ `${m.bodyStart}-${m.bodyEnd}`,
38
+ m.score.toFixed(4),
39
+ // Fusion ranks; these say whether the rank was worth anything.
40
+ [m.relevance.vector?.toFixed(2), m.relevance.text?.toFixed(1)]
41
+ .map((v) => v ?? '·')
42
+ .join(' / '),
43
+ m.headings.split('\n')[0] ?? '',
44
+ ]);
45
+ export const MATCH_HEADERS = ['id', 'kind', 'lines', 'score', 'vec / txt', 'heading'];
46
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1,7 @@
1
+ import { type AssembleOptions } from './assemble.ts';
2
+ import { type DocsIndex, type DocsQuery } from './search.ts';
3
+ export interface ReplSettings extends AssembleOptions {
4
+ quiet?: boolean;
5
+ }
6
+ export declare function repl(index: DocsIndex, initial: DocsQuery, settings?: ReplSettings): Promise<void>;
7
+ //# sourceMappingURL=repl.d.ts.map