fix(search): a licence republished as a tag is no longer searchable - #276
Merged
Conversation
PR #242 and its port PR #255 stopped searching the `license` FIELD, because every BAAM entry is Apache-2.0 and a licence separates nothing. The defect survived, one field over: the registry publishes the licence again as one of each entry's own TAG chips — and, for a skill, a third time among its keywords — and labels are searched, rightly. A test asserting "the license is not searched" passed while `PACS` still listed most of the catalog. Measured by driving the real Browse-extensions modal against the live 37-entry registry, with the field already gone: (empty) 37 PACS 31 pac 31 apache 31 Apache-2.0 32 zzzznope 0 `PACS` ≡ `pac` ≡ `apache` = 31 identifies the path: `PACS` → its singular `pac` → inside `apache` → the `Apache-2.0` chip on 31 rows, none of them about PACS (BenchlingAgent, DNAnexusAgent, OMEROAgent…). One rule, in all three copies: a label that says nothing its entry's own licence does not is dropped when the searchable text is assembled. By WORDS, not by equality — the tag is `Apache-2.0` and the keyword is `apache`, so an equality test drops the tag and leaves the keyword matching 49 skills. Over all 166 live entries the rule drops the 129 licence labels and nothing else. Counts after, identical on both matchers (Rust / TypeScript): (empty) 37 -> 37 ext, 129 -> 129 skill PACS 31 -> 1 ext, 51 -> 7 skill pac 31 -> 1 ext, 51 -> 7 skill apache 31 -> 0 ext, 49 -> 0 skill Apache-2.0 32 -> 1 ext, 49 -> 0 skill Every legitimate query is byte-identical before and after: `R scripting ggplot visualization`, `r-scripting`, `SPOKE knowledge graph`, `ggplot`, `heatmap`, `python`. Differential parity re-run over 1,192 shared queries × 2 catalogs: 0 set mismatches, before and after (the 391 tie-order differences are the one divergence the port's header already documents — Rust breaks ties by id, the document by registry order). The website shelf shared the defect and was worse, because its `data-license` is on every card rather than only the tagged ones: `apache`, `Apache-2.0` and `pac` each matched all 37 extension cards and all 132 skill cards. Registry data is untouched; only the haystack changed.
Broccolito
added a commit
that referenced
this pull request
Sep 12, 2026
#276 and #277 both change how the website's shelves are searched, from different ends, and the merge needs both: * #277 replaces the whole-phrase `hay.indexOf(q)` with the tokenised, ranked matcher in `landing/marketplace-search.js` (`hits.has(c)`), a port of `catalog_search.rs`. * #276 removes the LICENCE from what is searched — it reaches the haystack as `data-license`, as an `Apache-2.0` chip inside `textContent`, and as a tag. Taken naively the merge keeps #277's matcher and silently loses #276's fix, because #277 assembles its own fields in `cardFields` rather than through #276's `searchHaystack`. Composed instead: - The three conflicted call sites take #277's `hits.has(c)`. `searchHaystack` stays, because the FACET loop still reads `hay` and must read the licence-free one. - `cardFields` drops the licence by WORDS, not by equality: `apache` and `Apache-2.0` are one licence spelled two ways, and the skills shelf carries both. `namesOnlyTheLicense` is #276's own predicate. - And `data-tags` is not the tag list — it is `name + organization + version + description + tags`, so it also carried the VERSION. `v0.2.0` tokenises to `v0`, `2`, `0`, and a bare `2` or `0` — which is exactly what "Apache-2.0" tokenises to — matched **35 of 37** cards through their version numbers alone. Version tokens are dropped there and stripped from the searched `.ext-org` label. Neither `catalog_search.rs` nor the desktop port searches a version, so this is what keeps the three in step. That took `Apache-2.0` from 35 hits to 1, and the one that remains is `ucsfomopagent`, whose description reads "v0.2.0 adds built-in OMOP/SQL-Server context" — the digits, not the licence. The Rust and desktop matchers answer this query with the same single entry. So #276's website assertion was rewritten rather than satisfied. "nothing matches Apache-2.0" is a property of a whole-phrase matcher; under a token matcher the honest claim is that **the licence explains none of it**, and the test now asserts the hits for `Apache-2.0` equal the hits for `2 0`. `apache` and `APACHE` still assert exactly `[]`. landing: baam-privacy-facet 16/16, baam-search 17/17, build-registry 56/56, check-docs-privacy 21/21, `build-registry.mjs --check` current (37 extensions, 129 skills), `check-consistency.mjs --check` clean. No registry datum touched.
Broccolito
added a commit
that referenced
this pull request
Sep 12, 2026
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.
The finding, measured in the running app
The Browse-extensions modal on merged
main, driven against the live 37-entry registry:PACSpacapacheApache-2.0zzzznopePACS≡pac≡apache= 31 is the proof:PACS→ the matcher's plural→singular fallback →pac→ substring of "Apache-2.0" → 31 of 37 entries, none PACS-related (BenchlingAgent, DNAnexusAgent, LatchBioAgent, LabArchivesAgent, OMEROAgent…). Every match displays anApache-2.0tag.Diagnosis — the tag is the live path, verified
PR #242 (Rust) and PR #255 (the TypeScript port) both stopped searching the
licensefield. But the licence value is also published in each entry'stagsarray, and tags are searched — legitimately;MCP,Imaging,ELN,Registryare what a tag is for. So the field was removed and the same string kept matching through a different field, andsearch.test.ts'sdoes not search the license, in either catalogkept passing because its fixture entry has no tags.Confirmed three ways rather than inferred:
Apache-2.0tag — the same 31, by id, that the modal returned. 49 of 129 skills carry it too, and a separateapachekeyword.MarketplaceCatalog::search_extensions/search_skills(crates/biorouter/src/marketplace.rs) andrankSkills/rankExtensions(ui/desktop/src/components/baam/registry.ts) both omitlicenseand bothextendthe field list with everytag(and, for skills, everykeyword).apachein Rust and fails 4 of the new TS assertions.One detail worth recording:
extCardHtmlandbuildExtChipsinlanding/baam.htmlalready filter/^apache/i— for tag-row space and chip noise, not for search. That near-miss is part of why this looked handled.The fix
One rule, in all three copies: when assembling an entry's searchable text, drop any label that says nothing the entry's own
licensedoes not. Compared by words, not by equality.Equality is not enough, and this is the crux: the tag is
Apache-2.0and the skills keyword isapache. An equality test drops the tag and leaves the keyword matching 49 skills — the same half-fix, one field further over.crates/biorouter/src/catalog_search.rs—names_only_the_license, applied by the two field builders inmarketplace.rs.ui/desktop/src/components/baam/search.ts—namesOnlyTheLicense, applied bylabelFieldsinregistry.ts. (Its header says "A change to a rule below is a change to both files"; this is that change.)landing/baam.html— the third copy, assearchHaystack.Measured over all 166 live entries, the rule drops the 129 licence labels and nothing else.
Rejected alternatives
MCP,ELN,Imaging,Registry).buildExtChipsalready does this, which is how the near-miss above happened.)pac/apachematch directly anyway, so it would fix nothing.license— the suggested shape, and it is the half-fix described above.The cost, stated
A licence id built from a topical word (
Python-2.0,Ruby,PostgreSQL) on an entry that also tags itself with that word would lose that tag. No entry in the registry is such a case, and an equality test pays a smaller version of the same cost. Documented at both call sites.Before / after
The two app matchers — identical counts, both sides
PACSpacapacheApache-2.0zzzznopeR scripting ggplot visualizationr-scriptingSPOKE knowledge graphggplotheatmappythonThe surviving
PACShits are the substring rule working, not the defect:pacinside PacBio (long-read-sequencing), package (replication-package-audit,skill-creator), workspace (latchbioagent), plusbiomedical-imaging-pathology, which really does keywordpacs. Every legitimate query is unchanged, ids included.The website shelf — the same defect, and worse
data-licenseis on every card, not only the 31/49 that carry the tag, so the licence matched the whole shelf:apacheApache-2.0pacMCPggplot/heatmap/pythonThree licence paths there:
data-license, theApache-2.0chip inside an authored skill card'stextContent, and theapachekeyword in a skill'sdata-tags. All three closed; the chip still renders. Registry data is untouched —build-registry.mjs --checkandcheck-consistency.mjs --checkboth pass.Parity
Differential harness re-run over 1,192 shared queries × 2 catalogs (both sides fed one generated query file: every registry id, name, organization, tag and keyword, plus description phrases and the QA queries):
search.ts's header already documents: "Only a tie can fall differently, because each side breaks ties by its own registry order — the document's here, the id's in Rust."Tests
Each fails before the fix and passes after.
crates/biorouter/src/marketplace.rs—a_licence_republished_as_a_label_is_not_searchable_through_it, against the embedded live registry, not a fixture (a fixture without the licence tag cannot fail — that is how this got through). Carries a vacuity guard: it asserts the registry still republishes its licence through all three label paths, and says so loudly if it stops.crates/biorouter/src/catalog_search.rs—a_label_naming_only_the_licence_is_recognised_in_either_spelling.ui/desktop/src/components/baam/search.test.ts—does not search the license republished as a tag or a keyword; anamesOnlyTheLicenseblock mirroring the Rust cases; and a block over the bundledregistry.fallback.jsonincluding "changes nothing else about any query", which compares the real registry against a copy with the licence labels removed from the data, so an over-broad rule (dropping every tag) fails there even though it would satisfy every other assertion.landing/scripts/baam-privacy-facet.test.mjs— two Playwright tests driving the real#baam-searchinput on both shelves, with their own vacuity guards. This file is in theshelfjob and in thecheckjob the Pages deploy depends on, so the website gate is consulted before publishing.Verification run
cargo test -p biorouter --libcargo test -p biorouter --lib -- catalog_search marketplacevitest src/components/baam/node --test landing/scripts/baam-privacy-facet.test.mjsnode --test landing/scripts/build-registry.test.mjsnode --test landing/scripts/check-docs-privacy.test.mjsbuild-registry.mjs --check/check-consistency.mjs --checkcargo fmt --check,tsc --noEmit,eslint,prettier --check⚠
./scripts/clippy-lint.shfails on something this PR does not touch.clippy::too_many_linesonhandle_execute_code(crates/biorouter/src/agents/code_execution_extension.rs:1837, 102/100 lines), from PR #246. Verified pre-existing: reverting this PR's two Rust files toorigin/mainand re-running reproduces the identical failure. Nothing else in the script fails.🤖 Generated with Claude Code