@aiwayds/dsh-mcp-adapter 0.5.0 → 0.6.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.
@@ -0,0 +1,364 @@
1
+ /**
2
+ * The three System One seams (jev business wiring) — and ONLY these three
3
+ * places in the plugin may call out to a decision backend.
4
+ *
5
+ * WHERE THEY ARE ALLOWED TO RUN (red line ①, hot paths stay remote-free):
6
+ * - {@link askKeepSuggestions} — the background keep-suggestion batch, fired
7
+ * by the usage counter and awaited by nobody;
8
+ * - {@link rankListQuery} — `mcp_list {query}` only, i.e. when the MODEL
9
+ * explicitly asked for a ranked view;
10
+ * - {@link rankDidYouMean} — `mcp_call`'s unknown-tool branch, i.e. an error
11
+ * the model is going to read anyway.
12
+ * The assemble waterfall, `mcp_list` without a query, and a SUCCESSFUL
13
+ * dispatch never appear in this file's call graph.
14
+ *
15
+ * FAIL-OPEN (red line ②): every export returns a "no opinion" value on ANY
16
+ * failure — disabled, timeout, 5xx, bad JSON, a thrown hook, an unparseable
17
+ * answer. None of them throws. The caller's lexical order then stands, which
18
+ * is the pre-seam behavior; a jev outage is indistinguishable from jev being
19
+ * off.
20
+ *
21
+ * CALIBRATION: the batch in {@link askKeepSuggestions} is the one seam that
22
+ * reads a band (thresholds.ts, kind 'suggest') — and only as a DISPLAY label
23
+ * on a suggestion row, and only for a PRIMARY probability. The two ranking
24
+ * seams log their probabilities with band 'record': a rank is relative
25
+ * ordering, and adopting/vetoing thresholds ported from another plugin's
26
+ * protocol would be a category error. An laya probability is not calibrated at
27
+ * all (dual.ts: relative use only), so a pace-maker answer NEVER takes a band
28
+ * and NEVER becomes a suggestion row — see {@link askKeepSuggestions}.
29
+ *
30
+ * @module jev/seams
31
+ */
32
+ import { jevAskDual } from "./dual.js";
33
+ import { band } from "./thresholds.js";
34
+ import { digestFor } from "./log.js";
35
+ import { RANK_CANDIDATE_LIMIT } from "../rank.js";
36
+ import { suggestionBasis } from "../usage.js";
37
+ /** A probability is usable only when it is a finite 0..1 number. */
38
+ export function noulProbability(answer) {
39
+ if (typeof answer !== 'object' || answer === null)
40
+ return undefined;
41
+ const p = answer.noul;
42
+ return typeof p === 'number' && Number.isFinite(p) && p >= 0 && p <= 1 ? p : undefined;
43
+ }
44
+ /** The option-probability map of a `choice` answer, when present and usable. */
45
+ function choiceProbabilities(answer) {
46
+ if (typeof answer !== 'object' || answer === null)
47
+ return undefined;
48
+ const raw = answer.probabilities;
49
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
50
+ return undefined;
51
+ const out = {};
52
+ for (const [option, value] of Object.entries(raw)) {
53
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1)
54
+ out[option] = value;
55
+ }
56
+ return Object.keys(out).length === 0 ? undefined : out;
57
+ }
58
+ /** The single winning option of a `choice` answer, when present. */
59
+ function choiceWinner(answer) {
60
+ if (typeof answer !== 'object' || answer === null)
61
+ return undefined;
62
+ const choice = answer.choice;
63
+ return typeof choice === 'string' && choice !== '' ? choice : undefined;
64
+ }
65
+ const RANK_QUESTION_ID = 'r1';
66
+ /**
67
+ * The ranking question both rank seams share (one batched choice request
68
+ * each). The option KEY is the tool name, so the answer maps back onto the
69
+ * candidate pool without an index that could drift.
70
+ */
71
+ function rankQuestion(instructions, candidates) {
72
+ const criteria = {};
73
+ for (const candidate of candidates) {
74
+ const blurb = candidate.description.replace(/\s+/gu, ' ').trim().slice(0, 200);
75
+ criteria[candidate.name] = blurb === '' ? candidate.name : `${candidate.name}: ${blurb}`;
76
+ }
77
+ return { type: 'choice', instructions, criteria };
78
+ }
79
+ /**
80
+ * Turn one jev ranking answer into an order. A full probability map sorts by
81
+ * probability descending (ties keep the lexical order); a bare `choice` field
82
+ * only moves the winner to the front; anything unparseable reports "no
83
+ * opinion" so the caller keeps the order it already had.
84
+ *
85
+ * @param candidates - the pool the question was asked about, in lexical order.
86
+ * @param answer - the raw answer value for {@link RANK_QUESTION_ID}.
87
+ * @returns the reordered names, or undefined for "no opinion".
88
+ */
89
+ export function orderFromChoice(candidates, answer) {
90
+ const names = candidates.map(candidate => candidate.name);
91
+ const index = new Map(names.map((name, position) => [name, position]));
92
+ const raw = choiceProbabilities(answer);
93
+ // A probability map that scores NONE of the candidates says nothing about
94
+ // their order — treat it as no opinion, not as "all equal".
95
+ const probabilities = raw === undefined
96
+ ? undefined
97
+ : Object.fromEntries(Object.entries(raw).filter(([name]) => index.has(name)));
98
+ if (probabilities !== undefined && Object.keys(probabilities).length > 0) {
99
+ return [...names].sort((left, right) => {
100
+ const a = probabilities[left] ?? -1;
101
+ const b = probabilities[right] ?? -1;
102
+ return (b - a) || ((index.get(left) ?? 0) - (index.get(right) ?? 0));
103
+ });
104
+ }
105
+ const winner = choiceWinner(answer);
106
+ if (winner === undefined || !index.has(winner))
107
+ return undefined;
108
+ return [winner, ...names.filter(name => name !== winner)];
109
+ }
110
+ /**
111
+ * Log one verdict row per candidate the ranking answer scored. `band: 'record'`
112
+ * and `agree: 'n/a'` are deliberate: a relative order has no adopt/veto
113
+ * threshold to reconcile against, and inventing one would poison the
114
+ * calibration analysis with a band nothing acts on.
115
+ */
116
+ async function logRankVerdicts(log, lane, state, instructions, candidates, answer, backend, degraded) {
117
+ const probabilities = choiceProbabilities(answer);
118
+ if (probabilities === undefined)
119
+ return;
120
+ const at = new Date().toISOString();
121
+ const rows = candidates
122
+ .map(candidate => ({ candidate, p: probabilities[candidate.name] }))
123
+ .filter((entry) => entry.p !== undefined)
124
+ .map(entry => ({
125
+ at,
126
+ lane,
127
+ questionId: RANK_QUESTION_ID,
128
+ qtype: 'choice',
129
+ ref: `tool:${entry.candidate.name}`,
130
+ digest: digestFor(state, RANK_QUESTION_ID, instructions),
131
+ probability: entry.p,
132
+ band: 'record',
133
+ agree: 'n/a',
134
+ backend,
135
+ degraded,
136
+ }));
137
+ await log.logVerdicts(rows);
138
+ }
139
+ /**
140
+ * Seam B: rerank `mcp_list {query}` candidates by relevance.
141
+ *
142
+ * The incoming order is the LEXICAL one (see ../rank.ts) and is also the
143
+ * fail-open result: any failure, disabled switch, or unusable answer returns
144
+ * `undefined`, which the caller reads as "keep what you had".
145
+ *
146
+ * A DEGRADED answer (the local laya pace-maker took over because the primary
147
+ * backend failed) is usable here: relative ordering is exactly what a rank
148
+ * is, and jev/dual.ts's contract allows degraded results for relative use
149
+ * only — which is all this seam does.
150
+ *
151
+ * @param query - the model's query text.
152
+ * @param ordered - lexical order (best first), already capped by the caller.
153
+ * @param env - the live jev runtime.
154
+ * @returns the jev order, or undefined for "no opinion".
155
+ */
156
+ export async function rankListQuery(query, ordered, env) {
157
+ const candidates = ordered.slice(0, RANK_CANDIDATE_LIMIT);
158
+ if (candidates.length < 2)
159
+ return undefined;
160
+ const instructions = `下面这些 MCP 工具里,哪一个与检索 query 最相关?请选出相关性最高的那一个。query:${query}`;
161
+ const state = [
162
+ '任务背景:模型正在通过 mcp_list 浏览被折叠出常驻 prompt 的 MCP 工具,需要按与当前任务的相关性挑选。',
163
+ `检索 query:${query}`,
164
+ `候选工具数:${candidates.length}`,
165
+ ].join('\n');
166
+ return rankSeam('list-rank', state, instructions, candidates, env);
167
+ }
168
+ /**
169
+ * Seam C: rerank the near-miss candidates of an unregistered tool name.
170
+ *
171
+ * Same fail-open contract as {@link rankListQuery}. Only runs when the lexical
172
+ * prefilter found MORE THAN ONE candidate — a single lexical hit needs no
173
+ * ranking, so the cold branch stays free when it has nothing to offer.
174
+ *
175
+ * @param tool - the name dispatch could not resolve.
176
+ * @param ordered - lexical near-miss order (best first).
177
+ * @param env - the live jev runtime.
178
+ * @returns the jev order, or undefined for "no opinion".
179
+ */
180
+ export async function rankDidYouMean(tool, ordered, env) {
181
+ const candidates = ordered.slice(0, RANK_CANDIDATE_LIMIT);
182
+ if (candidates.length < 2)
183
+ return undefined;
184
+ const instructions = `模型刚刚尝试调用 MCP 工具「${tool}」,但这个名字没有注册。哪一个候选最可能就是它想调用的工具?`;
185
+ const state = [
186
+ '任务背景:模型通过 mcp_call 分发被折叠出常驻 prompt 的 MCP 工具,工具名拼错时会走到 not-registered 分支。',
187
+ `未注册的工具名:${tool}`,
188
+ `候选工具数:${candidates.length}`,
189
+ ].join('\n');
190
+ return rankSeam('didyoumean', state, instructions, candidates, env);
191
+ }
192
+ /** Shared body of both ranking seams (one batched choice request each). */
193
+ async function rankSeam(lane, state, instructions, candidates, env) {
194
+ try {
195
+ if (!env.enabled())
196
+ return undefined;
197
+ const dual = await jevAskDual({
198
+ state,
199
+ questions: { [RANK_QUESTION_ID]: rankQuestion(instructions, candidates) },
200
+ config: env.config(),
201
+ lane,
202
+ // fail-open is real here: the caller's lexical order IS the fallback.
203
+ fallback: true,
204
+ log: env.log(),
205
+ });
206
+ if (!dual.ok)
207
+ return undefined;
208
+ const answer = dual.answers[RANK_QUESTION_ID];
209
+ const order = orderFromChoice(candidates, answer);
210
+ if (order === undefined)
211
+ return undefined;
212
+ // Telemetry must never turn a good answer into a failed seam.
213
+ await logRankVerdicts(env.log(), lane, state, instructions, candidates, answer, dual.source, dual.source === 'laya').catch(() => undefined);
214
+ return order;
215
+ }
216
+ catch {
217
+ // contained — the seam has no throw path, by contract
218
+ return undefined;
219
+ }
220
+ }
221
+ /** The keep-suggestion question for one candidate (two-sided noul criteria). */
222
+ function keepQuestion(candidate) {
223
+ const instructions = `候选 MCP 工具「${candidate.tool}」当前不在常驻 prompt 里(已折叠,模型每次要用都得先 mcp_list 查到它)。`
224
+ + `记录到的使用情况:${suggestionBasis(candidate)}。描述:${candidate.description}。`
225
+ + '该工具值得在每次对话中都常驻 prompt(即加入 keep 名单)吗?';
226
+ return {
227
+ type: 'noul',
228
+ instructions,
229
+ criteria: {
230
+ true: `值得常驻——「${candidate.tool}」被反复调用,把它的 schema 常驻 prompt 能省掉每次查找与展开的成本`,
231
+ false: `不值得常驻——「${candidate.tool}」的调用频次或不可替代性不足以抵消常驻 schema 的 token 成本,保持折叠即可`,
232
+ },
233
+ };
234
+ }
235
+ /** The state text the keep-suggestion batch is asked against. */
236
+ function keepState(candidates) {
237
+ const lines = candidates.map(candidate => [
238
+ `- ${candidate.tool}: ${suggestionBasis(candidate)}`,
239
+ candidate.description === '' ? '' : ` ${candidate.description}`,
240
+ ].filter(line => line !== '').join('\n'));
241
+ return [
242
+ '任务背景:模型通过 mcp_list/mcp_call 使用被折叠出常驻 prompt 的 MCP 工具,每个折叠工具的 schema 都要按需展开。',
243
+ `候选工具数:${candidates.length}(记录到的调用情况与工具描述)`,
244
+ ...lines,
245
+ ].join('\n');
246
+ }
247
+ /**
248
+ * Seam A: ask, in ONE batched noul request, whether each folded-and-used tool
249
+ * earns a permanent place in every prompt.
250
+ *
251
+ * Fire-and-forget by contract: the caller never awaits this on a hot path.
252
+ * `null` means "this round produced nothing" (disabled, call failed, no
253
+ * candidates, unusable answers, DEGRADED primary) — the caller then keeps the
254
+ * PREVIOUS batch in suggestions.json, and the usage record is unaffected
255
+ * either way.
256
+ *
257
+ * ABSOLUTE SCORES ONLY FROM THE PRIMARY: a suggestion row is a calibrated
258
+ * probability plus a band plus an actionable "keep it resident" line that a
259
+ * human reads in `/mcp suggest`. When the primary failed and the local laya
260
+ * pace-maker took over, that answer is usable for RELATIVE ranking only
261
+ * (dual.ts) and laya's negatives sit in the same band as its positives — so a
262
+ * degraded round returns `null` instead of rendering uncalibrated numbers. The
263
+ * answer is still logged (band 'record', degraded=true): the laya-vs-jev bench
264
+ * and the fallback-rate health metric must not lose a round.
265
+ *
266
+ * @param candidates - folded tools with recorded calls, most-used first.
267
+ * @param env - the live jev runtime.
268
+ * @returns one row per answered candidate, or null.
269
+ */
270
+ export async function askKeepSuggestions(candidates, env) {
271
+ const batch = candidates.slice(0, RANK_CANDIDATE_LIMIT);
272
+ if (batch.length === 0)
273
+ return null;
274
+ const state = keepState(batch);
275
+ const questions = {};
276
+ const meta = [];
277
+ for (const candidate of batch) {
278
+ const qid = `c${meta.length + 1}`;
279
+ const question = keepQuestion(candidate);
280
+ questions[qid] = question;
281
+ meta.push({ qid, candidate, instructions: question.instructions });
282
+ }
283
+ try {
284
+ if (!env.enabled())
285
+ return null;
286
+ const dual = await jevAskDual({
287
+ state,
288
+ questions,
289
+ config: env.config(),
290
+ lane: 'usage-suggest',
291
+ // Nothing to fall back to: suggestions are additive display, and a
292
+ // failed round simply leaves the previous batch in place.
293
+ fallback: true,
294
+ log: env.log(),
295
+ });
296
+ if (!dual.ok)
297
+ return null;
298
+ // The driving source produces the rows; the pace-maker (when it answered
299
+ // alongside a healthy primary) contributes comparison rows only, exactly
300
+ // as §6.2 prescribes.
301
+ const driving = {
302
+ answers: dual.answers,
303
+ backend: dual.source,
304
+ degraded: dual.source === 'laya',
305
+ };
306
+ const sources = [driving];
307
+ if (dual.source === 'primary' && dual.layaResult?.ok === true) {
308
+ sources.push({ answers: dual.layaResult.answers, backend: 'laya', degraded: false });
309
+ }
310
+ const at = new Date().toISOString();
311
+ const records = [];
312
+ const rows = [];
313
+ for (const source of sources) {
314
+ for (const entry of meta) {
315
+ const probability = noulProbability(source.answers[entry.qid]);
316
+ if (probability === undefined)
317
+ continue; // unparseable answer: no fabricated row
318
+ // A pace-maker probability is uncalibrated, so it never takes a band
319
+ // (`band()` would be a category error) and never becomes a
320
+ // suggestion row — whichever way it got here: a comparison row
321
+ // beside a healthy primary, or a degraded takeover.
322
+ const fromPaceMaker = source.backend !== 'primary';
323
+ const verdictBand = fromPaceMaker ? 'record' : band(probability, 'suggest');
324
+ rows.push({
325
+ at,
326
+ lane: 'usage-suggest',
327
+ questionId: entry.qid,
328
+ qtype: 'noul',
329
+ ref: `tool:${entry.candidate.tool}`,
330
+ digest: digestFor(state, entry.qid, entry.instructions),
331
+ probability,
332
+ band: verdictBand,
333
+ // Candidates are folded by construction, so there is no legacy
334
+ // "it was already kept" disposition to reconcile against.
335
+ agree: 'n/a',
336
+ backend: source.backend,
337
+ degraded: source.degraded,
338
+ });
339
+ if (fromPaceMaker)
340
+ continue; // telemetry/bench row only — never a suggestion
341
+ records.push({
342
+ tool: entry.candidate.tool,
343
+ probability,
344
+ band: verdictBand,
345
+ basis: suggestionBasis(entry.candidate),
346
+ _at: at,
347
+ });
348
+ }
349
+ }
350
+ await env.log().logVerdicts(rows).catch(() => undefined);
351
+ // A degraded round produced verdict rows and nothing else: no primary
352
+ // drove it, so there is no calibrated number to show and no record to
353
+ // write — the previous batch stands (see the module doc).
354
+ if (records.length === 0)
355
+ return null;
356
+ records.sort((left, right) => right.probability - left.probability || left.tool.localeCompare(right.tool));
357
+ return records;
358
+ }
359
+ catch {
360
+ // contained — a broken batch must never reach the dispatch path
361
+ return null;
362
+ }
363
+ }
364
+ //# sourceMappingURL=seams.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seams.js","sourceRoot":"","sources":["../../src/jev/seams.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * System One decision bands (design 2026-09-25 §5), ported from
3
+ * dsh-topics-memory `src/jev/thresholds.ts` with the band kind renamed
4
+ * `rerank` → `suggest` for this plugin's `/mcp suggest` seam.
5
+ *
6
+ * !!! CALIBRATION NOTICE — READ BEFORE ACTING ON A BAND !!!
7
+ * The numbers below are PLACEHOLDERS copied over from topics-memory to keep
8
+ * the file's shape identical for diffing. They were calibrated for THAT
9
+ * plugin's own question protocol and corpus; this plugin's protocol differs
10
+ * (different candidates, different instructions, batch shape), so the
11
+ * absolute probabilities move. Every value MUST be re-swept against this
12
+ * plugin's own protocol before any band is treated as a calibration
13
+ * conclusion — do NOT carry over topics-memory's numbers.
14
+ *
15
+ * WHERE A BAND IS READ TODAY: exactly one place — seam A (`jev/seams.ts`
16
+ * askKeepSuggestions) reads `band(p, 'suggest')` as the DISPLAY label on a
17
+ * `/mcp suggest` row that already carries the raw probability. Nothing
18
+ * auto-acts on a band, and a pace-maker (laya) probability never takes one:
19
+ * uncalibrated scores are logged as band 'record' and produce no suggestion
20
+ * row, so a degraded round yields no band at all. Until this plugin's own
21
+ * re-sweep lands, a band must be read as "above the placeholder cutoff", not
22
+ * as "adopt/veto is justified".
23
+ *
24
+ * @module jev/thresholds
25
+ */
26
+ /** `/mcp suggest` (建议补的 mcp 工具): adopt ≥ 0.60, strong-veto < 0.10. */
27
+ export declare const SUGGEST_ADOPT = 0.6;
28
+ export declare const SUGGEST_FALLBACK = 0.1;
29
+ /** Second band kind, kept for shape parity with the source file and for the
30
+ * lanes that are not calibrated yet — the values are placeholders too. */
31
+ export declare const MERGE_PAIR_ADOPT = 0.5;
32
+ export declare const MERGE_PAIR_FALLBACK = 0.15;
33
+ export type JevBand = 'adopt' | 'record' | 'fallback';
34
+ export type JevBandKind = 'suggest' | 'mergePair';
35
+ /** Map a noul probability to its band: adopt → act on it, record → only log
36
+ * it, fallback → hard veto back to the legacy behavior. */
37
+ export declare function band(probability: number, kind: JevBandKind): JevBand;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * System One decision bands (design 2026-09-25 §5), ported from
3
+ * dsh-topics-memory `src/jev/thresholds.ts` with the band kind renamed
4
+ * `rerank` → `suggest` for this plugin's `/mcp suggest` seam.
5
+ *
6
+ * !!! CALIBRATION NOTICE — READ BEFORE ACTING ON A BAND !!!
7
+ * The numbers below are PLACEHOLDERS copied over from topics-memory to keep
8
+ * the file's shape identical for diffing. They were calibrated for THAT
9
+ * plugin's own question protocol and corpus; this plugin's protocol differs
10
+ * (different candidates, different instructions, batch shape), so the
11
+ * absolute probabilities move. Every value MUST be re-swept against this
12
+ * plugin's own protocol before any band is treated as a calibration
13
+ * conclusion — do NOT carry over topics-memory's numbers.
14
+ *
15
+ * WHERE A BAND IS READ TODAY: exactly one place — seam A (`jev/seams.ts`
16
+ * askKeepSuggestions) reads `band(p, 'suggest')` as the DISPLAY label on a
17
+ * `/mcp suggest` row that already carries the raw probability. Nothing
18
+ * auto-acts on a band, and a pace-maker (laya) probability never takes one:
19
+ * uncalibrated scores are logged as band 'record' and produce no suggestion
20
+ * row, so a degraded round yields no band at all. Until this plugin's own
21
+ * re-sweep lands, a band must be read as "above the placeholder cutoff", not
22
+ * as "adopt/veto is justified".
23
+ *
24
+ * @module jev/thresholds
25
+ */
26
+ /** `/mcp suggest` (建议补的 mcp 工具): adopt ≥ 0.60, strong-veto < 0.10. */
27
+ export const SUGGEST_ADOPT = 0.6;
28
+ export const SUGGEST_FALLBACK = 0.1;
29
+ /** Second band kind, kept for shape parity with the source file and for the
30
+ * lanes that are not calibrated yet — the values are placeholders too. */
31
+ export const MERGE_PAIR_ADOPT = 0.5;
32
+ export const MERGE_PAIR_FALLBACK = 0.15;
33
+ /** Map a noul probability to its band: adopt → act on it, record → only log
34
+ * it, fallback → hard veto back to the legacy behavior. */
35
+ export function band(probability, kind) {
36
+ const floor = kind === 'suggest' ? SUGGEST_FALLBACK : MERGE_PAIR_FALLBACK;
37
+ const adopt = kind === 'suggest' ? SUGGEST_ADOPT : MERGE_PAIR_ADOPT;
38
+ if (probability >= adopt)
39
+ return 'adopt';
40
+ if (probability >= floor)
41
+ return 'record';
42
+ return 'fallback';
43
+ }
44
+ //# sourceMappingURL=thresholds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thresholds.js","sourceRoot":"","sources":["../../src/jev/thresholds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,sEAAsE;AACtE,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAA;AAChC,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAA;AAEnC;2EAC2E;AAC3E,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAA;AACnC,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAA;AAKvC;4DAC4D;AAC5D,MAAM,UAAU,IAAI,CAAC,WAAmB,EAAE,IAAiB;IACzD,MAAM,KAAK,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,mBAAmB,CAAA;IACzE,MAAM,KAAK,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,gBAAgB,CAAA;IACnE,IAAI,WAAW,IAAI,KAAK;QAAE,OAAO,OAAO,CAAA;IACxC,IAAI,WAAW,IAAI,KAAK;QAAE,OAAO,QAAQ,CAAA;IACzC,OAAO,UAAU,CAAA;AACnB,CAAC"}
package/lib/rank.d.ts ADDED
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Lexical pre-ranking for the two model-facing search seams (`mcp_list
3
+ * {query}` and `mcp_call`'s unknown-tool branch).
4
+ *
5
+ * WHY A SEPARATE MODULE: these are the pure, dependency-free halves of seams
6
+ * B and C. They must stay callable from `node --test` without the meta-tool
7
+ * definitions, AND from `jev/seams.ts` (which reranks whatever this module
8
+ * produced) — so they cannot live in src/index.ts without an import cycle
9
+ * (index.ts → jev/seams.ts → index.ts).
10
+ *
11
+ * CONTRACT (red line ②): everything here is deterministic, synchronous and
12
+ * offline. A jev answer is only ever a REORDER of what these functions already
13
+ * produced (see {@link mergeNameOrder}); any jev failure degrades to the
14
+ * order computed below, so this file is the fail-open baseline of both seams.
15
+ *
16
+ * SCRIPT: the lexical prefilter runs over the ASCII tokens when the query has
17
+ * any, and falls back to a CJK substring match when it has none (see
18
+ * {@link cjkQuery}) — the model asks this plugin's questions in Chinese.
19
+ *
20
+ * @module rank
21
+ */
22
+ /** Cap on the candidate list handed to a jev batch (both ranking seams). */
23
+ export declare const RANK_CANDIDATE_LIMIT = 40;
24
+ /** How many ranked tools a `mcp_list {query}` answer actually shows. */
25
+ export declare const LIST_RESULT_LIMIT = 10;
26
+ /** How many near-misses the unknown-tool error names. */
27
+ export declare const DID_YOU_MEAN_LIMIT = 3;
28
+ /** Max edit distance a near-miss may have to be offered. */
29
+ export declare const DID_YOU_MEAN_MAX_DISTANCE = 3;
30
+ /** The minimal shape ranking needs from a tool. */
31
+ export interface RankableEntry {
32
+ readonly name: string;
33
+ readonly description: string;
34
+ }
35
+ /**
36
+ * What the ranking seams pass around: a tool plus the blurb that goes into a
37
+ * decision question (or into the lexical score). Named separately from
38
+ * {@link RankableEntry} because it crosses the seam boundary (index.ts builds
39
+ * the pools, jev/seams.ts asks about them).
40
+ */
41
+ export type RankCandidate = RankableEntry;
42
+ /**
43
+ * Lowercase alphanumeric tokens of one string, split on non-alphanumeric runs
44
+ * AND on camelCase / letter-digit boundaries — so `create_issue`,
45
+ * `createIssue` and `createissue` all reduce to overlapping tokens. Tool
46
+ * naming conventions vary per MCP vendor; the search seam must not depend on
47
+ * one of them.
48
+ *
49
+ * ASCII ONLY, on purpose: the token weights in {@link scoreQueryMatch} are
50
+ * calibrated on token equality, and a CJK query has no ASCII tokens to
51
+ * compare. Chinese/other CJK text is matched by the substring fallback
52
+ * ({@link cjkQuery}) instead, and that fallback is a SEPARATE path so the
53
+ * ASCII ranking cannot drift.
54
+ */
55
+ export declare function tokenize(text: string): string[];
56
+ /**
57
+ * Lexical score of one entry against a tokenized query. 0 means "no match at
58
+ * all" and is what the rankers filter on — an unmatched entry is never shown
59
+ * as a result, however long its description is.
60
+ *
61
+ * Weights: exact full-name equality dominates; a query token found in the
62
+ * NAME beats one found only in the DESCRIPTION; a whole-phrase substring hit
63
+ * beats a scattered per-token hit.
64
+ *
65
+ * @param entry - the tool being scored.
66
+ * @param tokens - query tokens from {@link tokenize}.
67
+ * @param queryCompact - the whole query squashed to alphanumerics.
68
+ * @returns the score (0 = no match).
69
+ */
70
+ export declare function scoreQueryMatch(entry: RankableEntry, tokens: readonly string[], queryCompact: string): number;
71
+ /**
72
+ * Rank entries against a free-text query: best score first, name ascending as
73
+ * the deterministic tie-break, entries with no lexical match dropped, at most
74
+ * `limit` survivors.
75
+ *
76
+ * A query with no ASCII token falls back to the CJK substring prefilter
77
+ * ({@link cjkQuery}) — the model writes this plugin's queries in Chinese, and
78
+ * "no tokens" must not read as "nothing matched". A query with neither ASCII
79
+ * nor CJK content (blank, punctuation) still matches nothing.
80
+ *
81
+ * Pure and synchronous — this is the order a failed/failed-open jev rerank
82
+ * degrades to, and the only order produced when `jevEnabled` is false.
83
+ *
84
+ * @param query - the model's raw query text.
85
+ * @param entries - the catalog entries to score (input is never mutated).
86
+ * @param limit - max survivors; omit for "every match".
87
+ * @returns the surviving entries, best first.
88
+ */
89
+ export declare function rankByQuery<T extends RankableEntry>(query: string, entries: readonly T[], limit?: number): T[];
90
+ /**
91
+ * Reorder a list of NAMES by an external ranking (the jev answer). Names the
92
+ * ranking does not mention — and names it repeats — keep their original
93
+ * relative order at the tail, so a short or partial answer degrades to "jev's
94
+ * prefix + lexical remainder" instead of dropping candidates. Names that were
95
+ * not in `ordered` at all are ignored.
96
+ *
97
+ * @param ordered - the lexical order being permuted.
98
+ * @param ranking - tool names, best first, as returned by the reranker.
99
+ * @returns a new array; `ordered` is never mutated.
100
+ */
101
+ export declare function mergeNameOrder(ordered: readonly string[], ranking: readonly string[]): string[];
102
+ /**
103
+ * Levenshtein distance between two names, or `undefined` when it is provably
104
+ * greater than `max`. The length pre-check plus the per-row minimum make the
105
+ * common "clearly not a near-miss" case O(max) instead of O(n·m); names are
106
+ * short anyway, but the pool can be a full catalog and this runs inside a
107
+ * dispatch error path.
108
+ *
109
+ * @param a - first name (case-insensitive).
110
+ * @param b - second name (case-insensitive).
111
+ * @param max - distances above this report "no".
112
+ * @returns the distance, or undefined when it exceeds `max`.
113
+ */
114
+ export declare function editDistanceWithin(a: string, b: string, max: number): number | undefined;
115
+ /** Where one near-miss landed: a typo of the name, or merely a shared token. */
116
+ export type DidYouMeanTier = 'edit' | 'token';
117
+ /** One scored near-miss candidate. */
118
+ export interface DidYouMeanCandidate {
119
+ /** The candidate's registered tool name. */
120
+ name: string;
121
+ /** How it matched: within the edit distance, or on a shared token. */
122
+ tier: DidYouMeanTier;
123
+ /** Edit distance (0 for token-only matches, which are not distance-graded). */
124
+ distance: number;
125
+ /** Tokens shared with the mistyped name. */
126
+ shared: number;
127
+ }
128
+ /**
129
+ * Lexical near-miss candidates for one unregistered tool name: everything
130
+ * within {@link DID_YOU_MEAN_MAX_DISTANCE} edits, plus anything sharing a
131
+ * name token, best first. This is the always-on floor of the unknown-tool
132
+ * branch — it runs whether or not jev is enabled, needs no network, and is
133
+ * the order that stands when a jev rerank fails.
134
+ *
135
+ * @param tool - the name dispatch could not resolve.
136
+ * @param pool - candidate names (production: the visible prefix tools).
137
+ * @param limit - max candidates (default {@link DID_YOU_MEAN_LIMIT}).
138
+ * @param ignoreTokens - tokens that must NOT count as a shared-token match.
139
+ * The caller's fold prefix goes here: every candidate in the pool starts
140
+ * with it, so "shares mcp" is not evidence of anything.
141
+ * @returns the candidates, best first (edit tier, then fewest edits, then
142
+ * most shared tokens, then shorter, then alphabetical).
143
+ */
144
+ export declare function didYouMeanCandidates(tool: string, pool: readonly string[], limit?: number, ignoreTokens?: readonly string[]): DidYouMeanCandidate[];
145
+ /** Which ordering produced a suggestion list — shown in the error text. */
146
+ export type DidYouMeanSource = 'lexical' | 'jev';
147
+ /**
148
+ * The suffix appended to the unknown-tool error. The base message stays
149
+ * byte-identical to the pre-seam one, so a caller that only knew the old text
150
+ * still matches it; the suggestion is an addition, and its provenance is
151
+ * always stated (an unranked guess must not look like a ranked one).
152
+ *
153
+ * @param names - suggested tool names, best first.
154
+ * @param source - who produced the order.
155
+ * @returns `''` when there is nothing to suggest.
156
+ */
157
+ export declare function didYouMeanMessage(names: readonly string[], source: DidYouMeanSource): string;