@zenera/rag 1.1.5 → 1.1.8

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 (41) hide show
  1. package/README.md +68 -6
  2. package/dist/command.js +41 -674
  3. package/dist/common/embedder.d.ts +3 -0
  4. package/dist/common/embedder.js +64 -0
  5. package/dist/common/locate.d.ts +18 -0
  6. package/dist/common/locate.js +155 -0
  7. package/dist/common/manifest.d.ts +50 -0
  8. package/dist/common/manifest.js +62 -0
  9. package/dist/{schema → common}/match.d.ts +4 -0
  10. package/dist/{schema → common}/match.js +7 -0
  11. package/dist/common/progress.d.ts +57 -0
  12. package/dist/common/progress.js +155 -0
  13. package/dist/common/prose.d.ts +13 -0
  14. package/dist/common/prose.js +56 -0
  15. package/dist/index.d.ts +6 -3
  16. package/dist/index.js +6 -3
  17. package/dist/schema/build.js +6 -2
  18. package/dist/schema/command.d.ts +3 -0
  19. package/dist/schema/command.js +819 -0
  20. package/dist/schema/files.d.ts +5 -27
  21. package/dist/schema/files.js +9 -29
  22. package/dist/schema/lookup.d.ts +12 -2
  23. package/dist/schema/lookup.js +29 -2
  24. package/dist/{present.d.ts → schema/present.d.ts} +5 -5
  25. package/dist/{present.js → schema/present.js} +2 -2
  26. package/dist/{query.d.ts → schema/query.d.ts} +1 -1
  27. package/dist/schema/readme.d.ts +6 -0
  28. package/dist/schema/readme.js +122 -0
  29. package/dist/schema/render.d.ts +8 -0
  30. package/dist/schema/render.js +12 -2
  31. package/dist/{repl.d.ts → schema/repl.d.ts} +1 -1
  32. package/dist/schema/search.js +2 -1
  33. package/dist/schema/tools.d.ts +3 -1
  34. package/dist/schema/tools.js +152 -19
  35. package/dist/schema/trace.d.ts +52 -0
  36. package/dist/schema/trace.js +144 -0
  37. package/package.json +3 -3
  38. package/dist/schema/progress.d.ts +0 -26
  39. package/dist/schema/progress.js +0 -316
  40. /package/dist/{query.js → schema/query.js} +0 -0
  41. /package/dist/{repl.js → schema/repl.js} +0 -0
@@ -1,8 +1,10 @@
1
+ import { type IndexHead, type IndexSpec } from '../common/manifest.ts';
1
2
  import type { ApiGraph } from './graph.ts';
2
3
  import type { Schema } from './schema.ts';
3
4
  import type { Operation } from './spec.ts';
4
5
  export declare const INDEX_VERSION = 3;
5
- export declare const MANIFEST_FILE = "manifest.json";
6
+ /** How a schema index is found, read, and refused. */
7
+ export declare const SCHEMA_INDEX: IndexSpec;
6
8
  export declare const GRAPH_FILE = "graph.json";
7
9
  export declare const SCHEMAS_FILE = "schemas.json";
8
10
  export declare const OPERATIONS_FILE = "operations.json";
@@ -30,23 +32,9 @@ export interface Counts {
30
32
  properties: number;
31
33
  entities: number;
32
34
  }
33
- export interface Manifest {
34
- version: number;
35
- createdAt: string;
36
- indexer: string;
37
- /** `ref` as it was typed, `id` as the embedder answers to it */
38
- embedding: {
39
- ref: string;
40
- id: string;
41
- dimensions: number;
42
- };
35
+ export interface Manifest extends IndexHead {
43
36
  sources: SourceRecord[];
44
37
  counts: Counts;
45
- /** whether the table carries an fts index, and whether it carries a vector one */
46
- indexes: {
47
- fts: boolean;
48
- vector: boolean;
49
- };
50
38
  }
51
39
  export interface WrittenIndex {
52
40
  manifest: Manifest;
@@ -75,15 +63,5 @@ export declare function openIndex(dir: string): Promise<OpenIndex>;
75
63
  * of what the caller can do next.
76
64
  */
77
65
  export declare function readSource(dir: string, name: string): Promise<string | undefined>;
78
- export declare function readManifest(dir: string): Promise<Manifest>;
79
- /**
80
- * A store answers with the neighbours of a vector, and a vector means nothing
81
- * without the model that produced it. Asking one model's index a question
82
- * embedded by another returns rows, in an order that is noise.
83
- *
84
- * Either spelling is accepted, because `openai:text-embedding-3-small` and
85
- * `text-embedding-3-small` are one model and which of them was typed is not
86
- * something anyone should have to remember.
87
- */
88
- export declare function assertSameEmbedding(manifest: Manifest, ref: string): void;
66
+ export declare const readManifest: (dir: string) => Promise<Manifest>;
89
67
  //# sourceMappingURL=files.d.ts.map
@@ -2,6 +2,7 @@ import { CliError, EXIT } from '@zenera/cli/lib';
2
2
  import { MultiDirectedGraph } from 'graphology';
3
3
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { MANIFEST_FILE, readHead } from "../common/manifest.js";
5
6
  // ---------------------------------------------------------------------------
6
7
  // What an index is, on disk
7
8
  //
@@ -20,7 +21,13 @@ import { join } from 'node:path';
20
21
  // model — without going looking for the files it was made from.
21
22
  // ---------------------------------------------------------------------------
22
23
  export const INDEX_VERSION = 3;
23
- export const MANIFEST_FILE = 'manifest.json';
24
+ /** How a schema index is found, read, and refused. */
25
+ export const SCHEMA_INDEX = {
26
+ kind: 'schema',
27
+ version: INDEX_VERSION,
28
+ defaultDir: './schema-db',
29
+ envName: 'ZEN_SCHEMA_DB',
30
+ };
24
31
  export const GRAPH_FILE = 'graph.json';
25
32
  export const SCHEMAS_FILE = 'schemas.json';
26
33
  export const OPERATIONS_FILE = 'operations.json';
@@ -77,32 +84,5 @@ export async function readSource(dir, name) {
77
84
  }
78
85
  return await readFile(join(dir, record.path), 'utf8');
79
86
  }
80
- export async function readManifest(dir) {
81
- let text;
82
- try {
83
- text = await readFile(join(dir, MANIFEST_FILE), 'utf8');
84
- }
85
- catch {
86
- throw new CliError(`${dir} does not hold an index`, EXIT.invalid, 'build one first with `zen rag schema index`');
87
- }
88
- const manifest = JSON.parse(text);
89
- if (manifest.version !== INDEX_VERSION) {
90
- throw new CliError(`${dir} is a version ${manifest.version} index, and this indexer reads version ${INDEX_VERSION}`, EXIT.invalid, 'rebuild it with `zen rag schema index`');
91
- }
92
- return manifest;
93
- }
94
- /**
95
- * A store answers with the neighbours of a vector, and a vector means nothing
96
- * without the model that produced it. Asking one model's index a question
97
- * embedded by another returns rows, in an order that is noise.
98
- *
99
- * Either spelling is accepted, because `openai:text-embedding-3-small` and
100
- * `text-embedding-3-small` are one model and which of them was typed is not
101
- * something anyone should have to remember.
102
- */
103
- export function assertSameEmbedding(manifest, ref) {
104
- if (ref !== manifest.embedding.ref && ref !== manifest.embedding.id) {
105
- throw new CliError(`this index was built with ${manifest.embedding.ref}, not ${ref}`, EXIT.invalid, `search it with --embedding ${manifest.embedding.ref}, or rebuild it`);
106
- }
107
- }
87
+ export const readManifest = (dir) => readHead(dir, SCHEMA_INDEX);
108
88
  //# sourceMappingURL=files.js.map
@@ -1,5 +1,5 @@
1
- import type { ApiGraph, NodeAttrs, NodeKind } from './graph.ts';
2
- import { type Matcher } from './match.ts';
1
+ import { type Matcher } from '../common/match.ts';
2
+ import { type ApiGraph, type NodeAttrs, type NodeKind } from './graph.ts';
3
3
  export interface Row extends NodeAttrs {
4
4
  id: string;
5
5
  }
@@ -28,6 +28,9 @@ export interface Match {
28
28
  export interface GrepFilter {
29
29
  kinds?: readonly string[];
30
30
  source?: string;
31
+ /** the same two constraints `list` takes, so one question has one spelling */
32
+ name?: readonly Matcher[];
33
+ path?: readonly Matcher[];
31
34
  limit?: number;
32
35
  }
33
36
  export interface Grep {
@@ -41,4 +44,11 @@ export declare function grepNodes(graph: ApiGraph, match: Matcher, filter?: Grep
41
44
  export declare function propertyCount(graph: ApiGraph, id: string): number;
42
45
  /** That count, said properly, in the one phrasing the CLI and the tools share. */
43
46
  export declare const fields: (n: number) => string;
47
+ /**
48
+ * The route a node sits on. An operation carries its own; a parameter carries
49
+ * the operation's name instead, so it is looked up. A schema has no route at
50
+ * all and never will — the same DTO is returned by half the API — which is
51
+ * why a `--path` filter is a filter on the operations and what hangs off them.
52
+ */
53
+ export declare function routeOf(graph: ApiGraph, id: string, a: NodeAttrs): string;
44
54
  //# sourceMappingURL=lookup.d.ts.map
@@ -1,5 +1,6 @@
1
+ import { PatternError } from "../common/match.js";
1
2
  import { textOf } from "./entities.js";
2
- import { PatternError } from "./match.js";
3
+ import { methodId } from "./graph.js";
3
4
  /** A scan is bounded, because a pattern may have come from a model. */
4
5
  const DEADLINE_MS = 2000;
5
6
  const DEADLINE_EVERY = 500;
@@ -12,7 +13,7 @@ export function listNodes(graph, filter) {
12
13
  if (filter.name && !filter.name.some((match) => match(a.name))) {
13
14
  return;
14
15
  }
15
- if (filter.path && !filter.path.some((match) => match(a.path))) {
16
+ if (filter.path && !matchesRoute(graph, id, a, filter.path)) {
16
17
  return;
17
18
  }
18
19
  rows.push({ ...a, id });
@@ -36,6 +37,12 @@ export function grepNodes(graph, match, filter = {}) {
36
37
  if (filter.source && a.source !== filter.source) {
37
38
  continue;
38
39
  }
40
+ if (filter.name && !filter.name.some((match) => match(a.name))) {
41
+ continue;
42
+ }
43
+ if (filter.path && !matchesRoute(graph, id, a, filter.path)) {
44
+ continue;
45
+ }
39
46
  const text = textOf(graph, id);
40
47
  if (match(text)) {
41
48
  matches.push({ id, attributes: a, text });
@@ -53,7 +60,27 @@ export function propertyCount(graph, id) {
53
60
  }
54
61
  /** That count, said properly, in the one phrasing the CLI and the tools share. */
55
62
  export const fields = (n) => `${n} ${n === 1 ? 'field' : 'fields'}`;
63
+ /**
64
+ * The route a node sits on. An operation carries its own; a parameter carries
65
+ * the operation's name instead, so it is looked up. A schema has no route at
66
+ * all and never will — the same DTO is returned by half the API — which is
67
+ * why a `--path` filter is a filter on the operations and what hangs off them.
68
+ */
69
+ export function routeOf(graph, id, a) {
70
+ if (a.path) {
71
+ return a.path;
72
+ }
73
+ if (a.kind !== 'property' || !a.parent) {
74
+ return '';
75
+ }
76
+ const owner = methodId(a.parent);
77
+ return graph.hasNode(owner) ? graph.getNodeAttribute(owner, 'path') : '';
78
+ }
56
79
  // ---------------------------------------------------------------------------
80
+ function matchesRoute(graph, id, a, patterns) {
81
+ const route = routeOf(graph, id, a);
82
+ return route !== '' && patterns.some((match) => match(route));
83
+ }
57
84
  function passes(a, filter) {
58
85
  if (filter.source && a.source !== filter.source) {
59
86
  return false;
@@ -1,8 +1,8 @@
1
- import { type HydrateOptions } from './schema/hydrate.ts';
2
- import { type RenderOptions } from './schema/render.ts';
3
- import type { Schema } from './schema/schema.ts';
4
- import type { Operation } from './schema/spec.ts';
5
- import type { Subgraph } from './schema/subgraph.ts';
1
+ import { type HydrateOptions } from './hydrate.ts';
2
+ import { type RenderOptions } from './render.ts';
3
+ import type { Schema } from './schema.ts';
4
+ import type { Operation } from './spec.ts';
5
+ import type { Subgraph } from './subgraph.ts';
6
6
  export declare const FORMATS: readonly ["text", "mermaid", "mermaid-flowchart", "ts", "openapi"];
7
7
  export type Format = (typeof FORMATS)[number];
8
8
  export declare function isFormat(value: string): value is Format;
@@ -1,5 +1,5 @@
1
- import { toOpenApi, toTypeScript } from "./schema/hydrate.js";
2
- import { render } from "./schema/render.js";
1
+ import { toOpenApi, toTypeScript } from "./hydrate.js";
2
+ import { render } from "./render.js";
3
3
  // ---------------------------------------------------------------------------
4
4
  // One subgraph, in whichever of the four shapes was asked for
5
5
  //
@@ -1,4 +1,4 @@
1
- import type { SchemaQuery } from './schema/search.ts';
1
+ import type { SchemaQuery } from './search.ts';
2
2
  export declare class QueryError extends Error {
3
3
  }
4
4
  export declare function parseQuery(value: unknown): SchemaQuery;
@@ -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' | 'graph' | 'embedding' | 'writing';
4
+ export declare const PHASES: Record<Phase, string>;
5
+ export declare const SCHEMA_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',
6
+ graph: 'building the graph',
7
+ embedding: 'embedding',
8
+ writing: 'writing the store',
9
+ };
10
+ export const SCHEMA_REPORT = { building, complete, failed };
11
+ function building(state) {
12
+ const rows = [
13
+ ['documents', state.documents.join(', ')],
14
+ ['embedding', state.embedding],
15
+ [
16
+ 'started',
17
+ `${new Date(state.started).toISOString()} (${duration(state.now - state.started)} ago)`,
18
+ ],
19
+ ['step', state.step],
20
+ ];
21
+ if (state.summary) {
22
+ rows.push(['found', entities(state.summary)]);
23
+ }
24
+ if (state.total > 0) {
25
+ const percent = Math.round((state.done / state.total) * 100);
26
+ rows.push(['embedded', `${state.done} of ${state.total} · ${percent}%`]);
27
+ }
28
+ rows.push(['updated', new Date(state.now).toISOString()]);
29
+ return [
30
+ '# Schema index — being built',
31
+ '',
32
+ 'A searchable index of the API documents named below, written by `zen rag schema index`.',
33
+ '**It is incomplete. Nothing should read it yet.**',
34
+ '',
35
+ ...fields(rows),
36
+ '',
37
+ `These lines are refreshed at most every ${INTERVAL_MS / 1000} seconds while the build runs, and`,
38
+ 'the whole file is replaced by a description of the index when it finishes. If it still says',
39
+ '"being built" and `.lock` names no living process, the build died part way.',
40
+ '',
41
+ ].join('\n');
42
+ }
43
+ function complete(state) {
44
+ const { manifest } = state;
45
+ const titles = manifest.sources.map((s) => s.title).filter(Boolean);
46
+ const what = titles.length > 0 ? titles.join(', ') : basename(state.dir);
47
+ return [
48
+ `# Schema index — ${what}`,
49
+ '',
50
+ `A searchable index of ${plural(manifest.sources.length, 'API document')}, built with`,
51
+ `${manifest.embedding.ref} (${manifest.embedding.dimensions}d) in ${duration(state.ms)}.`,
52
+ 'Ask it for the operations and types behind a question and it answers with a subgraph:',
53
+ 'the endpoints that match, the schemas they carry, and the fields inside those — printed',
54
+ 'as text, Mermaid, TypeScript or OpenAPI.',
55
+ '',
56
+ '## What it covers',
57
+ '',
58
+ ...sourceTable(manifest.sources),
59
+ '',
60
+ `${entities(manifest.counts)},`,
61
+ `${searched(manifest.indexes)}.`,
62
+ '',
63
+ '## Files',
64
+ '',
65
+ ...fields([
66
+ ['manifest.json', 'what this index is and what built it — read this first'],
67
+ ['graph.json', 'the nodes and edges: operations, types, fields'],
68
+ ['schemas.json', 'the JSON Schema of every type'],
69
+ ['operations.json', 'every operation, with its parameters and responses'],
70
+ ...(manifest.sources.some((s) => s.path)
71
+ ? [['sources/', 'the documents themselves, bundled, exactly as indexed']]
72
+ : []),
73
+ ['lance/', 'the LanceDB table: the search text, the vectors, the filter columns'],
74
+ ]),
75
+ '',
76
+ '## Asking it something',
77
+ '',
78
+ 'From this directory:',
79
+ '',
80
+ '```',
81
+ 'zen rag schema search --dir . --all "how do I cancel a subscription"',
82
+ '```',
83
+ '',
84
+ `Built by ${manifest.indexer} on ${manifest.createdAt}.`,
85
+ '',
86
+ ].join('\n');
87
+ }
88
+ function failed(state) {
89
+ return [
90
+ '# Schema index — failed',
91
+ '',
92
+ 'This index was not finished and what is here is incomplete. Nothing should read it;',
93
+ 'build it again with `zen rag schema index`.',
94
+ '',
95
+ ...fields([
96
+ ['documents', state.documents.join(', ')],
97
+ ['step', state.step],
98
+ ['reason', message(state.reason)],
99
+ ['started', new Date(state.started).toISOString()],
100
+ [
101
+ 'failed',
102
+ `${new Date().toISOString()} (after ${duration(Date.now() - state.started)})`,
103
+ ],
104
+ ]),
105
+ '',
106
+ ].join('\n');
107
+ }
108
+ // ---------------------------------------------------------------------------
109
+ const HEADERS = ['document', 'dialect', 'paths', 'operations', 'schemas', 'fields'];
110
+ const sourceTable = (sources) => grid(HEADERS, sources.map((s) => [
111
+ s.file,
112
+ s.dialect,
113
+ String(s.paths),
114
+ String(s.methods),
115
+ String(s.types),
116
+ String(s.properties),
117
+ ]));
118
+ function entities(counts) {
119
+ return (`${plural(counts.entities, 'entity', 'entities')}: ` +
120
+ `${counts.methods} operations, ${counts.types} schemas, ${counts.properties} fields`);
121
+ }
122
+ //# sourceMappingURL=readme.js.map
@@ -3,7 +3,15 @@ export type RenderFormat = 'text' | 'mermaid' | 'mermaid-flowchart';
3
3
  export interface RenderOptions {
4
4
  docs?: boolean;
5
5
  maxDoc?: number;
6
+ /** name the document each operation and schema came from */
7
+ source?: boolean;
6
8
  }
9
+ /**
10
+ * Which document something came from, spelled one way everywhere. An index
11
+ * over four revisions of one API has four `GET /infra/tier-0s`, and a listing
12
+ * that does not say which is which is a listing you have to go and check.
13
+ */
14
+ export declare const sourceTag: (source: string) => string;
7
15
  export declare function render(sub: Subgraph, format: RenderFormat, options?: RenderOptions): string;
8
16
  export declare function toText(sub: Subgraph, options?: RenderOptions): string;
9
17
  export declare function toMermaid(sub: Subgraph, options?: RenderOptions): string;
@@ -1,4 +1,10 @@
1
1
  const HIT = '»';
2
+ /**
3
+ * Which document something came from, spelled one way everywhere. An index
4
+ * over four revisions of one API has four `GET /infra/tier-0s`, and a listing
5
+ * that does not say which is which is a listing you have to go and check.
6
+ */
7
+ export const sourceTag = (source) => (source ? `[source: ${source}]` : '');
2
8
  export function render(sub, format, options = {}) {
3
9
  switch (format) {
4
10
  case 'mermaid':
@@ -44,7 +50,7 @@ export function toText(sub, options = {}) {
44
50
  function methodLines(view, method, options) {
45
51
  const a = method.attributes;
46
52
  const out = [
47
- ` ${mark(method)}${a.httpMethod} ${a.path} ${a.name}${doc(method, options, ' —')}`,
53
+ ` ${mark(method)}${a.httpMethod} ${a.path} ${a.name}${from(method, options)}${doc(method, options, ' —')}`,
48
54
  ];
49
55
  for (const edge of view.out(method.id, 'HAS_PARAM')) {
50
56
  const node = view.node(edge.target);
@@ -62,7 +68,7 @@ function methodLines(view, method, options) {
62
68
  }
63
69
  function typeLines(view, type, options) {
64
70
  const out = [
65
- ` ${mark(type)}${type.attributes.name}${side(type.attributes)}${doc(type, options, ' —')}`,
71
+ ` ${mark(type)}${type.attributes.name}${side(type.attributes)}${from(type, options)}${doc(type, options, ' —')}`,
66
72
  ];
67
73
  const composes = view.out(type.id, 'COMPOSES').map((e) => view.name(e.target));
68
74
  if (composes.length > 0) {
@@ -96,6 +102,10 @@ function doc(node, options, lead) {
96
102
  }
97
103
  return `${lead} ${clip(node.attributes.doc, options.maxDoc ?? 120)}`;
98
104
  }
105
+ function from(node, options) {
106
+ const tag = options.source ? sourceTag(node.attributes.source) : '';
107
+ return tag ? ` ${tag}` : '';
108
+ }
99
109
  // ---------------------------------------------------------------------------
100
110
  // Mermaid
101
111
  // ---------------------------------------------------------------------------
@@ -1,5 +1,5 @@
1
1
  import { type Format, type OutputOptions } from './present.ts';
2
- import type { SchemaIndex, SchemaQuery } from './schema/search.ts';
2
+ import type { SchemaIndex, SchemaQuery } from './search.ts';
3
3
  interface Settings extends OutputOptions {
4
4
  format: Format;
5
5
  }
@@ -1,4 +1,5 @@
1
- import { assertSameEmbedding, openIndex } from "./files.js";
1
+ import { assertSameEmbedding } from "../common/manifest.js";
2
+ import { openIndex } from "./files.js";
2
3
  import { EntityStore } from "./store.js";
3
4
  import { DEFAULT_MAX_HOPS, DEFAULT_MAX_NODES, stitch, } from "./subgraph.js";
4
5
  export const DEFAULT_LIMIT = 5;
@@ -1,10 +1,12 @@
1
1
  import { type AnyTool } from '@zenera/neo';
2
- import { type Format } from '../present.ts';
2
+ import { type Format } from './present.ts';
3
3
  import type { SchemaIndex } from './search.ts';
4
4
  export interface SchemaToolOptions {
5
5
  /** what `format` defaults to when the model does not say */
6
6
  format?: Format;
7
7
  docs?: boolean;
8
+ /** name the document each answer came from; on by default past one document */
9
+ source?: boolean;
8
10
  }
9
11
  export declare function schemaTools<TCtx = unknown>(index: SchemaIndex, options?: SchemaToolOptions): AnyTool<TCtx>[];
10
12
  //# sourceMappingURL=tools.d.ts.map