kizlo 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/category/index.js +3 -3
- package/dist/category/schema.d.ts +1 -1
- package/dist/cli/mcp/schema.js +140 -32
- package/dist/cli/mcp/search.js +255 -0
- package/dist/cli/mcp/server.js +111 -20
- package/dist/cli/mcp/tools.js +65 -52
- package/dist/comment/index.js +4 -4
- package/dist/email/service.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/menu/utils.js +2 -2
- package/dist/package.js +1 -1
- package/dist/page/index.js +4 -4
- package/dist/page/schema.d.ts +1 -1
- package/dist/post/index.js +4 -4
- package/dist/post/schema.d.ts +5 -5
- package/dist/procedures.d.ts +10 -10
- package/dist/seo/index.js +13 -11
- package/dist/seo/schema.d.ts +5 -5
- package/dist/seo/types.d.ts +4 -4
- package/dist/settings/service.d.ts +11 -11
- package/dist/settings/service.js +15 -15
- package/dist/tag/index.js +3 -3
- package/dist/tag/schema.d.ts +1 -1
- package/dist/wordpress/endpoint.d.ts +13 -3
- package/dist/wordpress/endpoint.js +24 -12
- package/dist/wordpress/generate.js +51 -8
- package/dist/wordpress/index.d.ts +1 -1
- package/dist/wordpress/introspection.js +26 -11
- package/dist/wordpress/types.d.ts +16 -2
- package/package.json +3 -2
package/dist/category/index.js
CHANGED
|
@@ -18,7 +18,7 @@ const CATEGORY_PROCEDURES = {
|
|
|
18
18
|
}, async ({ input, context, errors }) => {
|
|
19
19
|
const identifier = parseIdentifier(input.params.identifier);
|
|
20
20
|
if (!identifier) throw errors.CATEGORY_NOT_FOUND();
|
|
21
|
-
const response = await context.wordpress.taxonomies.category.retrieve({ identifier: String(identifier.value) });
|
|
21
|
+
const response = await context.wordpress.kizlo.taxonomies.category.retrieve({ params: { identifier: String(identifier.value) } });
|
|
22
22
|
if (response.error) switch (response.error.code) {
|
|
23
23
|
case "invalid_taxonomy":
|
|
24
24
|
case "term_not_found":
|
|
@@ -41,7 +41,7 @@ const CATEGORY_PROCEDURES = {
|
|
|
41
41
|
output: schemaType(CategoryList),
|
|
42
42
|
errors: LIST_CATEGORY_ERROR_MAP
|
|
43
43
|
}, async ({ input, context, errors }) => {
|
|
44
|
-
const response = await context.wordpress.taxonomies.category.list({
|
|
44
|
+
const response = await context.wordpress.kizlo.taxonomies.category.list({ query: {
|
|
45
45
|
page: input.query?.page,
|
|
46
46
|
per_page: input.query?.perPage,
|
|
47
47
|
search: input.query?.search,
|
|
@@ -53,7 +53,7 @@ const CATEGORY_PROCEDURES = {
|
|
|
53
53
|
parent: input.query?.parent,
|
|
54
54
|
post: input.query?.post,
|
|
55
55
|
slug: normalizeArrayableValue(input.query?.slug)
|
|
56
|
-
});
|
|
56
|
+
} });
|
|
57
57
|
if (response.error) switch (response.error.code) {
|
|
58
58
|
case "rest_post_invalid_page_number": throw errors.CATEGORY_INVALID_PAGE();
|
|
59
59
|
case "rest_no_route": throw errors.CATEGORY_NOT_FOUND();
|
|
@@ -4,7 +4,7 @@ import * as _kizlo_shared0 from "@kizlo/shared";
|
|
|
4
4
|
import z from "zod/v4";
|
|
5
5
|
|
|
6
6
|
//#region src/category/schema.d.ts
|
|
7
|
-
type CategoryCustomFields = WP_CustomFields<"taxonomies.category">;
|
|
7
|
+
type CategoryCustomFields = WP_CustomFields<"kizlo.taxonomies.category">;
|
|
8
8
|
declare const CategoryCustomFieldsSchema: z.ZodType<CategoryCustomFields, CategoryCustomFields>;
|
|
9
9
|
declare const Category: z.ZodObject<{
|
|
10
10
|
id: z.ZodNumber;
|
package/dist/cli/mcp/schema.js
CHANGED
|
@@ -1,32 +1,120 @@
|
|
|
1
1
|
//#region src/cli/mcp/schema.ts
|
|
2
|
+
/** Where a converted definition is emitted, and the prefix a pointer to one carries. */
|
|
3
|
+
const DEFS = "$defs";
|
|
4
|
+
/**
|
|
5
|
+
* Keywords that mean the same in both dialects and are copied straight across. The validation half
|
|
6
|
+
* is what `SchemaCoercer` accepts on the WordPress side, so a bound the contract publishes reaches
|
|
7
|
+
* the client that has to satisfy it instead of being dropped in translation.
|
|
8
|
+
*/
|
|
9
|
+
const COPIED = [
|
|
10
|
+
"title",
|
|
11
|
+
"description",
|
|
12
|
+
"deprecated",
|
|
13
|
+
"default",
|
|
14
|
+
"enum",
|
|
15
|
+
"format",
|
|
16
|
+
"pattern",
|
|
17
|
+
"minLength",
|
|
18
|
+
"maxLength",
|
|
19
|
+
"minItems",
|
|
20
|
+
"maxItems",
|
|
21
|
+
"minProperties",
|
|
22
|
+
"maxProperties",
|
|
23
|
+
"minimum",
|
|
24
|
+
"maximum",
|
|
25
|
+
"multipleOf",
|
|
26
|
+
"uniqueItems",
|
|
27
|
+
"exclusiveMinimum",
|
|
28
|
+
"exclusiveMaximum"
|
|
29
|
+
];
|
|
30
|
+
function run(document) {
|
|
31
|
+
return {
|
|
32
|
+
document,
|
|
33
|
+
defs: /* @__PURE__ */ new Map(),
|
|
34
|
+
extending: /* @__PURE__ */ new Set()
|
|
35
|
+
};
|
|
36
|
+
}
|
|
2
37
|
/**
|
|
3
38
|
* Turn an introspection schema into the JSON Schema an MCP client validates a tool call against.
|
|
4
39
|
*
|
|
5
40
|
* Three things separate the two dialects. Introspection marks a field required on the field itself,
|
|
6
41
|
* where JSON Schema lists the names on the object holding them. `$ref` and `$extends` name entries in
|
|
7
|
-
* `document.schemas` rather than being JSON pointers,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
42
|
+
* `document.schemas` rather than being JSON pointers, so both are rewritten here: a `$ref` becomes a
|
|
43
|
+
* pointer into `$defs`, and an `$extends` is flattened. And `file` is not a JSON Schema type; it
|
|
44
|
+
* becomes the `string`/`binary` pair OpenAPI uses for the same thing.
|
|
10
45
|
*/
|
|
11
46
|
function toJsonSchema(schema, document) {
|
|
12
|
-
|
|
47
|
+
const current = run(document);
|
|
48
|
+
return withDefs(convert(schema, current), current);
|
|
13
49
|
}
|
|
14
50
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
51
|
+
* The same, for an operation's request rather than one schema: each part it declares becomes a
|
|
52
|
+
* property, so a caller validates and builds `{ params, query, body }` — the shape the request
|
|
53
|
+
* builder takes. A part is required when the schema under it demands anything.
|
|
18
54
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
55
|
+
* The three parts share one run, so a definition two of them reach is emitted once at this root and
|
|
56
|
+
* referenced from both rather than copied into each.
|
|
21
57
|
*/
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
58
|
+
function inputJsonSchema(input, document) {
|
|
59
|
+
const current = run(document);
|
|
60
|
+
const properties = {};
|
|
61
|
+
const required = [];
|
|
62
|
+
for (const part of [
|
|
63
|
+
"params",
|
|
64
|
+
"query",
|
|
65
|
+
"body"
|
|
66
|
+
]) {
|
|
67
|
+
const schema = input[part];
|
|
68
|
+
if (!schema) continue;
|
|
69
|
+
properties[part] = convert(schema, current);
|
|
70
|
+
if (schema.type === "object" ? demandsProperties(schema, document, /* @__PURE__ */ new Set()) : schema.required !== false) required.push(part);
|
|
71
|
+
}
|
|
72
|
+
return withDefs({
|
|
73
|
+
type: "object",
|
|
74
|
+
properties,
|
|
75
|
+
...required.length > 0 ? { required } : {}
|
|
76
|
+
}, current);
|
|
77
|
+
}
|
|
78
|
+
/** Attach what the result reached, so the schema a client receives can be read without the document. */
|
|
79
|
+
function withDefs(schema, current) {
|
|
80
|
+
if (current.defs.size === 0) return schema;
|
|
81
|
+
return {
|
|
82
|
+
...schema,
|
|
83
|
+
[DEFS]: Object.fromEntries(current.defs)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Whether an object schema demands anything, through the references that carry its properties. */
|
|
87
|
+
function demandsProperties(schema, document, seen) {
|
|
88
|
+
if (Object.values(schema.properties ?? {}).some((child) => child.required)) return true;
|
|
89
|
+
for (const id of [...schema.$ref ? [schema.$ref] : [], ...names(schema.$extends)]) {
|
|
90
|
+
const target = document.schemas[id];
|
|
91
|
+
if (!target || seen.has(id)) continue;
|
|
92
|
+
if (demandsProperties(target, document, new Set([...seen, id]))) return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A schema is its own keywords laid over whatever it references, and the two kinds of reference are
|
|
98
|
+
* rewritten differently. `$extends` is an overlay whose purpose is redeclaring an inherited property,
|
|
99
|
+
* which a pointer cannot express, so its base is flattened and the schema's own keywords win.
|
|
100
|
+
* `$ref` names the whole shape, so it stays a reference: one definition, pointed at from everywhere
|
|
101
|
+
* that reaches it, which is what lets a schema describe its own recursion instead of being truncated.
|
|
102
|
+
*/
|
|
103
|
+
function convert(schema, current) {
|
|
104
|
+
let result = own(schema, current);
|
|
105
|
+
for (const id of names(schema.$extends)) result = merge(inline(id, current), result);
|
|
106
|
+
if (schema.$ref !== void 0) {
|
|
107
|
+
const pointer = define(schema.$ref, current);
|
|
108
|
+
if (pointer !== void 0) result = {
|
|
109
|
+
...result,
|
|
110
|
+
$ref: pointer
|
|
111
|
+
};
|
|
112
|
+
}
|
|
25
113
|
return schema.nullable ? widenToNull(result) : result;
|
|
26
114
|
}
|
|
27
115
|
/**
|
|
28
116
|
* Let a converted schema accept `null`. A type takes it in place; a schema whose shape came from a
|
|
29
|
-
*
|
|
117
|
+
* reference or a combinator has no type of its own to widen, so the union carries it instead.
|
|
30
118
|
*/
|
|
31
119
|
function widenToNull(schema) {
|
|
32
120
|
const type = schema.type;
|
|
@@ -40,11 +128,33 @@ function widenToNull(schema) {
|
|
|
40
128
|
};
|
|
41
129
|
return { anyOf: [schema, { type: "null" }] };
|
|
42
130
|
}
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Ensure one `document.schemas` entry is defined, and answer with the pointer that addresses it.
|
|
133
|
+
* Absent for a reference the document does not carry, which the caller leaves inlined.
|
|
134
|
+
*/
|
|
135
|
+
function define(id, current) {
|
|
136
|
+
const target = current.document.schemas[id];
|
|
137
|
+
if (!target) return void 0;
|
|
138
|
+
if (!current.defs.has(id)) {
|
|
139
|
+
current.defs.set(id, {});
|
|
140
|
+
current.defs.set(id, convert(target, current));
|
|
141
|
+
}
|
|
142
|
+
return `#/${DEFS}/${escapePointer(id)}`;
|
|
143
|
+
}
|
|
144
|
+
/** Resolve an `$extends` base for flattening, or the empty schema when it is absent or already open. */
|
|
145
|
+
function inline(id, current) {
|
|
146
|
+
const target = current.document.schemas[id];
|
|
147
|
+
if (!target || current.extending.has(id)) return {};
|
|
148
|
+
current.extending.add(id);
|
|
149
|
+
try {
|
|
150
|
+
return convert(target, current);
|
|
151
|
+
} finally {
|
|
152
|
+
current.extending.delete(id);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** RFC 6901 escaping, so an id carrying `~` or `/` still addresses the entry it named. */
|
|
156
|
+
function escapePointer(id) {
|
|
157
|
+
return id.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
48
158
|
}
|
|
49
159
|
function names(value) {
|
|
50
160
|
if (value === void 0) return [];
|
|
@@ -69,36 +179,34 @@ function merge(base, own$1) {
|
|
|
69
179
|
return result;
|
|
70
180
|
}
|
|
71
181
|
/** The schema's own keywords, with the references it is built from left to {@link convert}. */
|
|
72
|
-
function own(schema,
|
|
182
|
+
function own(schema, current) {
|
|
73
183
|
const result = {};
|
|
184
|
+
for (const keyword of COPIED) {
|
|
185
|
+
const value = schema[keyword];
|
|
186
|
+
if (value !== void 0) result[keyword] = value;
|
|
187
|
+
}
|
|
74
188
|
if (schema.type === "file") {
|
|
75
189
|
result.type = "string";
|
|
76
190
|
result.format = "binary";
|
|
77
191
|
} else if (schema.type) result.type = schema.type;
|
|
78
|
-
if (schema.title !== void 0) result.title = schema.title;
|
|
79
|
-
if (schema.description !== void 0) result.description = schema.description;
|
|
80
|
-
if (schema.deprecated !== void 0) result.deprecated = schema.deprecated;
|
|
81
|
-
if (schema.default !== void 0) result.default = schema.default;
|
|
82
|
-
if (schema.enum !== void 0) result.enum = schema.enum;
|
|
83
|
-
if (schema.format !== void 0) result.format = schema.format;
|
|
84
192
|
if (schema.properties) {
|
|
85
193
|
const properties = {};
|
|
86
194
|
const required = [];
|
|
87
195
|
for (const [name, child] of Object.entries(schema.properties)) {
|
|
88
|
-
properties[name] = convert(child,
|
|
196
|
+
properties[name] = convert(child, current);
|
|
89
197
|
if (child.required) required.push(name);
|
|
90
198
|
}
|
|
91
199
|
result.properties = properties;
|
|
92
200
|
if (required.length > 0) result.required = required;
|
|
93
201
|
}
|
|
94
|
-
if (schema.items) result.items = convert(schema.items,
|
|
95
|
-
if (schema.anyOf) result.anyOf = schema.anyOf.map((member) => convert(member,
|
|
96
|
-
if (schema.oneOf) result.oneOf = schema.oneOf.map((member) => convert(member,
|
|
202
|
+
if (schema.items) result.items = convert(schema.items, current);
|
|
203
|
+
if (schema.anyOf) result.anyOf = schema.anyOf.map((member) => convert(member, current));
|
|
204
|
+
if (schema.oneOf) result.oneOf = schema.oneOf.map((member) => convert(member, current));
|
|
97
205
|
if (typeof schema.additionalProperties === "boolean") result.additionalProperties = schema.additionalProperties;
|
|
98
|
-
else if (schema.additionalProperties) result.additionalProperties = convert(schema.additionalProperties,
|
|
99
|
-
if (schema.patternProperties) result.patternProperties = Object.fromEntries(Object.entries(schema.patternProperties).map(([pattern, child]) => [pattern, convert(child,
|
|
206
|
+
else if (schema.additionalProperties) result.additionalProperties = convert(schema.additionalProperties, current);
|
|
207
|
+
if (schema.patternProperties) result.patternProperties = Object.fromEntries(Object.entries(schema.patternProperties).map(([pattern, child]) => [pattern, convert(child, current)]));
|
|
100
208
|
return result;
|
|
101
209
|
}
|
|
102
210
|
|
|
103
211
|
//#endregion
|
|
104
|
-
export { toJsonSchema };
|
|
212
|
+
export { inputJsonSchema, toJsonSchema };
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { routesOf, sortRoutes } from "./routes.js";
|
|
2
|
+
import MiniSearch from "minisearch";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/mcp/search.ts
|
|
5
|
+
/**
|
|
6
|
+
* How deep a schema is walked for searchable terms. Deep enough to reach the properties an agent
|
|
7
|
+
* would name, shallow enough that a broad contract does not turn one search into a tree walk.
|
|
8
|
+
*/
|
|
9
|
+
const MAX_DEPTH = 6;
|
|
10
|
+
/** Methods that only read, which is what the read/write filter divides the catalog on. */
|
|
11
|
+
const READ_METHODS = new Set([
|
|
12
|
+
"GET",
|
|
13
|
+
"HEAD",
|
|
14
|
+
"OPTIONS"
|
|
15
|
+
]);
|
|
16
|
+
/** Default page size, and the ceiling a caller may ask for. */
|
|
17
|
+
const DEFAULT_LIMIT = 20;
|
|
18
|
+
const MAX_LIMIT = 100;
|
|
19
|
+
/**
|
|
20
|
+
* How far below the best score in its tier a result may fall and still be returned.
|
|
21
|
+
*
|
|
22
|
+
* Terms are combined permissively, so one common token reaches an enormous number of routes: `post`
|
|
23
|
+
* or `rest` alone selects most of a WordPress contract. Without a floor the tail of those matches is
|
|
24
|
+
* counted as a match, and a caller told "504 of 504" learns nothing and has no reason to trust the
|
|
25
|
+
* ranking. The floor is relative and applied within a tier, so it trims each tier's tail without
|
|
26
|
+
* letting a strong identifier match suppress a weaker but differently-evidenced one.
|
|
27
|
+
*/
|
|
28
|
+
const SCORE_FLOOR = .25;
|
|
29
|
+
/**
|
|
30
|
+
* The indexed fields, and the evidence each one reports when it matches. Two fields answer to
|
|
31
|
+
* `description` because a summary and a description are the same thing to someone reading a result.
|
|
32
|
+
*/
|
|
33
|
+
const EVIDENCE = {
|
|
34
|
+
route: "route",
|
|
35
|
+
path: "path",
|
|
36
|
+
namespace: "namespace",
|
|
37
|
+
summary: "description",
|
|
38
|
+
description: "description",
|
|
39
|
+
errors: "error",
|
|
40
|
+
inputParams: "input.params.property",
|
|
41
|
+
inputQuery: "input.query.property",
|
|
42
|
+
inputBody: "input.body.property",
|
|
43
|
+
responses: "response.property"
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Field weights. `settings` appears in dozens of route names and must count for almost nothing next
|
|
47
|
+
* to a term that appears in two input schemas, which is what TF-IDF gives by construction; these
|
|
48
|
+
* only express the tier preference on top of it.
|
|
49
|
+
*/
|
|
50
|
+
const BOOST = {
|
|
51
|
+
route: 8,
|
|
52
|
+
path: 6,
|
|
53
|
+
namespace: 4,
|
|
54
|
+
inputParams: 3,
|
|
55
|
+
inputQuery: 3,
|
|
56
|
+
inputBody: 3,
|
|
57
|
+
errors: 3,
|
|
58
|
+
summary: 2,
|
|
59
|
+
description: 2,
|
|
60
|
+
responses: 1
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Rank the document's routes against a query.
|
|
64
|
+
*
|
|
65
|
+
* The index is built per call, because the watcher replaces the document whenever WordPress moves
|
|
66
|
+
* and a cached index would answer for routes that no longer exist. Exact identifier matches are
|
|
67
|
+
* found before the index is consulted, so looking a route up by name is deterministic rather than
|
|
68
|
+
* something a scorer has to be trusted to get right.
|
|
69
|
+
*/
|
|
70
|
+
function searchRoutes(document, options = {}) {
|
|
71
|
+
const all = sortRoutes(routesOf(document).values());
|
|
72
|
+
const namespaces = [...new Set(all.map((route) => route.namespace))].sort();
|
|
73
|
+
const ranked = rank(all.filter((route) => matchesFilters(route, options)), document, (options.query ?? "").trim());
|
|
74
|
+
const limit = Math.min(Math.max(options.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
|
|
75
|
+
const offset = decodeCursor(options.cursor);
|
|
76
|
+
const next = offset + limit;
|
|
77
|
+
return {
|
|
78
|
+
count: ranked.length,
|
|
79
|
+
routes: ranked.slice(offset, next),
|
|
80
|
+
...next < ranked.length ? { nextCursor: encodeCursor(next) } : {},
|
|
81
|
+
namespaces
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function matchesFilters(route, options) {
|
|
85
|
+
if (options.namespace !== void 0 && route.namespace !== options.namespace) return false;
|
|
86
|
+
if (options.method !== void 0 && route.method !== options.method.toUpperCase()) return false;
|
|
87
|
+
if (options.access === "read" && !READ_METHODS.has(route.method)) return false;
|
|
88
|
+
if (options.access === "write" && READ_METHODS.has(route.method)) return false;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* An empty query browses through the wildcard rather than around the index, so one path answers both
|
|
93
|
+
* and a browse cannot drift from a search in ordering or shape.
|
|
94
|
+
*/
|
|
95
|
+
function rank(routes, document, query) {
|
|
96
|
+
const browsing = query === "";
|
|
97
|
+
const index = new MiniSearch({
|
|
98
|
+
fields: Object.keys(BOOST),
|
|
99
|
+
storeFields: ["id"]
|
|
100
|
+
});
|
|
101
|
+
index.addAll(routes.map((route) => indexed(route, document)));
|
|
102
|
+
const byName = new Map(routes.map((route) => [route.name, route]));
|
|
103
|
+
const merged = /* @__PURE__ */ new Map();
|
|
104
|
+
const term = query.toLowerCase();
|
|
105
|
+
if (!browsing) for (const route of routes) {
|
|
106
|
+
const evidence = [];
|
|
107
|
+
if (route.name.toLowerCase() === term) evidence.push({
|
|
108
|
+
field: "route",
|
|
109
|
+
term: query
|
|
110
|
+
});
|
|
111
|
+
if (route.path.toLowerCase() === term) evidence.push({
|
|
112
|
+
field: "path",
|
|
113
|
+
term: query
|
|
114
|
+
});
|
|
115
|
+
if (route.namespace.toLowerCase() === term) evidence.push({
|
|
116
|
+
field: "namespace",
|
|
117
|
+
term: query
|
|
118
|
+
});
|
|
119
|
+
if (evidence.length > 0) merged.set(route.name, {
|
|
120
|
+
tier: 0,
|
|
121
|
+
score: Number.MAX_SAFE_INTEGER,
|
|
122
|
+
evidence
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const found = browsing ? index.search(MiniSearch.wildcard) : index.search(query, {
|
|
126
|
+
prefix: true,
|
|
127
|
+
fuzzy: .2,
|
|
128
|
+
boost: BOOST
|
|
129
|
+
});
|
|
130
|
+
for (const result of found) {
|
|
131
|
+
if (!browsing && result.score <= 0) continue;
|
|
132
|
+
const name = String(result.id);
|
|
133
|
+
const evidence = evidenceOf(result.match);
|
|
134
|
+
const already = merged.get(name);
|
|
135
|
+
if (already) already.evidence = dedupe([...already.evidence, ...evidence]);
|
|
136
|
+
else merged.set(name, {
|
|
137
|
+
tier: tierOf(evidence),
|
|
138
|
+
score: result.score,
|
|
139
|
+
evidence
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const entries = [...merged.entries()];
|
|
143
|
+
const bestByTier = /* @__PURE__ */ new Map();
|
|
144
|
+
for (const [, entry] of entries) bestByTier.set(entry.tier, Math.max(bestByTier.get(entry.tier) ?? 0, entry.score));
|
|
145
|
+
return (browsing ? entries : entries.filter(([, entry]) => entry.score >= (bestByTier.get(entry.tier) ?? 0) * SCORE_FLOOR)).sort(([leftName, left], [rightName, right]) => {
|
|
146
|
+
if (left.tier !== right.tier) return left.tier - right.tier;
|
|
147
|
+
if (left.score !== right.score) return right.score - left.score;
|
|
148
|
+
return leftName.localeCompare(rightName);
|
|
149
|
+
}).flatMap(([name, entry]) => {
|
|
150
|
+
const route = byName.get(name);
|
|
151
|
+
return route ? [describe(route, entry.evidence)] : [];
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Which tier a hit belongs to, below the exact identifier matches that take tier 0.
|
|
156
|
+
*
|
|
157
|
+
* What a route takes as an argument comes first, because that is what someone searching for
|
|
158
|
+
* `custom_fields` means. Response properties rank behind it, since generic words such as `title`,
|
|
159
|
+
* `status`, and `date` otherwise select nearly every content route. A partial identifier or a word
|
|
160
|
+
* from a summary ranks last: prefix matching means `custom` reaches every `customers` route, and
|
|
161
|
+
* those must not crowd out the route that actually accepts the field.
|
|
162
|
+
*/
|
|
163
|
+
function tierOf(evidence) {
|
|
164
|
+
const fields = new Set(evidence.map((item) => item.field));
|
|
165
|
+
if ([...fields].some((field) => field.startsWith("input.") || field === "error")) return 1;
|
|
166
|
+
if (fields.has("response.property")) return 2;
|
|
167
|
+
return 3;
|
|
168
|
+
}
|
|
169
|
+
function evidenceOf(match) {
|
|
170
|
+
const evidence = [];
|
|
171
|
+
for (const [term, fields] of Object.entries(match)) for (const field of fields) {
|
|
172
|
+
const named = EVIDENCE[field];
|
|
173
|
+
if (named) evidence.push({
|
|
174
|
+
field: named,
|
|
175
|
+
term
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return dedupe(evidence);
|
|
179
|
+
}
|
|
180
|
+
function dedupe(evidence) {
|
|
181
|
+
const seen = /* @__PURE__ */ new Set();
|
|
182
|
+
return evidence.filter((item) => {
|
|
183
|
+
const key = `${item.field} ${item.term}`;
|
|
184
|
+
if (seen.has(key)) return false;
|
|
185
|
+
seen.add(key);
|
|
186
|
+
return true;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function describe(route, evidence) {
|
|
190
|
+
return {
|
|
191
|
+
name: route.name,
|
|
192
|
+
namespace: route.namespace,
|
|
193
|
+
method: route.method,
|
|
194
|
+
path: route.path,
|
|
195
|
+
...route.summary !== void 0 ? { summary: route.summary } : {},
|
|
196
|
+
...route.deprecated ? { deprecated: true } : {},
|
|
197
|
+
evidence
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function indexed(route, document) {
|
|
201
|
+
const { input, responses, errors } = route.operation;
|
|
202
|
+
return {
|
|
203
|
+
id: route.name,
|
|
204
|
+
route: route.name.replace(/[.\-_]/g, " "),
|
|
205
|
+
path: route.path.replace(/[/{}\-_]/g, " "),
|
|
206
|
+
namespace: route.namespace,
|
|
207
|
+
summary: route.summary ?? "",
|
|
208
|
+
description: route.description ?? "",
|
|
209
|
+
errors: errors.join(" "),
|
|
210
|
+
inputParams: terms(input.params, document),
|
|
211
|
+
inputQuery: terms(input.query, document),
|
|
212
|
+
inputBody: terms(input.body, document),
|
|
213
|
+
responses: Object.values(responses).map((response) => `${terms(response.body, document)} ${terms(response.headers, document)}`).join(" ")
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function terms(schema, document) {
|
|
217
|
+
return collect(schema, document, 0, /* @__PURE__ */ new Set()).join(" ");
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Property names, titles, and descriptions reachable from a schema. Bounded by depth and guarded by
|
|
221
|
+
* the ids already open on this branch, so a schema describing its own nesting cannot stall a search.
|
|
222
|
+
*/
|
|
223
|
+
function collect(schema, document, depth, seen) {
|
|
224
|
+
if (!schema || depth > MAX_DEPTH) return [];
|
|
225
|
+
const found = [];
|
|
226
|
+
if (schema.title) found.push(schema.title);
|
|
227
|
+
if (schema.description) found.push(schema.description);
|
|
228
|
+
for (const [name, child] of Object.entries(schema.properties ?? {})) found.push(name.replace(/[._-]/g, " "), ...collect(child, document, depth + 1, seen));
|
|
229
|
+
found.push(...collect(schema.items, document, depth + 1, seen));
|
|
230
|
+
for (const member of [...schema.anyOf ?? [], ...schema.oneOf ?? []]) found.push(...collect(member, document, depth + 1, seen));
|
|
231
|
+
if (typeof schema.additionalProperties === "object") found.push(...collect(schema.additionalProperties, document, depth + 1, seen));
|
|
232
|
+
for (const child of Object.values(schema.patternProperties ?? {})) found.push(...collect(child, document, depth + 1, seen));
|
|
233
|
+
for (const id of [...schema.$ref ? [schema.$ref] : [], ...refs(schema.$extends)]) {
|
|
234
|
+
if (seen.has(id)) continue;
|
|
235
|
+
found.push(...collect(document.schemas[id], document, depth + 1, new Set([...seen, id])));
|
|
236
|
+
}
|
|
237
|
+
return found;
|
|
238
|
+
}
|
|
239
|
+
function refs(value) {
|
|
240
|
+
if (value === void 0) return [];
|
|
241
|
+
return Array.isArray(value) ? value : [value];
|
|
242
|
+
}
|
|
243
|
+
/** An opaque page marker, so a caller pages by handing one back rather than doing arithmetic. */
|
|
244
|
+
function encodeCursor(offset) {
|
|
245
|
+
return Buffer.from(`offset:${offset}`, "utf8").toString("base64url");
|
|
246
|
+
}
|
|
247
|
+
function decodeCursor(cursor) {
|
|
248
|
+
if (cursor === void 0) return 0;
|
|
249
|
+
const decoded = Buffer.from(cursor, "base64url").toString("utf8");
|
|
250
|
+
const offset = Number.parseInt(decoded.replace(/^offset:/, ""), 10);
|
|
251
|
+
return Number.isSafeInteger(offset) && offset >= 0 ? offset : 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
//#endregion
|
|
255
|
+
export { searchRoutes };
|