@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.
@@ -60,13 +60,30 @@ function once(load) {
60
60
  let pending;
61
61
  return () => (pending ??= load());
62
62
  }
63
+ /**
64
+ * The bundled document as it was indexed. Kept only when the index was built
65
+ * with sources, which is why the manifest is asked first: the difference
66
+ * between "no such document" and "this index did not keep them" is the whole
67
+ * of what the caller can do next.
68
+ */
69
+ export async function readSource(dir, name) {
70
+ const manifest = await readManifest(dir);
71
+ const record = manifest.sources.find((s) => s.name === name);
72
+ if (!record) {
73
+ throw new CliError(`${dir} holds no document called ${name}`, EXIT.failed, `it has: ${manifest.sources.map((s) => s.name).join(', ')}`);
74
+ }
75
+ if (!record.path) {
76
+ return undefined;
77
+ }
78
+ return await readFile(join(dir, record.path), 'utf8');
79
+ }
63
80
  export async function readManifest(dir) {
64
81
  let text;
65
82
  try {
66
83
  text = await readFile(join(dir, MANIFEST_FILE), 'utf8');
67
84
  }
68
85
  catch {
69
- throw new CliError(`${dir} does not hold an index`, EXIT.invalid, 'build one first with `zen rag schema index`');
86
+ throw new CliError(`${dir} does not hold an index`, EXIT.invalid, 'build one with `zen rag schema index`, or name an existing one with --dir or $ZEN_SCHEMA_DB');
70
87
  }
71
88
  const manifest = JSON.parse(text);
72
89
  if (manifest.version !== INDEX_VERSION) {
@@ -0,0 +1,20 @@
1
+ /** The name a new index is given. Nothing searches for it; only `index --out` writes it. */
2
+ export declare const DEFAULT_DIR = "./schema-db";
3
+ export declare const DIR_ENV = "ZEN_SCHEMA_DB";
4
+ /** How the directory was arrived at, which is what decides whether to say so. */
5
+ export type DirSource = 'flag' | 'env' | 'found' | 'default';
6
+ export interface Located {
7
+ dir: string;
8
+ from: DirSource;
9
+ }
10
+ export interface LocateOptions {
11
+ env?: NodeJS.ProcessEnv;
12
+ /** do not climb above this; the home directory, or the root, by default */
13
+ ceiling?: string;
14
+ }
15
+ export declare function locateIndex(cwd: string, flag?: string, options?: LocateOptions): Located;
16
+ /** Where a new index goes: the same environment variable, minus the search. */
17
+ export declare function outputDir(cwd: string, flag?: string, env?: NodeJS.ProcessEnv): string;
18
+ /** An index is a directory with a manifest in it; nothing else is asserted here. */
19
+ export declare function isIndex(dir: string): boolean;
20
+ //# sourceMappingURL=locate.d.ts.map
@@ -0,0 +1,141 @@
1
+ import { CliError, EXIT } from '@zenera/cli/lib';
2
+ import { readdirSync, statSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, isAbsolute, join, parse, relative, resolve } from 'node:path';
5
+ import { MANIFEST_FILE } from "./files.js";
6
+ // ---------------------------------------------------------------------------
7
+ // Which index, when nobody said
8
+ //
9
+ // `--dir` and `$ZEN_SCHEMA_DB` are taken as written, missing or not: naming a
10
+ // directory that turns out not to hold an index has to fail saying so, because
11
+ // quietly using a different one would be a worse answer than an error.
12
+ //
13
+ // With neither, the directory is looked for. There is no list of blessed names
14
+ // here and there should not be — an index is self-describing, so what is being
15
+ // looked for is a `manifest.json`, not a directory called `schema-db`.
16
+ // `schema-db` is only the name a *new* index is given, and nothing reads it
17
+ // back. The search is nearest-first: this directory, then what is under it,
18
+ // then up a level and again, so `/assets/…/whatever` is reachable from
19
+ // `/workspace` because the two meet at a shared root on the way up.
20
+ //
21
+ // Three things bound it, and each is bounding a different kind of mistake.
22
+ // Depth and a visit budget bound the cost. The ceiling — the home directory,
23
+ // or the filesystem root when the search began outside it — bounds the
24
+ // blast radius, because an index in someone else's tree is not yours. And two
25
+ // indexes at the same distance is an ambiguity rather than a tie to break:
26
+ // choosing one silently is the one failure worth ruling out entirely, since
27
+ // the wrong index does not error, it answers confidently about another API.
28
+ // ---------------------------------------------------------------------------
29
+ /** The name a new index is given. Nothing searches for it; only `index --out` writes it. */
30
+ export const DEFAULT_DIR = './schema-db';
31
+ export const DIR_ENV = 'ZEN_SCHEMA_DB';
32
+ /** Far enough to climb out of a package into its workspace, not far enough to roam. */
33
+ const MAX_LEVELS = 6;
34
+ /** How far below a directory an index may sit and still count as being in it. */
35
+ const MAX_DEPTH = 3;
36
+ /** A directory with more entries than this is a data store, not a place to keep an index. */
37
+ const MAX_ENTRIES = 128;
38
+ /** Directories the whole search may read, however it is shaped. */
39
+ const MAX_VISITS = 400;
40
+ export function locateIndex(cwd, flag, options = {}) {
41
+ const env = options.env ?? process.env;
42
+ if (flag) {
43
+ return { dir: resolve(cwd, flag), from: 'flag' };
44
+ }
45
+ const named = env[DIR_ENV]?.trim();
46
+ if (named) {
47
+ return { dir: resolve(cwd, named), from: 'env' };
48
+ }
49
+ const found = search(resolve(cwd), options.ceiling ?? ceilingFor(cwd));
50
+ // Nothing found still answers with the default, so the error names the
51
+ // directory everyone expects rather than the last place that was searched.
52
+ return found
53
+ ? { dir: found, from: 'found' }
54
+ : { dir: resolve(cwd, DEFAULT_DIR), from: 'default' };
55
+ }
56
+ /** Where a new index goes: the same environment variable, minus the search. */
57
+ export function outputDir(cwd, flag, env = process.env) {
58
+ return resolve(cwd, flag ?? env[DIR_ENV]?.trim() ?? DEFAULT_DIR);
59
+ }
60
+ /** An index is a directory with a manifest in it; nothing else is asserted here. */
61
+ export function isIndex(dir) {
62
+ try {
63
+ return statSync(join(dir, MANIFEST_FILE)).isFile();
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ function search(cwd, ceiling) {
70
+ const budget = { left: MAX_VISITS, seen: new Set() };
71
+ let dir = cwd;
72
+ for (let level = 0; level < MAX_LEVELS; level++) {
73
+ const found = nearest(dir, budget);
74
+ if (found.length === 1) {
75
+ return found[0];
76
+ }
77
+ if (found.length > 1) {
78
+ throw new CliError(`more than one index is equally close to here: ${found.join(', ')}`, EXIT.usage, `say which with --dir, or set ${DIR_ENV}`);
79
+ }
80
+ budget.seen.add(dir);
81
+ const up = dirname(dir);
82
+ if (up === dir || dir === ceiling) {
83
+ break;
84
+ }
85
+ dir = up;
86
+ }
87
+ return undefined;
88
+ }
89
+ /** Every index at the shallowest depth that has any, so a tie can be reported as one. */
90
+ function nearest(root, budget) {
91
+ let frontier = [root];
92
+ for (let depth = 0; depth <= MAX_DEPTH && frontier.length > 0; depth++) {
93
+ const found = frontier.filter(isIndex);
94
+ if (found.length > 0) {
95
+ return found;
96
+ }
97
+ const next = [];
98
+ for (const dir of frontier) {
99
+ if (budget.left <= 0) {
100
+ return [];
101
+ }
102
+ budget.left--;
103
+ next.push(...children(dir).filter((child) => !budget.seen.has(child)));
104
+ }
105
+ frontier = next;
106
+ }
107
+ return [];
108
+ }
109
+ /**
110
+ * The subdirectories of one directory. Hidden directories and `node_modules`
111
+ * are skipped: an index kept out of sight is not one anybody meant to be found
112
+ * by looking.
113
+ */
114
+ function children(dir) {
115
+ let entries;
116
+ try {
117
+ entries = readdirSync(dir, { withFileTypes: true });
118
+ }
119
+ catch {
120
+ return [];
121
+ }
122
+ if (entries.length > MAX_ENTRIES) {
123
+ return [];
124
+ }
125
+ return entries
126
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
127
+ .map((e) => join(dir, e.name));
128
+ }
129
+ /**
130
+ * Home is the ceiling for anyone working inside it. Starting outside it —
131
+ * a container whose workspace is `/workspace`, a CI checkout — there is no
132
+ * home to stay within, so the root is the only stop.
133
+ */
134
+ function ceilingFor(cwd) {
135
+ const home = homedir();
136
+ const below = relative(home, resolve(cwd));
137
+ return below === '' || (!below.startsWith('..') && !isAbsolute(below))
138
+ ? home
139
+ : parse(resolve(cwd)).root;
140
+ }
141
+ //# sourceMappingURL=locate.js.map
@@ -0,0 +1,54 @@
1
+ import { type ApiGraph, type NodeAttrs, type NodeKind } from './graph.ts';
2
+ import { type Matcher } from './match.ts';
3
+ export interface Row extends NodeAttrs {
4
+ id: string;
5
+ }
6
+ export interface ListFilter {
7
+ kind: NodeKind;
8
+ /** any one matching is enough; none means every name passes */
9
+ name?: readonly Matcher[];
10
+ path?: readonly Matcher[];
11
+ source?: string;
12
+ methodType?: string;
13
+ direction?: string;
14
+ limit?: number;
15
+ }
16
+ export interface Listing {
17
+ /** how many matched, whatever was kept */
18
+ found: number;
19
+ rows: Row[];
20
+ truncated: boolean;
21
+ }
22
+ export interface Match {
23
+ id: string;
24
+ attributes: NodeAttrs;
25
+ /** the indexed text this matched against */
26
+ text: string;
27
+ }
28
+ export interface GrepFilter {
29
+ kinds?: readonly string[];
30
+ source?: string;
31
+ /** the same two constraints `list` takes, so one question has one spelling */
32
+ name?: readonly Matcher[];
33
+ path?: readonly Matcher[];
34
+ limit?: number;
35
+ }
36
+ export interface Grep {
37
+ found: number;
38
+ matches: Match[];
39
+ truncated: boolean;
40
+ }
41
+ export declare function listNodes(graph: ApiGraph, filter: ListFilter): Listing;
42
+ export declare function grepNodes(graph: ApiGraph, match: Matcher, filter?: GrepFilter): Grep;
43
+ /** How many fields a type carries, which is most of what a listing wants to say. */
44
+ export declare function propertyCount(graph: ApiGraph, id: string): number;
45
+ /** That count, said properly, in the one phrasing the CLI and the tools share. */
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;
54
+ //# sourceMappingURL=lookup.d.ts.map
@@ -0,0 +1,110 @@
1
+ import { textOf } from "./entities.js";
2
+ import { methodId } from "./graph.js";
3
+ import { PatternError } from "./match.js";
4
+ /** A scan is bounded, because a pattern may have come from a model. */
5
+ const DEADLINE_MS = 2000;
6
+ const DEADLINE_EVERY = 500;
7
+ export function listNodes(graph, filter) {
8
+ const rows = [];
9
+ graph.forEachNode((id, a) => {
10
+ if (a.kind !== filter.kind || !passes(a, filter)) {
11
+ return;
12
+ }
13
+ if (filter.name && !filter.name.some((match) => match(a.name))) {
14
+ return;
15
+ }
16
+ if (filter.path && !matchesRoute(graph, id, a, filter.path)) {
17
+ return;
18
+ }
19
+ rows.push({ ...a, id });
20
+ });
21
+ rows.sort(order(filter.kind));
22
+ return cut(rows, filter.limit);
23
+ }
24
+ export function grepNodes(graph, match, filter = {}) {
25
+ const kinds = filter.kinds?.length ? new Set(filter.kinds) : undefined;
26
+ const matches = [];
27
+ const until = Date.now() + DEADLINE_MS;
28
+ let seen = 0;
29
+ for (const id of graph.nodes()) {
30
+ if (++seen % DEADLINE_EVERY === 0 && Date.now() > until) {
31
+ throw new PatternError(`the pattern is still running after ${DEADLINE_MS / 1000}s — it is too expensive to be useful`);
32
+ }
33
+ const a = graph.getNodeAttributes(id);
34
+ if (kinds && !kinds.has(a.kind)) {
35
+ continue;
36
+ }
37
+ if (filter.source && a.source !== filter.source) {
38
+ continue;
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
+ }
46
+ const text = textOf(graph, id);
47
+ if (match(text)) {
48
+ matches.push({ id, attributes: a, text });
49
+ }
50
+ }
51
+ matches.sort((a, b) => a.id.localeCompare(b.id));
52
+ const kept = cut(matches, filter.limit);
53
+ return { found: kept.found, matches: kept.rows, truncated: kept.truncated };
54
+ }
55
+ /** How many fields a type carries, which is most of what a listing wants to say. */
56
+ export function propertyCount(graph, id) {
57
+ return graph
58
+ .outEdges(id)
59
+ .filter((e) => graph.getEdgeAttribute(e, 'relation') === 'HAS_PROPERTY').length;
60
+ }
61
+ /** That count, said properly, in the one phrasing the CLI and the tools share. */
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
+ }
79
+ // ---------------------------------------------------------------------------
80
+ function matchesRoute(graph, id, a, patterns) {
81
+ const route = routeOf(graph, id, a);
82
+ return route !== '' && patterns.some((match) => match(route));
83
+ }
84
+ function passes(a, filter) {
85
+ if (filter.source && a.source !== filter.source) {
86
+ return false;
87
+ }
88
+ if (filter.methodType && a.methodType !== filter.methodType) {
89
+ return false;
90
+ }
91
+ if (filter.direction && a.direction !== filter.direction && a.direction !== 'both') {
92
+ return false;
93
+ }
94
+ return true;
95
+ }
96
+ /** Operations read as a table of routes; everything else reads as a list of names. */
97
+ function order(kind) {
98
+ if (kind !== 'method') {
99
+ return (a, b) => a.id.localeCompare(b.id);
100
+ }
101
+ return (a, b) => a.path.localeCompare(b.path) || a.httpMethod.localeCompare(b.httpMethod);
102
+ }
103
+ function cut(rows, limit) {
104
+ const found = rows.length;
105
+ if (!limit || limit >= found) {
106
+ return { found, rows, truncated: false };
107
+ }
108
+ return { found, rows: rows.slice(0, limit), truncated: true };
109
+ }
110
+ //# sourceMappingURL=lookup.js.map
@@ -0,0 +1,40 @@
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
@@ -0,0 +1,92 @@
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
@@ -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
  // ---------------------------------------------------------------------------
@@ -37,6 +37,12 @@ export interface StitchOptions {
37
37
  export declare const DEFAULT_MAX_HOPS = 3;
38
38
  export declare const DEFAULT_MAX_NODES = 200;
39
39
  export declare function stitch(graph: ApiGraph, seeds: readonly Seed[], options?: StitchOptions): Subgraph[];
40
+ /**
41
+ * Exactly the nodes named, and only the edges between them. The counterpart to
42
+ * `stitch`: no neighbours are gathered, because the caller is not asking what
43
+ * this is connected to — they already know what they want and want it whole.
44
+ */
45
+ export declare function select(graph: ApiGraph, ids: readonly string[]): Subgraph;
40
46
  /** Breadth-first, ignoring edge direction, giving up past `maxHops`. */
41
47
  export declare function path(graph: ApiGraph, from: string, to: string, maxHops: number): string[] | undefined;
42
48
  //# sourceMappingURL=subgraph.d.ts.map
@@ -150,6 +150,33 @@ function connect(graph, seeds, maxHops) {
150
150
  }
151
151
  return out;
152
152
  }
153
+ /**
154
+ * Exactly the nodes named, and only the edges between them. The counterpart to
155
+ * `stitch`: no neighbours are gathered, because the caller is not asking what
156
+ * this is connected to — they already know what they want and want it whole.
157
+ */
158
+ export function select(graph, ids) {
159
+ const kept = new Set(ids.filter((id) => graph.hasNode(id)));
160
+ const nodes = [...kept].map((id) => ({
161
+ id,
162
+ kind: graph.getNodeAttribute(id, 'kind'),
163
+ attributes: graph.getNodeAttributes(id),
164
+ hit: true,
165
+ score: 1,
166
+ }));
167
+ const edges = [];
168
+ for (const id of kept) {
169
+ for (const edge of graph.outEdges(id)) {
170
+ const target = graph.target(edge);
171
+ if (!kept.has(target)) {
172
+ continue;
173
+ }
174
+ const a = graph.getEdgeAttributes(edge);
175
+ edges.push({ source: id, target, relation: a.relation, status: a.status, in: a.in });
176
+ }
177
+ }
178
+ return { nodes, edges, hits: [...kept], score: kept.size, truncated: false };
179
+ }
153
180
  /** Breadth-first, ignoring edge direction, giving up past `maxHops`. */
154
181
  export function path(graph, from, to, maxHops) {
155
182
  if (from === to) {
@@ -5,6 +5,8 @@ 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