@zenera/rag 1.1.0
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/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/command.d.ts +3 -0
- package/dist/command.js +436 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/present.d.ts +17 -0
- package/dist/present.js +40 -0
- package/dist/query.d.ts +7 -0
- package/dist/query.js +88 -0
- package/dist/repl.d.ts +8 -0
- package/dist/repl.js +119 -0
- package/dist/schema/build.d.ts +28 -0
- package/dist/schema/build.js +79 -0
- package/dist/schema/entities.d.ts +18 -0
- package/dist/schema/entities.js +71 -0
- package/dist/schema/files.d.ts +74 -0
- package/dist/schema/files.js +79 -0
- package/dist/schema/graph.d.ts +48 -0
- package/dist/schema/graph.js +320 -0
- package/dist/schema/hydrate.d.ts +19 -0
- package/dist/schema/hydrate.js +182 -0
- package/dist/schema/render.d.ts +11 -0
- package/dist/schema/render.js +254 -0
- package/dist/schema/schema.d.ts +11 -0
- package/dist/schema/schema.js +189 -0
- package/dist/schema/search.d.ts +50 -0
- package/dist/schema/search.js +142 -0
- package/dist/schema/spec.d.ts +58 -0
- package/dist/schema/spec.js +309 -0
- package/dist/schema/store.d.ts +32 -0
- package/dist/schema/store.js +126 -0
- package/dist/schema/subgraph.d.ts +42 -0
- package/dist/schema/subgraph.js +272 -0
- package/dist/schema/tools.d.ts +10 -0
- package/dist/schema/tools.js +242 -0
- package/dist/schema/typescript.d.ts +22 -0
- package/dist/schema/typescript.js +246 -0
- package/package.json +59 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
const HIT = '»';
|
|
2
|
+
export function render(sub, format, options = {}) {
|
|
3
|
+
switch (format) {
|
|
4
|
+
case 'mermaid':
|
|
5
|
+
return toMermaid(sub, options);
|
|
6
|
+
case 'mermaid-flowchart':
|
|
7
|
+
return toFlowchart(sub, options);
|
|
8
|
+
default:
|
|
9
|
+
return toText(sub, options);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Text
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
export function toText(sub, options = {}) {
|
|
16
|
+
const view = new View(sub);
|
|
17
|
+
const lines = [];
|
|
18
|
+
const methods = view.of('method');
|
|
19
|
+
if (methods.length > 0) {
|
|
20
|
+
lines.push('methods');
|
|
21
|
+
for (const method of methods) {
|
|
22
|
+
lines.push(...methodLines(view, method, options));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const types = view.of('type');
|
|
26
|
+
if (types.length > 0) {
|
|
27
|
+
lines.push(methods.length > 0 ? '' : '', 'types');
|
|
28
|
+
for (const type of types) {
|
|
29
|
+
lines.push(...typeLines(view, type, options));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const loose = view.of('property').filter((p) => !view.owned.has(p.id));
|
|
33
|
+
if (loose.length > 0) {
|
|
34
|
+
lines.push('', 'properties');
|
|
35
|
+
for (const property of loose) {
|
|
36
|
+
lines.push(` ${mark(property)}${field(property)}${doc(property, options, ' ')}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (sub.truncated) {
|
|
40
|
+
lines.push('', '… truncated to stay inside the node budget');
|
|
41
|
+
}
|
|
42
|
+
return lines.filter((line, at) => line !== '' || at > 0).join('\n');
|
|
43
|
+
}
|
|
44
|
+
function methodLines(view, method, options) {
|
|
45
|
+
const a = method.attributes;
|
|
46
|
+
const out = [
|
|
47
|
+
` ${mark(method)}${a.httpMethod} ${a.path} ${a.name}${doc(method, options, ' —')}`,
|
|
48
|
+
];
|
|
49
|
+
for (const edge of view.out(method.id, 'HAS_PARAM')) {
|
|
50
|
+
const node = view.node(edge.target);
|
|
51
|
+
if (node) {
|
|
52
|
+
out.push(` ${mark(node)}${field(node)} (${edge.in})${doc(node, options, ' —')}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const edge of view.out(method.id, 'TAKES_INPUT')) {
|
|
56
|
+
out.push(` accepts ${view.name(edge.target)}`);
|
|
57
|
+
}
|
|
58
|
+
for (const edge of view.out(method.id, 'RETURNS_OUTPUT')) {
|
|
59
|
+
out.push(` returns ${view.name(edge.target)} (${edge.status})`);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function typeLines(view, type, options) {
|
|
64
|
+
const out = [
|
|
65
|
+
` ${mark(type)}${type.attributes.name}${side(type.attributes)}${doc(type, options, ' —')}`,
|
|
66
|
+
];
|
|
67
|
+
const composes = view.out(type.id, 'COMPOSES').map((e) => view.name(e.target));
|
|
68
|
+
if (composes.length > 0) {
|
|
69
|
+
out.push(` composes ${composes.join(', ')}`);
|
|
70
|
+
}
|
|
71
|
+
const items = view.out(type.id, 'ITEM_OF').map((e) => view.name(e.target));
|
|
72
|
+
if (items.length > 0) {
|
|
73
|
+
out.push(` array of ${items.join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
for (const edge of view.out(type.id, 'HAS_PROPERTY')) {
|
|
76
|
+
const node = view.node(edge.target);
|
|
77
|
+
if (node) {
|
|
78
|
+
out.push(` ${mark(node)}${field(node)}${doc(node, options, ' —')}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function field(node) {
|
|
84
|
+
const a = node.attributes;
|
|
85
|
+
return `${a.name}${a.required ? '' : '?'}: ${a.signature || 'unknown'}`;
|
|
86
|
+
}
|
|
87
|
+
function side(a) {
|
|
88
|
+
return a.direction === 'none' ? '' : ` (${a.direction})`;
|
|
89
|
+
}
|
|
90
|
+
function mark(node) {
|
|
91
|
+
return node.hit ? `${HIT} ` : ' ';
|
|
92
|
+
}
|
|
93
|
+
function doc(node, options, lead) {
|
|
94
|
+
if (!options.docs || !node.attributes.doc) {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
return `${lead} ${clip(node.attributes.doc, options.maxDoc ?? 120)}`;
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Mermaid
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
export function toMermaid(sub, options = {}) {
|
|
103
|
+
const view = new View(sub);
|
|
104
|
+
const lines = ['classDiagram'];
|
|
105
|
+
for (const type of view.of('type')) {
|
|
106
|
+
const fields = view
|
|
107
|
+
.out(type.id, 'HAS_PROPERTY')
|
|
108
|
+
.map((e) => view.node(e.target))
|
|
109
|
+
.filter(Boolean);
|
|
110
|
+
lines.push(` class ${view.id(type.id)} {`);
|
|
111
|
+
for (const property of fields) {
|
|
112
|
+
const a = property.attributes;
|
|
113
|
+
lines.push(` ${a.required ? '+' : '-'}${safe(a.name)} : ${safe(a.signature)}`);
|
|
114
|
+
}
|
|
115
|
+
lines.push(' }');
|
|
116
|
+
}
|
|
117
|
+
for (const method of view.of('method')) {
|
|
118
|
+
const id = view.id(method.id);
|
|
119
|
+
const a = method.attributes;
|
|
120
|
+
lines.push(` class ${id} {`, ` <<${a.httpMethod} ${safe(a.path)}>>`, ' }');
|
|
121
|
+
}
|
|
122
|
+
for (const edge of sub.edges) {
|
|
123
|
+
if (!view.node(edge.source) || !view.node(edge.target)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// A field is a line inside a class, not a class of its own, so the type
|
|
127
|
+
// it is of has to be joined from whatever holds the field.
|
|
128
|
+
if (edge.relation === 'OF_TYPE') {
|
|
129
|
+
const owner = view.owner(edge.source);
|
|
130
|
+
if (owner) {
|
|
131
|
+
const label = safe(view.name(edge.source));
|
|
132
|
+
lines.push(` ${view.id(owner)} --> ${view.id(edge.target)} : ${label}`);
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const arrow = ARROWS[edge.relation];
|
|
137
|
+
if (!arrow) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const label = edge.relation === 'RETURNS_OUTPUT' ? `returns ${edge.status}` : arrow.label;
|
|
141
|
+
lines.push(` ${view.id(edge.source)} ${arrow.line} ${view.id(edge.target)} : ${label}`);
|
|
142
|
+
}
|
|
143
|
+
for (const node of sub.nodes) {
|
|
144
|
+
if (node.hit && options.docs && node.attributes.doc && node.kind !== 'property') {
|
|
145
|
+
const text = clip(node.attributes.doc, options.maxDoc ?? 100).replace(/"/g, "'");
|
|
146
|
+
lines.push(` note for ${view.id(node.id)} "${text}"`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
151
|
+
const ARROWS = {
|
|
152
|
+
TAKES_INPUT: { line: '..>', label: 'accepts' },
|
|
153
|
+
RETURNS_OUTPUT: { line: '-->', label: 'returns' },
|
|
154
|
+
COMPOSES: { line: '--|>', label: 'composes' },
|
|
155
|
+
ITEM_OF: { line: 'o--', label: 'array of' },
|
|
156
|
+
};
|
|
157
|
+
export function toFlowchart(sub, options = {}) {
|
|
158
|
+
const view = new View(sub);
|
|
159
|
+
const lines = ['flowchart LR'];
|
|
160
|
+
for (const node of sub.nodes) {
|
|
161
|
+
const a = node.attributes;
|
|
162
|
+
const id = view.id(node.id);
|
|
163
|
+
const label = node.kind === 'method'
|
|
164
|
+
? `${a.httpMethod} ${a.path}`
|
|
165
|
+
: node.kind === 'property'
|
|
166
|
+
? `${a.name}: ${a.signature || 'unknown'}`
|
|
167
|
+
: a.name;
|
|
168
|
+
const [open, close] = node.kind === 'method' ? ['([', '])'] : ['[', ']'];
|
|
169
|
+
lines.push(` ${id}${open}"${safe(node.hit ? `${HIT} ${label}` : label)}"${close}`);
|
|
170
|
+
}
|
|
171
|
+
for (const edge of sub.edges) {
|
|
172
|
+
const label = edge.relation === 'RETURNS_OUTPUT' ? `returns ${edge.status}` : LABELS[edge.relation];
|
|
173
|
+
lines.push(` ${view.id(edge.source)} -->|${label}| ${view.id(edge.target)}`);
|
|
174
|
+
}
|
|
175
|
+
void options;
|
|
176
|
+
return lines.join('\n');
|
|
177
|
+
}
|
|
178
|
+
const LABELS = {
|
|
179
|
+
TAKES_INPUT: 'accepts',
|
|
180
|
+
HAS_PARAM: 'param',
|
|
181
|
+
RETURNS_OUTPUT: 'returns',
|
|
182
|
+
HAS_PROPERTY: 'has',
|
|
183
|
+
OF_TYPE: 'of type',
|
|
184
|
+
COMPOSES: 'composes',
|
|
185
|
+
ITEM_OF: 'array of',
|
|
186
|
+
};
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
/** Indexed access to one subgraph, so a renderer is a loop and nothing else. */
|
|
189
|
+
class View {
|
|
190
|
+
owned = new Set();
|
|
191
|
+
#nodes = new Map();
|
|
192
|
+
#out = new Map();
|
|
193
|
+
#owner = new Map();
|
|
194
|
+
#ids = new Map();
|
|
195
|
+
constructor(sub) {
|
|
196
|
+
for (const node of sub.nodes) {
|
|
197
|
+
this.#nodes.set(node.id, node);
|
|
198
|
+
}
|
|
199
|
+
for (const edge of sub.edges) {
|
|
200
|
+
const list = this.#out.get(edge.source);
|
|
201
|
+
if (list) {
|
|
202
|
+
list.push(edge);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
this.#out.set(edge.source, [edge]);
|
|
206
|
+
}
|
|
207
|
+
if (edge.relation === 'HAS_PROPERTY' || edge.relation === 'HAS_PARAM') {
|
|
208
|
+
this.owned.add(edge.target);
|
|
209
|
+
this.#owner.set(edge.target, edge.source);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const taken = new Set();
|
|
213
|
+
for (const node of sub.nodes) {
|
|
214
|
+
let id = safeId(node.attributes.name || node.id);
|
|
215
|
+
for (let n = 2; taken.has(id); n++) {
|
|
216
|
+
id = `${safeId(node.attributes.name || node.id)}_${n}`;
|
|
217
|
+
}
|
|
218
|
+
taken.add(id);
|
|
219
|
+
this.#ids.set(node.id, id);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
node(id) {
|
|
223
|
+
return this.#nodes.get(id);
|
|
224
|
+
}
|
|
225
|
+
name(id) {
|
|
226
|
+
return this.#nodes.get(id)?.attributes.name ?? id;
|
|
227
|
+
}
|
|
228
|
+
/** The type or method a property hangs off, for renderers that draw it inline. */
|
|
229
|
+
owner(id) {
|
|
230
|
+
return this.#owner.get(id);
|
|
231
|
+
}
|
|
232
|
+
id(id) {
|
|
233
|
+
return this.#ids.get(id) ?? safeId(id);
|
|
234
|
+
}
|
|
235
|
+
of(kind) {
|
|
236
|
+
return [...this.#nodes.values()].filter((n) => n.kind === kind);
|
|
237
|
+
}
|
|
238
|
+
out(id, relation) {
|
|
239
|
+
return (this.#out.get(id) ?? []).filter((e) => e.relation === relation);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function clip(text, max) {
|
|
243
|
+
const one = text.replace(/\s+/g, ' ').trim();
|
|
244
|
+
return one.length > max ? `${one.slice(0, max - 1)}…` : one;
|
|
245
|
+
}
|
|
246
|
+
/** Mermaid takes the label apart on quotes, brackets and newlines. */
|
|
247
|
+
function safe(text) {
|
|
248
|
+
return text.replace(/["\n]/g, ' ').replace(/[[\]{}<>|]/g, '');
|
|
249
|
+
}
|
|
250
|
+
function safeId(text) {
|
|
251
|
+
const cleaned = text.replace(/[^A-Za-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
|
|
252
|
+
return /^[A-Za-z_]/.test(cleaned) ? cleaned : `n_${cleaned}`;
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type Schema = Record<string, unknown>;
|
|
2
|
+
export type Dialect = 'swagger-2.0' | 'openapi-3.0' | 'openapi-3.1';
|
|
3
|
+
export declare const isObject: (v: unknown) => v is Schema;
|
|
4
|
+
/** Converts one schema, and everything under it, into 2020-12. */
|
|
5
|
+
export declare function normalize(root: unknown, dialect: Dialect): Schema;
|
|
6
|
+
/** The component name a `$ref` points at, or undefined for anything foreign. */
|
|
7
|
+
export declare function refName(value: unknown): string | undefined;
|
|
8
|
+
/** Every component name reachable from a schema, one level of `$ref` deep. */
|
|
9
|
+
export declare function refsIn(schema: unknown): string[];
|
|
10
|
+
export declare function docOf(schema: unknown): string;
|
|
11
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Schemas, made uniform — without losing their names
|
|
3
|
+
//
|
|
4
|
+
// Three dialects arrive here and one leaves, the same conversion the faker
|
|
5
|
+
// does. What is deliberately *not* done here is dereferencing: a `$ref` is
|
|
6
|
+
// left as the string it is, because a component's name is this package's
|
|
7
|
+
// primary key. `#/components/schemas/ResetPasswordPayload` is the edge that
|
|
8
|
+
// makes the graph a graph, and the word the model is going to say back.
|
|
9
|
+
//
|
|
10
|
+
// Nothing here recurses through a `$ref`, so nothing here can meet a cycle.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/** Subschema positions, by the shape of what sits in them. */
|
|
13
|
+
const ONE = [
|
|
14
|
+
'not',
|
|
15
|
+
'if',
|
|
16
|
+
'then',
|
|
17
|
+
'else',
|
|
18
|
+
'contains',
|
|
19
|
+
'propertyNames',
|
|
20
|
+
'additionalProperties',
|
|
21
|
+
'unevaluatedProperties',
|
|
22
|
+
'additionalItems',
|
|
23
|
+
'unevaluatedItems',
|
|
24
|
+
];
|
|
25
|
+
const MAP = ['properties', 'patternProperties', '$defs', 'definitions'];
|
|
26
|
+
const LIST = ['allOf', 'anyOf', 'oneOf', 'prefixItems'];
|
|
27
|
+
/**
|
|
28
|
+
* Annotations that cost tokens and change no meaning. `discriminator` is
|
|
29
|
+
* conspicuously absent — the faker drops it, and here it is the whole reason a
|
|
30
|
+
* `oneOf` can be printed as a TypeScript tagged union rather than a bare one.
|
|
31
|
+
*/
|
|
32
|
+
const DROP = new Set(['externalDocs', 'xml', 'x-internal']);
|
|
33
|
+
export const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
34
|
+
/** Converts one schema, and everything under it, into 2020-12. */
|
|
35
|
+
export function normalize(root, dialect) {
|
|
36
|
+
if (!isObject(root)) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
return convert(root, dialect);
|
|
40
|
+
}
|
|
41
|
+
function walk(value, dialect) {
|
|
42
|
+
return isObject(value) ? convert(value, dialect) : value;
|
|
43
|
+
}
|
|
44
|
+
function convert(node, dialect) {
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const [key, value] of Object.entries(node)) {
|
|
47
|
+
if (DROP.has(key) || value === undefined) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (ONE.includes(key)) {
|
|
51
|
+
out[key] = typeof value === 'boolean' ? value : walk(value, dialect);
|
|
52
|
+
}
|
|
53
|
+
else if (MAP.includes(key) && isObject(value)) {
|
|
54
|
+
const target = key === 'definitions' ? '$defs' : key;
|
|
55
|
+
out[target] = Object.fromEntries(Object.entries(value).map(([k, v]) => [
|
|
56
|
+
k,
|
|
57
|
+
typeof v === 'boolean' ? v : walk(v, dialect),
|
|
58
|
+
]));
|
|
59
|
+
}
|
|
60
|
+
else if (LIST.includes(key) && Array.isArray(value)) {
|
|
61
|
+
out[key] = value.map((v) => walk(v, dialect));
|
|
62
|
+
}
|
|
63
|
+
else if (key === 'items') {
|
|
64
|
+
// Draft-4 and Swagger 2.0 spell tuples as an array here; 2020-12
|
|
65
|
+
// spells them `prefixItems` and keeps `items` for the tail.
|
|
66
|
+
out[Array.isArray(value) ? 'prefixItems' : 'items'] = Array.isArray(value)
|
|
67
|
+
? value.map((v) => walk(v, dialect))
|
|
68
|
+
: walk(value, dialect);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
out[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (dialect !== 'openapi-3.1') {
|
|
75
|
+
widenNullable(node, out);
|
|
76
|
+
fixExclusive(out);
|
|
77
|
+
}
|
|
78
|
+
fixPattern(out);
|
|
79
|
+
// `type: file` is Swagger 2.0's way of saying "bytes".
|
|
80
|
+
if (out.type === 'file') {
|
|
81
|
+
out.type = 'string';
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(out.required) && out.required.length === 0) {
|
|
84
|
+
delete out.required;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* `pattern: /^[_a-z0-9-]+$/` — a JavaScript regex *literal*, delimiters and all
|
|
90
|
+
* — is written in real documents, and with the slashes left on it matches
|
|
91
|
+
* nothing. They come off, and an expression that still will not compile is
|
|
92
|
+
* dropped rather than shown to anyone.
|
|
93
|
+
*/
|
|
94
|
+
function fixPattern(out) {
|
|
95
|
+
const raw = out.pattern;
|
|
96
|
+
if (typeof raw !== 'string') {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const literal = /^\/(.+)\/[dgimsuvy]*$/s.exec(raw);
|
|
100
|
+
const body = literal ? literal[1] : raw;
|
|
101
|
+
try {
|
|
102
|
+
new RegExp(body);
|
|
103
|
+
out.pattern = body;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
delete out.pattern;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** `nullable: true` is not a 2020-12 keyword; a union type is. */
|
|
110
|
+
function widenNullable(node, out) {
|
|
111
|
+
if (node.nullable !== true) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
delete out.nullable;
|
|
115
|
+
const type = out.type;
|
|
116
|
+
if (typeof type === 'string') {
|
|
117
|
+
out.type = [type, 'null'];
|
|
118
|
+
}
|
|
119
|
+
else if (Array.isArray(type)) {
|
|
120
|
+
out.type = type.includes('null') ? type : [...type, 'null'];
|
|
121
|
+
}
|
|
122
|
+
else if (Array.isArray(out.enum) && !out.enum.includes(null)) {
|
|
123
|
+
out.enum = [...out.enum, null];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Draft-4 spells an exclusive bound as a boolean flag on the inclusive one, so
|
|
128
|
+
* `exclusiveMinimum: true` left alone reads as "the minimum is 1".
|
|
129
|
+
*/
|
|
130
|
+
function fixExclusive(out) {
|
|
131
|
+
for (const [flag, bound] of [
|
|
132
|
+
['exclusiveMinimum', 'minimum'],
|
|
133
|
+
['exclusiveMaximum', 'maximum'],
|
|
134
|
+
]) {
|
|
135
|
+
if (typeof out[flag] !== 'boolean') {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const value = out[bound];
|
|
139
|
+
if (out[flag] === true && typeof value === 'number') {
|
|
140
|
+
out[flag] = value;
|
|
141
|
+
delete out[bound];
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
delete out[flag];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** The component name a `$ref` points at, or undefined for anything foreign. */
|
|
149
|
+
export function refName(value) {
|
|
150
|
+
if (!isObject(value) || typeof value.$ref !== 'string') {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
const match = /^#\/(?:components\/schemas|definitions|\$defs)\/(.+)$/.exec(value.$ref);
|
|
154
|
+
return match
|
|
155
|
+
? decodeURIComponent(match[1].replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
156
|
+
: undefined;
|
|
157
|
+
}
|
|
158
|
+
/** Every component name reachable from a schema, one level of `$ref` deep. */
|
|
159
|
+
export function refsIn(schema) {
|
|
160
|
+
const out = new Set();
|
|
161
|
+
const stack = [schema];
|
|
162
|
+
while (stack.length > 0) {
|
|
163
|
+
const node = stack.pop();
|
|
164
|
+
if (Array.isArray(node)) {
|
|
165
|
+
stack.push(...node);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!isObject(node)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const name = refName(node);
|
|
172
|
+
if (name !== undefined) {
|
|
173
|
+
out.add(name);
|
|
174
|
+
// A `$ref` may carry siblings in 2020-12, but none of them are the
|
|
175
|
+
// pointed-at schema, so there is nothing further down this branch.
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
stack.push(...Object.values(node));
|
|
179
|
+
}
|
|
180
|
+
return [...out];
|
|
181
|
+
}
|
|
182
|
+
export function docOf(schema) {
|
|
183
|
+
if (!isObject(schema)) {
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
const description = schema.description ?? schema.title;
|
|
187
|
+
return typeof description === 'string' ? description.trim() : '';
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Embedder } from '@zenera/neo';
|
|
2
|
+
import { type Manifest, type OpenIndex } from './files.ts';
|
|
3
|
+
import type { ApiGraph } from './graph.ts';
|
|
4
|
+
import type { Schema } from './schema.ts';
|
|
5
|
+
import type { Operation } from './spec.ts';
|
|
6
|
+
import { EntityStore } from './store.ts';
|
|
7
|
+
import { type Seed, type Subgraph } from './subgraph.ts';
|
|
8
|
+
export type DirectionFilter = 'input' | 'output' | 'any';
|
|
9
|
+
export type MethodTypeFilter = 'read_only' | 'read_write' | 'any';
|
|
10
|
+
export interface SchemaQuery {
|
|
11
|
+
/** searched against everything, unfiltered */
|
|
12
|
+
all?: readonly string[];
|
|
13
|
+
methods?: readonly string[];
|
|
14
|
+
method_type?: MethodTypeFilter;
|
|
15
|
+
types?: readonly string[];
|
|
16
|
+
input_types?: readonly string[];
|
|
17
|
+
output_types?: readonly string[];
|
|
18
|
+
properties?: readonly string[];
|
|
19
|
+
input_properties?: readonly string[];
|
|
20
|
+
output_properties?: readonly string[];
|
|
21
|
+
/** the side `types` and `properties` are read on; the explicit fields override it */
|
|
22
|
+
direction?: DirectionFilter;
|
|
23
|
+
exclude_ids?: readonly string[];
|
|
24
|
+
exclude_methods?: readonly string[];
|
|
25
|
+
exclude_types?: readonly string[];
|
|
26
|
+
exclude_properties?: readonly string[];
|
|
27
|
+
/** seeds kept per query string */
|
|
28
|
+
limit?: number;
|
|
29
|
+
max_hops?: number;
|
|
30
|
+
max_nodes?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface SearchResult {
|
|
33
|
+
seeds: Seed[];
|
|
34
|
+
subgraphs: Subgraph[];
|
|
35
|
+
/** query strings that matched nothing once exclusions were applied */
|
|
36
|
+
empty: string[];
|
|
37
|
+
}
|
|
38
|
+
export declare const DEFAULT_LIMIT = 5;
|
|
39
|
+
export declare class SchemaIndex {
|
|
40
|
+
#private;
|
|
41
|
+
readonly manifest: Manifest;
|
|
42
|
+
readonly graph: ApiGraph;
|
|
43
|
+
constructor(index: OpenIndex, store: EntityStore, embedder: Embedder);
|
|
44
|
+
static open(dir: string, embedder: Embedder): Promise<SchemaIndex>;
|
|
45
|
+
schemas(): Promise<Record<string, Schema>>;
|
|
46
|
+
operations(): Promise<Operation[]>;
|
|
47
|
+
close(): void;
|
|
48
|
+
search(query: SchemaQuery, signal?: AbortSignal): Promise<SearchResult>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { assertSameEmbedding, openIndex } from "./files.js";
|
|
2
|
+
import { EntityStore } from "./store.js";
|
|
3
|
+
import { DEFAULT_MAX_HOPS, DEFAULT_MAX_NODES, stitch, } from "./subgraph.js";
|
|
4
|
+
export const DEFAULT_LIMIT = 5;
|
|
5
|
+
/** Reciprocal rank fusion; the constant is the usual one and damps the top. */
|
|
6
|
+
const RRF_K = 60;
|
|
7
|
+
export class SchemaIndex {
|
|
8
|
+
manifest;
|
|
9
|
+
graph;
|
|
10
|
+
#index;
|
|
11
|
+
#store;
|
|
12
|
+
#embedder;
|
|
13
|
+
constructor(index, store, embedder) {
|
|
14
|
+
this.manifest = index.manifest;
|
|
15
|
+
this.graph = index.graph;
|
|
16
|
+
this.#index = index;
|
|
17
|
+
this.#store = store;
|
|
18
|
+
this.#embedder = embedder;
|
|
19
|
+
}
|
|
20
|
+
static async open(dir, embedder) {
|
|
21
|
+
const index = await openIndex(dir);
|
|
22
|
+
assertSameEmbedding(index.manifest, embedder.id);
|
|
23
|
+
return new SchemaIndex(index, await EntityStore.open(dir), embedder);
|
|
24
|
+
}
|
|
25
|
+
schemas() {
|
|
26
|
+
return this.#index.schemas();
|
|
27
|
+
}
|
|
28
|
+
operations() {
|
|
29
|
+
return this.#index.operations();
|
|
30
|
+
}
|
|
31
|
+
close() {
|
|
32
|
+
this.#store.close();
|
|
33
|
+
}
|
|
34
|
+
async search(query, signal) {
|
|
35
|
+
const terms = termsOf(query);
|
|
36
|
+
if (terms.length === 0) {
|
|
37
|
+
return { seeds: [], subgraphs: [], empty: [] };
|
|
38
|
+
}
|
|
39
|
+
const limit = query.limit ?? DEFAULT_LIMIT;
|
|
40
|
+
const excluded = exclusion(query);
|
|
41
|
+
const response = await this.#embedder.embed({
|
|
42
|
+
input: terms.map((t) => t.text),
|
|
43
|
+
taskType: 'query',
|
|
44
|
+
signal,
|
|
45
|
+
});
|
|
46
|
+
const seeds = [];
|
|
47
|
+
const empty = [];
|
|
48
|
+
for (const [at, term] of terms.entries()) {
|
|
49
|
+
const vector = Float32Array.from(response.vectors[at]);
|
|
50
|
+
// Over-fetch by what may be thrown away, so an exclusion list
|
|
51
|
+
// shortens the answer instead of emptying it.
|
|
52
|
+
const hits = await this.#store.search(term.text, vector, term.filter, limit + Math.min(excluded.size, limit * 4));
|
|
53
|
+
const kept = hits.filter((hit) => !excluded.has(hit.record)).slice(0, limit);
|
|
54
|
+
if (kept.length === 0) {
|
|
55
|
+
empty.push(term.text);
|
|
56
|
+
}
|
|
57
|
+
for (const hit of kept) {
|
|
58
|
+
seeds.push({
|
|
59
|
+
id: hit.record.id,
|
|
60
|
+
term: term.text,
|
|
61
|
+
field: term.field,
|
|
62
|
+
score: 1 / (RRF_K + hit.rank),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const subgraphs = stitch(this.graph, seeds, {
|
|
67
|
+
maxHops: query.max_hops ?? DEFAULT_MAX_HOPS,
|
|
68
|
+
maxNodes: query.max_nodes ?? DEFAULT_MAX_NODES,
|
|
69
|
+
});
|
|
70
|
+
return { seeds, subgraphs, empty };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
function termsOf(query) {
|
|
75
|
+
const method = methodTypes(query.method_type);
|
|
76
|
+
const loose = query.direction ?? 'any';
|
|
77
|
+
return [
|
|
78
|
+
...group(query.all, 'all', { methodTypes: method.mixed }),
|
|
79
|
+
...group(query.methods, 'methods', { kinds: ['method'], methodTypes: method.only }),
|
|
80
|
+
...group(query.types, 'types', { kinds: ['type'], directions: sides(loose) }),
|
|
81
|
+
...group(query.input_types, 'input_types', { kinds: ['type'], directions: sides('input') }),
|
|
82
|
+
...group(query.output_types, 'output_types', {
|
|
83
|
+
kinds: ['type'],
|
|
84
|
+
directions: sides('output'),
|
|
85
|
+
}),
|
|
86
|
+
...group(query.properties, 'properties', {
|
|
87
|
+
kinds: ['property'],
|
|
88
|
+
directions: sides(loose),
|
|
89
|
+
}),
|
|
90
|
+
...group(query.input_properties, 'input_properties', {
|
|
91
|
+
kinds: ['property'],
|
|
92
|
+
directions: sides('input'),
|
|
93
|
+
}),
|
|
94
|
+
...group(query.output_properties, 'output_properties', {
|
|
95
|
+
kinds: ['property'],
|
|
96
|
+
directions: sides('output'),
|
|
97
|
+
}),
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
function group(texts, field, filter) {
|
|
101
|
+
return (texts ?? [])
|
|
102
|
+
.map((text) => text.trim())
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.map((text) => ({ field, text, filter }));
|
|
105
|
+
}
|
|
106
|
+
function sides(direction) {
|
|
107
|
+
return direction === 'any' ? undefined : [direction, 'both'];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A method-type filter is about methods. Applied whole to an unfiltered query
|
|
111
|
+
* it would also throw away every type and property, since those carry `n/a`.
|
|
112
|
+
*/
|
|
113
|
+
function methodTypes(filter) {
|
|
114
|
+
if (!filter || filter === 'any') {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
return { only: [filter], mixed: [filter, 'n/a'] };
|
|
118
|
+
}
|
|
119
|
+
function exclusion(query) {
|
|
120
|
+
const ids = new Set(query.exclude_ids ?? []);
|
|
121
|
+
const methods = new Set(query.exclude_methods ?? []);
|
|
122
|
+
const types = new Set(query.exclude_types ?? []);
|
|
123
|
+
const properties = new Set(query.exclude_properties ?? []);
|
|
124
|
+
const size = ids.size + methods.size + types.size + properties.size;
|
|
125
|
+
return {
|
|
126
|
+
size,
|
|
127
|
+
has(record) {
|
|
128
|
+
if (ids.has(record.id)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
switch (record.kind) {
|
|
132
|
+
case 'method':
|
|
133
|
+
return methods.has(record.name);
|
|
134
|
+
case 'type':
|
|
135
|
+
return types.has(record.name);
|
|
136
|
+
default:
|
|
137
|
+
return properties.has(record.name);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=search.js.map
|