@fungi.computer/forage 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
  }