@fungi.computer/forage 0.2.0 → 0.2.2

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 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
- /** Compile App validators and their Standard JSON Schema descriptions.
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
- /** Compile discoverable App methods only; direct RPC validation uses appMethodSchemas. */
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 jsonSchema = z.lazy(() => z.union([z.record(z.string(), z.json()), z.boolean()]));
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
- /** Validate JSON with Zod; describe the declaration without the redundant JSON guard. */
13
- function appMethodSchema(source) {
14
- const declared = jsonSchemaToZod(source);
15
- const validator = z.intersection(declared, z.json());
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) => validator.parse(value, options),
18
- safeParse: (value, options) => validator.safeParse(value, options),
32
+ parse: (value, options) => validate().parse(value, options),
33
+ safeParse: (value, options) => validate().safeParse(value, options),
19
34
  "~standard": {
20
- ...validator["~standard"],
21
- jsonSchema: declared["~standard"].jsonSchema,
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
- /** Compile App validators and their Standard JSON Schema descriptions.
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: appMethodSchema(declaration.input),
38
- output: appMethodSchema(declaration.output),
57
+ input: appJsonSchema(declaration.input),
58
+ output: appJsonSchema(declaration.output),
39
59
  },
40
60
  ]));
41
61
  }
42
- /** Compile discoverable App methods only; direct RPC validation uses appMethodSchemas. */
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(appMethodSchema(list.output).parse(reply));
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: appMethodSchema(row.input),
101
- output: appMethodSchema(row.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 { renderOperationTypes, schemaProjectionSize } from "./rendering.js";
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 = schemaProjectionSize(operation.input, "input");
26
- if (inputProjection === undefined) {
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
- ? schemaProjectionSize(operation.output, "output")
56
+ ? schemaText(operation.output, "output").length
31
57
  : 0;
32
- if (outputProjection === undefined) {
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
- inputProjection + outputProjection + operation.path.length;
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.rendered]),
26
+ types: renderOperationTypes([renderOperation(operation)]),
24
27
  kind: "operation",
25
- inputSchema: operation.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", "Description types exceed the bounded output");
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((operation) => operation.rendered));
52
+ types = renderNamespaceIndex(namespaceOperations.map(renderOperation));
50
53
  }
51
54
  else if (rawInput.methods?.length && selected.length === 0) {
52
- types = renderNamespaceIndex(namespaceOperations.map((operation) => operation.rendered));
55
+ types = renderNamespaceIndex(namespaceOperations.map(renderOperation));
53
56
  }
54
57
  else {
55
- types = renderOperationTypes((selected.length > 0 ? selected : namespaceOperations).map((operation) => operation.rendered));
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";
@@ -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;
@@ -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 (value !== null &&
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 hash = 0xcbf29ce484222325n;
96
- const prime = 0x100000001b3n;
97
- const mask = (1n << 64n) - 1n;
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
- hash ^= BigInt(value.charCodeAt(index));
100
- hash = (hash * prime) & mask;
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 hash.toString(16).padStart(16, "0");
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);
@@ -1,26 +1,20 @@
1
1
  import type { JsonValue } from "./normalization.js";
2
- import { type JsonSchemaSource } from "./schemas.js";
3
- /** Installation-time TypeScript projections for one catalog operation. */
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
- /** Prepare all TypeScript descriptions for one operation at installation. */
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 { canonicalStringify, compareCodeUnits, isJsonObject, jsonValueSchema, } from "./normalization.js";
4
- import { maximumSampleBytes, maximumSampleDepth, maximumSampleEntries, } from "./schemas.js";
5
- const schemaNode = z.lazy(() => z.union([
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(), schemaNode).optional(),
10
- definitions: z.record(z.string(), schemaNode).optional(),
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(schemaNode).optional(),
14
- anyOf: z.array(schemaNode).optional(),
15
- allOf: z.array(schemaNode).optional(),
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(), schemaNode).optional(),
18
- additionalProperties: schemaNode.optional(),
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(schemaNode).optional(),
21
- items: z.union([schemaNode, z.array(schemaNode)]).optional(),
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
- * Facts already derived, per schema object and direction. A Standard JSON
28
- * Schema describes an immutable schema, so its projection is a pure function
29
- * of the object: compiling a catalog asks for the same projection several
30
- * times per operation (size, fingerprint, sample, rendering), and a host that
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 node;
33
+ let raw;
42
34
  try {
43
- node = schemaNode.parse(schema["~standard"].jsonSchema[io]({ target: "draft-2020-12" }));
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
- const json = jsonValueSchema.safeParse(node);
49
- const facts = {
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
- schemaFacts.set(schema, entry);
44
+ projections.set(schema, entry);
56
45
  }
57
- entry[io] = facts;
58
- return facts;
46
+ entry[io] = projection;
47
+ return projection;
59
48
  }
60
- function schemaFor(schema, io) {
61
- return factsFor(schema, io).node;
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
- function isNodeList(value) {
64
- return Array.isArray(value);
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, refs) {
110
- if (node === undefined || node === true || node === false || depth > 12) {
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 member = renderNode(value, depth + 1, refs);
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(value, refs) ? `(${member})` : member;
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 && !isNodeList(items)) {
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 && isNodeList(items))
172
- return `[${items.map((value) => renderNode(value, depth + 1, refs)).join(", ")}][]`;
173
- const item = renderNode(items, depth + 1, refs);
174
- const compoundItem = items !== undefined &&
175
- items !== true &&
176
- items !== false &&
177
- (items.oneOf !== undefined ||
178
- items.anyOf !== undefined ||
179
- items.allOf !== undefined);
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
- const root = schemaFor(schema, io);
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
- /** Prepare all TypeScript descriptions for one operation at installation. */
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
- return Object.freeze({
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 = schemaFor(schema, "input");
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
- if (value === true || value === false)
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 === null)
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(record.data)) {
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 rendered: RenderedOperation;
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 { canonicalStringify, compareCodeUnits, hashString64, isStopword, normalizeQuery, operationMethod, operationNamespace, stemVariants, tokenize, } from "./normalization.js";
3
- import { deriveSample, inputSchemaProjection, renderOperation, schemaFingerprint, } from "./rendering.js";
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: schemaFingerprint(operation.input),
10
- inputSchema: inputSchemaProjection(operation.input),
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
- function fieldWeight(field) {
25
- switch (field) {
26
- case "path":
27
- return 12;
28
- case "namespace":
29
- return 8;
30
- case "method":
31
- return 10;
32
- case "description":
33
- return 5;
34
- default:
35
- return 5;
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) => canonicalStringify([
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
- let matchedTokens = [];
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(field);
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, normalizedQuery, queryTokens, coverageTokens, threshold) {
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
- const coverage = matched / coverageTokens.length;
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 ? 25 : coverage * 10;
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 += 8;
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
- score: baseScore + bonus,
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, normalizedQuery, queryTokens, coverageTokens, threshold);
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
- Compile App validators and their Standard JSON Schema descriptions. Validation
632
- enforces both the declaration and JSON-only values; discovery describes the
633
- declaration without adding JSON guard nesting. Agar's emitted output already
634
- includes Pending for opted-in methods.
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
- Compile discoverable App methods only; direct RPC validation uses
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.0",
3
+ "version": "0.2.2",
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.2.0",
32
+ "@fungi.computer/puffball": "0.4.0",
33
33
  "yaml": "2.9.0",
34
34
  "zod": "4.4.3"
35
35
  },