Skip to content

fix(search): a licence republished as a tag is no longer searchable - #276

Merged
Broccolito merged 1 commit into
mainfrom
fix/license-tag-still-searched
Sep 12, 2026
Merged

fix(search): a licence republished as a tag is no longer searchable#276
Broccolito merged 1 commit into
mainfrom
fix/license-tag-still-searched

Conversation

@Broccolito

Copy link
Copy Markdown
Collaborator

The finding, measured in the running app

The Browse-extensions modal on merged main, driven against the live 37-entry registry:

query matches
(empty) 37
PACS 31
pac 31
apache 31
Apache-2.0 32
zzzznope 0

PACSpacapache = 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 an Apache-2.0 tag.

Diagnosis — the tag is the live path, verified

PR #242 (Rust) and PR #255 (the TypeScript port) both stopped searching the license field. But the licence value is also published in each entry's tags array, and tags are searched — legitimately; MCP, Imaging, ELN, Registry are what a tag is for. So the field was removed and the same string kept matching through a different field, and search.test.ts's does not search the license, in either catalog kept passing because its fixture entry has no tags.

Confirmed three ways rather than inferred:

  1. Data. Exactly 31 of 37 extensions carry an Apache-2.0 tag — the same 31, by id, that the modal returned. 49 of 129 skills carry it too, and a separate apache keyword.
  2. Field assembly. MarketplaceCatalog::search_extensions / search_skills (crates/biorouter/src/marketplace.rs) and rankSkills / rankExtensions (ui/desktop/src/components/baam/registry.ts) both omit license and both extend the field list with every tag (and, for skills, every keyword).
  3. Fail-before. Neutralising the new predicate reproduces exactly 31 extensions for apache in Rust and fails 4 of the new TS assertions.

One detail worth recording: extCardHtml and buildExtChips in landing/baam.html already 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 license does not. Compared by words, not by equality.

Equality is not enough, and this is the crux: the tag is Apache-2.0 and the skills keyword is apache. 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.rsnames_only_the_license, applied by the two field builders in marketplace.rs.
  • ui/desktop/src/components/baam/search.tsnamesOnlyTheLicense, applied by labelFields in registry.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, as searchHaystack.

Measured over all 166 live entries, the rule drops the 129 licence labels and nothing else.

Rejected alternatives

  • Drop all tags — loses real search value (MCP, ELN, Imaging, Registry).
  • Hard-code "Apache" — every future licence reopens it. (buildExtChips already does this, which is how the near-miss above happened.)
  • Disable the plural→singular fallback — it is wanted, and pac/apache match directly anyway, so it would fix nothing.
  • Equality with 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

query ext before → after skill before → after
(empty) 37 → 37 129 → 129
PACS 31 → 1 51 → 7
pac 31 → 1 51 → 7
apache 31 → 0 49 → 0
Apache-2.0 32 → 1 49 → 0
zzzznope 0 → 0 0 → 0
R scripting ggplot visualization 1 → 1 11 → 11
r-scripting 1 → 1 9 → 9
SPOKE knowledge graph 3 → 3 4 → 4
ggplot 0 → 0 2 → 2
heatmap 0 → 0 1 → 1
python 0 → 0 6 → 6

The surviving PACS hits are the substring rule working, not the defect: pac inside PacBio (long-read-sequencing), package (replication-package-audit, skill-creator), workspace (latchbioagent), plus biomedical-imaging-pathology, which really does keyword pacs. Every legitimate query is unchanged, ids included.

The website shelf — the same defect, and worse

data-license is on every card, not only the 31/49 that carry the tag, so the licence matched the whole shelf:

query ext cards before → after skill cards before → after
apache 37 → 0 132 → 0
Apache-2.0 37 → 0 132 → 0
pac 37 → 1 132 → 7
MCP 37 → 37 1 → 1
ggplot / heatmap / python 0 → 0 3/1/6 → 3/1/6

Three licence paths there: data-license, the Apache-2.0 chip inside an authored skill card's textContent, and the apache keyword in a skill's data-tags. All three closed; the chip still renders. Registry data is untouchedbuild-registry.mjs --check and check-consistency.mjs --check both 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):

  • 0 set mismatches, after the change — and 0 before it, so nothing diverged.
  • 391 tie-order-only differences, which is the one divergence 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.rsa_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.rsa_label_naming_only_the_licence_is_recognised_in_either_spelling.
  • ui/desktop/src/components/baam/search.test.tsdoes not search the license republished as a tag or a keyword; a namesOnlyTheLicense block mirroring the Rust cases; and a block over the bundled registry.fallback.json including "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-search input on both shelves, with their own vacuity guards. This file is in the shelf job and in the check job the Pages deploy depends on, so the website gate is consulted before publishing.

Verification run

gate result
cargo test -p biorouter --lib 3969 passed, 0 failed
cargo test -p biorouter --lib -- catalog_search marketplace 34 passed
vitest src/components/baam/ 102 passed (5 files)
node --test landing/scripts/baam-privacy-facet.test.mjs 16 passed
node --test landing/scripts/build-registry.test.mjs 56 passed
node --test landing/scripts/check-docs-privacy.test.mjs passed
build-registry.mjs --check / check-consistency.mjs --check both current
cargo fmt --check, tsc --noEmit, eslint, prettier --check clean

./scripts/clippy-lint.sh fails on something this PR does not touch. clippy::too_many_lines on handle_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 to origin/main and re-running reproduces the identical failure. Nothing else in the script fails.

🤖 Generated with Claude Code

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
Broccolito merged commit d6fa86c into main Sep 12, 2026
16 checks passed
@Broccolito
Broccolito deleted the fix/license-tag-still-searched branch September 12, 2026 08:33
Broccolito added a commit that referenced this pull request Sep 12, 2026
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