fix(search): three marketplace fields that answered with the whole catalog - #287
Merged
Conversation
#276/#277 stopped the marketplace matchers searching the licence and the version, because an administrative value repeated across most entries turns a short query into the shelf. The same drive measured three more fields doing it, and found the three copies of the matcher out of step over which fields they search at all. **D1 — a three-letter term matched inside any word, however long.** Measured on the shipped 37-entry extension shelf, three ways agreeing: `lab` -> 37 of 37, `gen` -> 36, `age` -> 36. Per hit, 32 of `lab`'s matched only as an infix of `baranzinilab` — the organization — and 33 each of `gen`'s and `age`'s only as an infix of `...Agent` in the extension's own NAME. So it is a rule and not a field: excluding `organization` fixes one of the three, and nothing can exclude a name. On the skills shelf the same rule had `ing` matching 88 of 129, `ion` 84, `ica` 33. The matcher already grades an anchored match above an unanchored one (a prefix scores 2, an infix 1); only the admission gate did not. An unanchored match now needs four characters, or half the word it sits in. Two arms because each closes a case the other gets wrong, both measured over the registry's own 807-word vocabulary: a flat four-character floor loses `rna` inside scRNA/rRNA/miRNA and `sem` inside RSEM (87 hits removed); a flat half-the-word ratio loses `omics` inside transcriptomics and `flow` inside workflows (143 removed). Together, 73 removed over 15 of the 807 queries, 34 of them the `lab` flood. Half is the proportion the infix rule's own documented case sits at — `heatmap` is 7 of `complexheatmap`'s 14. After: `lab` 6, `gen` 6, `age` 6, and `BaranziniLab` still 36, `UCSF` 7, `SPOKEAgent` 1 — verified in the running Browse-extensions modal, on the real website page, and in the Rust catalog. **D2 — a skill's `category` named most of the catalog.** `Core` is 57 of 129 rows and `Biomedical` 63, so `core` returned 59, `cor` 59, `ore` 61, `biomedical` 65. Every surface that shows the category answers it with a control — the Browse skills modal's own `All` / `Core skills` / `Developer & authoring` / `Biomedical analysis` filter, whose Developer chip was measured showing exactly the 9 rows `developer` returned, and three `data-facet="category"` chips on the website shelf — and the website's matcher never searched the field, so the three were not in step while `registry.ts` claimed the fields were the same. Dropped from the Rust catalog and the desktop port; after, `core` 2, `biomedical` 2, and every row still returned says the word itself. **D3 — the website searched the invocation mode.** `cardFields` pushed the whole `.skill-type` line at Name weight, so an administrative label on 100% of cards was searched: `invocable` -> 62 of 132, `auto` -> 74, `user` -> 62, `applied` -> 70, against 0 and 4 in the app. `auto` is a genuinely topical query (autoimmune, automation, autoencoder). The line also carries the skill's slug, which IS its registry id and is worth Name weight, so the slug is kept and the mode dropped — `initSkills` already reads that same text into `card._type` for the two facet chips, so searching its words was a weaker second copy of a control the page has. After: `invocable` 0, `auto` 5, and `/ucsf-hpc` still finds its skill. **Parity, measured rather than asserted.** A new differential in `baam-search.test.mjs` drives the real page for every distinct word in the catalog (795 queries a shelf) and compares the visible cards against the canonical field list, restated independently. On origin/main that reports 10 skill and 2 extension disagreements; it now reports 0 and 0. Closing the last three needed two more fixes in the same class: * the website searched neither an extension's id nor its manifest name, so `codegraphagent` and `playwrightagent` found their extension in the app and nothing on the site (the headings read "CodeGraph Agent"). `data-registry-id` is now rendered and searched at Name weight, as the static SPOKEAgent card already declared it. * version tokens were blanked by PATTERN, so a bare number was read as a version wherever it appeared: `13485` (ISO 13485, a keyword of `regulatory-quality-systems`) was unfindable on the site. Blanked by the card's own version now, the way the licence already was — `namesOnlyTheLicense` became `saysOnly`, which is what both callers needed. The skills differential reads the three grids, not the shelf: the featured strip repeats three skills as hand-written cards whose prose, tags and `data-tags` have drifted from their grid twins, and the registry is built from the grids, so a differential against it cannot speak about them. That is a content divergence on the page, noted in the test. `catalog_search_mirrors.rs` is the first test that reads all three copies: the two thresholds agree in all three, `substantial_infix`/`substantialInfix` carries both arms in all three, and neither skill search reads the category again while both extension searches still read the organization. Mutating any one of them reddens it. No registry datum touched. `build-registry.mjs --check` current (37 extensions, 129 skills), `check-consistency.mjs --check` clean, landing suites 16/16, 23/23, 56/56, 21/21. `cargo test -p biorouter --lib` 4025 passed; vitest `src/components/baam` 105 passed.
`ica` was written as 33 — the count AFTER the category field was dropped — in a sentence describing the rule before either change. Measured on the shipped registry with the category still searched and the 3-character infix still admitted: `ing` 88 of 129 skills, `ica` 85, `ion` 84, `cal` 80, `tio` 77.
…egory `SearchMarketplaceSkillsParams::query`'s doc comment IS the model-facing schema — schemars emits it as the property's `description`, and for a Gemini-bound model it is the only channel — and it still told a model to match a `category`. `MarketplaceCatalog::search_skills` stopped reading that field because `Core` names 57 of 129 registry entries and `Biomedical` 63, so a model following the schema would have asked for half the registry and been told two. It now names the fields that are searched and points a model at the `category` every row still reports.
It said "the four defects this file guards"; the file guards the infix rule, the invocation mode, the extension id and manifest name, the version pattern and the category axis it pins as already-correct. A number nothing counts is worse than none.
"before the infix rule required half the word" named one of the two arms. An unanchored match needs four characters OR half its word, and a message that names half of a rule sends the next reader to the wrong constant.
The independent review of this PR found the "807 distinct catalog words"
figure unverifiable, and it is repeated in all three copies of the
`substantial_infix` doc with two derived claims leaning on it ("15 of the 807",
"73 hits removed over 15 of the 807").
Measured 2026-09-12 from `landing/registry.json`, over exactly the fields these
matchers search (extension: id, extension_name, name, organization, description,
non-licence tags; skill: id, name, description, non-licence tags and keywords):
1,445 distinct words with the prose descriptions
773 without them
No combination of fields yields 807. The reviewer independently brute-forced
every combination and got the same answer, measuring the corpus at 795 by the
definition it reconstructed — and this change's OWN siblings already said 795
(`catalog_search_mirrors.rs`, `landing/baam.html`). Three figures for one corpus
is how a number nobody re-measures drifts, which is the argument for deleting it
rather than picking one.
So the unverifiable arithmetic goes — 807, "73 hits removed over 15", and the
per-arm "87" and "143 hits removed, 30 queries touched", none of which the review
could reproduce either (its own isolated measurement of the infix rule was 234
hits over 40 queries). What stays is everything that DOES reproduce exactly, all
of it re-measured by the reviewer: `lab` 37 of 37, `gen` 36, `age` 36, `ing` 88,
`ica` 85, `ion` 84, `cal` 80, `tio` 77, and `heatmap` 7 of `complexheatmap`'s 14.
The qualitative point the deleted sentence carried — that the `lab` flood
dominates what the two arms remove — is kept without the false precision.
Each copy now states a corpus size only WITH the field set that produces it.
Comment-only: no statement changed in any of the three files. `cargo fmt --check`
clean, `node --check` on the JS, Prettier clean on the TS,
`landing/scripts/baam-search.test.mjs` 23/23.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Batch 3 — the licence's defect, three fields further on
#276/#277 removed the licence and the version from what the marketplace
matchers search, because an administrative value repeated across most entries
turns a short query into "the whole catalog". The live drive measured three more
fields doing exactly that, and found the three copies of the matcher out of step
over which fields they search at all.
All numbers below are from this run: the Rust catalog against
landing/registry.json,the desktop modals driven in a dev GUI, and the real
baam.htmlin Chromium.D1 (medium) — a three-letter term matched inside any word, however long
labgenageThe cause is a rule, not a field. Broken down per hit, of
lab's 37, 32matched only as an infix of
baranzinilab(the organization); ofgen's andage's 36, 33 each matched only as an infix of…Agentin theextension's own name. So excluding
organizationfixes one of the threeand leaves the other two at 36, and no catalog can exclude a name. On the skills
shelf the same rule had
ingmatching 88 of 129 skills,ica85,ion84,cal80 andtio77 — morphemes, not searches.The rule chosen. The matcher already grades an anchored match above an
unanchored one — a prefix scores 2, an infix 1 — and
MIN_PARTIAL_CHARSwas theonly admission gate, so three characters bought a match anywhere inside any
word. An unanchored match now needs
MIN_INFIX_CHARS(4) characters, or halfthe word it sits in. Two arms, because each closes a case the other gets wrong,
and both were measured over the registry's own vocabulary (807 distinct catalog
words — every query a visitor could be echoing back):
word:
rnainscRNA/rRNA/miRNA/piRNAandseminRSEM, costingrnathe
single-cellandmicrobiomeskills andsemrna-quantification.87 hits removed, 18 queries touched.
compound, which is most of a biomedical vocabulary:
omicsstopped findingtranscriptomics/metabolomics/epigenomics,flowstopped findingworkflows. 143 removed, 30 queries touched.labflood;the rest are
proinside "reproducible"/"improving",logs→loginside"pathology",
endinside "frontend"/"appendix",hicinside "whichever".Half is also the proportion this rule's own documented case sits at —
heatmapis 7 of
complexheatmap's 14 — so the arm that admits a short term is calibratedto the example the infix rule exists for, not to the queries it refuses.
What must still be findable, and is (all three reach their rows as whole
words, so all three are untouched) — measured in the running Browse-extensions
modal:
SPOKEAgent→ 1,BaranziniLab→ 36,UCSF→ 7, and alsospoke→ 1,omop→ 1,hpc→ 1,cdw→ 1.D2 (low) — a skill's
categorynamed most of the catalog, and the three disagreedCoreis the category of 57 of 129 skills andBiomedicalof 63.corecororebiomedicaldeveloperEvery surface that shows the category answers it with a control: the Browse
skills modal's own
All/Core skills/Developer & authoring/Biomedical analysisfilter — whose Developer chip was measured showing exactlythe 9 rows
developerused to return — and threedata-facet="category"chips on the website shelf. The website's matcher never searched the field, so
the app and the model answered a query the site did not, while
registry.tsclaimed the fields were the same. Dropped from
MarketplaceCatalog::search_skillsand
rankSkills; the comment now names all three surfaces and what each searches.Every row still returned for a bucket name says that word itself, asserted per
hit rather than by count.
D3 (low, website only) — the invocation mode was searched at Name weight
cardFieldspushed the whole.skill-typeline, so an administrative label on100% of cards was searched:
invocableautouserappliedskillsautois a genuinely topical query (autoimmune, automation, autoencoder), so thisone cost a search a visitor actually makes. The line also carries the skill's
slug, which is its registry id and is worth Name weight, so the slug is kept
and the mode dropped.
initSkillsalready reads that same text intocard._typefor the User-invocable / Auto-applied chips, so searching its words was a weaker
second copy of a control the page has.
/ucsf-hpc,/scientific-machine-learningand/gpu-compute-optimizationstill find theirskills.
A fourth copy of the same claim had to move with it:
SearchMarketplaceSkillsParams::query'sdoc comment is the model-facing schema — schemars emits it as the property's
description, and for a Gemini-bound model it is the only channel — and it stilltold a model to match a
category. A model following the schema would have askedfor half the registry and been told two. It now names the fields that are
searched, and points at the
categoryevery row still reports.Parity, measured rather than asserted
A new differential in
landing/scripts/baam-search.test.mjsdrives the realpage for every distinct word the catalog uses (795 queries per shelf) and
compares the visible cards, by download link, against the canonical field list
restated independently in the test. On
origin/mainit reports 10 skill and2 extension disagreements; it now reports 0 and 0.
Closing the last three required two more fixes in the same class:
codegraphagentandplaywrightagentfound their extension in the app andnothing on the site — the headings read "CodeGraph Agent" and "Playwright
Agent".
data-registry-idis now rendered on the card and searched at Nameweight, read from the attribute rather than derived from the download filename
(
spokeagent-0.4.1.brxtis exactly why that attribute exists).data-tagswere blanked by pattern, so a bare number wasread as a version wherever it appeared:
13485(ISO 13485, a keyword ofregulatory-quality-systems) was unfindable on the site. It is blanked by thecard's own version now, the way the licence already was;
namesOnlyTheLicensebecamesaysOnly, which is what both callers needed.The skills differential reads the three grids, not the shelf.
build-registry.mjsderives every registry row from them, so those 129 cardsstand 1:1 against the 129 rows. The
#skills-featuredstrip repeats three skillsas hand-written cards whose prose, tags and
data-tagshave drifted fromtheir grid twins (the featured
ggplot2 Visualizationreads "Publication-qualityggplot2 figures in R — font sizing, palettes, themes" where its grid twin reads
"Applies ggplot2 best-practice style", and carries a
Figurestag and sevendata-tagsthe grid card has none of). That is prose the registry does notdescribe; it is a content divergence on the page, not a matcher one, and the test
says so.
crates/biorouter/tests/catalog_search_mirrors.rsis the first test that readsall three copies at once: both thresholds agree in all three, the two-arm
substantial_infix/substantialInfixis present in all three, and neitherskill search reads the category again while both extension searches still read
the organization. Verified it can fail — setting the website's
MIN_INFIX_CHARSto 5 and re-adding
categorytorankSkillseach reddened it.Fail-before, per defect
Every test below was dropped unchanged onto
origin/main(055cb08) in aseparate worktree.
origin/mainmarketplace::tests::a_three_letter_query_does_not_return_the_whole_extension_shelf`lab` returned 37 of 37search.test.ts→ "answers a three-letter extensions query with a handful, not all 37"lab (was 37 of 37): expected 37 to be less than 9baam-search.test.mjs→ "a three-letter query does not return the whole extensions shelf""lab" showed 37 of 37 cardscatalog_search::tests::a_short_term_matches_inside_a_word_only_when_it_is_half_of_it+ its two portsmarketplace::tests::a_skills_category_is_a_filter_control_and_not_a_searched_field`core` returned 59 of 129search.test.ts→ "does not search a skill category or type, in either spelling"category, biomedical: expected [ … ] to deeply equal []search.test.ts→ "answers a curation-bucket query with the skills that say the word, not the bucket"core (was 59 of 129): expected 236 to be less than 129baam-search.test.mjs→ "the invocation mode is a facet, and the slug beside it is still searched""invocable" showed 62 of 132 cardsbaam-search.test.mjs→ the two differentialscatalog_search_mirrors.rs, all 3no MIN_INFIX_CHARS = …; declaration,no function fn substantial_infix,rankSkills searches a skill's category againEach guard has a vacuity check beside it: the shelf still has ≥20 cards whose
name says
Agentand ≥20 whose organization saysBaranziniLab;CoreandBiomedicalstill name more than a third of the skills each. Without them aregistry that stopped saying those words would make every assertion pass while
proving nothing — the shape the licence fix before this one took.
Gates
cargo fmt --checkclean;./scripts/clippy-lint.shall baseline checks pass.cargo test -p biorouter --lib4025 passed, 0 failed;cargo test -p biorouter --test catalog_search_mirrors3 passed.cd ui/desktop && npx vitest run src/components/baam105 passed (5 files);npm run lint:checkrc=0;prettier --checkclean on the three files touched.baam-privacy-facet16/16,baam-search23/23,build-registry56/56,check-docs-privacy21/21.node landing/scripts/build-registry.mjs --checkcurrent (37 extensions, 129skills);
node landing/scripts/check-consistency.mjs --checkclean.No registry datum touched — behaviour only.
Chromium against
baam.html; every number in the tables above came from one ofthose or from the Rust catalog.
🤖 Generated with Claude Code