@fungi.computer/forage 0.2.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/dist/app-api.d.ts +11 -2
- package/dist/app-api.js +36 -16
- package/dist/catalog.js +48 -17
- 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/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 +36 -5
- package/package.json +2 -2
package/dist/app-api.d.ts
CHANGED
|
@@ -3,7 +3,16 @@ import type { AppApi } from "@fungi.computer/puffball";
|
|
|
3
3
|
import type { Operation } from "./schemas.js";
|
|
4
4
|
/** The shared JSON-Schema compiler for App methods and OpenAPI operations. */
|
|
5
5
|
export declare function jsonSchemaToZod(source: unknown, defaultTarget?: "draft-2020-12" | "openapi-3.0"): z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* One decoded, immutable App declaration as a JSON-only Standard Schema.
|
|
8
|
+
* Release and family-listing decoders own its JSON boundary. A catalog
|
|
9
|
+
* describes every installed method on each capture but a turn validates only
|
|
10
|
+
* the few it calls. Compiling every method eagerly exhausted an Agent Durable
|
|
11
|
+
* Object's isolate. A draft 2020-12 description is the declaration itself;
|
|
12
|
+
* validators compile for the call in hand and are never kept on the surface.
|
|
13
|
+
*/
|
|
14
|
+
export declare function appJsonSchema(declaration: AppApi["methods"][string]["input"] | boolean): Pick<z.ZodType<z.output<z.ZodJSONSchema>>, "parse" | "safeParse" | "~standard">;
|
|
15
|
+
/** Describe App methods with call-scoped JSON validators.
|
|
7
16
|
* Validation enforces both the declaration and JSON-only values; discovery
|
|
8
17
|
* describes the declaration without adding JSON guard nesting.
|
|
9
18
|
* Agar's emitted output already includes Pending for opted-in methods. */
|
|
@@ -17,7 +26,7 @@ export declare function appMethodSchemas(api: AppApi): {
|
|
|
17
26
|
readonly output: Pick<z.ZodType<z.core.util.JSONType, unknown, z.core.$ZodTypeInternals<z.core.util.JSONType, unknown>>, "~standard" | "parse" | "safeParse">;
|
|
18
27
|
};
|
|
19
28
|
};
|
|
20
|
-
/**
|
|
29
|
+
/** Describe discoverable App methods only; direct RPC validation uses appMethodSchemas. */
|
|
21
30
|
export declare function appToolSchemas(api: AppApi): {
|
|
22
31
|
[k: string]: {
|
|
23
32
|
readonly description: string;
|
package/dist/app-api.js
CHANGED
|
@@ -4,25 +4,45 @@ const bindingName = z.lazy(() => z
|
|
|
4
4
|
.min(1)
|
|
5
5
|
.max(75)
|
|
6
6
|
.regex(/^[A-Za-z][A-Za-z0-9_]*$/));
|
|
7
|
-
const
|
|
7
|
+
const jsonSchemaDocument = z.lazy(() => z.record(z.string(), z.json()));
|
|
8
|
+
const jsonSchema = z.lazy(() => z.union([jsonSchemaDocument, z.boolean()]));
|
|
8
9
|
/** The shared JSON-Schema compiler for App methods and OpenAPI operations. */
|
|
9
10
|
export function jsonSchemaToZod(source, defaultTarget = "draft-2020-12") {
|
|
10
11
|
return z.fromJSONSchema(jsonSchema.parse(source), { defaultTarget });
|
|
11
12
|
}
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/**
|
|
14
|
+
* One decoded, immutable App declaration as a JSON-only Standard Schema.
|
|
15
|
+
* Release and family-listing decoders own its JSON boundary. A catalog
|
|
16
|
+
* describes every installed method on each capture but a turn validates only
|
|
17
|
+
* the few it calls. Compiling every method eagerly exhausted an Agent Durable
|
|
18
|
+
* Object's isolate. A draft 2020-12 description is the declaration itself;
|
|
19
|
+
* validators compile for the call in hand and are never kept on the surface.
|
|
20
|
+
*/
|
|
21
|
+
export function appJsonSchema(declaration) {
|
|
22
|
+
const build = () => jsonSchemaToZod(declaration);
|
|
23
|
+
const validate = () => z.intersection(build(), z.json());
|
|
24
|
+
const describe = (io, options) => {
|
|
25
|
+
if (options.target === "draft-2020-12" &&
|
|
26
|
+
declaration !== true &&
|
|
27
|
+
declaration !== false)
|
|
28
|
+
return declaration;
|
|
29
|
+
return build()["~standard"].jsonSchema[io](options);
|
|
30
|
+
};
|
|
16
31
|
return {
|
|
17
|
-
parse: (value, options) =>
|
|
18
|
-
safeParse: (value, options) =>
|
|
32
|
+
parse: (value, options) => validate().parse(value, options),
|
|
33
|
+
safeParse: (value, options) => validate().safeParse(value, options),
|
|
19
34
|
"~standard": {
|
|
20
|
-
|
|
21
|
-
|
|
35
|
+
version: 1,
|
|
36
|
+
vendor: "zod",
|
|
37
|
+
validate: (value, options) => validate()["~standard"].validate(value, options),
|
|
38
|
+
jsonSchema: {
|
|
39
|
+
input: (options) => describe("input", options),
|
|
40
|
+
output: (options) => describe("output", options),
|
|
41
|
+
},
|
|
22
42
|
},
|
|
23
43
|
};
|
|
24
44
|
}
|
|
25
|
-
/**
|
|
45
|
+
/** Describe App methods with call-scoped JSON validators.
|
|
26
46
|
* Validation enforces both the declaration and JSON-only values; discovery
|
|
27
47
|
* describes the declaration without adding JSON guard nesting.
|
|
28
48
|
* Agar's emitted output already includes Pending for opted-in methods. */
|
|
@@ -34,12 +54,12 @@ export function appMethodSchemas(api) {
|
|
|
34
54
|
effect: declaration.effect,
|
|
35
55
|
requires: declaration.requires,
|
|
36
56
|
pending: declaration.pending,
|
|
37
|
-
input:
|
|
38
|
-
output:
|
|
57
|
+
input: appJsonSchema(declaration.input),
|
|
58
|
+
output: appJsonSchema(declaration.output),
|
|
39
59
|
},
|
|
40
60
|
]));
|
|
41
61
|
}
|
|
42
|
-
/**
|
|
62
|
+
/** Describe discoverable App methods only; direct RPC validation uses appMethodSchemas. */
|
|
43
63
|
export function appToolSchemas(api) {
|
|
44
64
|
return appMethodSchemas({
|
|
45
65
|
...api,
|
|
@@ -83,7 +103,7 @@ export function appToolFamilySchemas(api, reply) {
|
|
|
83
103
|
const call = api.methods[family.call];
|
|
84
104
|
if (list === undefined || call === undefined)
|
|
85
105
|
throw new Error("Tool family references undeclared methods");
|
|
86
|
-
const inventory = familyInventory.parse(
|
|
106
|
+
const inventory = familyInventory.parse(appJsonSchema(list.output).parse(reply));
|
|
87
107
|
return {
|
|
88
108
|
name: family.name,
|
|
89
109
|
call: family.call,
|
|
@@ -97,8 +117,8 @@ export function appToolFamilySchemas(api, reply) {
|
|
|
97
117
|
description: [row.title, row.description]
|
|
98
118
|
.filter((text) => text.length > 0)
|
|
99
119
|
.join("\n"),
|
|
100
|
-
input:
|
|
101
|
-
output:
|
|
120
|
+
input: appJsonSchema(row.input),
|
|
121
|
+
output: appJsonSchema(row.output),
|
|
102
122
|
})),
|
|
103
123
|
};
|
|
104
124
|
}
|
package/dist/catalog.js
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { ForageError } from "./errors.js";
|
|
2
|
-
import { catalogInputSchema, forageDescribeInput, forageSearchInput, maximumDescriptionTypes, } from "./schemas.js";
|
|
3
|
+
import { catalogInputSchema, forageDescribeInput, forageSearchInput, maximumDescriptionTypes, maximumSampleBytes, maximumSampleDepth, maximumSampleEntries, jsonDepth, } from "./schemas.js";
|
|
3
4
|
import { describeCatalog } from "./describe.js";
|
|
4
5
|
import { catalogStamp, prepareOperation, searchCatalog, } from "./search.js";
|
|
5
6
|
import { compareCodeUnits } from "./normalization.js";
|
|
6
7
|
import { listOperations } from "./list.js";
|
|
7
|
-
import {
|
|
8
|
+
import { rawSchema, schemaText } from "./rendering.js";
|
|
8
9
|
const maximumCatalogProjectionBytes = 1_000_000;
|
|
10
|
+
/**
|
|
11
|
+
* A nesting bound on the raw producer document, not a second JSON decoder.
|
|
12
|
+
* The ceiling bounds recursion; sibling order cannot change the maximum.
|
|
13
|
+
* Indexed traversal avoids cold iterator results and heap stack entries.
|
|
14
|
+
*/
|
|
15
|
+
function schemaDepth(children, depth = 0) {
|
|
16
|
+
if (depth >= maximumSampleDepth)
|
|
17
|
+
return children.length === 0 ? depth : depth + 1;
|
|
18
|
+
let maximum = depth;
|
|
19
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
20
|
+
const child = children[index];
|
|
21
|
+
let childDepth = depth + 1;
|
|
22
|
+
if (Array.isArray(child))
|
|
23
|
+
childDepth = schemaDepth(child, childDepth);
|
|
24
|
+
else if (z.util.isObject(child))
|
|
25
|
+
childDepth = schemaDepth(Object.values(child), childDepth);
|
|
26
|
+
maximum = Math.max(maximum, childDepth);
|
|
27
|
+
if (maximum > maximumSampleDepth)
|
|
28
|
+
return maximum;
|
|
29
|
+
}
|
|
30
|
+
return maximum;
|
|
31
|
+
}
|
|
9
32
|
function parseCatalog(input) {
|
|
10
33
|
const parsed = catalogInputSchema.safeParse(input);
|
|
11
34
|
if (!parsed.success) {
|
|
@@ -16,24 +39,42 @@ function parseCatalog(input) {
|
|
|
16
39
|
}
|
|
17
40
|
const operations = [...parsed.data.operations].sort((left, right) => compareCodeUnits(left.path, right.path));
|
|
18
41
|
const paths = new Set();
|
|
42
|
+
const preparedOperations = [];
|
|
19
43
|
let projectionBytes = 0;
|
|
20
44
|
for (const operation of operations) {
|
|
21
45
|
if (paths.has(operation.path)) {
|
|
22
46
|
throw new ForageError("duplicate_operation", `Duplicate operation path: ${operation.path}`);
|
|
23
47
|
}
|
|
24
48
|
paths.add(operation.path);
|
|
25
|
-
const inputProjection =
|
|
26
|
-
if (inputProjection
|
|
49
|
+
const inputProjection = schemaText(operation.input, "input").length;
|
|
50
|
+
if (inputProjection > maximumDescriptionTypes ||
|
|
51
|
+
schemaDepth(Object.values(rawSchema(operation.input, "input"))) >
|
|
52
|
+
maximumSampleDepth) {
|
|
27
53
|
throw new ForageError("invalid_input", `Schema projection is too large: ${operation.path}`);
|
|
28
54
|
}
|
|
29
55
|
const outputProjection = operation.output
|
|
30
|
-
?
|
|
56
|
+
? schemaText(operation.output, "output").length
|
|
31
57
|
: 0;
|
|
32
|
-
if (outputProjection
|
|
58
|
+
if (outputProjection > maximumDescriptionTypes ||
|
|
59
|
+
(operation.output !== undefined &&
|
|
60
|
+
schemaDepth(Object.values(rawSchema(operation.output, "output"))) >
|
|
61
|
+
maximumSampleDepth)) {
|
|
33
62
|
throw new ForageError("invalid_input", `Schema output projection is too large: ${operation.path}`);
|
|
34
63
|
}
|
|
64
|
+
const prepared = prepareOperation(operation);
|
|
65
|
+
const sample = prepared.sample;
|
|
66
|
+
const sampleBytes = sample === undefined ? 0 : JSON.stringify(sample).length;
|
|
67
|
+
if (sample !== undefined &&
|
|
68
|
+
(Object.keys(sample).length > maximumSampleEntries ||
|
|
69
|
+
sampleBytes > maximumSampleBytes ||
|
|
70
|
+
jsonDepth(sample, 0) > maximumSampleDepth))
|
|
71
|
+
throw new ForageError("invalid_input", `Schema projection is too large: ${operation.path}`);
|
|
72
|
+
preparedOperations.push(prepared);
|
|
35
73
|
projectionBytes +=
|
|
36
|
-
|
|
74
|
+
prepared.inputBytes +
|
|
75
|
+
prepared.outputBytes +
|
|
76
|
+
sampleBytes +
|
|
77
|
+
operation.path.length;
|
|
37
78
|
projectionBytes += operation.description?.length ?? 0;
|
|
38
79
|
projectionBytes += operation.capabilityNotes?.length ?? 0;
|
|
39
80
|
projectionBytes += operation.instructions?.length ?? 0;
|
|
@@ -41,16 +82,6 @@ function parseCatalog(input) {
|
|
|
41
82
|
throw new ForageError("invalid_input", "Catalog metadata exceeds the bounded projection");
|
|
42
83
|
}
|
|
43
84
|
}
|
|
44
|
-
const preparedOperations = operations.map((operation) => prepareOperation(operation));
|
|
45
|
-
// Broad namespace descriptions can be narrowed through the existing methods
|
|
46
|
-
// selector or discovered through list pages. Every operation must still fit
|
|
47
|
-
// a complete description on its own; every response keeps the same bound.
|
|
48
|
-
for (const operation of preparedOperations) {
|
|
49
|
-
if (renderOperationTypes([operation.rendered]).length >
|
|
50
|
-
maximumDescriptionTypes) {
|
|
51
|
-
throw new ForageError("invalid_input", `Operation description exceeds the bounded output: ${operation.path}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
85
|
return {
|
|
55
86
|
revision: parsed.data.revision,
|
|
56
87
|
operations: preparedOperations,
|
package/dist/describe.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describeOutputSchema, maximumDescriptionTypes, } from "./schemas.js";
|
|
2
|
-
import { compareCodeUnits } from "./normalization.js";
|
|
3
|
-
import { renderNamespaceIndex, renderOperationTypes } from "./rendering.js";
|
|
2
|
+
import { compareCodeUnits, jsonValueSchema } from "./normalization.js";
|
|
3
|
+
import { rawSchema, renderOperation, renderNamespaceIndex, renderOperationTypes, } from "./rendering.js";
|
|
4
4
|
import { ForageError } from "./errors.js";
|
|
5
5
|
function boundedDescription(value) {
|
|
6
6
|
return value && value.length > 0 ? value : undefined;
|
|
@@ -17,16 +17,19 @@ export function describeCatalog(operations, rawInput) {
|
|
|
17
17
|
const operation = operations.find((candidate) => candidate.path === rawInput.path);
|
|
18
18
|
if (operation) {
|
|
19
19
|
const callPath = `env.${operation.path}`;
|
|
20
|
+
const inputSchema = jsonValueSchema.safeParse(rawSchema(operation.input, "input"));
|
|
21
|
+
if (!inputSchema.success)
|
|
22
|
+
throw new ForageError("invalid_input", "Schema cannot be projected to JSON Schema");
|
|
20
23
|
const output = {
|
|
21
24
|
path: operation.path,
|
|
22
25
|
description: boundedDescription(operation.description),
|
|
23
|
-
types: renderOperationTypes([operation
|
|
26
|
+
types: renderOperationTypes([renderOperation(operation)]),
|
|
24
27
|
kind: "operation",
|
|
25
|
-
inputSchema:
|
|
28
|
+
inputSchema: inputSchema.data,
|
|
26
29
|
usage: `await ${callPath}({ ... })`,
|
|
27
30
|
};
|
|
28
31
|
if (output.types.length > maximumDescriptionTypes) {
|
|
29
|
-
throw new ForageError("invalid_input", "
|
|
32
|
+
throw new ForageError("invalid_input", "Operation description exceeds the bounded output");
|
|
30
33
|
}
|
|
31
34
|
return checkedDescription(output);
|
|
32
35
|
}
|
|
@@ -46,13 +49,13 @@ export function describeCatalog(operations, rawInput) {
|
|
|
46
49
|
: namespaceOperations;
|
|
47
50
|
let types;
|
|
48
51
|
if (rawInput.detail === "index") {
|
|
49
|
-
types = renderNamespaceIndex(namespaceOperations.map(
|
|
52
|
+
types = renderNamespaceIndex(namespaceOperations.map(renderOperation));
|
|
50
53
|
}
|
|
51
54
|
else if (rawInput.methods?.length && selected.length === 0) {
|
|
52
|
-
types = renderNamespaceIndex(namespaceOperations.map(
|
|
55
|
+
types = renderNamespaceIndex(namespaceOperations.map(renderOperation));
|
|
53
56
|
}
|
|
54
57
|
else {
|
|
55
|
-
types = renderOperationTypes((selected.length > 0 ? selected : namespaceOperations).map(
|
|
58
|
+
types = renderOperationTypes((selected.length > 0 ? selected : namespaceOperations).map(renderOperation));
|
|
56
59
|
}
|
|
57
60
|
if (types.length > maximumDescriptionTypes) {
|
|
58
61
|
throw new ForageError("invalid_input", "Description types exceed the bounded output");
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { CatalogInput, DescribeOutput, ForageDescribeInput, ForageFailure,
|
|
|
3
3
|
export type { ForageDefinition } from "./definitions.js";
|
|
4
4
|
export { ForageError } from "./errors.js";
|
|
5
5
|
export { createCatalog } from "./catalog.js";
|
|
6
|
-
export { appApiOperations, appMethodSchemas, appToolSchemas, appToolFamilySchemas, jsonSchemaToZod, } from "./app-api.js";
|
|
6
|
+
export { appApiOperations, appJsonSchema, appMethodSchemas, appToolSchemas, appToolFamilySchemas, jsonSchemaToZod, } from "./app-api.js";
|
|
7
7
|
export { listOperations } from "./list.js";
|
|
8
8
|
export type { ForageCatalog } from "./catalog.js";
|
|
9
9
|
export { forageDescribeDefinition, forageSearchDefinition, forageListDefinition, } from "./definitions.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { catalogInputSchema, describeOutputSchema, forageDescribeInput, forageFailureSchema, forageSearchInput, operationSchema, searchOutputSchema, searchResultSchema, forageListInput, listOutputSchema, } from "./schemas.js";
|
|
2
2
|
export { ForageError } from "./errors.js";
|
|
3
3
|
export { createCatalog } from "./catalog.js";
|
|
4
|
-
export { appApiOperations, appMethodSchemas, appToolSchemas, appToolFamilySchemas, jsonSchemaToZod, } from "./app-api.js";
|
|
4
|
+
export { appApiOperations, appJsonSchema, appMethodSchemas, appToolSchemas, appToolFamilySchemas, jsonSchemaToZod, } from "./app-api.js";
|
|
5
5
|
export { listOperations } from "./list.js";
|
|
6
6
|
export { forageDescribeDefinition, forageSearchDefinition, forageListDefinition, } from "./definitions.js";
|
|
7
7
|
import { createCatalog } from "./catalog.js";
|
package/dist/normalization.d.ts
CHANGED
|
@@ -16,7 +16,6 @@ export declare function normalizeQuery(query: string): string;
|
|
|
16
16
|
export declare function tokenize(text: string): string[];
|
|
17
17
|
export declare function isStopword(token: string): boolean;
|
|
18
18
|
export declare function stemVariants(token: string): string[];
|
|
19
|
-
export declare function canonicalStringify(value: JsonValue): string;
|
|
20
19
|
export declare function hashString64(value: string): string;
|
|
21
20
|
export declare function operationMethod(path: Operation["path"]): string;
|
|
22
21
|
export declare function operationNamespace(path: Operation["path"]): string;
|
package/dist/normalization.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const jsonValueSchema = z.json();
|
|
3
|
-
const jsonPrimitiveSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean()]));
|
|
4
3
|
/**
|
|
5
4
|
* Narrow an already-parsed JSON value to its object case. The value was
|
|
6
5
|
* validated once at its boundary, so each nested level is told apart by its
|
|
@@ -8,9 +7,7 @@ const jsonPrimitiveSchema = z.lazy(() => z.union([z.string(), z.number(), z.bool
|
|
|
8
7
|
* made walking a large schema quadratic.
|
|
9
8
|
*/
|
|
10
9
|
export function isJsonObject(value) {
|
|
11
|
-
return
|
|
12
|
-
!Array.isArray(value) &&
|
|
13
|
-
!jsonPrimitiveSchema.safeParse(value).success);
|
|
10
|
+
return value !== null && !Array.isArray(value) && typeof value === "object";
|
|
14
11
|
}
|
|
15
12
|
export function compareCodeUnits(left, right) {
|
|
16
13
|
if (left === right)
|
|
@@ -63,6 +60,9 @@ export function stemVariants(token) {
|
|
|
63
60
|
add(token.slice(0, -3));
|
|
64
61
|
if (token.endsWith("ed") && token.length > 3)
|
|
65
62
|
add(token.slice(0, -2));
|
|
63
|
+
if (token.endsWith("ation") && token.length > 6) {
|
|
64
|
+
add(`${token.slice(0, -3)}e`);
|
|
65
|
+
}
|
|
66
66
|
if (token.endsWith("ies") && token.length > 4) {
|
|
67
67
|
add(token.slice(0, -3));
|
|
68
68
|
add(`${token.slice(0, -3)}y`);
|
|
@@ -74,32 +74,29 @@ export function stemVariants(token) {
|
|
|
74
74
|
}
|
|
75
75
|
return variants;
|
|
76
76
|
}
|
|
77
|
-
export function canonicalStringify(value) {
|
|
78
|
-
if (value === null)
|
|
79
|
-
return "null";
|
|
80
|
-
const primitive = jsonPrimitiveSchema.safeParse(value);
|
|
81
|
-
if (primitive.success) {
|
|
82
|
-
return JSON.stringify(primitive.data);
|
|
83
|
-
}
|
|
84
|
-
if (Array.isArray(value)) {
|
|
85
|
-
return `[${value.map((item) => canonicalStringify(item)).join(",")}]`;
|
|
86
|
-
}
|
|
87
|
-
if (!isJsonObject(value))
|
|
88
|
-
return "null";
|
|
89
|
-
const entries = Object.entries(value).sort(([left], [right]) => compareCodeUnits(left, right));
|
|
90
|
-
return `{${entries
|
|
91
|
-
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalStringify(entry)}`)
|
|
92
|
-
.join(",")}}`;
|
|
93
|
-
}
|
|
94
77
|
export function hashString64(value) {
|
|
95
|
-
let
|
|
96
|
-
|
|
97
|
-
|
|
78
|
+
let word0 = 0x2325;
|
|
79
|
+
let word1 = 0x8422;
|
|
80
|
+
let word2 = 0x9ce4;
|
|
81
|
+
let word3 = 0xcbf2;
|
|
98
82
|
for (let index = 0; index < value.length; index += 1) {
|
|
99
|
-
|
|
100
|
-
|
|
83
|
+
const low = word0 ^ value.charCodeAt(index);
|
|
84
|
+
// The prime is 2^40 + 435. Four 16-bit limbs preserve modulo 2^64;
|
|
85
|
+
// every intermediate stays below 2^26, including before V8 optimizes
|
|
86
|
+
// this loop. Unsigned 32-bit limbs allocated boxed numbers when cold.
|
|
87
|
+
const product0 = low * 435;
|
|
88
|
+
const product1 = word1 * 435 + (product0 >>> 16);
|
|
89
|
+
const product2 = word2 * 435 + (product1 >>> 16) + (low << 8);
|
|
90
|
+
const product3 = word3 * 435 + (product2 >>> 16) + (word1 << 8);
|
|
91
|
+
word0 = product0 & 0xffff;
|
|
92
|
+
word1 = product1 & 0xffff;
|
|
93
|
+
word2 = product2 & 0xffff;
|
|
94
|
+
word3 = product3 & 0xffff;
|
|
101
95
|
}
|
|
102
|
-
return
|
|
96
|
+
return (word3.toString(16).padStart(4, "0") +
|
|
97
|
+
word2.toString(16).padStart(4, "0") +
|
|
98
|
+
word1.toString(16).padStart(4, "0") +
|
|
99
|
+
word0.toString(16).padStart(4, "0"));
|
|
103
100
|
}
|
|
104
101
|
export function operationMethod(path) {
|
|
105
102
|
return path.slice(path.indexOf(".") + 1);
|
package/dist/rendering.d.ts
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
import type { JsonValue } from "./normalization.js";
|
|
2
|
-
import {
|
|
3
|
-
/**
|
|
2
|
+
import type { JsonSchemaSource, Operation } from "./schemas.js";
|
|
3
|
+
/** Requested TypeScript projections for one catalog operation. */
|
|
4
4
|
export type RenderedOperation = Readonly<{
|
|
5
5
|
types: string;
|
|
6
6
|
environment: string;
|
|
7
7
|
indexEnvironment: string;
|
|
8
8
|
}>;
|
|
9
|
+
/** The producer's document, without a recursive projection decode or copy. */
|
|
10
|
+
export declare function rawSchema(schema: JsonSchemaSource, io: "input" | "output"): object;
|
|
11
|
+
/** Native serialization owns the document's text representation and failures. */
|
|
12
|
+
export declare function schemaText(schema: JsonSchemaSource, io: "input" | "output"): string;
|
|
9
13
|
/** Render a namespace index as a callable nested environment. */
|
|
10
14
|
export declare function renderNamespaceIndex(operations: readonly RenderedOperation[]): string;
|
|
11
15
|
/** Render whole-operation schemas and their callable nested environment. */
|
|
12
16
|
export declare function renderOperationTypes(operations: readonly RenderedOperation[]): string;
|
|
13
|
-
/**
|
|
14
|
-
export declare function renderOperation(operation:
|
|
15
|
-
path: string;
|
|
16
|
-
input: JsonSchemaSource;
|
|
17
|
-
output?: JsonSchemaSource | undefined;
|
|
18
|
-
}): RenderedOperation;
|
|
17
|
+
/** Render only requested operations, once per immutable prepared operation. */
|
|
18
|
+
export declare function renderOperation(operation: Operation): RenderedOperation;
|
|
19
19
|
/** Derive sample arguments solely from input-schema examples and defaults. */
|
|
20
20
|
export declare function deriveSample(schema: JsonSchemaSource): Record<string, JsonValue> | undefined;
|
|
21
|
-
/** Preserve validator constraints in the operation's Standard JSON Schema. */
|
|
22
|
-
export declare function inputSchemaProjection(schema: JsonSchemaSource): JsonValue;
|
|
23
|
-
/** Produce a canonical schema representation used in peer identity and stamps. */
|
|
24
|
-
export declare function schemaFingerprint(schema: JsonSchemaSource, io?: "input" | "output"): string;
|
|
25
|
-
/** Validate the bounded JSON projection before a catalog is installed. */
|
|
26
|
-
export declare function schemaProjectionSize(schema: JsonSchemaSource, io: "input" | "output"): number | undefined;
|
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
|
}
|
package/docs/api/index.md
CHANGED
|
@@ -622,16 +622,47 @@ The shared JSON-Schema compiler for App methods and OpenAPI operations.
|
|
|
622
622
|
|
|
623
623
|
---
|
|
624
624
|
|
|
625
|
+
### appJsonSchema()
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
function appJsonSchema(
|
|
629
|
+
declaration,
|
|
630
|
+
): Pick<
|
|
631
|
+
z.ZodType<z.output<z.ZodJSONSchema>>,
|
|
632
|
+
"parse" | "safeParse" | "~standard"
|
|
633
|
+
>;
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
One decoded, immutable App declaration as a JSON-only Standard Schema. Release
|
|
637
|
+
and family-listing decoders own its JSON boundary. A catalog describes every
|
|
638
|
+
installed method on each capture but a turn validates only the few it calls.
|
|
639
|
+
Compiling every method eagerly exhausted an Agent Durable Object's isolate. A
|
|
640
|
+
draft 2020-12 description is the declaration itself; validators compile for the
|
|
641
|
+
call in hand and are never kept on the surface.
|
|
642
|
+
|
|
643
|
+
#### Parameters
|
|
644
|
+
|
|
645
|
+
| Parameter | Type |
|
|
646
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
647
|
+
| `declaration` | \| `boolean` \| \{ \[`key`: `string`\]: \| `null` \| `string` \| `number` \| `false` \| `true` \| \{ \[`index`: `number`\]: AppApiJsonValue; `length`: `number`; \} \| AppApiJsonObject; \} |
|
|
648
|
+
|
|
649
|
+
#### Returns
|
|
650
|
+
|
|
651
|
+
`Pick`\<`z.ZodType`\<`z.output`\<`z.ZodJSONSchema`\>\>, `"parse"` \|
|
|
652
|
+
`"safeParse"` \| `"~standard"`\>
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
625
656
|
### appMethodSchemas()
|
|
626
657
|
|
|
627
658
|
```ts
|
|
628
659
|
function appMethodSchemas(api): object;
|
|
629
660
|
```
|
|
630
661
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
662
|
+
Describe App methods with call-scoped JSON validators. Validation enforces both
|
|
663
|
+
the declaration and JSON-only values; discovery describes the declaration
|
|
664
|
+
without adding JSON guard nesting. Agar's emitted output already includes
|
|
665
|
+
Pending for opted-in methods.
|
|
635
666
|
|
|
636
667
|
#### Parameters
|
|
637
668
|
|
|
@@ -651,7 +682,7 @@ includes Pending for opted-in methods.
|
|
|
651
682
|
function appToolSchemas(api): object;
|
|
652
683
|
```
|
|
653
684
|
|
|
654
|
-
|
|
685
|
+
Describe discoverable App methods only; direct RPC validation uses
|
|
655
686
|
appMethodSchemas.
|
|
656
687
|
|
|
657
688
|
#### Parameters
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fungi.computer/forage",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"author": "Levi <levi@technomancy.dev>",
|
|
5
5
|
"description": "Pure immutable Zod-typed capability discovery",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@apidevtools/json-schema-ref-parser": "16.0.3",
|
|
32
|
-
"@fungi.computer/puffball": "0.
|
|
32
|
+
"@fungi.computer/puffball": "0.3.0",
|
|
33
33
|
"yaml": "2.9.0",
|
|
34
34
|
"zod": "4.4.3"
|
|
35
35
|
},
|