@browserstack/mcp-server 1.5.0-beta.10 → 1.5.0-beta.12
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/capability/loadtesting.capability-index.json +33 -7
- package/capability/tm.capability-index.json +313 -112
- package/dist/tools/capability-registry/bind.js +159 -5
- package/dist/tools/capability-registry/register.d.ts +6 -0
- package/dist/tools/capability-registry/register.js +144 -77
- package/dist/tools/capability-registry/search.d.ts +10 -4
- package/dist/tools/capability-registry/search.js +80 -22
- package/dist/tools/capability-registry/types.d.ts +42 -6
- package/package.json +1 -1
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* `_` used to be a word character, which made `test_case` a single token while every
|
|
18
18
|
* haystack rendered it as "test case" — so the two could never match. That is the exact
|
|
19
|
-
* string `
|
|
19
|
+
* string `listProducts` hands back, so a caller following the documented flow searched with
|
|
20
20
|
* a term guaranteed to score zero: "list test_runs" matched 19 capabilities and put an
|
|
21
21
|
* admin settings endpoint first, where "list test runs" matched 103 and put the test-runs
|
|
22
22
|
* listing first.
|
|
@@ -337,7 +337,7 @@ function score(capability, wanted, weights, aliases, hint, plural) {
|
|
|
337
337
|
if (wanted.length === 0)
|
|
338
338
|
return { matched: 1, ranked: 1 };
|
|
339
339
|
const haystacks = [
|
|
340
|
-
[identityText(capability),
|
|
340
|
+
[identityText(capability), IDENTITY_WEIGHT],
|
|
341
341
|
[capability.entity, 4],
|
|
342
342
|
[(aliases[capability.entity] || []).join(" "), 4],
|
|
343
343
|
[capability.intent || "", 2],
|
|
@@ -412,19 +412,71 @@ function score(capability, wanted, weights, aliases, hint, plural) {
|
|
|
412
412
|
return { matched, ranked };
|
|
413
413
|
}
|
|
414
414
|
/**
|
|
415
|
-
*
|
|
415
|
+
* How much of a perfect match the best hit actually achieved, below which we doubt it.
|
|
416
416
|
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
417
|
+
* A RATIO, because the raw score is corpus-relative and an absolute threshold therefore
|
|
418
|
+
* cannot travel. `rarity` is measured over whatever is being searched, so the same query
|
|
419
|
+
* scores differently depending on scope: "list my load tests" tops out at 1.9 against Load
|
|
420
|
+
* Testing alone and 10.1 against both products. The old absolute cut of 3 called the first
|
|
421
|
+
* of those weak and the second strong — the same query, the same right answer, opposite
|
|
422
|
+
* verdicts, with the smaller corpus penalised precisely because it is smaller.
|
|
421
423
|
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
424
|
+
* Both halves of this ratio move with the corpus, so it does not:
|
|
425
|
+
*
|
|
426
|
+
* coverage = top_matched / (identity weight x Σ rarity of the query's terms)
|
|
427
|
+
*
|
|
428
|
+
* — "what fraction of a perfect identity match did the best hit manage". Above 1 is
|
|
429
|
+
* ordinary, since a good hit matches several fields, not just the route.
|
|
430
|
+
*
|
|
431
|
+
* Measured across tm-only, LT-only and both: genuine misses land at 0.00–0.18, queries
|
|
432
|
+
* that work at 0.44–1.94. The absolute score cannot separate those (misses reach 2.9,
|
|
433
|
+
* good queries drop to 1.9); this does, with the gap in the same place in all three
|
|
434
|
+
* corpora, which is the property that was missing.
|
|
435
|
+
*/
|
|
436
|
+
const WEAK_COVERAGE = 0.25;
|
|
437
|
+
/** The heaviest field, and so the yardstick a perfect match is measured against. */
|
|
438
|
+
const IDENTITY_WEIGHT = 6;
|
|
439
|
+
/** Slots a matching product is guaranteed, before the rest of the page fills by rank. */
|
|
440
|
+
const PRODUCT_FLOOR = 2;
|
|
441
|
+
/**
|
|
442
|
+
* Fill the page by rank, but never let one product's SIZE shut another out entirely.
|
|
443
|
+
*
|
|
444
|
+
* Scores are comparable across products — rarity spans the whole corpus by design — but
|
|
445
|
+
* the number of chances to score is not: tm has 173 capabilities to Load Testing's 20. On
|
|
446
|
+
* a query using words both products share ("test", "config", "tag"), tm simply has more
|
|
447
|
+
* entries near the top and takes the page. Measured before this: "add a tag to xyz test"
|
|
448
|
+
* returned tm tm LT tm tm, and an agent reading the first row went to the wrong product.
|
|
449
|
+
*
|
|
450
|
+
* So each product that matched at all is guaranteed a couple of slots, and everything else
|
|
451
|
+
* is still strict rank order. This does NOT reorder anything or touch scoring — the best
|
|
452
|
+
* hit stays the best hit — it only refuses to let a product be invisible because it is
|
|
453
|
+
* small. The cost when a query really is single-product is a row or two of another
|
|
454
|
+
* product's shortlist, which since the split is a few hundred bytes.
|
|
426
455
|
*/
|
|
427
|
-
|
|
456
|
+
function withEveryProductRepresented(scored, limit) {
|
|
457
|
+
if (scored.length <= limit)
|
|
458
|
+
return scored;
|
|
459
|
+
const products = new Set(scored.map((s) => s.product));
|
|
460
|
+
if (products.size < 2)
|
|
461
|
+
return scored.slice(0, limit);
|
|
462
|
+
const taken = new Set();
|
|
463
|
+
// Reserve first, so a product near the bottom of the ranking still gets its footing.
|
|
464
|
+
for (const product of products) {
|
|
465
|
+
for (const hit of scored
|
|
466
|
+
.filter((s) => s.product === product)
|
|
467
|
+
.slice(0, PRODUCT_FLOOR)) {
|
|
468
|
+
if (taken.size < limit)
|
|
469
|
+
taken.add(hit);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
for (const hit of scored) {
|
|
473
|
+
if (taken.size >= limit)
|
|
474
|
+
break;
|
|
475
|
+
taken.add(hit);
|
|
476
|
+
}
|
|
477
|
+
// Emit in the original ranked order: the floor decides WHO appears, never in what order.
|
|
478
|
+
return scored.filter((hit) => taken.has(hit));
|
|
479
|
+
}
|
|
428
480
|
/**
|
|
429
481
|
* The product's vocabulary, for a caller whose words are not the product's words.
|
|
430
482
|
*
|
|
@@ -434,10 +486,15 @@ const WEAK_MATCH = 3;
|
|
|
434
486
|
* language model, and given tm's entity list it maps bucket -> folder without effort. It
|
|
435
487
|
* just cannot guess the list unprompted.
|
|
436
488
|
*
|
|
437
|
-
* Aliases only,
|
|
438
|
-
*
|
|
439
|
-
*
|
|
489
|
+
* Aliases only, and `title` dropped as a near-duplicate of `entity` ("Test run" next to
|
|
490
|
+
* `test_run` buys nothing). The entity `key_facts` are richer prose but ten times the size,
|
|
491
|
+
* and a caller who needs them can ask describeEntity once it knows which entity to ask
|
|
440
492
|
* about — which is exactly what this hands over.
|
|
493
|
+
*
|
|
494
|
+
* Its share of the response grew when search became a shortlist: 3.2KB against a 38KB full
|
|
495
|
+
* search was 8%, against a 6.8KB shortlist it is nearly half. The absolute cost did not
|
|
496
|
+
* change and is still under a thousand tokens — far less than one wrong invoke — so this
|
|
497
|
+
* is budgeted in bytes rather than as a fraction of a baseline that now moves.
|
|
441
498
|
*/
|
|
442
499
|
export function vocabularyOf(products, only) {
|
|
443
500
|
const out = {};
|
|
@@ -449,9 +506,6 @@ export function vocabularyOf(products, only) {
|
|
|
449
506
|
const aliases = (doc.aliases || []);
|
|
450
507
|
entries.push({
|
|
451
508
|
entity,
|
|
452
|
-
...(doc.title
|
|
453
|
-
? { title: doc.title }
|
|
454
|
-
: {}),
|
|
455
509
|
...(aliases.length ? { aliases } : {}),
|
|
456
510
|
});
|
|
457
511
|
}
|
|
@@ -515,13 +569,17 @@ export function searchCapabilities(products, query, options = {}) {
|
|
|
515
569
|
// different questions: the mode and cardinality constants can lift a weakly-matched
|
|
516
570
|
// capability to the top of a page that is entirely wrong.
|
|
517
571
|
const topMatched = scored.reduce((best, s) => Math.max(best, s.matched), 0);
|
|
572
|
+
// What a perfect identity match would have scored for THIS query in THIS corpus. Both
|
|
573
|
+
// this and `topMatched` scale with the corpus, so their ratio is comparable across
|
|
574
|
+
// products and across `product`/`entity` narrowing — which the raw score is not.
|
|
575
|
+
const perfect = weights.reduce((sum, w) => sum + IDENTITY_WEIGHT * w, 0);
|
|
576
|
+
const coverage = perfect > 0 ? topMatched / perfect : 1;
|
|
518
577
|
return {
|
|
519
|
-
hits: scored
|
|
520
|
-
.slice(0, limit)
|
|
521
|
-
.map(({ product, capability }) => ({ product, capability })),
|
|
578
|
+
hits: withEveryProductRepresented(scored, limit).map(({ product, capability }) => ({ product, capability })),
|
|
522
579
|
truncated: scored.length > limit,
|
|
523
580
|
total_matched: scored.length,
|
|
524
581
|
top_matched: topMatched,
|
|
525
|
-
|
|
582
|
+
coverage,
|
|
583
|
+
weak: wanted.length > 0 && perfect > 0 && coverage < WEAK_COVERAGE,
|
|
526
584
|
};
|
|
527
585
|
}
|
|
@@ -27,7 +27,47 @@ export declare const SUPPORTED_SCHEMA_VERSION = 1;
|
|
|
27
27
|
export declare const ENVELOPE_KEYS: readonly ["schema_version", "version", "build_id", "harness_commit", "products"];
|
|
28
28
|
export type Mode = "read" | "write" | "destructive";
|
|
29
29
|
/** One parameter, under the name the OpenAPI spec itself gives it. */
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The constraints a parameter may declare beyond its type.
|
|
32
|
+
*
|
|
33
|
+
* ALL OPTIONAL, and absent means unchecked. The released artifact carries none of these
|
|
34
|
+
* yet — tm's spec holds 59 `format`, 50 `default`, 23 `minimum`, 12 `maximum`, 10
|
|
35
|
+
* `minItems`, 7 `maxItems`, 4 `minLength`, 3 `pattern` and 2 `maxLength` that the export
|
|
36
|
+
* currently drops — so this ships inert and starts working the moment the export carries
|
|
37
|
+
* them. An older index must keep binding exactly as it does today.
|
|
38
|
+
*/
|
|
39
|
+
export interface WireConstraints {
|
|
40
|
+
/** Numbers. `multipleOf` is checked on the coerced number, not the raw string. */
|
|
41
|
+
minimum?: number;
|
|
42
|
+
maximum?: number;
|
|
43
|
+
multipleOf?: number;
|
|
44
|
+
/** Strings. `pattern` is an unanchored regex, compiled with `u` for `\p{…}` classes. */
|
|
45
|
+
minLength?: number;
|
|
46
|
+
maxLength?: number;
|
|
47
|
+
pattern?: string;
|
|
48
|
+
format?: string;
|
|
49
|
+
/** Arrays. */
|
|
50
|
+
minItems?: number;
|
|
51
|
+
maxItems?: number;
|
|
52
|
+
uniqueItems?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The value the product uses when the field is omitted.
|
|
55
|
+
*
|
|
56
|
+
* PUBLISHED, NOT INJECTED. Telling a caller the default is what stops the redundant send;
|
|
57
|
+
* filling it in here would put a value on the wire that the caller never chose, and pin
|
|
58
|
+
* a server-side default that is free to change. Omission and "explicitly the default" are
|
|
59
|
+
* different requests, and only the product knows whether that difference matters.
|
|
60
|
+
*/
|
|
61
|
+
default?: unknown;
|
|
62
|
+
}
|
|
63
|
+
/** One field inside an array item or a nested object — same constraints, one level down. */
|
|
64
|
+
export interface WireField extends WireConstraints {
|
|
65
|
+
name: string;
|
|
66
|
+
type: string;
|
|
67
|
+
required?: true;
|
|
68
|
+
values?: unknown[];
|
|
69
|
+
}
|
|
70
|
+
export interface WireParam extends WireConstraints {
|
|
31
71
|
name: string;
|
|
32
72
|
type: string;
|
|
33
73
|
required?: true;
|
|
@@ -35,11 +75,7 @@ export interface WireParam {
|
|
|
35
75
|
example?: unknown;
|
|
36
76
|
description?: string;
|
|
37
77
|
/** Field names/types one level inside an array item or nested object. */
|
|
38
|
-
fields?:
|
|
39
|
-
name: string;
|
|
40
|
-
type: string;
|
|
41
|
-
required?: true;
|
|
42
|
-
}[];
|
|
78
|
+
fields?: WireField[];
|
|
43
79
|
/**
|
|
44
80
|
* Where a body field sits in the JSON, when that differs from its name. Published
|
|
45
81
|
* because the nesting is not guessable and getting it wrong fails silently — tm's folder
|