@zenera/rag 1.1.4 → 1.1.6
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 +77 -4
- package/dist/command.js +334 -24
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/schema/entities.d.ts +2 -0
- package/dist/schema/entities.js +2 -1
- package/dist/schema/files.d.ts +7 -0
- package/dist/schema/files.js +18 -1
- package/dist/schema/locate.d.ts +20 -0
- package/dist/schema/locate.js +141 -0
- package/dist/schema/lookup.d.ts +54 -0
- package/dist/schema/lookup.js +110 -0
- package/dist/schema/match.d.ts +40 -0
- package/dist/schema/match.js +92 -0
- package/dist/schema/render.d.ts +8 -0
- package/dist/schema/render.js +12 -2
- package/dist/schema/subgraph.d.ts +6 -0
- package/dist/schema/subgraph.js +27 -0
- package/dist/schema/tools.d.ts +2 -0
- package/dist/schema/tools.js +183 -29
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -46,6 +46,40 @@ zen rag schema stats # what is in the index, and what built it
|
|
|
46
46
|
zen rag schema show Type:Invoice # a named node, with no search in between
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
Search ranks, which means it returns the top of a list — useful when the
|
|
50
|
+
question is vague, and no use at all when the question is whether something
|
|
51
|
+
exists. For that there is exact matching, which needs no embedder, no
|
|
52
|
+
credential and no network:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
zen rag schema list methods --path "*/users*" # every route under /users
|
|
56
|
+
zen rag schema list types --name "*Password*" # every schema so named
|
|
57
|
+
zen rag schema grep password # every literal occurrence
|
|
58
|
+
zen rag schema grep "pass(word|phrase)" --regex
|
|
59
|
+
zen rag schema show --method GetCurrentUserInfo --format openapi --exact
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Patterns are globs by default and regular expressions under `--regex`, on
|
|
63
|
+
`list` as well as `grep`, which is the only way to say "one of these prefixes":
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
zen rag schema list methods --regex --path "^/(users|teams)/"
|
|
67
|
+
zen rag schema grep status --path "/invoices/*" --kind property
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`grep` takes the same `--name` and `--path` constraints `list` takes, so a
|
|
71
|
+
common word can be narrowed to one corner of the API instead of being read out
|
|
72
|
+
of every document at once.
|
|
73
|
+
|
|
74
|
+
When an index holds more than one document, `--show-source` names the one each
|
|
75
|
+
row came from — `[source: billing_api_v2]` — on `list`, `grep`, `search` and
|
|
76
|
+
`show` alike, so the document does not have to be recovered from `--json`.
|
|
77
|
+
|
|
78
|
+
`list` and `grep` report `found` as the true total even when `--limit` cuts the
|
|
79
|
+
printed rows, so a shortened answer still tells you how much there is. Nothing
|
|
80
|
+
matching exits 0 — an empty answer is an answer, and here it is a trustworthy
|
|
81
|
+
one: if `grep` finds nothing, the word is not in the description.
|
|
82
|
+
|
|
49
83
|
Non-interactive search is a machine interface: every field is a flag, the whole
|
|
50
84
|
query can arrive as one JSON object, the `--json` output keeps the same
|
|
51
85
|
structure from run to run, no terminal is required, and an empty result exits 0.
|
|
@@ -61,12 +95,30 @@ zen rag schema search --query - --format ts <<'JSON'
|
|
|
61
95
|
JSON
|
|
62
96
|
```
|
|
63
97
|
|
|
98
|
+
## Which index
|
|
99
|
+
|
|
100
|
+
Every reading command takes `-d, --dir`. Without one, `$ZEN_SCHEMA_DB` is used
|
|
101
|
+
if it is set; without that, the nearest index to the working directory is found
|
|
102
|
+
and named on stderr as it is used.
|
|
103
|
+
|
|
104
|
+
Nearest means what it says: this directory, then a short way down into it, then
|
|
105
|
+
up a level and again, stopping at your home directory. What is looked for is a
|
|
106
|
+
`manifest.json` — an index is self-describing, so nothing here searches for a
|
|
107
|
+
directory called `schema-db`, and an index called anything else is found just
|
|
108
|
+
the same. `schema-db` is only the name a new one is given.
|
|
109
|
+
|
|
110
|
+
Two indexes the same distance away is a question, not a tie to break, and it is
|
|
111
|
+
refused: the wrong index does not fail, it answers confidently about a
|
|
112
|
+
different API. Name one with `--dir`, or set `ZEN_SCHEMA_DB`.
|
|
113
|
+
|
|
64
114
|
## Commands
|
|
65
115
|
|
|
66
116
|
```
|
|
67
117
|
zen rag schema index <spec...> Read the documents and write a searchable index.
|
|
68
118
|
zen rag schema search Ask it something. --interactive for a prompt.
|
|
69
|
-
zen rag schema
|
|
119
|
+
zen rag schema list <what> Every method, type or property. No ranking.
|
|
120
|
+
zen rag schema grep <pattern> Every literal match across the whole index.
|
|
121
|
+
zen rag schema show [id...] Print named nodes, with no search in between.
|
|
70
122
|
zen rag schema stats What is in an index, and what built it.
|
|
71
123
|
```
|
|
72
124
|
|
|
@@ -76,6 +128,22 @@ by `--direction`, `--method-type`, `--limit`, `--max-hops`, `--max-nodes` and
|
|
|
76
128
|
the four `--exclude-*` filters, and rendered by `--format text | mermaid |
|
|
77
129
|
mermaid-flowchart | ts | openapi`. `zen help rag` prints the full table.
|
|
78
130
|
|
|
131
|
+
`list` and `grep` share `--name`, `--path`, `--regex`, `--case-sensitive`,
|
|
132
|
+
`--source`, `--show-source` and `--limit`; `grep` adds `--kind` and
|
|
133
|
+
`--ids-only`. A pattern with `*` or `?` in it is a glob matched against the
|
|
134
|
+
whole name; a plain word is a substring, so `--name password` finds
|
|
135
|
+
`ResetPasswordPayload` rather than nothing; under `--regex` it is a regular
|
|
136
|
+
expression either way. `--path` selects on the route an operation sits on, and
|
|
137
|
+
on the route a parameter's operation sits on — a schema belongs to no one
|
|
138
|
+
route, so `--path` never selects one. `show` takes ids, or `--method` and
|
|
139
|
+
`--type` by name, or `--source` for a whole document, and `--exact` to print
|
|
140
|
+
only what was named instead of its neighbourhood.
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
# Everything that mentions a token, rendered as TypeScript.
|
|
144
|
+
zen rag schema grep token --ids-only | xargs zen rag schema show --format ts
|
|
145
|
+
```
|
|
146
|
+
|
|
79
147
|
## From an agent
|
|
80
148
|
|
|
81
149
|
```ts
|
|
@@ -89,18 +157,23 @@ const index = await SchemaIndex.open(
|
|
|
89
157
|
const project = await loadProject('./my-project', { tools: schemaTools(index) });
|
|
90
158
|
```
|
|
91
159
|
|
|
92
|
-
|
|
160
|
+
Five tools in the group `schema`, selectable as `schema:*`:
|
|
93
161
|
|
|
94
162
|
| Tool | For |
|
|
95
163
|
| -------------------------- | ------------------------------------------------------- |
|
|
96
164
|
| `search_api` | the connected piece of the API that matches an intent |
|
|
97
165
|
| `describe_types` | named schemas as declarations that compile on their own |
|
|
98
166
|
| `find_types_with_property` | which types have a field of this name — no search |
|
|
99
|
-
| `
|
|
167
|
+
| `list_api` | the shape of the API: methods, types or fields |
|
|
168
|
+
| `grep_api` | every literal occurrence of a string — no search |
|
|
100
169
|
|
|
101
|
-
|
|
170
|
+
Only the first of those ranks anything. The rest are exact, because a model
|
|
171
|
+
told "no results" by a vector search has learned nothing: a ranking returns the
|
|
172
|
+
top of a list, so an empty answer and an absent thing look identical.
|
|
173
|
+
`find_types_with_property` is the one for the repair loop — when `tsc` says
|
|
102
174
|
`'password' does not exist in type 'PublicUserProfile'`, the model does not
|
|
103
175
|
need the word explained again, it needs the list of types that have one.
|
|
176
|
+
`grep_api` is the same instinct widened to the whole description.
|
|
104
177
|
|
|
105
178
|
## What an index is
|
|
106
179
|
|
package/dist/command.js
CHANGED
|
@@ -5,9 +5,13 @@ import { isFormat, present } from "./present.js";
|
|
|
5
5
|
import { isEmpty, parseQuery, QueryError } from "./query.js";
|
|
6
6
|
import { repl } from "./repl.js";
|
|
7
7
|
import { buildIndex } from "./schema/build.js";
|
|
8
|
-
import { assertSameEmbedding, openIndex, readManifest } from "./schema/files.js";
|
|
8
|
+
import { assertSameEmbedding, openIndex, readManifest, readSource, } from "./schema/files.js";
|
|
9
|
+
import { DEFAULT_DIR, DIR_ENV, locateIndex, outputDir } from "./schema/locate.js";
|
|
10
|
+
import { fields, grepNodes, listNodes, propertyCount } from "./schema/lookup.js";
|
|
11
|
+
import { isGlob, loose, matcher, PatternError, wildcard } from "./schema/match.js";
|
|
12
|
+
import { sourceTag } from "./schema/render.js";
|
|
9
13
|
import { SchemaIndex } from "./schema/search.js";
|
|
10
|
-
import { stitch } from "./schema/subgraph.js";
|
|
14
|
+
import { select, stitch } from "./schema/subgraph.js";
|
|
11
15
|
// ---------------------------------------------------------------------------
|
|
12
16
|
// zen rag — an api description, as something to search
|
|
13
17
|
//
|
|
@@ -18,11 +22,19 @@ import { stitch } from "./schema/subgraph.js";
|
|
|
18
22
|
// required, and exit 0 when nothing matched — an empty answer is an answer, and
|
|
19
23
|
// a caller that has to tell "no results" from "the index is missing" by parsing
|
|
20
24
|
// stderr will get it wrong.
|
|
25
|
+
//
|
|
26
|
+
// `list`, `grep` and `show` are the other half, and they are deliberately not
|
|
27
|
+
// searches. A ranking can only ever hand back the top of a list, so it cannot
|
|
28
|
+
// answer "is there a field called `password` anywhere" — the honest answer to
|
|
29
|
+
// that question is every match or none, and these three give it without asking
|
|
30
|
+
// a model or a credential for permission.
|
|
21
31
|
// ---------------------------------------------------------------------------
|
|
22
|
-
const USAGE = 'zen rag schema <index|search|show|stats> [spec...]';
|
|
32
|
+
const USAGE = 'zen rag schema <index|search|list|grep|show|stats> [spec...]';
|
|
23
33
|
const INDEX_USAGE = 'zen rag schema index --embedding <ref> [--out <dir>] <spec...>';
|
|
24
34
|
const SEARCH_USAGE = 'zen rag schema search [--dir <dir>] [query...]';
|
|
25
|
-
const
|
|
35
|
+
const LIST_USAGE = 'zen rag schema list <methods|types|properties> [--dir <dir>]';
|
|
36
|
+
const GREP_USAGE = 'zen rag schema grep <pattern> [--dir <dir>]';
|
|
37
|
+
const SHOW_USAGE = 'zen rag schema show [id...] [--method <name>] [--type <name>]';
|
|
26
38
|
export const command = {
|
|
27
39
|
summary: 'Search an openapi/swagger document as a graph.',
|
|
28
40
|
usage: USAGE,
|
|
@@ -31,7 +43,9 @@ export const command = {
|
|
|
31
43
|
...table([
|
|
32
44
|
[' index <spec...>', dim('Read the documents and write a searchable index.')],
|
|
33
45
|
[' search', dim('Ask it something. --interactive for a prompt.')],
|
|
34
|
-
['
|
|
46
|
+
[' list <what>', dim('Every method, type or property matching a pattern.')],
|
|
47
|
+
[' grep <pattern>', dim('Every literal match, ranked by nothing.')],
|
|
48
|
+
[' show [id...]', dim('Print named nodes, with no search in between.')],
|
|
35
49
|
[' stats', dim('What is in an index, and what built it.')],
|
|
36
50
|
]),
|
|
37
51
|
'',
|
|
@@ -41,7 +55,10 @@ export const command = {
|
|
|
41
55
|
' --embedding <ref>',
|
|
42
56
|
dim('Which embedder makes the vectors. Omit it to be shown the choices.'),
|
|
43
57
|
],
|
|
44
|
-
[
|
|
58
|
+
[
|
|
59
|
+
' -o, --out <dir>',
|
|
60
|
+
dim(`Where the index goes. Default ${DEFAULT_DIR}, or ${DIR_ENV}.`),
|
|
61
|
+
],
|
|
45
62
|
[
|
|
46
63
|
' --batch <n>',
|
|
47
64
|
dim('Texts per embedding request, and how often progress prints. Default 96.'),
|
|
@@ -65,7 +82,7 @@ export const command = {
|
|
|
65
82
|
'',
|
|
66
83
|
'Search filters and shape',
|
|
67
84
|
...table([
|
|
68
|
-
[' -d, --dir <dir>', dim(`Which index.
|
|
85
|
+
[' -d, --dir <dir>', dim(`Which index. Found from here if unset; see ${DIR_ENV}.`)],
|
|
69
86
|
[' --embedding <ref>', dim('Must be the one the index was built with.')],
|
|
70
87
|
[' --direction <d>', dim('input | output | any. Default any.')],
|
|
71
88
|
[' --method-type <t>', dim('read_only | read_write | any. Default any.')],
|
|
@@ -77,11 +94,46 @@ export const command = {
|
|
|
77
94
|
[' --max-hops <n>', dim('How far apart two hits may be. Default 3.')],
|
|
78
95
|
[' --max-nodes <n>', dim('Nodes per result. Default 200.')],
|
|
79
96
|
[' --format <f>', dim('text | mermaid | mermaid-flowchart | ts | openapi.')],
|
|
97
|
+
[' --show-source', dim('Name the document each operation and schema came from.')],
|
|
80
98
|
[' --no-docs', dim('Leave the descriptions out.')],
|
|
81
99
|
[' --interactive', dim('Prompt, search, refine. Needs a terminal.')],
|
|
82
100
|
[' --quiet', dim('No narration.')],
|
|
83
101
|
]),
|
|
84
102
|
'',
|
|
103
|
+
'Exact listing — no embedder, no credential',
|
|
104
|
+
...table([
|
|
105
|
+
[' list methods', dim('Operations. Filter with --path and --name.')],
|
|
106
|
+
[' list types', dim('Schemas. Filter with --name.')],
|
|
107
|
+
[' list properties', dim('Fields and parameters. Filter with --name and --path.')],
|
|
108
|
+
[' grep <pattern>', dim('Substring over every node; --regex for a regex.')],
|
|
109
|
+
[' --regex', dim('Read every pattern as a regex, list and grep alike.')],
|
|
110
|
+
[' --case-sensitive', dim('Match the capitals too.')],
|
|
111
|
+
[' --kind <k>', dim('grep: method | type | property. Repeatable.')],
|
|
112
|
+
[' --name <p>', dim('grep too: only nodes whose name matches. Repeatable.')],
|
|
113
|
+
[' --path <p>', dim('grep too: only what sits on a matching route.')],
|
|
114
|
+
[' --ids-only', dim('grep: bare ids, to pipe into show.')],
|
|
115
|
+
[' --source <name>', dim('Only this document, as `stats` names it.')],
|
|
116
|
+
[' --show-source', dim('Print which document each row came from.')],
|
|
117
|
+
[' --limit <n>', dim('Keep at most n; the count still reports them all.')],
|
|
118
|
+
]),
|
|
119
|
+
'',
|
|
120
|
+
dim(' A pattern with * or ? is a glob over the whole name; otherwise it is'),
|
|
121
|
+
dim(' a substring, so --name password finds ResetPasswordPayload. With'),
|
|
122
|
+
dim(' --regex it is a regex either way, so --path "^/(users|teams)/" works.'),
|
|
123
|
+
'',
|
|
124
|
+
'Show',
|
|
125
|
+
...table([
|
|
126
|
+
[' <id...>', dim('Node ids, e.g. Type:User or Property:User.email.')],
|
|
127
|
+
[' --method <name>', dim('An operation by name. * to take more. Repeatable.')],
|
|
128
|
+
[' --type <name>', dim('A schema by name. * to take more. Repeatable.')],
|
|
129
|
+
[' --source <name>', dim('A whole document, as it was indexed.')],
|
|
130
|
+
[' --show-source', dim('Name the document each node came from.')],
|
|
131
|
+
[' --exact', dim('Only what was named, without the neighbours.')],
|
|
132
|
+
]),
|
|
133
|
+
'',
|
|
134
|
+
dim(`Without --dir, the index is the nearest one at or above the working`),
|
|
135
|
+
dim(`directory; ${cyan(DIR_ENV)} names it outright.`),
|
|
136
|
+
'',
|
|
85
137
|
dim(`Credentials come from the ${cyan('zen')} keyring — try ${cyan('zen key ls')}.`),
|
|
86
138
|
],
|
|
87
139
|
async run(ctx) {
|
|
@@ -94,6 +146,10 @@ export const command = {
|
|
|
94
146
|
return await index(tail, ctx);
|
|
95
147
|
case 'search':
|
|
96
148
|
return await search(tail, ctx);
|
|
149
|
+
case 'list':
|
|
150
|
+
return await list(tail, ctx);
|
|
151
|
+
case 'grep':
|
|
152
|
+
return await grep(tail, ctx);
|
|
97
153
|
case 'show':
|
|
98
154
|
return await show(tail, ctx);
|
|
99
155
|
case 'stats':
|
|
@@ -114,7 +170,7 @@ async function index(args, ctx) {
|
|
|
114
170
|
if (positionals.length === 0) {
|
|
115
171
|
throw usageError('no document given', INDEX_USAGE);
|
|
116
172
|
}
|
|
117
|
-
const out =
|
|
173
|
+
const out = outputDir(ctx.cwd, values.out);
|
|
118
174
|
const loud = !values.quiet && !ctx.json;
|
|
119
175
|
const chosen = await embedder(values.embedding);
|
|
120
176
|
const started = Date.now();
|
|
@@ -208,14 +264,19 @@ const SEARCH_OPTIONS = {
|
|
|
208
264
|
format: { type: 'string' },
|
|
209
265
|
'no-docs': { type: 'boolean' },
|
|
210
266
|
'only-hits': { type: 'boolean' },
|
|
267
|
+
'show-source': { type: 'boolean' },
|
|
211
268
|
interactive: { type: 'boolean' },
|
|
212
269
|
quiet: { type: 'boolean' },
|
|
213
270
|
};
|
|
214
271
|
async function search(args, ctx) {
|
|
215
272
|
const { values, positionals } = parse(args, SEARCH_OPTIONS, SEARCH_USAGE);
|
|
216
|
-
const dir =
|
|
273
|
+
const dir = indexDir(ctx, values.dir);
|
|
217
274
|
const format = formatOf(values.format);
|
|
218
|
-
const options = {
|
|
275
|
+
const options = {
|
|
276
|
+
docs: !values['no-docs'],
|
|
277
|
+
onlyHits: values['only-hits'],
|
|
278
|
+
source: values['show-source'],
|
|
279
|
+
};
|
|
219
280
|
const query = { ...(await fromStdin(values.query)), ...fromFlags(values, positionals) };
|
|
220
281
|
// Everything that can be wrong about the invocation is settled before a
|
|
221
282
|
// credential is asked for, so a typo is a usage error and not a login.
|
|
@@ -317,36 +378,285 @@ function check(value) {
|
|
|
317
378
|
}
|
|
318
379
|
}
|
|
319
380
|
// ---------------------------------------------------------------------------
|
|
320
|
-
//
|
|
381
|
+
// list, grep
|
|
382
|
+
//
|
|
383
|
+
// The deterministic half. Neither takes an embedder, because neither ranks
|
|
384
|
+
// anything: `list` filters on the attributes a node already has and `grep`
|
|
385
|
+
// reads the same materialized string the index was built from. What comes back
|
|
386
|
+
// is every match, and where a limit cut the list the count still reports the
|
|
387
|
+
// total — being shown three of three hundred is only useful if you are told
|
|
388
|
+
// which of the two happened.
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
const SUBJECTS = {
|
|
391
|
+
methods: 'method',
|
|
392
|
+
types: 'type',
|
|
393
|
+
properties: 'property',
|
|
394
|
+
};
|
|
395
|
+
async function list(args, ctx) {
|
|
396
|
+
const { values, positionals } = parse(args, {
|
|
397
|
+
dir: { type: 'string', short: 'd' },
|
|
398
|
+
name: MANY,
|
|
399
|
+
path: MANY,
|
|
400
|
+
source: { type: 'string' },
|
|
401
|
+
'method-type': { type: 'string' },
|
|
402
|
+
direction: { type: 'string' },
|
|
403
|
+
regex: { type: 'boolean' },
|
|
404
|
+
'case-sensitive': { type: 'boolean' },
|
|
405
|
+
'show-source': { type: 'boolean' },
|
|
406
|
+
limit: { type: 'string' },
|
|
407
|
+
quiet: { type: 'boolean' },
|
|
408
|
+
}, LIST_USAGE);
|
|
409
|
+
const subject = positionals[0];
|
|
410
|
+
const kind = subject ? SUBJECTS[subject] : undefined;
|
|
411
|
+
if (!kind) {
|
|
412
|
+
throw usageError(subject ? `cannot list "${subject}"` : 'nothing named to list', `expected one of ${Object.keys(SUBJECTS).join(', ')}`);
|
|
413
|
+
}
|
|
414
|
+
if (positionals.length > 1) {
|
|
415
|
+
throw usageError('one subject at a time', LIST_USAGE);
|
|
416
|
+
}
|
|
417
|
+
const how = { regex: values.regex, caseSensitive: values['case-sensitive'] };
|
|
418
|
+
const index = await openIndex(indexDir(ctx, values.dir));
|
|
419
|
+
const found = listNodes(index.graph, {
|
|
420
|
+
kind,
|
|
421
|
+
name: patterns(values.name, '--name', how),
|
|
422
|
+
path: patterns(values.path, '--path', how),
|
|
423
|
+
source: values.source,
|
|
424
|
+
methodType: oneOf(values['method-type'], ['read_only', 'read_write'], '--method-type'),
|
|
425
|
+
direction: oneOf(values.direction, ['input', 'output'], '--direction'),
|
|
426
|
+
limit: values.limit ? count(values.limit, '--limit') : undefined,
|
|
427
|
+
});
|
|
428
|
+
if (ctx.json) {
|
|
429
|
+
json({ found: found.found, truncated: found.truncated, rows: found.rows });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const lines = rowLines(index.graph, kind, found.rows, values['show-source']);
|
|
433
|
+
if (lines.length > 0) {
|
|
434
|
+
write(lines.join('\n'));
|
|
435
|
+
}
|
|
436
|
+
if (!values.quiet) {
|
|
437
|
+
note(dim(` ${found.found} ${subject}${shown(found.found, found.rows.length)}`));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function rowLines(graph, kind, rows, showSource = false) {
|
|
441
|
+
const from = (r) => (showSource ? [dim(sourceTag(r.source))] : []);
|
|
442
|
+
if (kind === 'method') {
|
|
443
|
+
return table(rows.map((r) => [`${r.httpMethod} ${r.path}`, r.name, ...from(r), doc(r.doc)]));
|
|
444
|
+
}
|
|
445
|
+
if (kind === 'type') {
|
|
446
|
+
return table(rows.map((r) => [
|
|
447
|
+
r.name,
|
|
448
|
+
dim(fields(propertyCount(graph, r.id))),
|
|
449
|
+
dim(r.direction === 'none' ? '' : `(${r.direction})`),
|
|
450
|
+
...from(r),
|
|
451
|
+
doc(r.doc),
|
|
452
|
+
]));
|
|
453
|
+
}
|
|
454
|
+
return table(rows.map((r) => [
|
|
455
|
+
`${r.parent ? `${r.parent}.` : ''}${r.name}${r.required ? '' : '?'}`,
|
|
456
|
+
`: ${r.signature || 'unknown'}`,
|
|
457
|
+
...from(r),
|
|
458
|
+
doc(r.doc),
|
|
459
|
+
]));
|
|
460
|
+
}
|
|
461
|
+
async function grep(args, ctx) {
|
|
462
|
+
const { values, positionals } = parse(args, {
|
|
463
|
+
dir: { type: 'string', short: 'd' },
|
|
464
|
+
regex: { type: 'boolean' },
|
|
465
|
+
'case-sensitive': { type: 'boolean' },
|
|
466
|
+
kind: MANY,
|
|
467
|
+
name: MANY,
|
|
468
|
+
path: MANY,
|
|
469
|
+
source: { type: 'string' },
|
|
470
|
+
'show-source': { type: 'boolean' },
|
|
471
|
+
limit: { type: 'string' },
|
|
472
|
+
'ids-only': { type: 'boolean' },
|
|
473
|
+
quiet: { type: 'boolean' },
|
|
474
|
+
}, GREP_USAGE);
|
|
475
|
+
if (positionals.length === 0) {
|
|
476
|
+
throw usageError('no pattern given', GREP_USAGE);
|
|
477
|
+
}
|
|
478
|
+
if (positionals.length > 1) {
|
|
479
|
+
throw usageError('one pattern at a time — quote it if it has spaces', GREP_USAGE);
|
|
480
|
+
}
|
|
481
|
+
const kinds = (values.kind ?? []).map((k) => oneOf(k, ['method', 'type', 'property'], '--kind'));
|
|
482
|
+
// The pattern is read as the flags say; the constraints are always names,
|
|
483
|
+
// so they stay globs-or-substrings even under --regex on the pattern.
|
|
484
|
+
const how = { caseSensitive: values['case-sensitive'] };
|
|
485
|
+
const index = await openIndex(indexDir(ctx, values.dir));
|
|
486
|
+
const result = pattern(() => grepNodes(index.graph, matcher(positionals[0], {
|
|
487
|
+
regex: values.regex,
|
|
488
|
+
caseSensitive: values['case-sensitive'],
|
|
489
|
+
}), {
|
|
490
|
+
kinds,
|
|
491
|
+
source: values.source,
|
|
492
|
+
name: patterns(values.name, '--name', how),
|
|
493
|
+
path: patterns(values.path, '--path', how),
|
|
494
|
+
limit: values.limit ? count(values.limit, '--limit') : undefined,
|
|
495
|
+
}));
|
|
496
|
+
if (ctx.json) {
|
|
497
|
+
json({
|
|
498
|
+
found: result.found,
|
|
499
|
+
truncated: result.truncated,
|
|
500
|
+
matches: result.matches.map((m) => ({ id: m.id, ...m.attributes, text: m.text })),
|
|
501
|
+
});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (result.matches.length > 0) {
|
|
505
|
+
const lines = values['ids-only']
|
|
506
|
+
? result.matches.map((m) => m.id)
|
|
507
|
+
: table(result.matches.map((m) => [
|
|
508
|
+
m.id,
|
|
509
|
+
...(values['show-source'] ? [dim(sourceTag(m.attributes.source))] : []),
|
|
510
|
+
dim(clip(m.text, 140)),
|
|
511
|
+
]));
|
|
512
|
+
write(lines.join('\n'));
|
|
513
|
+
}
|
|
514
|
+
if (!values.quiet && !values['ids-only']) {
|
|
515
|
+
note(dim(` ${result.found} match(es)${shown(result.found, result.matches.length)}`));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
321
518
|
// ---------------------------------------------------------------------------
|
|
519
|
+
const shown = (found, kept) => (kept < found ? `, showing ${kept}` : '');
|
|
520
|
+
/**
|
|
521
|
+
* Where the index is, said out loud when nobody named it. Finding one and not
|
|
522
|
+
* saying which would make every answer here unattributable.
|
|
523
|
+
*/
|
|
524
|
+
function indexDir(ctx, flag) {
|
|
525
|
+
const { dir, from } = locateIndex(ctx.cwd, flag);
|
|
526
|
+
if (from === 'found' && !ctx.json) {
|
|
527
|
+
note(dim(` using ${relative(ctx.cwd, dir) || dir}`));
|
|
528
|
+
}
|
|
529
|
+
return dir;
|
|
530
|
+
}
|
|
531
|
+
function patterns(values, flag, options = {}) {
|
|
532
|
+
if (!values || values.length === 0) {
|
|
533
|
+
return undefined;
|
|
534
|
+
}
|
|
535
|
+
return values.map((p) => pattern(() => loose(p, options), flag));
|
|
536
|
+
}
|
|
537
|
+
/** A bad pattern is a bad invocation, not a failure of the index. */
|
|
538
|
+
function pattern(run, flag) {
|
|
539
|
+
try {
|
|
540
|
+
return run();
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
if (err instanceof PatternError) {
|
|
544
|
+
throw usageError(`${flag ? `${flag}: ` : ''}${err.message}`, USAGE);
|
|
545
|
+
}
|
|
546
|
+
throw err;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function oneOf(value, allowed, flag) {
|
|
550
|
+
if (value === undefined || value === 'any') {
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
if (!allowed.includes(value)) {
|
|
554
|
+
throw usageError(`${flag} cannot be "${value}"`, `expected ${allowed.join(' or ')}`);
|
|
555
|
+
}
|
|
556
|
+
return value;
|
|
557
|
+
}
|
|
558
|
+
const doc = (text) => (text ? dim(`— ${clip(text.replace(/\s+/g, ' '), 90)}`) : '');
|
|
559
|
+
const clip = (text, max) => text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
322
560
|
/**
|
|
323
561
|
* No embedder and no store: naming a node is a graph lookup, and asking for a
|
|
324
562
|
* credential to print something already on disk would be theatre.
|
|
563
|
+
*
|
|
564
|
+
* Ids are the precise way in, and `--method`/`--type` are the way in for
|
|
565
|
+
* someone who has a name rather than an id — which, with `--format openapi
|
|
566
|
+
* --exact`, is how a resolved slice of the document is got out.
|
|
325
567
|
*/
|
|
326
568
|
async function show(args, ctx) {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
569
|
+
const { values, positionals } = parse(args, {
|
|
570
|
+
dir: { type: 'string', short: 'd' },
|
|
571
|
+
method: MANY,
|
|
572
|
+
type: MANY,
|
|
573
|
+
source: { type: 'string' },
|
|
574
|
+
exact: { type: 'boolean' },
|
|
575
|
+
format: { type: 'string' },
|
|
576
|
+
'max-nodes': { type: 'string' },
|
|
577
|
+
'no-docs': { type: 'boolean' },
|
|
578
|
+
'show-source': { type: 'boolean' },
|
|
579
|
+
quiet: { type: 'boolean' },
|
|
580
|
+
}, SHOW_USAGE);
|
|
332
581
|
const format = formatOf(values.format);
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
582
|
+
const dir = indexDir(ctx, values.dir);
|
|
583
|
+
// A whole document, verbatim: the copy kept at index time is the resolved
|
|
584
|
+
// original, and anything rebuilt from the graph would be a paraphrase.
|
|
585
|
+
if (values.source && format === 'openapi' && positionals.length === 0 && !named(values)) {
|
|
586
|
+
const document = await readSource(dir, values.source);
|
|
587
|
+
if (document) {
|
|
588
|
+
write(document);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (!values.quiet) {
|
|
592
|
+
note(dim(' this index kept no copy of the documents — rebuilding it from the graph'));
|
|
593
|
+
}
|
|
337
594
|
}
|
|
338
|
-
const
|
|
339
|
-
const
|
|
595
|
+
const index = await openIndex(dir);
|
|
596
|
+
const ids = resolveIds(index.graph, positionals, values);
|
|
597
|
+
const subgraphs = values.exact
|
|
598
|
+
? [select(index.graph, ids)]
|
|
599
|
+
: stitch(index.graph, ids.map((id) => ({ id, term: id, field: 'show', score: 1 })), {
|
|
600
|
+
maxNodes: values['max-nodes']
|
|
601
|
+
? count(values['max-nodes'], '--max-nodes')
|
|
602
|
+
: undefined,
|
|
603
|
+
});
|
|
604
|
+
const text = await present(index, subgraphs, format, {
|
|
605
|
+
docs: !values['no-docs'],
|
|
606
|
+
source: values['show-source'],
|
|
607
|
+
});
|
|
340
608
|
if (ctx.json) {
|
|
341
|
-
json({ subgraphs, rendered: text });
|
|
609
|
+
json({ ids, subgraphs, rendered: text });
|
|
342
610
|
}
|
|
343
611
|
else if (text) {
|
|
344
612
|
write(text);
|
|
345
613
|
}
|
|
346
614
|
}
|
|
615
|
+
const named = (values) => Boolean(values.method?.length || values.type?.length);
|
|
616
|
+
/** Ids as given, plus whatever the name selectors resolve to. */
|
|
617
|
+
function resolveIds(graph, ids, values) {
|
|
618
|
+
if (ids.length === 0 && !named(values) && !values.source) {
|
|
619
|
+
throw usageError('no node named', SHOW_USAGE);
|
|
620
|
+
}
|
|
621
|
+
const missing = ids.filter((id) => !graph.hasNode(id));
|
|
622
|
+
if (missing.length > 0) {
|
|
623
|
+
throw new CliError(`no such node: ${missing.join(', ')}`, EXIT.failed, 'ids look like `Type:User` or `Property:User.email`');
|
|
624
|
+
}
|
|
625
|
+
const out = new Set(ids);
|
|
626
|
+
for (const kind of ['method', 'type']) {
|
|
627
|
+
for (const wanted of values[kind] ?? []) {
|
|
628
|
+
// Selecting, not searching: a bare name means that name. A star is
|
|
629
|
+
// the way to ask for more than one.
|
|
630
|
+
const match = isGlob(wanted)
|
|
631
|
+
? pattern(() => wildcard(wanted), `--${kind}`)
|
|
632
|
+
: (name) => name === wanted;
|
|
633
|
+
const rows = listNodes(graph, { kind, name: [match], source: values.source });
|
|
634
|
+
// A selector that matched nothing is a wrong answer, not an empty
|
|
635
|
+
// one: the caller named something they believe is there.
|
|
636
|
+
if (rows.found === 0) {
|
|
637
|
+
throw new CliError(`no ${kind} called ${wanted}`, EXIT.failed, `try: zen rag schema list ${kind}s --name "${wanted}"`);
|
|
638
|
+
}
|
|
639
|
+
for (const row of rows.rows) {
|
|
640
|
+
out.add(row.id);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
// `--source` on its own means the whole document.
|
|
645
|
+
if (out.size === 0 && values.source) {
|
|
646
|
+
for (const kind of ['method', 'type']) {
|
|
647
|
+
for (const row of listNodes(graph, { kind, source: values.source }).rows) {
|
|
648
|
+
out.add(row.id);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (out.size === 0) {
|
|
652
|
+
throw new CliError(`nothing in this index came from ${values.source}`, EXIT.failed);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return [...out];
|
|
656
|
+
}
|
|
347
657
|
async function stats(args, ctx) {
|
|
348
658
|
const { values } = parse(args, { dir: { type: 'string', short: 'd' } }, 'zen rag schema stats [--dir <dir>]');
|
|
349
|
-
const dir =
|
|
659
|
+
const dir = indexDir(ctx, values.dir);
|
|
350
660
|
const manifest = await readManifest(dir);
|
|
351
661
|
if (ctx.json) {
|
|
352
662
|
json(manifest);
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export * from './schema/entities.ts';
|
|
|
5
5
|
export * from './schema/files.ts';
|
|
6
6
|
export * from './schema/graph.ts';
|
|
7
7
|
export * from './schema/hydrate.ts';
|
|
8
|
+
export * from './schema/locate.ts';
|
|
9
|
+
export * from './schema/lookup.ts';
|
|
10
|
+
export * from './schema/match.ts';
|
|
8
11
|
export * from './schema/render.ts';
|
|
9
12
|
export * from './schema/schema.ts';
|
|
10
13
|
export * from './schema/search.ts';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,9 @@ export * from "./schema/entities.js";
|
|
|
5
5
|
export * from "./schema/files.js";
|
|
6
6
|
export * from "./schema/graph.js";
|
|
7
7
|
export * from "./schema/hydrate.js";
|
|
8
|
+
export * from "./schema/locate.js";
|
|
9
|
+
export * from "./schema/lookup.js";
|
|
10
|
+
export * from "./schema/match.js";
|
|
8
11
|
export * from "./schema/render.js";
|
|
9
12
|
export * from "./schema/schema.js";
|
|
10
13
|
export * from "./schema/search.js";
|
|
@@ -15,4 +15,6 @@ export interface EntityRecord {
|
|
|
15
15
|
text: string;
|
|
16
16
|
}
|
|
17
17
|
export declare function toEntities(graph: ApiGraph): EntityRecord[];
|
|
18
|
+
/** Exported so a literal search reads the same string the index was built from. */
|
|
19
|
+
export declare function textOf(graph: ApiGraph, id: string): string;
|
|
18
20
|
//# sourceMappingURL=entities.d.ts.map
|
package/dist/schema/entities.js
CHANGED
|
@@ -22,7 +22,8 @@ export function toEntities(graph) {
|
|
|
22
22
|
}
|
|
23
23
|
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
/** Exported so a literal search reads the same string the index was built from. */
|
|
26
|
+
export function textOf(graph, id) {
|
|
26
27
|
const a = graph.getNodeAttributes(id);
|
|
27
28
|
const parts = [];
|
|
28
29
|
if (a.kind === 'method') {
|
package/dist/schema/files.d.ts
CHANGED
|
@@ -68,6 +68,13 @@ export interface OpenIndex {
|
|
|
68
68
|
export declare const lancePath: (dir: string) => string;
|
|
69
69
|
export declare function writeIndex(dir: string, index: WrittenIndex): Promise<void>;
|
|
70
70
|
export declare function openIndex(dir: string): Promise<OpenIndex>;
|
|
71
|
+
/**
|
|
72
|
+
* The bundled document as it was indexed. Kept only when the index was built
|
|
73
|
+
* with sources, which is why the manifest is asked first: the difference
|
|
74
|
+
* between "no such document" and "this index did not keep them" is the whole
|
|
75
|
+
* of what the caller can do next.
|
|
76
|
+
*/
|
|
77
|
+
export declare function readSource(dir: string, name: string): Promise<string | undefined>;
|
|
71
78
|
export declare function readManifest(dir: string): Promise<Manifest>;
|
|
72
79
|
/**
|
|
73
80
|
* A store answers with the neighbours of a vector, and a vector means nothing
|