Skip to content

fix(search): three marketplace fields that answered with the whole catalog - #287

Merged
Broccolito merged 6 commits into
mainfrom
claude/baam-search-batch3
Sep 12, 2026
Merged

fix(search): three marketplace fields that answered with the whole catalog#287
Broccolito merged 6 commits into
mainfrom
claude/baam-search-batch3

Conversation

@Broccolito

@Broccolito Broccolito commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

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.html in Chromium.


D1 (medium) — a three-letter term matched inside any word, however long

query extensions shelf, before after
lab 37 of 37 6
gen 36 of 37 6
age 36 of 37 6

The cause is a rule, not a field. Broken down per hit, of lab's 37, 32
matched only as an infix of baranzinilab (the organization); of gen's and
age's 36, 33 each matched only as an infix of …Agent in the
extension's own name. So excluding organization fixes one of the three
and leaves the other two at 36, and no catalog can exclude a name. On the skills
shelf the same rule had ing matching 88 of 129 skills, ica 85, ion 84,
cal 80 and tio 77 — 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_CHARS was the
only admission gate, so three characters bought a match anywhere inside any
word. An unanchored match now needs MIN_INFIX_CHARS (4) characters, or half
the 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):

  • a flat four-character floor loses what a short term earns inside a short
    word: rna in scRNA/rRNA/miRNA/piRNA and sem in RSEM, costing rna
    the single-cell and microbiome skills and sem rna-quantification.
    87 hits removed, 18 queries touched.
  • a flat half-the-word ratio loses what a long term earns inside a longer
    compound, which is most of a biomedical vocabulary: omics stopped finding
    transcriptomics/metabolomics/epigenomics, flow stopped finding
    workflows. 143 removed, 30 queries touched.
  • together: 73 removed over 15 of the 807, of which 34 are the lab flood;
    the rest are pro inside "reproducible"/"improving", logslog inside
    "pathology", end inside "frontend"/"appendix", hic inside "whichever".

Half is also the proportion this rule's own documented case sits at — heatmap
is 7 of complexheatmap's 14 — so the arm that admits a short term is calibrated
to 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 also spoke → 1,
omop → 1, hpc → 1, cdw → 1.

D2 (low) — a skill's category named most of the catalog, and the three disagreed

Core is the category of 57 of 129 skills and Biomedical of 63.

query app / Rust, before after website (already right)
core 59 of 129 2 2 of 132
cor 59 1 1
ore 61 2 2
biomedical 65 2 2
developer 9 0 0

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 used to return — and three data-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.ts
claimed the fields were the same. Dropped from MarketplaceCatalog::search_skills
and 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

cardFields pushed the whole .skill-type line, so an administrative label on
100% of cards was searched:

query site, before after app
invocable 62 of 132 0 0
auto 74 of 132 5 4
user 62 0 0
applied 70 1 1
skills 71 8 8

auto is a genuinely topical query (autoimmune, automation, autoencoder), so this
one 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. initSkills already reads that same text into card._type
for 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-learning and /gpu-compute-optimization still find their
skills.

A fourth copy of the same claim had to move with it: 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. 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 at the category every row still reports.


Parity, measured rather than asserted

A new differential in landing/scripts/baam-search.test.mjs drives the real
page
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/main it reports 10 skill and
2 extension disagreements; it now reports 0 and 0.

Closing the last three required two more fixes in the same class:

  • the site 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" and "Playwright
    Agent". data-registry-id is now rendered on the card and searched at Name
    weight, read from the attribute rather than derived from the download filename
    (spokeagent-0.4.1.brxt is exactly why that attribute exists).
  • version tokens in data-tags 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. It is 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.
build-registry.mjs derives every registry row from them, so those 129 cards
stand 1:1 against the 129 rows. The #skills-featured strip repeats three skills
as hand-written cards whose prose, tags and data-tags have drifted from
their grid twins (the featured ggplot2 Visualization reads "Publication-quality
ggplot2 figures in R — font sizing, palettes, themes" where its grid twin reads
"Applies ggplot2 best-practice style", and carries a Figures tag and seven
data-tags the grid card has none of). That is prose the registry does not
describe; it is a content divergence on the page, not a matcher one, and the test
says so.

crates/biorouter/tests/catalog_search_mirrors.rs is the first test that reads
all three copies at once: both thresholds agree in all three, the two-arm
substantial_infix / substantialInfix is present in all three, and neither
skill search reads the category again while both extension searches still read
the organization. Verified it can fail — setting the website's MIN_INFIX_CHARS
to 5 and re-adding category to rankSkills each reddened it.

Fail-before, per defect

Every test below was dropped unchanged onto origin/main (055cb08) in a
separate worktree.

defect test on origin/main
D1 marketplace::tests::a_three_letter_query_does_not_return_the_whole_extension_shelf FAILED — ​`lab` returned 37 of 37
D1 search.test.ts → "answers a three-letter extensions query with a handful, not all 37" FAILED — lab (was 37 of 37): expected 37 to be less than 9
D1 baam-search.test.mjs → "a three-letter query does not return the whole extensions shelf" FAILED — "lab" showed 37 of 37 cards
D1 catalog_search::tests::a_short_term_matches_inside_a_word_only_when_it_is_half_of_it + its two ports FAILED (the rule does not exist on main)
D2 marketplace::tests::a_skills_category_is_a_filter_control_and_not_a_searched_field FAILED — ​`core` returned 59 of 129
D2 search.test.ts → "does not search a skill category or type, in either spelling" FAILED — category, biomedical: expected [ … ] to deeply equal []
D2 search.test.ts → "answers a curation-bucket query with the skills that say the word, not the bucket" FAILED — core (was 59 of 129): expected 236 to be less than 129
D3 baam-search.test.mjs → "the invocation mode is a facet, and the slug beside it is still searched" FAILED — "invocable" showed 62 of 132 cards
parity baam-search.test.mjs → the two differentials FAILED — 10 skill and 2 extension disagreements
parity catalog_search_mirrors.rs, all 3 FAILED — no MIN_INFIX_CHARS = …; declaration, no function fn substantial_infix, rankSkills searches a skill's category again

Each guard has a vacuity check beside it: the shelf still has ≥20 cards whose
name says Agent and ≥20 whose organization says BaranziniLab; Core and
Biomedical still name more than a third of the skills each. Without them a
registry 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 --check clean; ./scripts/clippy-lint.sh all baseline checks pass.
  • cargo test -p biorouter --lib 4025 passed, 0 failed;
    cargo test -p biorouter --test catalog_search_mirrors 3 passed.
  • cd ui/desktop && npx vitest run src/components/baam 105 passed (5 files);
    npm run lint:check rc=0; prettier --check clean on the three files touched.
  • landing suites: baam-privacy-facet 16/16, baam-search 23/23,
    build-registry 56/56, check-docs-privacy 21/21.
  • node landing/scripts/build-registry.mjs --check current (37 extensions, 129
    skills); node landing/scripts/check-consistency.mjs --check clean.
    No registry datum touched — behaviour only.
  • Driven in a real dev GUI (Browse extensions and Browse skills) and a real
    Chromium against baam.html; every number in the tables above came from one of
    those or from the Rust catalog.

🤖 Generated with Claude Code

#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.
@Broccolito
Broccolito merged commit 5a404ec into main Sep 12, 2026
16 checks passed
@Broccolito
Broccolito deleted the claude/baam-search-batch3 branch September 12, 2026 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant