Skip to content

fix(search): gate exact-match rank on trust and order tiers by adoption#3058

Merged
steipete merged 2 commits into
mainfrom
fix/search-ranking-squat-gate
Jul 10, 2026
Merged

fix(search): gate exact-match rank on trust and order tiers by adoption#3058
steipete merged 2 commits into
mainfrom
fix/search-ranking-squat-gate

Conversation

@steipete

@steipete steipete commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • What changed: Added a shared trust-aware ranking seam (convex/lib/searchRanking.ts) and wired it into both package catalog search (convex/packages.ts) and skill-as-package catalog search (convex/skills.ts). Two behavior changes: (1) an exact-name match with no strong trust signal (not official, no provenance/rebuild verification) and no measurable adoption is ranked with the lexical tier instead of the exact tier; (2) within a tier, a log-scale adoption bucket (identity-deduped downloads + installs) orders results before the raw text-match score. Existing tie-breakers (official, verification tier, stars, installs, downloads, recency) are unchanged. specs/search-relevance.md is amended with the new "Exact-Match Squat Gate" contract.
  • Why: One unverified publish whose name exactly equals a popular generic query used to headline that query above long-adopted, better-verified alternatives — display names are not even unique. A wrapper-plugin cluster was observed exploiting exactly this while connector suggestions were curated for the OpenClaw Control UI plugin store (feat: add Control UI plugin management openclaw#103176). Adoption is a defensible counter-signal because download metering is identity-deduped, so inflating it costs many identities while a name squat costs one publish.

Linked Issue

Screenshots

  • N/A (backend ranking change; no UI surface altered)

Behavioural Proof

  • Collection quota guard: demoted exact hits do not count toward the "enough results" short-circuits, so the fallback scan still gathers the adopted alternatives they are ranked against — without this, a top-1 query returned the squat unchallenged (found by structured review, fixed with limit-1 regression tests on both surfaces: convex/packages.public.test.ts, convex/skills.packageCatalog.test.ts).
  • New regression suite convex/lib/searchRanking.test.ts (15 tests), including the observed scenario: a zero-adoption, unverified exact-name match ("squat") now ranks below an adopted substring match, while official/verified/adopted exact matches keep today's top placement.
  • Existing search behavior suites pass unchanged: convex/packages.public.test.ts, convex/skills.packageCatalog.test.ts, convex/httpApiV1.handlers.test.ts, src/lib/packageApi.test.ts (707 tests total in those files).

Security / Trust Impact

  • Security/trust impact explained

This is abuse hardening for the search trust boundary: it removes the cheap top-slot path for name-squatting on generic queries and prices ranking influence in identity-deduped adoption or curated/provenance trust. Intended trade-off (documented in the spec): a brand-new package that exactly matches a query no longer outranks adopted related packages until it earns adoption or verification; it remains discoverable at the top of the unproven band by text relevance.

Data / Deploy Impact

  • No data/deploy impact (pure query-time ranking; no schema or stored-data changes)

Verification

  • bun run ci:static
  • Focused tests for touched behavior: bunx vitest run convex/lib/searchRanking.test.ts convex/packages.public.test.ts convex/skills.packageCatalog.test.ts convex/httpApiV1.handlers.test.ts src/lib/packageApi.test.ts
  • bun run ci:unit — the three isolated-pass/full-suite-timeout failures on this host (home hero, scan results, user badge; all unrelated React render tests) reproduce on a clean main baseline run as well (main shows 19 local failures), so they are a local-environment artifact; hosted CI is the arbiter
  • Broader gate when required: bun run ci:types-build
  • Other: N/A

An exact name match with no strong trust signal (official flag,
provenance/rebuild verification) and no measurable adoption now ranks
with the lexical tier, and a log-scale identity-deduped adoption bucket
orders results before raw text score within each tier. Shared seam in
convex/lib/searchRanking.ts covers package and skill catalog search.

Closes #3054
@steipete steipete requested review from a team and Patrick-Erichsen as code owners July 10, 2026 16:23
@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clawhub Building Building Preview, Comment Jul 10, 2026 5:15pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7d2ed33466

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread convex/packages.ts Outdated
return (
a.rankTier - b.rankTier ||
b.score - a.score ||
compareRankedSearchKeys(rankedSearchKey(a, signals(a)), rankedSearchKey(b, signals(b))) ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve trust-aware sort through HTTP fan-in

The new trust key only affects this local sort; the entries returned to the HTTP layer still carry the original rankTier/score, and the API fan-in resorts combined results with compareCatalogSearchEntries in convex/httpApiV1/packagesV1.ts using the old rankTier-then-score order. For /api/v1/plugins/search across code+bundle plugins, or /api/v1/packages/search with skills included, a zero-adoption exact-name result from one source can be promoted back above an adopted lexical result from another source, so the public API still exposes the squat vector this change is meant to close. Return an adjusted sort key/rank metadata or make the HTTP combiner use the shared trust-aware ordering.

Useful? React with 👍 / 👎.

const adoption = adoptionBucket(signals);
const demoteExact = match.rankTier === 0 && adoption === 0 && !hasStrongTrustSignal(signals);
return {
tier: demoteExact ? 1 : match.rankTier,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Collect candidates before truncating by requested limit

Demoting an untrusted exact match only works after all plausible candidates have been gathered, but both search collectors still decide whether to scan more from matches.length < targetCount before this adjusted key is applied. With limit=1, an exact slug/name hit from direct recall fills the page and prevents the fallback scan from ever seeing an adopted substring/topic/summary candidate, so the exact-name squat can remain the top and only result despite this demotion. Overfetch to the scan budget before sorting, or otherwise continue recall whenever collected entries may be demoted.

Useful? React with 👍 / 👎.

…s are collected

A demoted exact-name hit filled the collection quota before the fallback
scan ran, so top-1 queries returned the squat unchallenged. Demoted exact
matches no longer count toward the quota in package or skill catalog
search; regression tests cover the limit-1 scenario on both surfaces.
@steipete steipete merged commit fc55ba2 into main Jul 10, 2026
24 of 28 checks passed
@steipete steipete deleted the fix/search-ranking-squat-gate branch July 10, 2026 23:06
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.

Search ranking lets exact name matches outrank adoption and verification (name-squat vector)

1 participant