fix(marketplace, extension-manager): rank a multi-word marketplace query (F5); an empty list_resources explains itself (F8) - #242
Merged
Conversation
…mpty result (F5)
The 2026-09-10 composer QA run measured `skills__searchMarketplaceSkills
{query: "R scripting ggplot visualization"}` -> `total: 0` against a live
registry where `ggplot` alone found 2 and `r-scripting` found 1. The matcher
asked whether the WHOLE lowercased query was a substring of one field, so any
phrase failed, and the model told the user the marketplace had nothing.
Both marketplace searches (skills and extensions) shared that matcher, so both
move onto `marketplace/search.rs`: the query is split into terms at whitespace
and punctuation, an entry matching ANY term is a hit, and hits are ranked by a
verbatim match (what the old matcher found, so nothing is lost), then by how
many terms matched, then by where (id/name > labels > description). Terms under
three characters match whole words only, so `r` finds the R language rather
than every word containing an r, and filler words are dropped.
Each hit now carries `matchedTerms`, the result carries the `terms` the query
was read as, and a query that still matches nothing returns `guidance` saying
how many entries the registry holds and to try shorter terms. The extension
count is what the caller may see, never the registry's size, so a public model
is never told how many private rows exist.
…ot "" (F8) `extensionmanager__list_resources` answered `""` when no extension had a resource, so the model could only report that the tool "returned an empty string" — every other empty or refusing result explains itself. An empty listing now says which extensions were asked and that none is publishing anything, how many reachable extensions do not offer resources, and which could not be listed or were refused to this model (a cross-affiliation mismatch). Naming a named extension says it has none. Every name is filtered through Gate E's roster (`allowed_extension_keys`), the rule #219 set for the not-found message next door: a public caller is never named, or counted, a private extension the fan-out declined to reach.
…pty resource listing explains itself
…own method list_resources had grown to 105 lines, over the too_many_lines baseline. The Gate E filtering moves into unlisted_resources unchanged.
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.
Why
Two findings from the 2026-09-10 composer-driven QA run on merged
main7c96d79(
~/biorouter-runs/test-drive/qa-f/report.md, F5 at lines 796-805 and F8 at 825-829; themeasurements are checks 7 and 8, lines 297-335 and 354-385).
F5 (MEDIUM): the marketplace search did not tokenise a multi-word query. In one chat, against one
live registry (
source: "live",stale: false):R scripting ggplot visualizationggplotr-scriptingThe model reported "no matching marketplace skills found". That was false: the registry held every
skill the phrase names.
F8 (LOW):
extensionmanager__list_resourcesreturned"". The model could only report that"it returned an empty string". Every other empty or refusing result in that run explained itself.
Root cause
marketplace.rs'ssearchable()asked whether the WHOLE lowercased query was asubstring of one field. A single word worked, and any phrase failed.
search_skillsandsearch_extensionsboth called it, so the extension marketplace search had the same defect(measured by reading the code: they were its only two callers).
list_resources_from_extensionjoined an empty resource list into""and returned it asa listing. The fan-out passed that through, and so did the named branch.
What changed
F5:
marketplace/search.rs, shared by both marketplace searchesde-duplicated.
r-scriptingbecomesr+scripting, andggplot2stays one word.and how exactly (the whole word > its start > inside it);
rfinds the R language rather thanevery word containing an
r.a,about,for,or, … plusskill(s)/extension(s)for their owncatalog) are dropped, unless nothing else is left. In a union they would otherwise inflate the
term count of every entry whose prose uses them. A plural also falls back to its singular
(
heatmaps→heatmap).termsthe query was read as, plus eachhit's
matchedTerms. A model reading a long list can then tell an all-term hit from one thatmatched a single common word.
guidancesentence. For skills it says how manyskills the registry holds; for extensions, how many extensions are available to this model. It also
suggests one shorter term, or browsing with no query.
browse_extensions(caller)— what the caller may see — never theregistry's size, which for a public model would count the private rows. A test asserts that a
public caller missing on a hidden private row reads byte-for-byte like a miss on nothing, so
the guidance cannot become the private catalog's oracle.
The payload assembly moved into
marketplace_skill_page_json/marketplace_extensions_jsonsoit is testable without the network.
F8: an empty
list_resourcesis a sentenceNo resources to list. `computercontroller` supports resources and was asked, but is not publishing any right now. The other 11 extensions this chat can reach do not offer resources.The sentence also names extensions that support resources but failed to list, and ones refused to
this model by a cross-affiliation mismatch (DR-26 lists and marks those rather than hiding them).
Without that group, the count would describe them as extensions that "do not offer resources".
`developer` has no resources to list: it supports resources but is not publishing any right now.allowed_extension_keys(admitted)—Gate E's roster, the same verdict privacy: stop the resource surface handing a public caller the extension roster (M6), and stop it calling an absent extension private (M18) #219 used for the not-found message in
read_resource_tool.A refused private extension is never named and never counted.
admittedis threaded, neverresampled. For a named private extension, a public caller is still refused, in the same words
a name that is not installed gets (M18). It is never told "has no resources", which would confirm
the extension is installed.
list_resources_from_extensionnow returns nothing for an empty list, not"". Theempty-listing roster lives in
unlisted_resources, which keepslist_resourcesunder thetoo_many_linesbaseline.Tests
Fail-before was measured. I reverted only the production hunks, not the tests: F5's term loop
was disabled (leaving exactly the old verbatim-substring behaviour), and F8's empty-list handling was
put back. Then the new tests ran against that tree:
New tests:
marketplace.rs). Seven registry rows are copiedverbatim from
landing/registry.jsonat 7c96d79 and frozen, so the exact ranking cannot drift:ggplotreturns exactly the two QA measured;r-scriptingranksr-scriptingfirst;scientific-visual-communication(visual, notvisualization) is correctly not a hit.phrase finds every skill each of its words finds alone.
marketplace/search.rs): tokenising, filler, whole-word short terms,in-word and plural matching, union ranking, verbatim hits, browse.
best first).
termsandmatchedTermspresent,guidanceon zero hits with the registrysize, browsing unchanged. For extensions, the count is visible-only and a hidden-row miss reads
like a miss on nothing.
developerand not
ucsfomopagent, and the private server's contact counter stays at 0. The private sentencenames both.
indistinguishable from an absent name.
withheldgroup, driven end to end with a Stanford-affiliated privatemodel.
Results (
BIOROUTER_DISABLE_KEYRING=trueon every run):No OpenAPI regeneration: no route, handler signature or
#[utoipa::path]changed.Runtime verification
I ran my own sandboxed instance with
~/biorouter-runs/launch-dev-gui.sh(CDP 9391; the QAinstances on 9371-9376 were not touched). The daemon was this worktree's own seam build, confirmed
with
ps -p <DAEMON_PID>, and the model wasversa_azure/gpt-5.5-2026-04-24. Both prompts weretyped into the composer of one chat.
1. "Search the BAAM marketplace for a skill about R scripting or ggplot visualization"
The model called
skills.searchMarketplaceSkills({ query: "R scripting ggplot visualization" })inside
execute_code: the exact query QA measured attotal: 0. The raw tool result, abridged tothe envelope and each hit's id:
The model answered "Found 11 BAAM marketplace skill matches. The most relevant are: 1. ggplot2
Visualization … 2. R Scripting … 3. Data Visualization …".
2. "List the resources the extensions offer"
The model called
extensionmanager.list_resources({}). The raw tool result:The model answered: "No extension resources are currently available.
computercontrollersupportsresources, but it is not publishing any right now. The other reachable extensions/capabilities in this
chat do not offer resources."
This runtime model is private. The public-caller sentence is covered by the unit tests above, which
bind a public capability; a public runtime model is ruled out by the test-model rule
(
versa_azureonly).Not done here (same defect, different code)
ui/desktop/src/components/baam/registry.ts,skillMatches/extensionMatches) is a separate TypeScript copy of the old whole-phrasesubstring match. A user typing
R scripting ggplotinto the search box still gets nothing.skills__searchSkills,handle_search_skills) ANDs itswhitespace terms. The same phrase finds an installed skill only if one skill contains all four
words.
Neither shares this matcher, and QA measured neither, so both are left for a follow-up.
🤖 Generated with Claude Code