@typeship-ax/mcp 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.
Files changed (129) hide show
  1. package/AGENTS.md +15 -11
  2. package/README.md +22 -53
  3. package/api.json +9998 -10118
  4. package/api.md +8983 -9120
  5. package/dist/arguments.d.ts +47 -0
  6. package/dist/arguments.d.ts.map +1 -0
  7. package/dist/arguments.js +254 -0
  8. package/dist/core/http.d.ts +162 -19
  9. package/dist/core/http.d.ts.map +1 -1
  10. package/dist/core/http.js +381 -48
  11. package/dist/core/pagination.d.ts +42 -6
  12. package/dist/core/pagination.d.ts.map +1 -1
  13. package/dist/core/pagination.js +111 -17
  14. package/dist/credential-storage.d.ts +10 -3
  15. package/dist/credential-storage.d.ts.map +1 -1
  16. package/dist/credential-storage.js +15 -6
  17. package/dist/dates.d.ts +1 -1
  18. package/dist/dates.js +1 -1
  19. package/dist/errors.d.ts +20 -84
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +20 -108
  22. package/dist/fields.d.ts +29 -0
  23. package/dist/fields.d.ts.map +1 -0
  24. package/dist/fields.js +101 -0
  25. package/dist/index.d.ts +28 -18
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +35 -25
  28. package/dist/mcp-authorization.d.ts.map +1 -1
  29. package/dist/mcp-authorization.js +34 -10
  30. package/dist/mcp-protocol.d.ts +87 -44
  31. package/dist/mcp-protocol.d.ts.map +1 -1
  32. package/dist/mcp-protocol.js +552 -478
  33. package/dist/mcp.d.ts.map +1 -1
  34. package/dist/mcp.js +127 -28
  35. package/dist/named-credentials.d.ts +19 -0
  36. package/dist/named-credentials.d.ts.map +1 -1
  37. package/dist/named-credentials.js +81 -1
  38. package/dist/oauth-request.d.ts +7 -1
  39. package/dist/oauth-request.d.ts.map +1 -1
  40. package/dist/oauth-request.js +26 -4
  41. package/dist/oauth-session.d.ts +13 -1
  42. package/dist/oauth-session.d.ts.map +1 -1
  43. package/dist/oauth-session.js +34 -18
  44. package/dist/ops.d.ts +53 -5
  45. package/dist/ops.d.ts.map +1 -1
  46. package/dist/ops.js +49 -40
  47. package/dist/resources/api-keys.d.ts +10 -7
  48. package/dist/resources/api-keys.d.ts.map +1 -1
  49. package/dist/resources/api-keys.js +10 -31
  50. package/dist/resources/deliveries.d.ts +88 -4
  51. package/dist/resources/deliveries.d.ts.map +1 -1
  52. package/dist/resources/deliveries.js +95 -18
  53. package/dist/resources/drafts.d.ts +15 -15
  54. package/dist/resources/drafts.d.ts.map +1 -1
  55. package/dist/resources/drafts.js +11 -64
  56. package/dist/resources/files.d.ts +4 -4
  57. package/dist/resources/files.d.ts.map +1 -1
  58. package/dist/resources/files.js +3 -12
  59. package/dist/resources/generations.d.ts +14 -14
  60. package/dist/resources/generations.d.ts.map +1 -1
  61. package/dist/resources/generations.js +21 -45
  62. package/dist/resources/organization.d.ts +4 -4
  63. package/dist/resources/organization.d.ts.map +1 -1
  64. package/dist/resources/organization.js +3 -10
  65. package/dist/resources/{generate.d.ts → packages.d.ts} +16 -16
  66. package/dist/resources/packages.d.ts.map +1 -0
  67. package/dist/resources/{generate.js → packages.js} +13 -29
  68. package/dist/resources/projects.d.ts +50 -50
  69. package/dist/resources/projects.d.ts.map +1 -1
  70. package/dist/resources/projects.js +60 -116
  71. package/dist/resources/releases.d.ts +21 -16
  72. package/dist/resources/releases.d.ts.map +1 -1
  73. package/dist/resources/releases.js +18 -39
  74. package/dist/resources/spec-revisions.d.ts +15 -6
  75. package/dist/resources/spec-revisions.d.ts.map +1 -1
  76. package/dist/resources/spec-revisions.js +6 -28
  77. package/dist/resources/specs.d.ts +7 -7
  78. package/dist/resources/specs.d.ts.map +1 -1
  79. package/dist/resources/specs.js +6 -34
  80. package/dist/resources/targets.d.ts +48 -48
  81. package/dist/resources/targets.d.ts.map +1 -1
  82. package/dist/resources/targets.js +58 -114
  83. package/dist/schemas.d.ts.map +1 -1
  84. package/dist/schemas.js +78 -76
  85. package/dist/search.d.ts +54 -0
  86. package/dist/search.d.ts.map +1 -0
  87. package/dist/search.js +421 -0
  88. package/dist/types.d.ts +499 -339
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js +18 -18
  91. package/dist/worker.js +2 -2
  92. package/package.json +5 -2
  93. package/server.json +5 -5
  94. package/src/arguments.ts +242 -0
  95. package/src/core/http.ts +457 -58
  96. package/src/core/pagination.ts +129 -18
  97. package/src/credential-storage.ts +16 -6
  98. package/src/dates.ts +1 -1
  99. package/src/errors.ts +46 -115
  100. package/src/fields.ts +91 -0
  101. package/src/index.ts +45 -28
  102. package/src/mcp-authorization.ts +29 -9
  103. package/src/mcp-protocol.ts +580 -428
  104. package/src/mcp.ts +113 -26
  105. package/src/named-credentials.ts +66 -1
  106. package/src/oauth-request.ts +32 -6
  107. package/src/oauth-session.ts +37 -19
  108. package/src/ops.ts +82 -44
  109. package/src/resources/api-keys.ts +34 -48
  110. package/src/resources/deliveries.ts +211 -30
  111. package/src/resources/drafts.ts +60 -107
  112. package/src/resources/files.ts +19 -20
  113. package/src/resources/generations.ts +57 -75
  114. package/src/resources/organization.ts +11 -16
  115. package/src/resources/{generate.ts → packages.ts} +43 -51
  116. package/src/resources/projects.ts +145 -200
  117. package/src/resources/releases.ts +48 -65
  118. package/src/resources/spec-revisions.ts +38 -47
  119. package/src/resources/specs.ts +39 -59
  120. package/src/resources/targets.ts +143 -193
  121. package/src/schemas.ts +78 -76
  122. package/src/search.ts +434 -0
  123. package/src/types.ts +538 -357
  124. package/src/worker.ts +2 -2
  125. package/dist/resources/generate.d.ts.map +0 -1
  126. package/dist/resources/publications.d.ts +0 -47
  127. package/dist/resources/publications.d.ts.map +0 -1
  128. package/dist/resources/publications.js +0 -70
  129. package/src/resources/publications.ts +0 -140
package/dist/search.js ADDED
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Operation search shared by the generated CLI (`docs search`) and MCP
3
+ * server (`search_docs`). Generated by Typeship — https://typeship.dev
4
+ *
5
+ * Agents describe a task ("assign issue", "list calls"), not an operation
6
+ * name, so ranking works on stems, weighs rare words over common ones
7
+ * (IDF), and reads the verb of the query against what each operation does:
8
+ * its HTTP method and whether its path names one item or a collection, or
9
+ * its GraphQL root field. GraphQL mutations are also indexed by their input
10
+ * fields, so "assign issue" finds issueUpdate through assigneeId.
11
+ * Deterministic and dependency-free.
12
+ */
13
+ /** Reference matches per search page. */
14
+ export const SEARCH_PAGE_SIZE = 10;
15
+ /** Query verbs and the operation kinds they ask for, best first. */
16
+ const VERBS = {
17
+ list: ["list"], all: ["list"], browse: ["list"], enumerate: ["list"],
18
+ search: ["search", "list"], find: ["search", "list", "get"], query: ["search", "list"], lookup: ["get", "search"],
19
+ get: ["get", "list"], release: ["delete"], fetch: ["get", "list"], retrieve: ["get", "list"], read: ["get", "list"], show: ["get", "list"],
20
+ view: ["get", "list"], check: ["get"], describe: ["get"], inspect: ["get"], status: ["get"],
21
+ create: ["create"], add: ["create", "update"], new: ["create"], make: ["create"], send: ["create"], post: ["create"],
22
+ submit: ["create"], open: ["create"], start: ["create", "update"], upload: ["create", "update"], generate: ["create"],
23
+ buy: ["create"], purchase: ["create"], place: ["create"], invite: ["create"], write: ["create", "update"],
24
+ publish: ["create", "update"], register: ["create"],
25
+ update: ["update"], change: ["update"], edit: ["update"], modify: ["update"], set: ["update", "create"],
26
+ assign: ["update", "create"], rename: ["update"], move: ["update"], close: ["update", "delete"], reopen: ["update"],
27
+ mark: ["update"], archive: ["delete", "update"], unarchive: ["update"], enable: ["update"], disable: ["update"],
28
+ pause: ["update"], resume: ["update"], replace: ["update"], patch: ["update"], merge: ["update", "create"],
29
+ save: ["create", "update"], follow: ["create", "update"], unfollow: ["delete", "update"], comment: ["create"], reply: ["create"], request: ["create"], hang: ["update", "delete"],
30
+ delete: ["delete"], remove: ["delete", "update"], destroy: ["delete"], erase: ["delete"], drop: ["delete"],
31
+ revoke: ["delete"], cancel: ["delete", "update"], unassign: ["update", "delete"], clear: ["delete"],
32
+ };
33
+ /** Words that name the caller: "who am I", "my issues", "me". */
34
+ const IDENTITY_WORDS = new Set(["who", "whoami", "me", "my", "mine", "myself", "self"]);
35
+ const IDENTITY_TERMS = ["me", "self", "whoami", "authenticated"];
36
+ /** An operation about the caller: GET /user, /me/…, a viewer or me root
37
+ * field, users_get_authenticated. */
38
+ function isIdentityOperation(op) {
39
+ if (op.graphql?.field)
40
+ return /^(viewer|me|currentUser|whoami)$/.test(op.graphql.field);
41
+ const nameWords = words([op.tool, op.method ?? ""].join(" "));
42
+ if (nameWords.some((w) => w === "me" || w === "whoami" || w === "authenticated" || w === "self"))
43
+ return true;
44
+ return /^(\/v\d+)?\/(me|user|self|whoami)(\/|$)/i.test(op.path) || /\/(users\/)?me(\/|$)/i.test(op.path);
45
+ }
46
+ /** Words that narrow a request rather than name the operation. */
47
+ const QUALIFIERS = new Set(["yesterday", "today", "tomorrow", "recent", "recently", "latest", "last", "week", "month", "year", "daily"]);
48
+ /** A few everyday words for what APIs call something else. */
49
+ const SYNONYMS = {
50
+ now: ["current"], sms: ["message"], mms: ["message"], text: ["message"], document: ["file"], kyc: ["identity", "verification"],
51
+ };
52
+ const STOPWORDS = new Set([
53
+ "a", "an", "the", "to", "of", "for", "in", "on", "at", "from", "by", "with", "and", "or", "is", "are", "am", "be",
54
+ "do", "does", "it", "its", "this", "that", "these", "those", "into", "onto", "up", "how", "what", "which", "can",
55
+ "please", "via", "using", "some", "any", "there", "i", "you", "your", "we", "our",
56
+ ]);
57
+ /** Field weights: a word in the operation's name says most about it. */
58
+ const W_NAME = 10, W_SUMMARY = 6, W_PATH = 4, W_PARAM = 3, W_ENUM = 2, W_DESCRIPTION = 1.5, W_OUTPUT = 1;
59
+ /** A light English stemmer: plurals, -ing/-ed, a few derivational
60
+ * suffixes, a trailing e. It only has to map a word and its common
61
+ * variants to one stem ("assignee", "assigned", "assigns" to "assign";
62
+ * "moderate", "moderations" to "moder"). */
63
+ export function stem(word) {
64
+ let w = word.toLowerCase();
65
+ if (w.length <= 3)
66
+ return w;
67
+ if (w.endsWith("ies") && w.length > 4)
68
+ w = w.slice(0, -3) + "y";
69
+ else if (/(sses|xes|ches|shes|zes)$/.test(w))
70
+ w = w.slice(0, -2);
71
+ else if (w.endsWith("s") && !/(ss|us|is)$/.test(w))
72
+ w = w.slice(0, -1);
73
+ let stripped = false;
74
+ if (w.endsWith("ly") && w.length >= 6)
75
+ w = w.slice(0, -2);
76
+ for (const [suffix, min] of [["ing", 4], ["ed", 3]]) {
77
+ if (w.endsWith(suffix) && w.length - suffix.length >= min) {
78
+ w = w.slice(0, -suffix.length);
79
+ stripped = true;
80
+ break;
81
+ }
82
+ }
83
+ if (stripped) {
84
+ if (/(at|bl|iz)$/.test(w))
85
+ w += "e";
86
+ else if (/([^aeioulsz])\1$/.test(w))
87
+ w = w.slice(0, -1);
88
+ }
89
+ for (const [suffix, min] of [["ations", 4], ["ation", 4], ["ments", 4], ["ment", 4], ["ption", 4], ["ions", 4], ["ion", 4], ["ers", 4], ["er", 4], ["ee", 4], ["ate", 4]]) {
90
+ if (w.endsWith(suffix) && w.length - suffix.length >= min) {
91
+ w = w.slice(0, -suffix.length);
92
+ break;
93
+ }
94
+ }
95
+ if (w.endsWith("e") && w.length >= 5)
96
+ w = w.slice(0, -1);
97
+ return w;
98
+ }
99
+ /** Lowercase words of two or more characters, with snake, kebab, camel and
100
+ * letter-digit seams split so "createAccount" and "accounts_create" share
101
+ * words. */
102
+ function words(text) {
103
+ return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([a-zA-Z])([0-9])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 2);
104
+ }
105
+ function stems(text) {
106
+ return words(text).filter((w) => !STOPWORDS.has(w)).map(stem);
107
+ }
108
+ // A server keeps one operation list, and a hosted one filters the same
109
+ // operation objects per request: cache by both.
110
+ const INDEXES = new WeakMap();
111
+ const INDEXED = new WeakMap();
112
+ const READ_ACTIONS = ["list", "get", "search"];
113
+ const WRITE_ACTIONS = ["create", "update", "delete"];
114
+ /** The first of `words` (in order) that names an allowed action. */
115
+ function actionIn(candidates, allowed) {
116
+ for (const word of candidates) {
117
+ const action = VERBS[word]?.[0];
118
+ if (action && allowed.includes(action))
119
+ return { action, word };
120
+ }
121
+ return undefined;
122
+ }
123
+ /** What an operation does, and the word of its name that says so: its
124
+ * name's verb when it has one, else its HTTP method and whether the path
125
+ * ends at one item or a collection. A read never creates, updates or
126
+ * deletes, whatever its name ("query comment" is a read of a comment). */
127
+ function operationAction(op) {
128
+ const collection = () => {
129
+ const segments = op.path.replace(/\.[a-z]+$/i, "").split("/").filter(Boolean);
130
+ const last = segments.at(-1) ?? "";
131
+ if (last.startsWith("{") || last.startsWith(":"))
132
+ return "get";
133
+ return op.paginated ? "list" : "get";
134
+ };
135
+ if (op.graphql?.field) {
136
+ // issueUpdate, createIssue: the verb may sit at either end.
137
+ const fieldWords = words(op.graphql.field);
138
+ const read = op.graphql.kind !== "mutation";
139
+ const found = actionIn([...fieldWords].reverse(), read ? READ_ACTIONS : WRITE_ACTIONS);
140
+ if (found)
141
+ return found;
142
+ return { action: read ? (op.paginated || /s$/.test(op.graphql.field) ? "list" : "get") : "update", word: null };
143
+ }
144
+ const verb = op.httpMethod.toUpperCase();
145
+ const read = verb === "GET" || verb === "HEAD";
146
+ const found = actionIn(words(op.method ?? ""), read ? READ_ACTIONS : [...WRITE_ACTIONS, "search"]);
147
+ if (found)
148
+ return found;
149
+ if (verb === "DELETE")
150
+ return { action: "delete", word: null };
151
+ if (verb === "PUT" || verb === "PATCH")
152
+ return { action: "update", word: null };
153
+ if (verb === "POST")
154
+ return { action: "create", word: null };
155
+ return { action: collection(), word: null };
156
+ }
157
+ /** Literal path words: no parameters, versions or date segments. */
158
+ function pathWords(path) {
159
+ return path.replace(/\.[a-z]+$/i, "").split("/").filter((s) => s && !/^[{:]/.test(s) && !/^v\d+$/i.test(s) && !/^\d/.test(s)).join(" ");
160
+ }
161
+ /** Property names and enum values of a schema, one level into objects,
162
+ * and one more for object-typed properties (a GraphQL input object). */
163
+ function schemaWords(schema, depth, names, enums) {
164
+ if (!schema || typeof schema !== "object" || depth < 0)
165
+ return;
166
+ const node = schema;
167
+ if (Array.isArray(node.enum))
168
+ for (const value of node.enum)
169
+ if (typeof value === "string")
170
+ enums.push(value);
171
+ for (const key of ["anyOf", "oneOf", "allOf"]) {
172
+ if (Array.isArray(node[key]))
173
+ for (const part of node[key])
174
+ schemaWords(part, depth, names, enums);
175
+ }
176
+ if (node.items)
177
+ schemaWords(node.items, depth, names, enums);
178
+ const properties = node.properties;
179
+ if (properties && typeof properties === "object") {
180
+ for (const [name, child] of Object.entries(properties)) {
181
+ names.push(name);
182
+ schemaWords(child, depth - 1, names, enums);
183
+ }
184
+ }
185
+ }
186
+ function outputWords(schema, depth, out) {
187
+ if (!schema || typeof schema !== "object" || depth < 0)
188
+ return;
189
+ const node = schema;
190
+ if (typeof node.description === "string")
191
+ out.push(node.description.slice(0, 200));
192
+ if (node.items)
193
+ outputWords(node.items, depth, out);
194
+ for (const key of ["anyOf", "oneOf", "allOf"]) {
195
+ if (Array.isArray(node[key]))
196
+ for (const part of node[key])
197
+ outputWords(part, depth, out);
198
+ }
199
+ const properties = node.properties;
200
+ if (properties && typeof properties === "object") {
201
+ for (const [name, child] of Object.entries(properties)) {
202
+ out.push(name);
203
+ outputWords(child, depth - 1, out);
204
+ }
205
+ }
206
+ }
207
+ function firstSentence(text) {
208
+ return /^(.{1,200}?[.!?])(\s|$)/s.exec(text.trim())?.[1] ?? "";
209
+ }
210
+ function indexOperation(op) {
211
+ const weights = new Map();
212
+ const add = (text, weight) => {
213
+ for (const s of new Set(stems(text))) {
214
+ const entry = weights.get(s);
215
+ if (!entry)
216
+ weights.set(s, { best: weight, rest: 0 });
217
+ else if (weight > entry.best) {
218
+ entry.rest += entry.best;
219
+ entry.best = weight;
220
+ }
221
+ else
222
+ entry.rest += weight;
223
+ }
224
+ };
225
+ // GraphQL tools are named query_<field> / mutation_<field>; the prefix is
226
+ // the kind, not a word a task would use.
227
+ const name = op.graphql?.field
228
+ ? [op.graphql.field]
229
+ : [op.tool, op.resource ?? "", op.method ?? ""];
230
+ add(name.join(" "), W_NAME);
231
+ // Without a summary, the description's first sentence plays its part.
232
+ const summary = op.summary?.trim() || firstSentence(op.description ?? "");
233
+ add(summary, W_SUMMARY);
234
+ add(pathWords(op.path), W_PATH);
235
+ const paramNames = [];
236
+ const enumValues = [];
237
+ const paramText = [];
238
+ for (const param of op.params) {
239
+ paramNames.push(param.name);
240
+ if (param.enum)
241
+ enumValues.push(...param.enum);
242
+ if (param.description)
243
+ paramText.push(param.description.split("\n")[0].slice(0, 200));
244
+ }
245
+ const properties = (op.inputSchema?.properties ?? {});
246
+ for (const child of Object.values(properties))
247
+ schemaWords(child, 1, paramNames, enumValues);
248
+ add(paramNames.join(" "), W_PARAM);
249
+ add(enumValues.join(" "), W_ENUM);
250
+ add(op.description ?? "", W_DESCRIPTION);
251
+ add(paramText.join(" "), W_OUTPUT);
252
+ const output = [];
253
+ outputWords(op.outputSchema, 2, output);
254
+ add(output.join(" "), W_OUTPUT);
255
+ const { action, word } = operationAction(op);
256
+ const actionStem = word === null ? null : stem(word);
257
+ const nameStems = [...new Set(stems(name.join(" ")))].filter((s) => s !== actionStem);
258
+ return { weights, nameStems, action, actionStem, identity: isIdentityOperation(op), summary: stems(summary).join(" "), deprecated: op.deprecated === true || /^deprecated\b/i.test(op.summary ?? "") };
259
+ }
260
+ function indexFor(ops) {
261
+ const cached = INDEXES.get(ops);
262
+ if (cached)
263
+ return cached;
264
+ const docs = ops.map((op) => {
265
+ let doc = INDEXED.get(op);
266
+ if (!doc)
267
+ INDEXED.set(op, doc = indexOperation(op));
268
+ return doc;
269
+ });
270
+ const df = new Map();
271
+ for (const doc of docs)
272
+ for (const s of doc.weights.keys())
273
+ df.set(s, (df.get(s) ?? 0) + 1);
274
+ const index = { docs, df, vocabulary: [...df.keys()] };
275
+ INDEXES.set(ops, index);
276
+ return index;
277
+ }
278
+ /** A query stem's matches in the vocabulary: itself, and longer or
279
+ * shorter forms sharing a long prefix ("transcrib" and "transcript"),
280
+ * which count for less. */
281
+ function variants(term, index) {
282
+ const out = new Map();
283
+ if (index.df.has(term))
284
+ out.set(term, 1);
285
+ if (term.length < 5)
286
+ return out;
287
+ for (const word of index.vocabulary) {
288
+ if (word === term || word.length < 5)
289
+ continue;
290
+ let common = 0;
291
+ while (common < word.length && common < term.length && word[common] === term[common])
292
+ common++;
293
+ if (common >= 6 && Math.max(word.length, term.length) - common <= 3)
294
+ out.set(word, 0.6);
295
+ }
296
+ return out;
297
+ }
298
+ function parseQuery(query, index) {
299
+ const raw = words(query);
300
+ let verb = null;
301
+ let identity = false;
302
+ const terms = [];
303
+ const seen = new Set();
304
+ for (const word of raw) {
305
+ if (IDENTITY_WORDS.has(word)) {
306
+ identity = true;
307
+ continue;
308
+ }
309
+ if (STOPWORDS.has(word))
310
+ continue;
311
+ const s = stem(word);
312
+ if (seen.has(s))
313
+ continue;
314
+ seen.add(s);
315
+ const actions = VERBS[word] ?? VERBS[s] ?? null;
316
+ const isVerb = actions !== null && actions.length > 0 && verb === null;
317
+ if (isVerb)
318
+ verb = actions;
319
+ const matches = variants(s, index);
320
+ for (const synonym of SYNONYMS[word] ?? [])
321
+ for (const [v, strength] of variants(stem(synonym), index))
322
+ matches.set(v, Math.max(matches.get(v) ?? 0, strength * 0.8));
323
+ // The verb is scored against what the operation does, so its word
324
+ // counts for little; the first noun is usually the object of the task.
325
+ const qualifier = QUALIFIERS.has(word);
326
+ const primary = !isVerb && !qualifier && !terms.some((t) => t.primary);
327
+ const weight = isVerb || qualifier ? 0.3 : primary ? 1.3 : 1;
328
+ terms.push({ matches, weight, verb: isVerb ? actions : null, primary, stem: s });
329
+ }
330
+ if (identity) {
331
+ const matches = new Map();
332
+ for (const t of IDENTITY_TERMS)
333
+ for (const [s, strength] of variants(stem(t), index))
334
+ matches.set(s, Math.max(matches.get(s) ?? 0, strength));
335
+ terms.push({ matches, weight: 0.8, verb: null, primary: false, stem: "@me" });
336
+ }
337
+ return { terms, verb };
338
+ }
339
+ /**
340
+ * Operations for a free-text query, best first. Weak matches (under a
341
+ * fifth of the best score, or matching only the query's verb) are left
342
+ * out, and deprecated operations follow every current one.
343
+ */
344
+ export function rankOperations(ops, query) {
345
+ const index = indexFor(ops);
346
+ const { terms, verb } = parseQuery(query, index);
347
+ if (terms.length === 0)
348
+ return [];
349
+ const idf = (s) => Math.log(1 + index.docs.length / (index.df.get(s) ?? index.docs.length));
350
+ const nonVerbTerms = terms.filter((t) => t.verb === null).length;
351
+ const primary = terms.find((t) => t.primary);
352
+ const verbStem = terms.find((t) => t.verb !== null)?.stem ?? null;
353
+ const identityTerm = terms.find((t) => t.stem === "@me");
354
+ const identityIdf = Math.log(1 + index.docs.length / Math.max(1, index.docs.filter((d) => d.identity).length));
355
+ const scored = [];
356
+ const phrase = words(query).filter((w) => !STOPWORDS.has(w)).map(stem).join(" ");
357
+ index.docs.forEach((doc, position) => {
358
+ let score = 0;
359
+ let matchedNonVerb = 0;
360
+ let primaryInName = false;
361
+ let verbInName = false;
362
+ const covered = new Set();
363
+ for (const term of terms) {
364
+ let best = term === identityTerm && doc.identity ? W_NAME * identityIdf : 0;
365
+ for (const [s, strength] of term.matches) {
366
+ const entry = doc.weights.get(s);
367
+ if (!entry)
368
+ continue;
369
+ const value = (entry.best + 0.1 * Math.min(entry.rest, 10)) * strength * idf(s);
370
+ if (value > best)
371
+ best = value;
372
+ if (doc.nameStems.includes(s)) {
373
+ covered.add(s);
374
+ if (term === primary)
375
+ primaryInName = true;
376
+ // The query's verb as a noun of the name: upload_url for "upload".
377
+ if (term.verb !== null)
378
+ verbInName = true;
379
+ }
380
+ }
381
+ if (best > 0 && term.verb === null)
382
+ matchedNonVerb++;
383
+ score += best * term.weight;
384
+ }
385
+ if (score === 0)
386
+ return;
387
+ if (nonVerbTerms > 0 && matchedNonVerb === 0)
388
+ return;
389
+ // How much of the operation's name the query accounts for: call_list
390
+ // for "list calls" over sip_auth_calls_credential_list_mapping_delete.
391
+ const coverage = doc.nameStems.length === 0 ? 1 : covered.size / doc.nameStems.length;
392
+ score *= 0.6 + 0.8 * coverage;
393
+ // The query's verb against what the operation does.
394
+ if (verb) {
395
+ const rank = verb.indexOf(doc.action);
396
+ score *= rank === 0 ? 1.5 : rank > 0 ? 1.2 : doc.action === "search" && verb.includes("list") ? 1.2 : verbInName ? 1 : 0.55;
397
+ // The very word: issue_add_label for "add", over a generic create.
398
+ if (verbStem !== null && doc.actionStem === verbStem)
399
+ score *= 1.2;
400
+ }
401
+ // Every query word matched: a stronger signal than any single word.
402
+ if (matchedNonVerb === nonVerbTerms && nonVerbTerms > 1)
403
+ score *= 1.2;
404
+ if (phrase.includes(" ") && doc.nameStems.join(" ").includes(phrase))
405
+ score *= 1.2;
406
+ if (phrase.includes(" ") && doc.summary.includes(phrase))
407
+ score *= 1.3;
408
+ // The operation is named for the task's object: issue_update, not
409
+ // workflow_state_update, for "change issue state".
410
+ if (primaryInName)
411
+ score *= 1.3;
412
+ scored.push({ op: ops[position], score, deprecated: doc.deprecated });
413
+ });
414
+ if (scored.length === 0)
415
+ return [];
416
+ const top = Math.max(...scored.map((s) => s.score));
417
+ return scored
418
+ .filter((s) => s.score >= top * 0.2)
419
+ .sort((a, b) => Number(a.deprecated) - Number(b.deprecated) || b.score - a.score || a.op.tool.localeCompare(b.op.tool))
420
+ .map(({ op, score }) => ({ op, score: Math.round(score * 100) / 100 }));
421
+ }