@fungi.computer/forage 0.1.0 → 0.2.1
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 +7 -1
- package/dist/app-api.d.ts +14 -5
- package/dist/app-api.js +36 -16
- package/dist/catalog.js +48 -24
- package/dist/describe.js +11 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/normalization.d.ts +0 -1
- package/dist/normalization.js +24 -27
- package/dist/openapi.js +25 -14
- package/dist/rendering.d.ts +8 -14
- package/dist/rendering.js +87 -144
- package/dist/schemas.d.ts +1 -0
- package/dist/schemas.js +4 -6
- package/dist/search.d.ts +2 -3
- package/dist/search.js +43 -40
- package/docs/api/index.md +167 -168
- package/docs/api/openapi.md +1 -1
- package/package.json +2 -2
package/dist/rendering.js
CHANGED
|
@@ -1,67 +1,71 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ForageError } from "./errors.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
const schemaNode = z.
|
|
6
|
-
z.boolean(),
|
|
3
|
+
import { compareCodeUnits, jsonValueSchema } from "./normalization.js";
|
|
4
|
+
/** The JSON Schema subset Forage reads when it renders and samples a schema. */
|
|
5
|
+
const schemaNode = z.union([
|
|
7
6
|
z.looseObject({
|
|
8
7
|
$ref: z.string().optional(),
|
|
9
|
-
$defs: z.record(z.string(),
|
|
10
|
-
definitions: z.record(z.string(),
|
|
8
|
+
$defs: z.record(z.string(), z.unknown()).optional(),
|
|
9
|
+
definitions: z.record(z.string(), z.unknown()).optional(),
|
|
11
10
|
const: z.unknown().optional(),
|
|
12
11
|
enum: z.array(z.unknown()).optional(),
|
|
13
|
-
oneOf: z.array(
|
|
14
|
-
anyOf: z.array(
|
|
15
|
-
allOf: z.array(
|
|
12
|
+
oneOf: z.array(z.unknown()).optional(),
|
|
13
|
+
anyOf: z.array(z.unknown()).optional(),
|
|
14
|
+
allOf: z.array(z.unknown()).optional(),
|
|
16
15
|
type: z.union([z.string(), z.array(z.string())]).optional(),
|
|
17
|
-
properties: z.record(z.string(),
|
|
18
|
-
additionalProperties:
|
|
16
|
+
properties: z.record(z.string(), z.unknown()).optional(),
|
|
17
|
+
additionalProperties: z.unknown().optional(),
|
|
19
18
|
required: z.array(z.string()).optional(),
|
|
20
|
-
prefixItems: z.array(
|
|
21
|
-
items: z.
|
|
19
|
+
prefixItems: z.array(z.unknown()).optional(),
|
|
20
|
+
items: z.unknown().optional(),
|
|
22
21
|
examples: z.array(z.unknown()).optional(),
|
|
23
22
|
default: z.unknown().optional(),
|
|
24
23
|
}),
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* recompiles a catalog over the same installed schemas asks again on every
|
|
32
|
-
* compile. Projecting, validating, fingerprinting and sizing once per schema
|
|
33
|
-
* keeps catalog compilation proportional to what actually changed.
|
|
34
|
-
*/
|
|
35
|
-
const schemaFacts = new WeakMap();
|
|
36
|
-
function factsFor(schema, io) {
|
|
37
|
-
let entry = schemaFacts.get(schema);
|
|
24
|
+
z.boolean(),
|
|
25
|
+
]);
|
|
26
|
+
/** Standard JSON Schema projections are immutable, per source and direction. */
|
|
27
|
+
const projections = new WeakMap();
|
|
28
|
+
function projectionFor(schema, io) {
|
|
29
|
+
let entry = projections.get(schema);
|
|
38
30
|
const cached = entry?.[io];
|
|
39
31
|
if (cached !== undefined)
|
|
40
32
|
return cached;
|
|
41
|
-
let
|
|
33
|
+
let raw;
|
|
42
34
|
try {
|
|
43
|
-
|
|
35
|
+
raw = schema["~standard"].jsonSchema[io]({ target: "draft-2020-12" });
|
|
44
36
|
}
|
|
45
37
|
catch {
|
|
46
38
|
throw new ForageError("invalid_input", "Schema cannot be projected to JSON Schema");
|
|
47
39
|
}
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
node,
|
|
51
|
-
json: json.success ? json.data : undefined,
|
|
52
|
-
};
|
|
40
|
+
decodeNode(raw);
|
|
41
|
+
const projection = { raw };
|
|
53
42
|
if (entry === undefined) {
|
|
54
43
|
entry = {};
|
|
55
|
-
|
|
44
|
+
projections.set(schema, entry);
|
|
56
45
|
}
|
|
57
|
-
entry[io] =
|
|
58
|
-
return
|
|
46
|
+
entry[io] = projection;
|
|
47
|
+
return projection;
|
|
59
48
|
}
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
/** The producer's document, without a recursive projection decode or copy. */
|
|
50
|
+
export function rawSchema(schema, io) {
|
|
51
|
+
return projectionFor(schema, io).raw;
|
|
62
52
|
}
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
/** Native serialization owns the document's text representation and failures. */
|
|
54
|
+
export function schemaText(schema, io) {
|
|
55
|
+
const projection = projectionFor(schema, io);
|
|
56
|
+
try {
|
|
57
|
+
projection.text ??= JSON.stringify(projection.raw);
|
|
58
|
+
return projection.text;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new ForageError("invalid_input", "Schema cannot be projected to JSON Schema");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function decodeNode(value) {
|
|
65
|
+
const parsed = schemaNode.safeParse(value);
|
|
66
|
+
if (!parsed.success)
|
|
67
|
+
throw new ForageError("invalid_input", "Schema cannot be projected to JSON Schema");
|
|
68
|
+
return parsed.data;
|
|
65
69
|
}
|
|
66
70
|
/** Bounds the work and size of expanding shared `$defs` in one description. */
|
|
67
71
|
const maximumReferenceExpansions = 256;
|
|
@@ -77,7 +81,7 @@ function renderReference(reference, depth, refs) {
|
|
|
77
81
|
refs.budget.expansions += 1;
|
|
78
82
|
if (refs.budget.expansions > maximumReferenceExpansions)
|
|
79
83
|
throw new ForageError("invalid_input", `Schema references expand more than ${String(maximumReferenceExpansions)} times; flatten its shared definitions`);
|
|
80
|
-
return renderNode(target, depth, {
|
|
84
|
+
return renderNode(decodeNode(target), depth, {
|
|
81
85
|
definitions: refs.definitions,
|
|
82
86
|
expanding: new Set([...refs.expanding, name]),
|
|
83
87
|
budget: refs.budget,
|
|
@@ -94,7 +98,7 @@ function rendersUnion(node, refs) {
|
|
|
94
98
|
: refs.definitions[name];
|
|
95
99
|
return (name !== undefined &&
|
|
96
100
|
target !== undefined &&
|
|
97
|
-
rendersUnion(target, {
|
|
101
|
+
rendersUnion(decodeNode(target), {
|
|
98
102
|
definitions: refs.definitions,
|
|
99
103
|
expanding: new Set([...refs.expanding, name]),
|
|
100
104
|
budget: refs.budget,
|
|
@@ -106,10 +110,17 @@ function rendersUnion(node, refs) {
|
|
|
106
110
|
(node.oneOf !== undefined && node.oneOf.length > 1) ||
|
|
107
111
|
(node.anyOf !== undefined && node.anyOf.length > 1));
|
|
108
112
|
}
|
|
109
|
-
function renderNode(node, depth,
|
|
110
|
-
if (
|
|
113
|
+
function renderNode(node, depth, scope) {
|
|
114
|
+
if (depth > 12) {
|
|
111
115
|
return "unknown";
|
|
112
116
|
}
|
|
117
|
+
if (node === true || node === false)
|
|
118
|
+
return "unknown";
|
|
119
|
+
const refs = scope ?? {
|
|
120
|
+
definitions: { ...node.definitions, ...node.$defs },
|
|
121
|
+
expanding: new Set(),
|
|
122
|
+
budget: { expansions: 0 },
|
|
123
|
+
};
|
|
113
124
|
if (node.$ref)
|
|
114
125
|
return renderReference(node.$ref, depth, refs);
|
|
115
126
|
if (node.const !== undefined)
|
|
@@ -118,20 +129,21 @@ function renderNode(node, depth, refs) {
|
|
|
118
129
|
return node.enum.map((value) => JSON.stringify(value)).join(" | ");
|
|
119
130
|
if (node.oneOf) {
|
|
120
131
|
return node.oneOf
|
|
121
|
-
.map((value) => renderNode(value, depth + 1, refs))
|
|
132
|
+
.map((value) => renderNode(decodeNode(value), depth + 1, refs))
|
|
122
133
|
.join(" | ");
|
|
123
134
|
}
|
|
124
135
|
if (node.anyOf) {
|
|
125
136
|
return node.anyOf
|
|
126
|
-
.map((value) => renderNode(value, depth + 1, refs))
|
|
137
|
+
.map((value) => renderNode(decodeNode(value), depth + 1, refs))
|
|
127
138
|
.join(" | ");
|
|
128
139
|
}
|
|
129
140
|
if (node.allOf) {
|
|
130
141
|
return node.allOf
|
|
131
142
|
.map((value) => {
|
|
132
|
-
const
|
|
143
|
+
const child = decodeNode(value);
|
|
144
|
+
const member = renderNode(child, depth + 1, refs);
|
|
133
145
|
// `&` binds tighter than `|`: keep a union member whole.
|
|
134
|
-
return rendersUnion(
|
|
146
|
+
return rendersUnion(child, refs) ? `(${member})` : member;
|
|
135
147
|
})
|
|
136
148
|
.join(" & ");
|
|
137
149
|
}
|
|
@@ -142,13 +154,13 @@ function renderNode(node, depth, refs) {
|
|
|
142
154
|
if (additional === false)
|
|
143
155
|
return "Record<string, never>";
|
|
144
156
|
if (additional && additional !== true) {
|
|
145
|
-
return `Record<string, ${renderNode(additional, depth + 1, refs)}>`;
|
|
157
|
+
return `Record<string, ${renderNode(decodeNode(additional), depth + 1, refs)}>`;
|
|
146
158
|
}
|
|
147
159
|
return "Record<string, unknown>";
|
|
148
160
|
}
|
|
149
161
|
const required = new Set(node.required ?? []);
|
|
150
162
|
const lines = properties.map(([key, value]) => {
|
|
151
|
-
const child = renderNode(value, depth + 1, refs);
|
|
163
|
+
const child = renderNode(decodeNode(value), depth + 1, refs);
|
|
152
164
|
const optional = required.has(key) ? "" : "?";
|
|
153
165
|
const safeKey = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
154
166
|
? key
|
|
@@ -162,21 +174,21 @@ ${" ".repeat(depth)}}`;
|
|
|
162
174
|
if (node.type === "array") {
|
|
163
175
|
const items = node.items;
|
|
164
176
|
if (node.prefixItems && node.prefixItems.length > 0) {
|
|
165
|
-
const tuple = node.prefixItems.map((value) => renderNode(value, depth + 1, refs));
|
|
166
|
-
if (items !== undefined && items !== true && !
|
|
167
|
-
tuple.push(`...${renderNode(items, depth + 1, refs)}[]`);
|
|
177
|
+
const tuple = node.prefixItems.map((value) => renderNode(decodeNode(value), depth + 1, refs));
|
|
178
|
+
if (items !== undefined && items !== true && !Array.isArray(items)) {
|
|
179
|
+
tuple.push(`...${renderNode(decodeNode(items), depth + 1, refs)}[]`);
|
|
168
180
|
}
|
|
169
181
|
return `[${tuple.join(", ")}]`;
|
|
170
182
|
}
|
|
171
|
-
if (items !== undefined &&
|
|
172
|
-
return `[${items.map((value) => renderNode(value, depth + 1, refs)).join(", ")}][]`;
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
(
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
if (items !== undefined && Array.isArray(items))
|
|
184
|
+
return `[${items.map((value) => renderNode(decodeNode(value), depth + 1, refs)).join(", ")}][]`;
|
|
185
|
+
const itemNode = items === undefined ? true : decodeNode(items);
|
|
186
|
+
const item = renderNode(itemNode, depth + 1, refs);
|
|
187
|
+
const compoundItem = itemNode !== true &&
|
|
188
|
+
itemNode !== false &&
|
|
189
|
+
(itemNode.oneOf !== undefined ||
|
|
190
|
+
itemNode.anyOf !== undefined ||
|
|
191
|
+
itemNode.allOf !== undefined);
|
|
180
192
|
return `${compoundItem ? `(${item})` : item}[]`;
|
|
181
193
|
}
|
|
182
194
|
switch (node.type) {
|
|
@@ -195,15 +207,7 @@ ${" ".repeat(depth)}}`;
|
|
|
195
207
|
}
|
|
196
208
|
/** Render one schema's JSON Schema as deterministic TypeScript type text. */
|
|
197
209
|
function renderSchemaType(schema, io = "input") {
|
|
198
|
-
|
|
199
|
-
const definitions = root === true || root === false
|
|
200
|
-
? {}
|
|
201
|
-
: { ...root.definitions, ...root.$defs };
|
|
202
|
-
return renderNode(root, 0, {
|
|
203
|
-
definitions,
|
|
204
|
-
expanding: new Set(),
|
|
205
|
-
budget: { expansions: 0 },
|
|
206
|
-
});
|
|
210
|
+
return renderNode(decodeNode(rawSchema(schema, io)), 0);
|
|
207
211
|
}
|
|
208
212
|
function safeTypeName(name) {
|
|
209
213
|
return name.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
@@ -228,33 +232,38 @@ export function renderOperationTypes(operations) {
|
|
|
228
232
|
? types
|
|
229
233
|
: `${types}\nexport declare const env: ${operations.map((operation) => operation.environment).join(" & ")};`;
|
|
230
234
|
}
|
|
231
|
-
|
|
235
|
+
const renderedOperations = new WeakMap();
|
|
236
|
+
/** Render only requested operations, once per immutable prepared operation. */
|
|
232
237
|
export function renderOperation(operation) {
|
|
238
|
+
const cached = renderedOperations.get(operation);
|
|
239
|
+
if (cached !== undefined)
|
|
240
|
+
return cached;
|
|
233
241
|
const name = safeTypeName(operation.path.replace(/\./g, "$"));
|
|
234
242
|
const input = renderSchemaType(operation.input, "input");
|
|
235
243
|
const output = operation.output
|
|
236
244
|
? renderSchemaType(operation.output, "output")
|
|
237
245
|
: "unknown";
|
|
238
|
-
|
|
246
|
+
const rendered = Object.freeze({
|
|
239
247
|
indexEnvironment: renderEnvironment(operation.path, input, output),
|
|
240
248
|
environment: renderEnvironment(operation.path, `${name}_Input`, `${name}_Output`),
|
|
241
249
|
types: `export type ${name}_Input = ${input};\nexport type ${name}_Output = ${output};`,
|
|
242
250
|
});
|
|
251
|
+
renderedOperations.set(operation, rendered);
|
|
252
|
+
return rendered;
|
|
243
253
|
}
|
|
244
254
|
/** Derive sample arguments solely from input-schema examples and defaults. */
|
|
245
255
|
export function deriveSample(schema) {
|
|
246
|
-
const node =
|
|
256
|
+
const node = decodeNode(rawSchema(schema, "input"));
|
|
247
257
|
if (node === true || node === false || !node.properties) {
|
|
248
258
|
return undefined;
|
|
249
259
|
}
|
|
250
|
-
const properties = node.properties;
|
|
251
260
|
const sample = {};
|
|
252
261
|
let found = false;
|
|
253
|
-
const entries = Object.entries(properties).sort(([left], [right]) => compareCodeUnits(left, right));
|
|
262
|
+
const entries = Object.entries(node.properties).sort(([left], [right]) => compareCodeUnits(left, right));
|
|
254
263
|
for (const [key, value] of entries) {
|
|
255
|
-
|
|
264
|
+
const property = decodeNode(value);
|
|
265
|
+
if (property === true || property === false)
|
|
256
266
|
continue;
|
|
257
|
-
const property = value;
|
|
258
267
|
const examples = property.examples;
|
|
259
268
|
if (examples && examples.length > 0) {
|
|
260
269
|
const example = jsonValueSchema.safeParse(examples[0]);
|
|
@@ -273,69 +282,3 @@ export function deriveSample(schema) {
|
|
|
273
282
|
}
|
|
274
283
|
return found ? sample : undefined;
|
|
275
284
|
}
|
|
276
|
-
/** Preserve validator constraints in the operation's Standard JSON Schema. */
|
|
277
|
-
export function inputSchemaProjection(schema) {
|
|
278
|
-
const projection = factsFor(schema, "input").json;
|
|
279
|
-
if (projection === undefined) {
|
|
280
|
-
throw new ForageError("invalid_input", "Schema cannot be projected to JSON Schema");
|
|
281
|
-
}
|
|
282
|
-
return projection;
|
|
283
|
-
}
|
|
284
|
-
/** Produce a canonical schema representation used in peer identity and stamps. */
|
|
285
|
-
export function schemaFingerprint(schema, io = "input") {
|
|
286
|
-
const facts = factsFor(schema, io);
|
|
287
|
-
facts.fingerprint ??= canonicalStringify(facts.json ?? null);
|
|
288
|
-
return facts.fingerprint;
|
|
289
|
-
}
|
|
290
|
-
function schemaDepth(value, depth) {
|
|
291
|
-
if (depth > maximumSampleDepth)
|
|
292
|
-
return depth;
|
|
293
|
-
if (Array.isArray(value)) {
|
|
294
|
-
let maximum = depth;
|
|
295
|
-
for (const item of value) {
|
|
296
|
-
maximum = Math.max(maximum, schemaDepth(item, depth + 1));
|
|
297
|
-
}
|
|
298
|
-
return maximum;
|
|
299
|
-
}
|
|
300
|
-
if (!isJsonObject(value))
|
|
301
|
-
return depth;
|
|
302
|
-
let maximum = depth;
|
|
303
|
-
for (const entry of Object.values(value)) {
|
|
304
|
-
maximum = Math.max(maximum, schemaDepth(entry, depth + 1));
|
|
305
|
-
}
|
|
306
|
-
return maximum;
|
|
307
|
-
}
|
|
308
|
-
/** Validate the bounded JSON projection before a catalog is installed. */
|
|
309
|
-
export function schemaProjectionSize(schema, io) {
|
|
310
|
-
const facts = factsFor(schema, io);
|
|
311
|
-
facts.size ??= { bytes: measureProjection(schema, io, facts.json) };
|
|
312
|
-
return facts.size.bytes;
|
|
313
|
-
}
|
|
314
|
-
function measureProjection(schema, io, projection) {
|
|
315
|
-
if (!projection)
|
|
316
|
-
return undefined;
|
|
317
|
-
const encoded = JSON.stringify(projection);
|
|
318
|
-
if (encoded.length > 20_000 ||
|
|
319
|
-
schemaDepth(projection, 0) > maximumSampleDepth) {
|
|
320
|
-
return undefined;
|
|
321
|
-
}
|
|
322
|
-
if (io === "output")
|
|
323
|
-
return encoded.length;
|
|
324
|
-
const sample = deriveSample(schema);
|
|
325
|
-
if (!sample)
|
|
326
|
-
return encoded.length;
|
|
327
|
-
if (Object.keys(sample).length > maximumSampleEntries ||
|
|
328
|
-
schemaDepth(sample, 0) > maximumSampleDepth) {
|
|
329
|
-
return undefined;
|
|
330
|
-
}
|
|
331
|
-
let sampleText;
|
|
332
|
-
try {
|
|
333
|
-
sampleText = JSON.stringify(sample);
|
|
334
|
-
}
|
|
335
|
-
catch {
|
|
336
|
-
throw new ForageError("invalid_input", "Derived schema sample cannot be serialized");
|
|
337
|
-
}
|
|
338
|
-
if (!sampleText || sampleText.length > maximumSampleBytes)
|
|
339
|
-
return undefined;
|
|
340
|
-
return encoded.length + sampleText.length;
|
|
341
|
-
}
|
package/dist/schemas.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare const maximumDescriptionTypes = 20000;
|
|
|
3
3
|
export declare const maximumSampleEntries = 100;
|
|
4
4
|
export declare const maximumSampleBytes = 4000;
|
|
5
5
|
export declare const maximumSampleDepth = 16;
|
|
6
|
+
export declare function jsonDepth(value: z.output<z.ZodJSONSchema>, depth: number): number;
|
|
6
7
|
/** Input accepted by the pure catalog search seam. */
|
|
7
8
|
export declare const forageSearchInput: z.ZodLazy<z.ZodObject<{
|
|
8
9
|
query: z.ZodString;
|
package/dist/schemas.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { isJsonObject } from "./normalization.js";
|
|
2
3
|
const pathPattern = /^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/;
|
|
3
4
|
const maximumCatalogOperations = 1_000;
|
|
4
5
|
const maximumScore = 221;
|
|
@@ -6,7 +7,7 @@ export const maximumDescriptionTypes = 20_000;
|
|
|
6
7
|
export const maximumSampleEntries = 100;
|
|
7
8
|
export const maximumSampleBytes = 4_000;
|
|
8
9
|
export const maximumSampleDepth = 16;
|
|
9
|
-
function jsonDepth(value, depth) {
|
|
10
|
+
export function jsonDepth(value, depth) {
|
|
10
11
|
if (depth > maximumSampleDepth)
|
|
11
12
|
return depth;
|
|
12
13
|
if (Array.isArray(value)) {
|
|
@@ -16,13 +17,10 @@ function jsonDepth(value, depth) {
|
|
|
16
17
|
}
|
|
17
18
|
return maximum;
|
|
18
19
|
}
|
|
19
|
-
if (value
|
|
20
|
-
return depth;
|
|
21
|
-
const record = z.record(z.string(), z.json()).safeParse(value);
|
|
22
|
-
if (!record.success)
|
|
20
|
+
if (!isJsonObject(value))
|
|
23
21
|
return depth;
|
|
24
22
|
let maximum = depth;
|
|
25
|
-
for (const item of Object.values(
|
|
23
|
+
for (const item of Object.values(value)) {
|
|
26
24
|
maximum = Math.max(maximum, jsonDepth(item, depth + 1));
|
|
27
25
|
}
|
|
28
26
|
return maximum;
|
package/dist/search.d.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { type Operation, type SearchOutput } from "./schemas.js";
|
|
2
|
-
import { type RenderedOperation } from "./rendering.js";
|
|
3
2
|
import type { JsonValue } from "./normalization.js";
|
|
4
3
|
/** Private installation-time projection used by the pure ranking function. */
|
|
5
4
|
export type PreparedOperation = Operation & {
|
|
6
5
|
readonly fingerprint: string;
|
|
7
|
-
readonly inputSchema: JsonValue;
|
|
8
6
|
readonly sample: Record<string, JsonValue> | undefined;
|
|
9
|
-
readonly
|
|
7
|
+
readonly inputBytes: number;
|
|
8
|
+
readonly outputBytes: number;
|
|
10
9
|
};
|
|
11
10
|
/** Prepare schema-derived search metadata once for an immutable catalog revision. */
|
|
12
11
|
export declare function prepareOperation(operation: Operation): PreparedOperation;
|
package/dist/search.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { searchOutputSchema, } from "./schemas.js";
|
|
2
|
-
import {
|
|
3
|
-
import { deriveSample,
|
|
2
|
+
import { compareCodeUnits, hashString64, isStopword, normalizeQuery, operationMethod, operationNamespace, stemVariants, tokenize, } from "./normalization.js";
|
|
3
|
+
import { deriveSample, schemaText } from "./rendering.js";
|
|
4
4
|
import { ForageError } from "./errors.js";
|
|
5
5
|
/** Prepare schema-derived search metadata once for an immutable catalog revision. */
|
|
6
6
|
export function prepareOperation(operation) {
|
|
7
|
+
const input = schemaText(operation.input, "input");
|
|
8
|
+
const output = operation.output ? schemaText(operation.output, "output") : "";
|
|
7
9
|
return Object.freeze({
|
|
8
10
|
...operation,
|
|
9
|
-
fingerprint:
|
|
10
|
-
|
|
11
|
+
fingerprint: hashString64(input + "\n" + output),
|
|
12
|
+
inputBytes: input.length,
|
|
13
|
+
outputBytes: output.length,
|
|
11
14
|
sample: deriveSample(operation.input),
|
|
12
|
-
rendered: renderOperation(operation),
|
|
13
15
|
});
|
|
14
16
|
}
|
|
15
17
|
const matchWeight = {
|
|
@@ -21,33 +23,33 @@ const matchWeight = {
|
|
|
21
23
|
substring: 2,
|
|
22
24
|
includes: 1,
|
|
23
25
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
const fieldWeight = {
|
|
27
|
+
path: 12,
|
|
28
|
+
namespace: 8,
|
|
29
|
+
method: 10,
|
|
30
|
+
description: 5,
|
|
31
|
+
};
|
|
32
|
+
const fullCoverageBonus = 25;
|
|
33
|
+
const partialCoverageBonus = 10;
|
|
34
|
+
const firstTokenBonus = 8;
|
|
35
|
+
const exactMatchBonus = 20;
|
|
36
|
+
const maxBonus = Math.max(fullCoverageBonus, partialCoverageBonus) +
|
|
37
|
+
firstTokenBonus +
|
|
38
|
+
exactMatchBonus;
|
|
39
|
+
const dominanceDivisor = 1 +
|
|
40
|
+
Math.max(...Object.values(matchWeight)) *
|
|
41
|
+
Math.max(...Object.values(fieldWeight)) +
|
|
42
|
+
maxBonus;
|
|
38
43
|
/** Identify every model-visible part of the catalog, including schemas. */
|
|
39
44
|
function catalogStamp(operations) {
|
|
40
45
|
const lines = [...operations]
|
|
41
46
|
.sort((left, right) => compareCodeUnits(left.path, right.path))
|
|
42
|
-
.map((operation) =>
|
|
47
|
+
.map((operation) => JSON.stringify([
|
|
43
48
|
operation.path,
|
|
44
49
|
operation.description ?? null,
|
|
45
50
|
operation.capabilityNotes ?? null,
|
|
46
51
|
operation.instructions ?? null,
|
|
47
52
|
operation.fingerprint,
|
|
48
|
-
operation.output === undefined
|
|
49
|
-
? null
|
|
50
|
-
: schemaFingerprint(operation.output, "output"),
|
|
51
53
|
]));
|
|
52
54
|
return hashString64(lines.join("\n"));
|
|
53
55
|
}
|
|
@@ -74,7 +76,7 @@ function scoreField(rawValue, field, queryTokens) {
|
|
|
74
76
|
const value = normalizeQuery(rawValue);
|
|
75
77
|
let bestLevel = "none";
|
|
76
78
|
let bestScore = 0;
|
|
77
|
-
|
|
79
|
+
const matchedTokens = [];
|
|
78
80
|
for (const token of queryTokens) {
|
|
79
81
|
const terms = [token, ...stemVariants(token)];
|
|
80
82
|
let selected = "none";
|
|
@@ -87,14 +89,12 @@ function scoreField(rawValue, field, queryTokens) {
|
|
|
87
89
|
}
|
|
88
90
|
if (selected === "none")
|
|
89
91
|
continue;
|
|
90
|
-
const score = matchWeight[selected] * fieldWeight
|
|
92
|
+
const score = matchWeight[selected] * fieldWeight[field];
|
|
93
|
+
if (!matchedTokens.includes(token))
|
|
94
|
+
matchedTokens.push(token);
|
|
91
95
|
if (score > bestScore) {
|
|
92
96
|
bestLevel = selected;
|
|
93
97
|
bestScore = score;
|
|
94
|
-
matchedTokens = [token];
|
|
95
|
-
}
|
|
96
|
-
else if (score === bestScore && !matchedTokens.includes(token)) {
|
|
97
|
-
matchedTokens.push(token);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
return { level: bestLevel, score: bestScore, matchedTokens };
|
|
@@ -139,7 +139,7 @@ function emptyResult(stamp) {
|
|
|
139
139
|
catalogStamp: stamp,
|
|
140
140
|
});
|
|
141
141
|
}
|
|
142
|
-
function scoreCandidate(operation,
|
|
142
|
+
function scoreCandidate(operation, queryTokens, coverageTokens) {
|
|
143
143
|
const namespace = operationNamespace(operation.path);
|
|
144
144
|
const method = operationMethod(operation.path);
|
|
145
145
|
const description = operation.description ?? "";
|
|
@@ -149,23 +149,27 @@ function scoreCandidate(operation, normalizedQuery, queryTokens, coverageTokens,
|
|
|
149
149
|
const methodScore = scoreField(method, "method", queryTokens);
|
|
150
150
|
const descriptionScore = scoreField(description, "description", queryTokens);
|
|
151
151
|
const matched = coverageTokens.filter((token) => fields.some((field) => tokenMatches(field, token))).length;
|
|
152
|
-
|
|
153
|
-
const phraseMatch = normalizeQuery(`${operation.path} ${description}`).includes(normalizedQuery);
|
|
154
|
-
if (coverage < threshold && !phraseMatch)
|
|
152
|
+
if (matched === 0)
|
|
155
153
|
return undefined;
|
|
154
|
+
const coverage = matched / coverageTokens.length;
|
|
156
155
|
const baseScore = Math.max(pathScore.score, namespaceScore.score, methodScore.score, descriptionScore.score);
|
|
157
156
|
if (baseScore === 0)
|
|
158
157
|
return undefined;
|
|
159
|
-
let bonus = matched === coverageTokens.length
|
|
158
|
+
let bonus = matched === coverageTokens.length
|
|
159
|
+
? fullCoverageBonus
|
|
160
|
+
: coverage * partialCoverageBonus;
|
|
160
161
|
const firstToken = coverageTokens[0];
|
|
161
162
|
if (firstToken && fields.some((field) => tokenMatches(field, firstToken))) {
|
|
162
|
-
bonus +=
|
|
163
|
+
bonus += firstTokenBonus;
|
|
164
|
+
}
|
|
165
|
+
if (pathScore.level === "exact" || methodScore.level === "exact") {
|
|
166
|
+
bonus += exactMatchBonus;
|
|
163
167
|
}
|
|
164
|
-
if (pathScore.level === "exact" || methodScore.level === "exact")
|
|
165
|
-
bonus += 20;
|
|
166
168
|
const result = {
|
|
167
169
|
path: operation.path,
|
|
168
|
-
|
|
170
|
+
// A whole matched word outranks any field-quality difference; the
|
|
171
|
+
// derived divisor keeps all field and bonus evidence below one word.
|
|
172
|
+
score: matched + (baseScore + bonus) / dominanceDivisor,
|
|
169
173
|
};
|
|
170
174
|
if (description)
|
|
171
175
|
result.description = description;
|
|
@@ -246,10 +250,9 @@ export function searchCatalog(operations, query, maxResults = 50, stamp) {
|
|
|
246
250
|
const coverageTokens = queryTokens.filter((token) => !isStopword(token));
|
|
247
251
|
if (coverageTokens.length === 0)
|
|
248
252
|
return emptyResult(stamp);
|
|
249
|
-
const threshold = coverageTokens.length <= 2 ? 1 : 0.6;
|
|
250
253
|
const candidates = [];
|
|
251
254
|
for (const operation of operations) {
|
|
252
|
-
const candidate = scoreCandidate(operation,
|
|
255
|
+
const candidate = scoreCandidate(operation, queryTokens, coverageTokens);
|
|
253
256
|
if (candidate)
|
|
254
257
|
candidates.push(candidate);
|
|
255
258
|
}
|