@zenera/rag 1.1.24 → 1.1.25
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/README.md +15 -7
- package/dist/common/progress.js +6 -48
- package/dist/docs/command.js +93 -12
- package/dist/docs/load.js +1 -1
- package/dist/docs/lookup.d.ts +54 -1
- package/dist/docs/lookup.js +120 -1
- package/dist/docs/render.d.ts +6 -0
- package/dist/docs/render.js +33 -0
- package/dist/docs/search.d.ts +1 -1
- package/dist/docs/search.js +1 -1
- package/dist/docs/targets.d.ts +7 -0
- package/dist/docs/targets.js +52 -0
- package/dist/docs/tools.js +69 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/schema/command.js +1 -1
- package/dist/schema/lookup.d.ts +1 -1
- package/dist/schema/lookup.js +1 -1
- package/dist/schema/tools.js +1 -2
- package/dist/schema/trace.d.ts +1 -1
- package/package.json +3 -3
- package/dist/common/match.d.ts +0 -40
- package/dist/common/match.js +0 -92
package/README.md
CHANGED
|
@@ -313,7 +313,7 @@ zen rag docs ready Can this index answer? Exit 0 if it can, 3 if i
|
|
|
313
313
|
zen rag docs search [text] Ask it something. --interactive for a prompt.
|
|
314
314
|
zen rag docs list <what> Every document, section or table. No ranking.
|
|
315
315
|
zen rag docs grep <pattern> Every matching line, with the section it sits in.
|
|
316
|
-
zen rag docs show <
|
|
316
|
+
zen rag docs show <target...> Documents, sections or line ranges, several in one call.
|
|
317
317
|
zen rag docs stats What is in an index, and what built it.
|
|
318
318
|
```
|
|
319
319
|
|
|
@@ -377,11 +377,19 @@ zen rag docs list tables # every table, with its columns
|
|
|
377
377
|
zen rag docs grep "Retry-After" # every matching line, and its section
|
|
378
378
|
zen rag docs show api/routing.md --section "Rate limits"
|
|
379
379
|
zen rag docs show api/routing.md --lines 40-80
|
|
380
|
+
zen rag docs show "api/routing.md:24-31" "api/errors.md#Retries"
|
|
380
381
|
```
|
|
381
382
|
|
|
382
383
|
`grep` counts every match, not the top of a list, so unlike a search it can
|
|
383
384
|
answer whether a string appears at all.
|
|
384
385
|
|
|
386
|
+
`show` takes any number of targets written the way a citation is -
|
|
387
|
+
`<document>:<a>-<b>`, `<document>:<a>`, `<document>#<heading>`, or a bare name -
|
|
388
|
+
and prints each under a header that is itself a target, so reading every
|
|
389
|
+
passage an answer cites costs one call. Overlapping ranges print once, a
|
|
390
|
+
missing target is reported in place, and `--max-lines` (default 400) is shared
|
|
391
|
+
between them, each cut block naming the target that continues it.
|
|
392
|
+
|
|
385
393
|
<details>
|
|
386
394
|
<summary>Every flag</summary>
|
|
387
395
|
|
|
@@ -559,12 +567,12 @@ Only the first ranks. `find_types_with_property` is the one for the repair loop
|
|
|
559
567
|
|
|
560
568
|
Four tools in the group `docs`, selectable as `docs:*`:
|
|
561
569
|
|
|
562
|
-
| Tool | For
|
|
563
|
-
| ------------- |
|
|
564
|
-
| `search_docs` | the passages that match, quoted with their line numbers
|
|
565
|
-
| `list_docs` | the documents, their headings, or their tables - no search
|
|
566
|
-
| `grep_docs` | every matching line, counted in full - no search
|
|
567
|
-
| `read_docs` | a section or a line range, verbatim
|
|
570
|
+
| Tool | For |
|
|
571
|
+
| ------------- | ---------------------------------------------------------------- |
|
|
572
|
+
| `search_docs` | the passages that match, quoted with their line numbers |
|
|
573
|
+
| `list_docs` | the documents, their headings, or their tables - no search |
|
|
574
|
+
| `grep_docs` | every matching line, counted in full - no search |
|
|
575
|
+
| `read_docs` | a section or a line range, verbatim; several at once via `reads` |
|
|
568
576
|
|
|
569
577
|
`search_docs` is the way in when the question is vague; `grep_docs` is how "it
|
|
570
578
|
is not in here" can actually be concluded. Every answer carries line numbers and
|
package/dist/common/progress.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CliError, EXIT } from '@zenera/cli/lib';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { claimLock, ownLock } from '@zenera/neo';
|
|
3
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// A build that says what it is doing
|
|
@@ -32,14 +32,12 @@ export const INTERVAL_MS = 5000;
|
|
|
32
32
|
export function beginBuild(plan) {
|
|
33
33
|
const documents = plan.files.map((file) => basename(file));
|
|
34
34
|
const started = Date.now();
|
|
35
|
-
const lock = {
|
|
36
|
-
pid: process.pid,
|
|
37
|
-
host: hostname(),
|
|
35
|
+
const lock = ownLock({
|
|
38
36
|
startedAt: new Date(started).toISOString(),
|
|
39
37
|
indexer: plan.indexer,
|
|
40
38
|
embedding: plan.embedding,
|
|
41
39
|
documents,
|
|
42
|
-
};
|
|
40
|
+
});
|
|
43
41
|
mkdirSync(plan.dir, { recursive: true });
|
|
44
42
|
claim(join(plan.dir, LOCK_FILE), lock);
|
|
45
43
|
let phase = Object.keys(plan.phases)[0];
|
|
@@ -140,48 +138,8 @@ export function beginBuild(plan) {
|
|
|
140
138
|
},
|
|
141
139
|
};
|
|
142
140
|
}
|
|
143
|
-
/**
|
|
144
|
-
* `wx` makes the create and the check one operation, so two builds racing for
|
|
145
|
-
* the same directory cannot both win. A lock whose process is gone is stale by
|
|
146
|
-
* definition and is taken rather than respected — a crashed build must not make
|
|
147
|
-
* a directory permanently unbuildable.
|
|
148
|
-
*/
|
|
141
|
+
/** Two builds writing one index would interleave their store writes. */
|
|
149
142
|
function claim(path, lock) {
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
writeFileSync(path, body, { flag: 'wx' });
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
if (err.code !== 'EEXIST') {
|
|
157
|
-
throw err;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const held = readLock(path);
|
|
161
|
-
if (held && held.host === hostname() && alive(held.pid)) {
|
|
162
|
-
throw new CliError(`this index is already being built (pid ${held.pid}, since ${held.startedAt})`, EXIT.failed, `wait for it, or build elsewhere with --out`);
|
|
163
|
-
}
|
|
164
|
-
writeFileSync(path, body);
|
|
165
|
-
}
|
|
166
|
-
function readLock(path) {
|
|
167
|
-
try {
|
|
168
|
-
return JSON.parse(readFileSync(path, 'utf8'));
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* `kill(pid, 0)` sends no signal and only asks whether the process exists.
|
|
176
|
-
* EPERM means it exists and belongs to someone else, which still counts.
|
|
177
|
-
*/
|
|
178
|
-
function alive(pid) {
|
|
179
|
-
try {
|
|
180
|
-
process.kill(pid, 0);
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
|
-
catch (err) {
|
|
184
|
-
return err.code === 'EPERM';
|
|
185
|
-
}
|
|
143
|
+
claimLock(path, lock, (held) => new CliError(`this index is already being built (pid ${held.pid}, since ${held.startedAt})`, EXIT.failed, `wait for it, or build elsewhere with --out`));
|
|
186
144
|
}
|
|
187
145
|
//# sourceMappingURL=progress.js.map
|
package/dist/docs/command.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { bold, CliError, cyan, dim, EXIT, isInteractive, json, note, parse, paths, table, usageError, write, } from '@zenera/cli/lib';
|
|
2
|
+
import { PatternError } from '@zenera/neo';
|
|
2
3
|
import { existsSync } from 'node:fs';
|
|
3
4
|
import { join, relative, resolve } from 'node:path';
|
|
4
5
|
import { resolveEmbedder } from "../common/embedder.js";
|
|
5
6
|
import { locateIndex, outputDir } from "../common/locate.js";
|
|
6
7
|
import { assertSameEmbedding, inspectIndex } from "../common/manifest.js";
|
|
7
|
-
import { PatternError } from "../common/match.js";
|
|
8
8
|
import { INTERVAL_MS } from "../common/progress.js";
|
|
9
9
|
import { breakdown, grid, indexing } from "../common/prose.js";
|
|
10
10
|
import { announce, reportReady, stagingFor, swapIn } from "../common/restore.js";
|
|
@@ -13,11 +13,12 @@ import { buildIndex } from "./build.js";
|
|
|
13
13
|
import { CHUNK_KINDS } from "./chunk.js";
|
|
14
14
|
import { DOCS_INDEX, readManifest, SOURCES_DIR } from "./files.js";
|
|
15
15
|
import { DOC_EXTENSIONS } from "./load.js";
|
|
16
|
-
import { grepLines, listFiles, listSections, listTables, readRange, readSection, } from "./lookup.js";
|
|
17
|
-
import { MATCH_HEADERS, matchRows, renderAssembly } from "./render.js";
|
|
16
|
+
import { grepLines, isFailure, listFiles, listSections, listTables, readMany, readRange, readSection, spanOf, } from "./lookup.js";
|
|
17
|
+
import { MATCH_HEADERS, matchRows, renderAssembly, renderReads } from "./render.js";
|
|
18
18
|
import { repl } from "./repl.js";
|
|
19
19
|
import { DEFAULT_LIMIT, DocsIndex, legsOf, SEARCH_MODES, } from "./search.js";
|
|
20
20
|
import { ChunkStore } from "./store.js";
|
|
21
|
+
import { parseTarget, selects, TargetError } from "./targets.js";
|
|
21
22
|
const { defaultDir: DEFAULT_DIR, envName: DIR_ENV } = DOCS_INDEX;
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// zen rag docs — a pile of documents, as something to search
|
|
@@ -49,7 +50,7 @@ const READY_USAGE = 'zen rag docs ready [--dir <dir>] [--quiet]';
|
|
|
49
50
|
const SEARCH_USAGE = 'zen rag docs search [--dir <dir>] [query... | --text-query <text> --vector-query <text>]';
|
|
50
51
|
const LIST_USAGE = 'zen rag docs list <files|sections|tables> [--dir <dir>]';
|
|
51
52
|
const GREP_USAGE = 'zen rag docs grep <pattern> [--dir <dir>]';
|
|
52
|
-
const SHOW_USAGE = 'zen rag docs show <file> [--section <name>] [--lines <from-to>]';
|
|
53
|
+
const SHOW_USAGE = 'zen rag docs show <file> [--section <name>] [--lines <from-to>] | <target>... [--max-lines <n>]';
|
|
53
54
|
export const command = {
|
|
54
55
|
summary: 'Search a pile of markdown and text documents.',
|
|
55
56
|
usage: USAGE,
|
|
@@ -78,8 +79,8 @@ export const command = {
|
|
|
78
79
|
dim('Find every literal or regex line match; use it to prove whether text exists.'),
|
|
79
80
|
],
|
|
80
81
|
[
|
|
81
|
-
' show <
|
|
82
|
-
dim('Read
|
|
82
|
+
' show <target...>',
|
|
83
|
+
dim('Read documents, headings or line ranges verbatim, several in one call.'),
|
|
83
84
|
],
|
|
84
85
|
[' stats', dim('Inspect the index contents, size and embedding configuration.')],
|
|
85
86
|
]),
|
|
@@ -197,9 +198,20 @@ export const command = {
|
|
|
197
198
|
[' <file>', dim('A document name, as `list files` prints it.')],
|
|
198
199
|
[' --section <name>', dim('Just that heading and what nests under it.')],
|
|
199
200
|
[' --lines <from-to>', dim('Just those lines, e.g. --lines 40-80.')],
|
|
201
|
+
[' <doc>:<a>-<b>', dim('Lines a to b. <doc>:<a> is one line.')],
|
|
202
|
+
[' <doc>#<heading>', dim('A heading and what nests under it.')],
|
|
203
|
+
[
|
|
204
|
+
' --max-lines <n>',
|
|
205
|
+
dim(`A ceiling shared by every target. Default ${DEFAULT_MAX_LINES}.`),
|
|
206
|
+
],
|
|
200
207
|
[' --no-numbers', dim('Without the line-number gutter.')],
|
|
201
208
|
]),
|
|
202
209
|
'',
|
|
210
|
+
dim(' Name every passage you will read in one call, as the search printed them:'),
|
|
211
|
+
dim(` ${cyan('zen rag docs show "api/routing.md:24-60" "api/limits.md#Rate limits"')}`),
|
|
212
|
+
dim(' Each prints under a header that is itself a target, in the order given;'),
|
|
213
|
+
dim(' overlapping ranges print once. --section and --lines take one document.'),
|
|
214
|
+
'',
|
|
203
215
|
dim(`Without --dir, the index is the nearest one at or above the working`),
|
|
204
216
|
dim(`directory; ${cyan(DIR_ENV)} names it outright.`),
|
|
205
217
|
'',
|
|
@@ -615,32 +627,56 @@ async function show(args, ctx) {
|
|
|
615
627
|
dir: { type: 'string', short: 'd' },
|
|
616
628
|
section: { type: 'string', short: 's' },
|
|
617
629
|
lines: { type: 'string' },
|
|
630
|
+
'max-lines': { type: 'string' },
|
|
618
631
|
'no-numbers': { type: 'boolean' },
|
|
619
632
|
quiet: { type: 'boolean' },
|
|
620
633
|
}, SHOW_USAGE);
|
|
621
|
-
|
|
622
|
-
if (!name) {
|
|
634
|
+
if (positionals.length === 0) {
|
|
623
635
|
throw usageError('no document named', SHOW_USAGE);
|
|
624
636
|
}
|
|
637
|
+
const flagged = values.section !== undefined || values.lines !== undefined;
|
|
638
|
+
if (flagged && positionals.length > 1) {
|
|
639
|
+
throw usageError('--section and --lines read one document', 'name each part instead: <document>:<a>-<b> or <document>#<heading>');
|
|
640
|
+
}
|
|
641
|
+
const span = values.lines ? range(values.lines) : undefined;
|
|
642
|
+
const maxLines = values['max-lines'] ? count(values['max-lines'], '--max-lines') : undefined;
|
|
625
643
|
const found = await open(ctx, values.dir);
|
|
626
644
|
try {
|
|
645
|
+
const names = new Set(found.manifest.sources.map((s) => s.name));
|
|
646
|
+
const several = !flagged && (positionals.length > 1 || targeted(() => selects(positionals[0], names)));
|
|
647
|
+
if (several) {
|
|
648
|
+
const reads = targeted(() => positionals.map((raw) => parseTarget(raw, names)));
|
|
649
|
+
await showMany(ctx, found, reads, values, maxLines ?? DEFAULT_MAX_LINES);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const name = positionals[0];
|
|
627
653
|
const file = found.resolveFiles([name])[0];
|
|
628
654
|
if (!file) {
|
|
629
655
|
throw new CliError(`no document called ${name}`, EXIT.failed, 'list them with `zen rag docs list files`');
|
|
630
656
|
}
|
|
631
|
-
const
|
|
632
|
-
const excerpt = values.section
|
|
657
|
+
const whole = values.section
|
|
633
658
|
? await readSection(found, file, values.section)
|
|
634
659
|
: await readRange(found, file, span?.[0] ?? 1, span?.[1] ?? Infinity);
|
|
660
|
+
// Uncapped unless asked: this form printed whole documents before the cap existed.
|
|
661
|
+
const cut = maxLines !== undefined && whole.lines.length > maxLines;
|
|
662
|
+
const excerpt = cut
|
|
663
|
+
? { ...whole, lines: whole.lines.slice(0, maxLines), end: whole.start + maxLines - 1 }
|
|
664
|
+
: whole;
|
|
635
665
|
if (ctx.json) {
|
|
636
|
-
json(excerpt);
|
|
666
|
+
json(cut ? { ...excerpt, truncated: true } : excerpt);
|
|
637
667
|
return;
|
|
638
668
|
}
|
|
639
|
-
const width = String(
|
|
669
|
+
const width = String(whole.end).length;
|
|
640
670
|
write(excerpt.lines
|
|
641
671
|
.map((line, at) => values['no-numbers']
|
|
642
672
|
? line
|
|
643
673
|
: `${dim(String(excerpt.start + at).padStart(width))} ${dim('|')} ${line}`)
|
|
674
|
+
.concat(cut
|
|
675
|
+
? [
|
|
676
|
+
dim(`... truncated at line ${excerpt.end} of ${whole.end} - read ` +
|
|
677
|
+
`${spanOf(file, excerpt.end + 1, whole.end)} to continue`),
|
|
678
|
+
]
|
|
679
|
+
: [])
|
|
644
680
|
.join('\n'));
|
|
645
681
|
if (!values.quiet) {
|
|
646
682
|
note(dim(` ${file} · lines ${excerpt.start}-${excerpt.end} of ${excerpt.total}`));
|
|
@@ -650,6 +686,51 @@ async function show(args, ctx) {
|
|
|
650
686
|
found.close();
|
|
651
687
|
}
|
|
652
688
|
}
|
|
689
|
+
async function showMany(ctx, found, reads, values, maxLines) {
|
|
690
|
+
const read = await patterned(() => readMany(found, reads, { maxLines }));
|
|
691
|
+
if (ctx.json) {
|
|
692
|
+
json({
|
|
693
|
+
results: read.results.map((result) => isFailure(result)
|
|
694
|
+
? result
|
|
695
|
+
: {
|
|
696
|
+
target: result.target,
|
|
697
|
+
file: result.file,
|
|
698
|
+
from: result.from,
|
|
699
|
+
to: result.to,
|
|
700
|
+
section: result.section ?? null,
|
|
701
|
+
text: result.lines.join('\n'),
|
|
702
|
+
truncated: result.truncated,
|
|
703
|
+
...(result.continue ? { continue: result.continue } : {}),
|
|
704
|
+
}),
|
|
705
|
+
printed: read.printed,
|
|
706
|
+
failed: read.failed,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
write(renderReads(read, { numbers: !values['no-numbers'] }));
|
|
711
|
+
if (!values.quiet) {
|
|
712
|
+
const lines = read.results.reduce((sum, r) => sum + (isFailure(r) ? 0 : r.lines.length), 0);
|
|
713
|
+
const cut = read.results.some((r) => !isFailure(r) && r.truncated);
|
|
714
|
+
note(dim(` ${read.printed} of ${read.results.length} target(s) · ${lines} line(s)` +
|
|
715
|
+
(cut ? ` · cut to --max-lines ${maxLines}` : '')));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (read.printed === 0) {
|
|
719
|
+
throw new CliError('none of the targets could be read', EXIT.failed, '`zen rag docs list files` lists the document names');
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/** A malformed target is a bad invocation, so nothing is printed for any of them. */
|
|
723
|
+
function targeted(run) {
|
|
724
|
+
try {
|
|
725
|
+
return run();
|
|
726
|
+
}
|
|
727
|
+
catch (err) {
|
|
728
|
+
if (err instanceof TargetError) {
|
|
729
|
+
throw usageError(err.message, SHOW_USAGE);
|
|
730
|
+
}
|
|
731
|
+
throw err;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
653
734
|
async function stats(args, ctx) {
|
|
654
735
|
const { values } = parse(args, { dir: { type: 'string', short: 'd' } }, 'zen rag docs stats [--dir <dir>]');
|
|
655
736
|
const dir = indexDir(ctx, values.dir);
|
package/dist/docs/load.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { CliError, EXIT } from '@zenera/cli/lib';
|
|
2
|
+
import { isGlob, wildcard } from '@zenera/neo';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
4
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
4
5
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
-
import { isGlob, wildcard } from "../common/match.js";
|
|
6
6
|
import { NO_PARSE_CACHE, openParseCache, parseKey } from "./parse-cache.js";
|
|
7
7
|
import { normalize } from "./parse.js";
|
|
8
8
|
import { parseAll } from "./pool.js";
|
package/dist/docs/lookup.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type MatchOptions } from '
|
|
1
|
+
import { type MatchOptions } from '@zenera/neo';
|
|
2
2
|
import type { DocsIndex } from './search.ts';
|
|
3
3
|
export interface Listing<T> {
|
|
4
4
|
found: number;
|
|
@@ -77,4 +77,57 @@ export interface Verbatim {
|
|
|
77
77
|
/** A named section, verbatim: heading line to the line before the next peer. */
|
|
78
78
|
export declare function readSection(index: DocsIndex, file: string, section: string): Promise<Verbatim>;
|
|
79
79
|
export declare function readRange(index: DocsIndex, file: string, from: number, to: number): Promise<Verbatim>;
|
|
80
|
+
export interface Read {
|
|
81
|
+
/** what the caller wrote, so a failure can be put back next to it */
|
|
82
|
+
target: string;
|
|
83
|
+
file: string;
|
|
84
|
+
/** a heading title, id or path; wins over from/to */
|
|
85
|
+
section?: string;
|
|
86
|
+
from?: number;
|
|
87
|
+
to?: number;
|
|
88
|
+
}
|
|
89
|
+
export interface ReadBlock {
|
|
90
|
+
/** what was printed, as a read that would print it again */
|
|
91
|
+
target: string;
|
|
92
|
+
file: string;
|
|
93
|
+
/** first line printed */
|
|
94
|
+
from: number;
|
|
95
|
+
/** last line printed; `from - 1` when the budget left none */
|
|
96
|
+
to: number;
|
|
97
|
+
/** last line that was asked for, after clamping to the document */
|
|
98
|
+
end: number;
|
|
99
|
+
total: number;
|
|
100
|
+
/** the heading, when the block is exactly one section */
|
|
101
|
+
section?: string;
|
|
102
|
+
/** the range asked for, when it ran past the end of the document */
|
|
103
|
+
asked?: string;
|
|
104
|
+
lines: string[];
|
|
105
|
+
truncated: boolean;
|
|
106
|
+
/** the read that prints the rest of a cut block */
|
|
107
|
+
continue?: string;
|
|
108
|
+
}
|
|
109
|
+
export interface ReadFailure {
|
|
110
|
+
target: string;
|
|
111
|
+
file: string;
|
|
112
|
+
error: string;
|
|
113
|
+
hint: string;
|
|
114
|
+
}
|
|
115
|
+
export type ReadResult = ReadBlock | ReadFailure;
|
|
116
|
+
export interface ReadMany {
|
|
117
|
+
results: ReadResult[];
|
|
118
|
+
printed: number;
|
|
119
|
+
failed: number;
|
|
120
|
+
}
|
|
121
|
+
export interface ReadHints {
|
|
122
|
+
document: (file: string) => string;
|
|
123
|
+
section: (file: string) => string;
|
|
124
|
+
range: (file: string, total: number) => string;
|
|
125
|
+
}
|
|
126
|
+
export interface ReadManyOptions {
|
|
127
|
+
maxLines?: number;
|
|
128
|
+
hints?: Partial<ReadHints>;
|
|
129
|
+
}
|
|
130
|
+
export declare const isFailure: <T extends object>(result: T | ReadFailure) => result is ReadFailure;
|
|
131
|
+
export declare const spanOf: (file: string, from: number, to: number) => string;
|
|
132
|
+
export declare function readMany(index: DocsIndex, reads: readonly Read[], options?: ReadManyOptions): Promise<ReadMany>;
|
|
80
133
|
//# sourceMappingURL=lookup.d.ts.map
|
package/dist/docs/lookup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError, EXIT } from '@zenera/cli/lib';
|
|
2
|
-
import { loose } from
|
|
2
|
+
import { loose } from '@zenera/neo';
|
|
3
3
|
import { under } from "./search.js";
|
|
4
4
|
export const DEFAULT_ROWS = 50;
|
|
5
5
|
export const MAX_ROWS = 500;
|
|
@@ -120,6 +120,125 @@ export async function readRange(index, file, from, to) {
|
|
|
120
120
|
total: lines.length,
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
|
+
const CLI_HINTS = {
|
|
124
|
+
document: (file) => `\`zen rag docs list files --file "*${file.split('/').at(-1)}*"\` lists close names`,
|
|
125
|
+
section: (file) => `\`zen rag docs list sections --file "${file}"\` lists its headings`,
|
|
126
|
+
range: (file, total) => `read ${spanOf(file, 1, total)}`,
|
|
127
|
+
};
|
|
128
|
+
export const isFailure = (result) => 'error' in result;
|
|
129
|
+
export const spanOf = (file, from, to) => from === to ? `${file}:${from}` : `${file}:${from}-${to}`;
|
|
130
|
+
export async function readMany(index, reads, options = {}) {
|
|
131
|
+
const hints = { ...CLI_HINTS, ...options.hints };
|
|
132
|
+
const slots = [];
|
|
133
|
+
for (const read of reads) {
|
|
134
|
+
const span = await spanFor(index, read, hints);
|
|
135
|
+
if (isFailure(span)) {
|
|
136
|
+
slots.push(span);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// Everything it overlaps or touches folds into the first of them, so a
|
|
140
|
+
// line is printed once however many reads named it.
|
|
141
|
+
const touching = slots.filter((slot) => !isFailure(slot) &&
|
|
142
|
+
slot.file === span.file &&
|
|
143
|
+
span.from <= slot.end + 1 &&
|
|
144
|
+
span.end >= slot.from - 1);
|
|
145
|
+
if (touching.length === 0) {
|
|
146
|
+
slots.push(span);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const [first, ...rest] = touching;
|
|
150
|
+
const all = [first, ...rest, span];
|
|
151
|
+
const from = Math.min(...all.map((s) => s.from));
|
|
152
|
+
const end = Math.max(...all.map((s) => s.end));
|
|
153
|
+
const same = all.every((s) => s.from === from && s.end === end);
|
|
154
|
+
Object.assign(first, {
|
|
155
|
+
from,
|
|
156
|
+
end,
|
|
157
|
+
section: same && all.every((s) => s.section === first.section) ? first.section : undefined,
|
|
158
|
+
asked: same ? first.asked : undefined,
|
|
159
|
+
});
|
|
160
|
+
for (const gone of rest) {
|
|
161
|
+
slots.splice(slots.indexOf(gone), 1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const spans = slots.filter((slot) => !isFailure(slot));
|
|
165
|
+
const grants = share(spans.map((s) => s.end - s.from + 1), options.maxLines ?? Infinity);
|
|
166
|
+
const results = [];
|
|
167
|
+
for (const slot of slots) {
|
|
168
|
+
if (isFailure(slot)) {
|
|
169
|
+
results.push(slot);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const granted = grants[spans.indexOf(slot)];
|
|
173
|
+
const to = slot.from + granted - 1;
|
|
174
|
+
const lines = (await index.lines(slot.file)).slice(slot.from - 1, to);
|
|
175
|
+
const truncated = to < slot.end;
|
|
176
|
+
results.push({
|
|
177
|
+
target: spanOf(slot.file, slot.from, granted > 0 ? to : slot.end),
|
|
178
|
+
file: slot.file,
|
|
179
|
+
from: slot.from,
|
|
180
|
+
to,
|
|
181
|
+
end: slot.end,
|
|
182
|
+
total: slot.total,
|
|
183
|
+
...(slot.section ? { section: slot.section } : {}),
|
|
184
|
+
...(slot.asked ? { asked: slot.asked } : {}),
|
|
185
|
+
lines,
|
|
186
|
+
truncated,
|
|
187
|
+
...(truncated ? { continue: spanOf(slot.file, to + 1, slot.end) } : {}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const failed = results.filter(isFailure).length;
|
|
191
|
+
return { results, printed: results.length - failed, failed };
|
|
192
|
+
}
|
|
193
|
+
async function spanFor(index, read, hints) {
|
|
194
|
+
const fail = (error, hint) => ({
|
|
195
|
+
target: read.target,
|
|
196
|
+
file: read.file,
|
|
197
|
+
error,
|
|
198
|
+
hint,
|
|
199
|
+
});
|
|
200
|
+
const outline = index.file(read.file);
|
|
201
|
+
if (!outline) {
|
|
202
|
+
return fail('no document by that name', hints.document(read.file));
|
|
203
|
+
}
|
|
204
|
+
const total = (await index.lines(read.file)).length;
|
|
205
|
+
const base = { target: read.target, file: read.file, total };
|
|
206
|
+
if (read.section !== undefined) {
|
|
207
|
+
const found = index.resolveSections([read.section], [read.file]);
|
|
208
|
+
const wanted = read.section.toLowerCase();
|
|
209
|
+
const heading = found.find((h) => h.title.toLowerCase() === wanted) ?? found[0];
|
|
210
|
+
if (!heading) {
|
|
211
|
+
return fail(`no section called ${read.section}`, hints.section(read.file));
|
|
212
|
+
}
|
|
213
|
+
return { ...base, from: heading.line, end: heading.end, section: heading.title };
|
|
214
|
+
}
|
|
215
|
+
const from = read.from ?? 1;
|
|
216
|
+
const to = read.to ?? total;
|
|
217
|
+
if (from > total) {
|
|
218
|
+
return fail(`line ${from} is past the end; the document has ${total} lines`, hints.range(read.file, total));
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
...base,
|
|
222
|
+
from,
|
|
223
|
+
end: Math.min(to, total),
|
|
224
|
+
...(to > total && read.to !== undefined ? { asked: `${from}-${to}` } : {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Divides a budget so no request can starve the rest: an even share each, and
|
|
229
|
+
* what a short one does not use is shared again among those still wanting.
|
|
230
|
+
*/
|
|
231
|
+
function share(sizes, budget) {
|
|
232
|
+
const grants = sizes.map(() => 0);
|
|
233
|
+
const order = sizes.map((_, at) => at).sort((a, b) => sizes[a] - sizes[b]);
|
|
234
|
+
let left = budget;
|
|
235
|
+
for (const [rank, at] of order.entries()) {
|
|
236
|
+
const fair = Math.floor(left / (order.length - rank));
|
|
237
|
+
grants[at] = Math.min(sizes[at], fair === Infinity ? sizes[at] : fair);
|
|
238
|
+
left -= grants[at];
|
|
239
|
+
}
|
|
240
|
+
return grants;
|
|
241
|
+
}
|
|
123
242
|
// ---------------------------------------------------------------------------
|
|
124
243
|
function cut(rows, limit) {
|
|
125
244
|
const take = Math.min(Math.max(1, limit ?? DEFAULT_ROWS), MAX_ROWS);
|
package/dist/docs/render.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Assembly, Excerpt } from './assemble.ts';
|
|
2
|
+
import { type ReadMany } from './lookup.ts';
|
|
2
3
|
import type { Match } from './search.ts';
|
|
3
4
|
export interface RenderOptions {
|
|
4
5
|
/** the line-number gutter; on unless something else is going to eat this */
|
|
@@ -7,6 +8,11 @@ export interface RenderOptions {
|
|
|
7
8
|
}
|
|
8
9
|
export declare function renderAssembly(assembly: Assembly, options?: RenderOptions): string;
|
|
9
10
|
export declare function renderExcerpt(file: Excerpt, options?: RenderOptions): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Each read under a header that is itself a read, so a citation copied from
|
|
13
|
+
* the header, or the line that continues a cut block, pastes straight back.
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderReads(read: ReadMany, options?: RenderOptions): string;
|
|
10
16
|
/** The one-line-per-match view, for `--quiet` and for the prompt loop. */
|
|
11
17
|
export declare const matchRows: (matches: readonly Match[]) => string[][];
|
|
12
18
|
export declare const MATCH_HEADERS: readonly ["id", "kind", "lines", "score", "vec / txt", "heading"];
|
package/dist/docs/render.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { bold, dim } from '@zenera/cli/lib';
|
|
2
|
+
import { isFailure } from "./lookup.js";
|
|
2
3
|
export function renderAssembly(assembly, options = {}) {
|
|
3
4
|
const out = [];
|
|
4
5
|
for (const file of assembly.files) {
|
|
@@ -30,6 +31,38 @@ function renderPiece(piece, width, options, faint) {
|
|
|
30
31
|
: `${faint(String(piece.start + at).padStart(width))} ${faint('|')} ${line}`);
|
|
31
32
|
return [...lines, ''];
|
|
32
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Each read under a header that is itself a read, so a citation copied from
|
|
36
|
+
* the header, or the line that continues a cut block, pastes straight back.
|
|
37
|
+
*/
|
|
38
|
+
export function renderReads(read, options = {}) {
|
|
39
|
+
const paint = options.colour === false ? (s) => s : undefined;
|
|
40
|
+
const strong = paint ?? bold;
|
|
41
|
+
const faint = paint ?? dim;
|
|
42
|
+
const blocks = read.results.map((result) => isFailure(result)
|
|
43
|
+
? [`## ${strong(result.target)}`, `error: ${result.error} - ${result.hint}`]
|
|
44
|
+
: renderBlock(result, options, strong, faint));
|
|
45
|
+
return blocks.map((lines) => lines.join('\n')).join('\n\n');
|
|
46
|
+
}
|
|
47
|
+
function renderBlock(block, options, strong, faint) {
|
|
48
|
+
const notes = [
|
|
49
|
+
block.section ? `(#${block.section})` : '',
|
|
50
|
+
block.asked ? `(asked ${block.asked}; the document ends at ${block.total})` : '',
|
|
51
|
+
].filter(Boolean);
|
|
52
|
+
const width = String(block.end).length;
|
|
53
|
+
const out = [`## ${strong(block.target)}${notes.length ? ` ${faint(notes.join(' '))}` : ''}`];
|
|
54
|
+
for (const [at, line] of block.lines.entries()) {
|
|
55
|
+
out.push(options.numbers === false
|
|
56
|
+
? line
|
|
57
|
+
: `${faint(String(block.from + at).padStart(width))} ${faint('|')} ${line}`);
|
|
58
|
+
}
|
|
59
|
+
if (block.continue) {
|
|
60
|
+
out.push(faint(block.lines.length > 0
|
|
61
|
+
? `... truncated at line ${block.to} of ${block.end} - read ${block.continue} to continue`
|
|
62
|
+
: `... the line budget ran out before this one - read ${block.continue}`));
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
33
66
|
/** The one-line-per-match view, for `--quiet` and for the prompt loop. */
|
|
34
67
|
export const matchRows = (matches) => matches.map((m) => [
|
|
35
68
|
m.id,
|
package/dist/docs/search.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Embedder } from '@zenera/neo';
|
|
2
2
|
import { type FileOutline, type HeadingRecord, type Manifest, type OpenDocs, type Outline } from './files.ts';
|
|
3
3
|
import { ChunkStore } from './store.ts';
|
|
4
4
|
export type SearchMode = 'hybrid' | 'vector' | 'text';
|
package/dist/docs/search.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { loose, PatternError } from '@zenera/neo';
|
|
1
2
|
import { assertSameEmbedding } from "../common/manifest.js";
|
|
2
|
-
import { loose, PatternError } from "../common/match.js";
|
|
3
3
|
import { CHUNK_KINDS, parseLines } from "./chunk.js";
|
|
4
4
|
import { openIndex, readLines, } from "./files.js";
|
|
5
5
|
import { ChunkStore } from "./store.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Read } from './lookup.ts';
|
|
2
|
+
export declare class TargetError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export declare function parseTarget(raw: string, names: ReadonlySet<string>): Read;
|
|
5
|
+
/** Does this argument name a part of a document the index holds? */
|
|
6
|
+
export declare function selects(raw: string, names: ReadonlySet<string>): boolean;
|
|
7
|
+
//# sourceMappingURL=targets.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// A read, written as its citation
|
|
3
|
+
//
|
|
4
|
+
// `<document>:<a>-<b>`, `<document>:<a>`, `<document>#<heading>`, or a bare
|
|
5
|
+
// document name — the form an answer cites a passage in, so a citation pastes
|
|
6
|
+
// back unchanged.
|
|
7
|
+
//
|
|
8
|
+
// Splitting at the last `:` or `#` would be wrong both ways: a heading may hold
|
|
9
|
+
// a colon (`#Step 1: Install`) and a document name is any relative path. So the
|
|
10
|
+
// split is wherever the text before it is a document the index holds, longest
|
|
11
|
+
// first, and only a name nobody holds is split by guesswork.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export class TargetError extends Error {
|
|
14
|
+
}
|
|
15
|
+
export function parseTarget(raw, names) {
|
|
16
|
+
if (names.has(raw)) {
|
|
17
|
+
return { target: raw, file: raw };
|
|
18
|
+
}
|
|
19
|
+
for (let at = raw.length - 1; at > 0; at--) {
|
|
20
|
+
if ((raw[at] === ':' || raw[at] === '#') && names.has(raw.slice(0, at))) {
|
|
21
|
+
return selector(raw, raw.slice(0, at), raw[at], raw.slice(at + 1));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// A name the index does not hold: a heading is likelier to carry a colon
|
|
25
|
+
// than a document name a hash, so the first `#` wins over the last `:`.
|
|
26
|
+
const hash = raw.indexOf('#');
|
|
27
|
+
const at = hash > 0 ? hash : raw.lastIndexOf(':');
|
|
28
|
+
return at > 0
|
|
29
|
+
? selector(raw, raw.slice(0, at), raw[at], raw.slice(at + 1))
|
|
30
|
+
: { target: raw, file: raw };
|
|
31
|
+
}
|
|
32
|
+
/** Does this argument name a part of a document the index holds? */
|
|
33
|
+
export function selects(raw, names) {
|
|
34
|
+
const read = parseTarget(raw, names);
|
|
35
|
+
return names.has(read.file) && read.file !== raw;
|
|
36
|
+
}
|
|
37
|
+
function selector(raw, file, mark, rest) {
|
|
38
|
+
if (mark === '#') {
|
|
39
|
+
if (!rest.trim()) {
|
|
40
|
+
throw new TargetError(`"${raw}" names no heading after the #`);
|
|
41
|
+
}
|
|
42
|
+
return { target: raw, file, section: rest };
|
|
43
|
+
}
|
|
44
|
+
const found = /^(\d+)(?:-(\d+))?$/.exec(rest);
|
|
45
|
+
const from = Number(found?.[1]);
|
|
46
|
+
const to = found?.[2] === undefined ? from : Number(found[2]);
|
|
47
|
+
if (!found || from < 1 || to < from) {
|
|
48
|
+
throw new TargetError(`"${raw}" is not a line range - expected <document>:<a>-<b>`);
|
|
49
|
+
}
|
|
50
|
+
return { target: raw, file, from, to };
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=targets.js.map
|
package/dist/docs/tools.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { tool } from '@zenera/neo';
|
|
2
|
-
import { PatternError } from "../common/match.js";
|
|
1
|
+
import { PatternError, tool } from '@zenera/neo';
|
|
3
2
|
import { assemble } from "./assemble.js";
|
|
4
3
|
import { CHUNK_KINDS } from "./chunk.js";
|
|
5
|
-
import { grepLines, listFiles, listSections, listTables, readRange, readSection, } from "./lookup.js";
|
|
4
|
+
import { grepLines, isFailure, listFiles, listSections, listTables, readMany, readRange, readSection, } from "./lookup.js";
|
|
6
5
|
import { renderAssembly } from "./render.js";
|
|
7
6
|
import { SEARCH_MODES } from "./search.js";
|
|
8
7
|
// ---------------------------------------------------------------------------
|
|
@@ -248,7 +247,8 @@ export function docsTools(index, options = {}) {
|
|
|
248
247
|
group: GROUP,
|
|
249
248
|
description: 'Reads a document verbatim: a whole named section, or a line range as printed by ' +
|
|
250
249
|
'search_docs or grep_docs. Use it when a passage was found and the lines around ' +
|
|
251
|
-
'it are needed in full, with nothing omitted and nothing summarised.'
|
|
250
|
+
'it are needed in full, with nothing omitted and nothing summarised. To read ' +
|
|
251
|
+
'several passages, name them all in `reads` in one call instead of calling again.',
|
|
252
252
|
parameters: {
|
|
253
253
|
type: 'object',
|
|
254
254
|
properties: {
|
|
@@ -262,11 +262,38 @@ export function docsTools(index, options = {}) {
|
|
|
262
262
|
},
|
|
263
263
|
from: { type: 'integer', description: 'First line, 1-based. Default 1.' },
|
|
264
264
|
to: { type: 'integer', description: 'Last line. Default the end of the document.' },
|
|
265
|
+
reads: {
|
|
266
|
+
type: 'array',
|
|
267
|
+
description: 'Instead of file: every passage to read, each as {file, section} or ' +
|
|
268
|
+
`{file, from, to}. They share a ceiling of ${MAX_LINES} lines.`,
|
|
269
|
+
items: {
|
|
270
|
+
type: 'object',
|
|
271
|
+
properties: {
|
|
272
|
+
file: { type: 'string' },
|
|
273
|
+
section: { type: 'string' },
|
|
274
|
+
from: { type: 'integer' },
|
|
275
|
+
to: { type: 'integer' },
|
|
276
|
+
},
|
|
277
|
+
required: ['file'],
|
|
278
|
+
additionalProperties: false,
|
|
279
|
+
},
|
|
280
|
+
},
|
|
265
281
|
},
|
|
266
|
-
required: ['file'],
|
|
267
282
|
additionalProperties: false,
|
|
268
283
|
},
|
|
269
284
|
execute: async (args) => {
|
|
285
|
+
if (args.reads !== undefined) {
|
|
286
|
+
if (args.file !== undefined || args.reads.length === 0) {
|
|
287
|
+
return {
|
|
288
|
+
error: args.file ? 'send `file` or `reads`, not both' : '`reads` is empty',
|
|
289
|
+
hint: 'put every passage in `reads`, one {file, section} or {file, from, to} each',
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return await readSeveral(args.reads);
|
|
293
|
+
}
|
|
294
|
+
if (!args.file) {
|
|
295
|
+
return { error: 'nothing to read', hint: 'send `file`, or `reads` for several' };
|
|
296
|
+
}
|
|
270
297
|
const file = index.resolveFiles([args.file])[0];
|
|
271
298
|
if (!file) {
|
|
272
299
|
return {
|
|
@@ -293,6 +320,43 @@ export function docsTools(index, options = {}) {
|
|
|
293
320
|
},
|
|
294
321
|
});
|
|
295
322
|
return [searchDocs, listDocs, grepDocs, readDocs];
|
|
323
|
+
async function readSeveral(reads) {
|
|
324
|
+
const names = new Set(index.manifest.sources.map((s) => s.name));
|
|
325
|
+
const resolved = await guard(() => reads.map((r) => ({
|
|
326
|
+
target: r.file,
|
|
327
|
+
file: names.has(r.file) ? r.file : (index.resolveFiles([r.file])[0] ?? r.file),
|
|
328
|
+
section: r.section,
|
|
329
|
+
from: r.from,
|
|
330
|
+
to: r.to,
|
|
331
|
+
})));
|
|
332
|
+
if ('error' in resolved) {
|
|
333
|
+
return resolved;
|
|
334
|
+
}
|
|
335
|
+
const read = await readMany(index, resolved, {
|
|
336
|
+
maxLines: MAX_LINES,
|
|
337
|
+
hints: {
|
|
338
|
+
document: () => 'call list_docs for the names',
|
|
339
|
+
section: () => 'call list_docs with what "sections" for its headings',
|
|
340
|
+
range: (_, total) => `it has ${total} lines`,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
return {
|
|
344
|
+
printed: read.printed,
|
|
345
|
+
failed: read.failed,
|
|
346
|
+
results: read.results.map((r) => isFailure(r)
|
|
347
|
+
? { file: r.file, error: r.error, hint: r.hint }
|
|
348
|
+
: {
|
|
349
|
+
file: r.file,
|
|
350
|
+
...(r.section ? { section: r.section } : {}),
|
|
351
|
+
start: r.from,
|
|
352
|
+
end: r.to,
|
|
353
|
+
total: r.total,
|
|
354
|
+
truncated: r.truncated,
|
|
355
|
+
...(r.continue ? { continue: r.continue } : {}),
|
|
356
|
+
text: r.lines.map((line, at) => `${r.from + at} | ${line}`).join('\n'),
|
|
357
|
+
}),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
296
360
|
}
|
|
297
361
|
// ---------------------------------------------------------------------------
|
|
298
362
|
const strings = (description) => ({
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
export { anyOf, isGlob, loose, matcher, MAX_PATTERN, PatternError, wildcard, type Matcher, type MatchOptions, } from '@zenera/neo';
|
|
1
2
|
export * from './common/locate.ts';
|
|
2
3
|
export * from './common/manifest.ts';
|
|
3
|
-
export * from './common/match.ts';
|
|
4
4
|
export * as docs from './docs/index.ts';
|
|
5
5
|
export * from './schema/build.ts';
|
|
6
6
|
export * from './schema/entities.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
export { anyOf, isGlob, loose, matcher, MAX_PATTERN, PatternError, wildcard, } from '@zenera/neo';
|
|
1
2
|
export * from "./common/locate.js";
|
|
2
3
|
export * from "./common/manifest.js";
|
|
3
|
-
export * from "./common/match.js";
|
|
4
4
|
// A namespace rather than a re-export: both subjects have a `Manifest`, a
|
|
5
5
|
// `writeIndex` and a `search`, and flattening them would collide on every one.
|
|
6
6
|
export * as docs from "./docs/index.js";
|
package/dist/schema/command.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { bold, CliError, cyan, dim, EXIT, isInteractive, json, note, parse, paths, table, usageError, write, } from '@zenera/cli/lib';
|
|
2
|
+
import { isGlob, loose, matcher, PatternError, wildcard } from '@zenera/neo';
|
|
2
3
|
import { existsSync } from 'node:fs';
|
|
3
4
|
import { join, relative, resolve } from 'node:path';
|
|
4
5
|
import { resolveEmbedder } from "../common/embedder.js";
|
|
5
6
|
import { locateIndex, outputDir } from "../common/locate.js";
|
|
6
7
|
import { assertSameEmbedding, inspectIndex } from "../common/manifest.js";
|
|
7
|
-
import { isGlob, loose, matcher, PatternError, wildcard } from "../common/match.js";
|
|
8
8
|
import { breakdown, indexing } from "../common/prose.js";
|
|
9
9
|
import { announce, reportReady, stagingFor, swapIn } from "../common/restore.js";
|
|
10
10
|
import { buildIndex } from "./build.js";
|
package/dist/schema/lookup.d.ts
CHANGED
package/dist/schema/lookup.js
CHANGED
package/dist/schema/tools.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { tool } from '@zenera/neo';
|
|
2
|
-
import { loose, matcher, PatternError } from "../common/match.js";
|
|
1
|
+
import { loose, matcher, PatternError, tool } from '@zenera/neo';
|
|
3
2
|
import { toTypeScript } from "./hydrate.js";
|
|
4
3
|
import { fields, grepNodes, listNodes, propertyCount } from "./lookup.js";
|
|
5
4
|
import { FORMATS, isFormat, present } from "./present.js";
|
package/dist/schema/trace.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/rag",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.25",
|
|
4
4
|
"description": "Retrieval over a corpus: openapi/swagger documents as a searchable graph, markdown and text as searchable passages.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@apidevtools/swagger-parser": "^12.0.0",
|
|
63
63
|
"@lancedb/lancedb": "^0.38.0",
|
|
64
|
-
"@zenera/cli": "^1.1.
|
|
65
|
-
"@zenera/neo": "^1.1.
|
|
64
|
+
"@zenera/cli": "^1.1.25",
|
|
65
|
+
"@zenera/neo": "^1.1.25",
|
|
66
66
|
"graphology": "^0.26.0",
|
|
67
67
|
"remark-frontmatter": "^5.0.0",
|
|
68
68
|
"remark-gfm": "^4.0.1",
|
package/dist/common/match.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/** Long enough for any honest pattern, short enough to bound a bad one. */
|
|
2
|
-
export declare const MAX_PATTERN = 200;
|
|
3
|
-
export declare class PatternError extends Error {
|
|
4
|
-
}
|
|
5
|
-
export interface MatchOptions {
|
|
6
|
-
/** read the pattern as a regular expression rather than as a literal */
|
|
7
|
-
regex?: boolean;
|
|
8
|
-
caseSensitive?: boolean;
|
|
9
|
-
}
|
|
10
|
-
export type Matcher = (text: string) => boolean;
|
|
11
|
-
/**
|
|
12
|
-
* A predicate over a string. Literal by default: someone typing `user.id` means
|
|
13
|
-
* those seven characters, and a dot that quietly matched anything would be a
|
|
14
|
-
* worse answer than no answer.
|
|
15
|
-
*/
|
|
16
|
-
export declare function matcher(pattern: string, options?: MatchOptions): Matcher;
|
|
17
|
-
/**
|
|
18
|
-
* A glob, matched against the whole string. Globs rather than regexes because
|
|
19
|
-
* these are for naming things — a path, a schema — and a star is what everyone
|
|
20
|
-
* reaches for first.
|
|
21
|
-
*/
|
|
22
|
-
export declare function wildcard(pattern: string, options?: {
|
|
23
|
-
caseSensitive?: boolean;
|
|
24
|
-
}): Matcher;
|
|
25
|
-
/** Whether a pattern is asking to be read as a glob at all. */
|
|
26
|
-
export declare const isGlob: (pattern: string) => boolean;
|
|
27
|
-
/**
|
|
28
|
-
* What someone means when they type a name into a filter. With a star in it,
|
|
29
|
-
* a glob; without one, a substring — because `password` typed into `--name` is
|
|
30
|
-
* a search for the word, and a whole-string match would answer nothing and
|
|
31
|
-
* look like the field does not exist.
|
|
32
|
-
*
|
|
33
|
-
* `regex` settles it outright, and has to: a star is punctuation in both
|
|
34
|
-
* languages, so `^/(users|teams)/.*` read as a glob would match nothing and
|
|
35
|
-
* never say why.
|
|
36
|
-
*/
|
|
37
|
-
export declare function loose(pattern: string, options?: MatchOptions): Matcher;
|
|
38
|
-
/** True when any of the patterns matches; no patterns means no opinion. */
|
|
39
|
-
export declare function anyOf(matchers: readonly Matcher[]): Matcher | undefined;
|
|
40
|
-
//# sourceMappingURL=match.d.ts.map
|
package/dist/common/match.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Matching, with nothing learned in between
|
|
3
|
-
//
|
|
4
|
-
// Everything here is exact. A glob matches the characters it names and a
|
|
5
|
-
// substring is a substring, which is the whole point of the surfaces built on
|
|
6
|
-
// it: a vector index answers "what is near this", and near is a ranking, so it
|
|
7
|
-
// can only ever return the top of a list. When the question is "does the word
|
|
8
|
-
// `password` appear anywhere at all", a ranking is the wrong instrument and no
|
|
9
|
-
// amount of tuning makes it the right one.
|
|
10
|
-
//
|
|
11
|
-
// A pattern may arrive from a model, so a regex is a bounded promise: the
|
|
12
|
-
// length is capped here and the scan that uses it keeps a deadline.
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
/** Long enough for any honest pattern, short enough to bound a bad one. */
|
|
15
|
-
export const MAX_PATTERN = 200;
|
|
16
|
-
export class PatternError extends Error {
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* A predicate over a string. Literal by default: someone typing `user.id` means
|
|
20
|
-
* those seven characters, and a dot that quietly matched anything would be a
|
|
21
|
-
* worse answer than no answer.
|
|
22
|
-
*/
|
|
23
|
-
export function matcher(pattern, options = {}) {
|
|
24
|
-
guard(pattern);
|
|
25
|
-
if (!options.regex) {
|
|
26
|
-
if (options.caseSensitive) {
|
|
27
|
-
return (text) => text.includes(pattern);
|
|
28
|
-
}
|
|
29
|
-
const needle = pattern.toLowerCase();
|
|
30
|
-
return (text) => text.toLowerCase().includes(needle);
|
|
31
|
-
}
|
|
32
|
-
const expression = compile(pattern, options.caseSensitive ? '' : 'i');
|
|
33
|
-
// `lastIndex` is not carried between calls: the flags never include `g`.
|
|
34
|
-
return (text) => expression.test(text);
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* A glob, matched against the whole string. Globs rather than regexes because
|
|
38
|
-
* these are for naming things — a path, a schema — and a star is what everyone
|
|
39
|
-
* reaches for first.
|
|
40
|
-
*/
|
|
41
|
-
export function wildcard(pattern, options = {}) {
|
|
42
|
-
guard(pattern);
|
|
43
|
-
const source = [...pattern]
|
|
44
|
-
.map((char) => (char === '*' ? '.*' : char === '?' ? '.' : escape(char)))
|
|
45
|
-
.join('');
|
|
46
|
-
const expression = compile(`^${source}$`, options.caseSensitive ? '' : 'i');
|
|
47
|
-
return (text) => expression.test(text);
|
|
48
|
-
}
|
|
49
|
-
/** Whether a pattern is asking to be read as a glob at all. */
|
|
50
|
-
export const isGlob = (pattern) => /[*?]/.test(pattern);
|
|
51
|
-
/**
|
|
52
|
-
* What someone means when they type a name into a filter. With a star in it,
|
|
53
|
-
* a glob; without one, a substring — because `password` typed into `--name` is
|
|
54
|
-
* a search for the word, and a whole-string match would answer nothing and
|
|
55
|
-
* look like the field does not exist.
|
|
56
|
-
*
|
|
57
|
-
* `regex` settles it outright, and has to: a star is punctuation in both
|
|
58
|
-
* languages, so `^/(users|teams)/.*` read as a glob would match nothing and
|
|
59
|
-
* never say why.
|
|
60
|
-
*/
|
|
61
|
-
export function loose(pattern, options = {}) {
|
|
62
|
-
if (options.regex) {
|
|
63
|
-
return matcher(pattern, options);
|
|
64
|
-
}
|
|
65
|
-
return isGlob(pattern) ? wildcard(pattern, options) : matcher(pattern, options);
|
|
66
|
-
}
|
|
67
|
-
/** True when any of the patterns matches; no patterns means no opinion. */
|
|
68
|
-
export function anyOf(matchers) {
|
|
69
|
-
if (matchers.length === 0) {
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
return (text) => matchers.some((match) => match(text));
|
|
73
|
-
}
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
function guard(pattern) {
|
|
76
|
-
if (pattern.length === 0) {
|
|
77
|
-
throw new PatternError('the pattern is empty');
|
|
78
|
-
}
|
|
79
|
-
if (pattern.length > MAX_PATTERN) {
|
|
80
|
-
throw new PatternError(`the pattern is longer than ${MAX_PATTERN} characters`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
function compile(source, flags) {
|
|
84
|
-
try {
|
|
85
|
-
return new RegExp(source, flags);
|
|
86
|
-
}
|
|
87
|
-
catch (err) {
|
|
88
|
-
throw new PatternError(`invalid pattern: ${err.message}`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
const escape = (char) => char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
92
|
-
//# sourceMappingURL=match.js.map
|