Skip to content

Universal search: fix core RAG search defects and federate medications/services/forms/differentials/tools into one cross-entity search - #325

Merged
BigSimmo merged 6 commits into
mainfrom
claude/universal-search-algorithm-ryrps7
Jul 6, 2026
Merged

Universal search: fix core RAG search defects and federate medications/services/forms/differentials/tools into one cross-entity search#325
BigSimmo merged 6 commits into
mainfrom
claude/universal-search-algorithm-ryrps7

Conversation

@BigSimmo

@BigSimmoBigSimmo commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Core RAG search fixes (each independently revertable):
    • Finding Improve Clinical KB dashboard and RAG hardening #11 interim fix — memoize the LLM query-classifier verdict (15-min TTL, in-flight dedup, errors never cached) so the unsupported short-circuit can no longer flip run-to-run and intermittently return 0 results for valid queries like "bipolar disorder" (src/lib/rag.ts).
    • Complete the 1.0-saturation tiebreakrankClinicalResults now emits its pre-clamp boost sum (score_explanation.preClampFinalScore) and selectRetrievalEvidence uses it after score/lexical/rerank ties, so fully tied saturated candidates no longer order by arbitrary chunk id. Tie-only by construction; the clamped primary score is untouched.
    • Weak-match OR-augmentation (P8b extension) — strict-AND text retrieval that returns a sparse set of middling matches (or negligible top text_rank) now appends OR-relaxed recall behind the strict matches (append-only merge). A sparse set anchored by a strong lexical hit (top rank ≥ 0.3) does not relax, so exact lookups stay one-RPC retrievals. Kill switch: RAG_TEXT_WEAK_OR_RELAXATION=false.
    • RC9 observability — fabricated (non-cosine) similarities from the document-lookup, memory-card, and table-facts paths are tagged similarity_origin:"synthetic_text" and counted in telemetry ahead of any gate recalibration.
    • Housekeeping — the duplicated weak-search 0.48 literal is now weakRetrievalTopScoreThreshold in rag-routing.ts with an ordering test; stale 12000ms answer-budget docs reconciled with the deliberate 30000ms default; six follow-ups filed in docs/rag-hybrid-findings-and-todo.md.
  • Shared catalog-search primitives — one canonical normalizeSearchText and a generic rankCatalogRecords replace the four divergent per-domain rankers. Medications/services/forms wrappers keep their exact historical weights, reason strings, and match shapes (tests/{medications,services,forms}.test.ts pass unmodified as parity oracles). Differentials get real relevance ranking with alias expansion (previously an unranked includes() filter). The duplicated Tools dataset is consolidated into src/lib/tools-catalog.ts (icon-free so server code can use it). Forms stop masquerading as kind:"documents" (new honest forms kind removes the dashboard special-casing); prescribing deliberately keeps kind:"documents" with the rationale recorded inline.
  • Universal search endpoint — new GET /api/search/universal federates one parallel in-process fan-out (Promise.allSettled, per-domain timeouts, error isolation) across document retrieval (searchChunksWithTelemetry + fetchRelatedDocuments) and the shared registry rankers. Access ladder mirrors /api/registry/records (demo → fixtures, public → public catalogues, owner → lazily-seeded records, shared rate-limit bucket). No cross-domain score normalization — registry integer weights and [0,1] document scores are incommensurable, so ordering is fixed group order, ranked within group.
  • Universal typeahead UI — the live UniversalSearchCommandSurface gains grouped cross-entity results while typing (debounced hook with a monotonic race token, excluding the active mode's own domain). Selecting an item navigates straight to the record; each group ends with a "View all in {mode}" row reusing the existing cross-mode plumbing; Enter with nothing highlighted still runs the mode-scoped search. use-medication-catalog also now sends the auth header (owners were silently served fixture data).

Verification

  • npm run verify:cheap — green (lint, typecheck, 1139 vitest, runtime check, sitemap check)
  • npm run verify:ui equivalent — Chromium against a live demo-mode dev server: new ui-universal-search.spec.ts (3/3), ui-tools + ui-tools-task-directory (40/40), ui-smoke + ui-overlap (59/59); plus live curl of /api/search/universal (grouped payload, domain CSV filter, 400 on short query)
  • npm run verify:release — not run (release gate; needs live keys/Deno)
  • npm run format:check
  • npm run eval:retrieval:quality (must stay 23/23) — could NOT run in the authoring environment (no live Supabase/OpenAI keys). Must be run before merge. Pay attention to the weak-match OR-augmentation (setting RAG_TEXT_WEAK_OR_RELAXATION=false restores relax-on-empty exactly) and the retrieval-selection tiebreak (tie-only). The classifier memo also needs eval:quality -- --rag-only to confirm unsupported_correct_rate stays 1.0.
  • npm run eval:rag -- --limit 15 + npm run eval:quality -- --rag-only — answer generation itself is untouched, but the classifier memo affects routing inputs; run alongside the retrieval eval before merge.
  • npm run check:production-readiness behaviour unchanged for env/deployment; search-behaviour verification state recorded in docs/process-hardening.md
  • npm run check:deployment-readiness — no deployment/startup behaviour changed

Clinical Governance Preflight

  • Source-backed claims still require linked source verification before clinical use (answer pipeline and source rendering untouched)
  • No patient-identifiable document workflow was introduced or expanded (universal search queries follow the existing privacy-redaction telemetry paths; the new endpoint logs nothing)
  • Supabase target remains Clinical KB Database (sjrfecxgysukkwxsowpy) — no env/project changes
  • Service-role keys and private document access remain server-only (the universal route uses the same admin-client + owner-scope ladder as the registry routes)
  • Demo/synthetic content remains clearly separated (demo mode serves fixtures via the same isDemoMode() ladder and flags demoMode:true)
  • Source metadata, review status, and outdated/unknown-source behaviour remain conservative — governance metadata still never weights retrieval selection ordering; candidate scores stay clamped
  • TGA SaMD impact checked: no clinical decision-support output changed; registry typeahead surfaces existing records with existing governance badges

Notes

  • Eval gate is the one open item: the two golden evals need live keys, so they are a pre-merge manual step per docs/process-hardening.md ("Retrieval changes must pass the golden eval before merge"). Every retrieval change here was engineered tie-only/append-only, and the riskiest one has an env kill switch, specifically to make that gate easy to satisfy or roll back.
  • Follow-ups filed (items 17–22 in docs/rag-hybrid-findings-and-todo.md) include the optional Phase-5 registry-to-corpus embedding so Answer mode could eventually cite medications/services/forms — deliberately excluded here to keep the eval-gated pgvector corpus untouched.
  • The ui-universal-search spec mocks the universal endpoint; an end-to-end owner-auth spec needs the E2E_USER_* Playwright project.

🤖 Generated with Claude Code

https://claude.ai/code/session_011QiyE8jMm7VnrtknHF6jnJ


Generated by Claude Code


Note

High Risk
Touches core RAG text retrieval, classifier routing memoization, and evidence selection tiebreaks—areas that previously regressed golden evals without unit-test coverage; merge still depends on live eval:retrieval:quality / RAG quality gates.

Overview
Delivers universal search end-to-end: GET /api/search/universal runs a parallel, per-domain federated search (documents via RAG + medications/services/forms/differentials/tools via shared rankers), with demo/public/owner access aligned to registry routes and isolated failures per domain. The command surface adds debounced “Across Clinical KB” typeahead (excluding the active mode’s domain), direct navigation to records, and “view all in {mode}” rows; medication catalog fetches now send auth so owners get seeded data.

Catalog layer:catalog-search + tools-catalog replace duplicated normalizers/rankers and the launcher/tools fixture split. Forms use an honest forms search kind (not documents); dashboard/nav/composer paths are updated accordingly.

RAG retrieval changes (each with tests and/or kill switches where noted): LLM query-classifier verdict memoization (15m TTL, no error caching) for stable routing; pre-clamp score emitted and used as a tiebreak when clamped scores saturate at 1.0; weak strict-AND text matches can append OR-relaxed recall behind strict hits (RAG_TEXT_WEAK_OR_RELAXATION=false restores relax-on-empty); similarity_origin: synthetic_text and telemetry for non-cosine similarities; shared weakRetrievalTopScoreThreshold for weak-search logging. Registry/medication APIs use shared fetch-with-seed helpers for universal search parity.

Docs/env: RAG_TEXT_WEAK_OR_RELAXATION, answer timeout docs corrected to 30s default; process-hardening notes eval debt before merge.

Reviewed by Cursor Bugbot for commit 716b596. Configure here.

claude added 6 commits July 6, 2026 10:54
… follow-ups
Phase 0 of the search hardening + universal search workstream:
- weakRetrievalTopScoreThreshold (0.48) moves from duplicated magic literals in
/api/search into rag-routing.ts beside the routing thresholds, with an
ordering test (0.32 < 0.48 < 0.64 < 0.76).
- search-rag-master-context.md no longer claims a 12000ms answer budget; the
30000ms default in env.ts is a documented deliberate product decision.
- Six follow-ups filed in rag-hybrid-findings-and-todo.md (alias promotion,
index-unit HNSW, demo-fallback masking, governance-regression guard,
synthetic-similarity recalibration, registry-to-corpus embedding).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QiyE8jMm7VnrtknHF6jnJ
… weak-match recall
Phase 1 of the search hardening workstream — four independently revertable fixes:
1.1 Memoize the LLM query-classifier verdict (finding #11 interim fix per
docs/process-hardening.md): successful verdicts (accepted AND rejected)
are cached 15 min with in-flight dedup, so the unsupported short-circuit
can no longer flip run-to-run and intermittently return 0 results for
valid queries like "bipolar disorder". Transport errors stay retryable.
1.2 Complete the saturation tiebreak: rankClinicalResults now emits its
pre-clamp boost sum as score_explanation.preClampFinalScore, and
selectRetrievalEvidence uses it after score/lexical/rerank ties — fully
tied 1.0-saturated candidates no longer order by arbitrary chunk id.
Tie-only by construction; the clamped primary score is untouched.
1.3 OR-relaxation for weak-but-nonzero strict text matches (P8b extension):
when strict-AND retrieval returns a sparse set (<3) or negligible top
text_rank (<0.05), append OR-relaxed recall BEHIND the strict matches
(append-only merge; strict results keep precedence). Kill switch:
RAG_TEXT_WEAK_OR_RELAXATION=false. Telemetry: text_or_relaxation_used.
1.4 RC9 observability: fabricated (non-cosine) similarities from the
document-lookup, memory-card, and table-facts paths are tagged
similarity_origin="synthetic_text" and counted in telemetry
(synthetic_similarity_count) ahead of any gate recalibration.
Eval-gate note: eval:retrieval:quality (23/23) and eval:quality --rag-only
need live keys and must be run before merge per the governance preflight;
1.3 ships behind the env kill switch for exactly that reason.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QiyE8jMm7VnrtknHF6jnJ
Phase 2 of the search hardening + universal search workstream:
- New src/lib/catalog-search.ts: one canonical normalizeSearchText (the
medications superset — NFKD + diacritic strip, keeps + . / - for dose
strings) replacing four divergent per-domain normalizers, and a generic
rankCatalogRecords weighted ranker returning match signals so each domain
wrapper keeps its historical reason labels and match shapes.
- medications/services/forms rankers become thin wrappers with their exact
historical weights; tests/{medications,services,forms}.test.ts pass
unmodified as parity oracles.
- Differentials get real relevance ranking (rankDifferentialRecords) with
alias expansion, replacing the unranked includes() OR-filter; empty-query
full-catalogue browse preserved.
- Forms stop masquerading as documents: app-modes gains a "forms"
search/result kind, removing the ClinicalDashboard special case; forms no
longer qualify as a source-library shortcut (scope-tag clicks fall back to
documents mode). Prescribing deliberately keeps kind:"documents" — it
searches the corpus for dosing guidance (rationale recorded inline).
- Tools dataset deduplicated into src/lib/tools-catalog.ts (icon-free so
server code can use it) + rankToolRecords; the launcher derives its apps
from it and the mockup fixtures derive identity fields from it.
- Amend Phase-1 OR-relaxation heuristic: a sparse strict set anchored by a
strong lexical hit (top text_rank >= 0.3) no longer augments — exact
single-match lookups stay one-RPC retrievals (caught by the answer
coalescing test asserting a single text RPC).
New tests: catalog-search, tools-catalog; extended differentials and
app-modes contracts; sitemap regenerated (mode kind is embedded there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QiyE8jMm7VnrtknHF6jnJ
… catalogs
Phase 3 of the universal search workstream:
- New src/lib/universal-search.ts: runUniversalSearch() fans out in parallel
(Promise.allSettled) to the document retrieval pipeline
(searchChunksWithTelemetry + fetchRelatedDocuments) and the shared Phase-2
registry rankers (medications, services, forms, differentials, tools).
Per-domain timeouts (2.5s registry / 6s documents); a failed domain yields
an empty errored group so one broken adapter never blanks the response.
No cross-domain score normalization — registry integer weights and [0,1]
document scores are incommensurable, so ordering is by fixed group order,
ranked within each group.
- New GET /api/search/universal (typeahead-friendly): q (min 2), limit
(max 10), domains CSV. Access ladder mirrors /api/registry/records:
demo/local -> fixtures, public -> public catalogues, owner -> seeded
records, with the shared "registry" rate-limit bucket and
Cache-Control: private, no-store.
- The owner fetch + lazy-seed blocks are extracted from /api/medications and
/api/registry/records into fetchOwnerMedicationRowsWithSeed /
fetchOwnerRegistryRowsWithSeed so the routes and universal search share one
code path.
- Tests: universal-search lib (fixed group order, per-domain fixtures with
working hrefs, domain filtering, error isolation, demo document search)
and route (demoMode payload, min-length validation, unknown-domain CSV).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QiyE8jMm7VnrtknHF6jnJ
…d surface
Phase 4 of the universal search workstream:
- New use-universal-search hook: 250ms-debounced GET /api/search/universal,
enabled only while the command dropdown is open with a 2+ char query,
excluding the active mode's own domain (its results already come from the
mode search). Race handling mirrors the dashboard's monotonic request
token, and visible groups are derived from the fetched query so a stale
result set is never rendered against a newer query.
- UniversalSearchCommandSurface renders the grouped cross-entity results
("Medications · 3", "Forms · 1", ...) between suggestions and mode
actions; selecting an item navigates straight to the record, and each
group ends with a "View all in {mode}" row reusing the existing
cross-mode plumbing. Keyboard navigation and aria come free from the
existing flatItems listbox. Enter with nothing highlighted still runs the
mode-scoped search; errored/empty domains render nothing; a subtle
spinner row shows while the federated request is pending.
- use-medication-catalog now sends the auth session header (matching
use-registry-records) — an authenticated owner was previously served the
public fixture catalogue silently.
- New tests/ui-universal-search.spec.ts (registered in the Playwright
testMatch): grouped section renders while typing, selection navigates to
/medications/{slug}, Enter preserves mode search. Verified green on
Chromium against a live demo-mode dev server, along with the full
ui-tools suites (40/40) after the Phase-2 tools dedupe.
- Verification state and remaining eval debt recorded in
docs/process-hardening.md (golden retrieval eval requires live keys and
must run before merge per the standing gate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QiyE8jMm7VnrtknHF6jnJ
npm run format:check is part of the PR verification contract; nine files
from the preceding phases needed --write. No behaviour change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QiyE8jMm7VnrtknHF6jnJ
@supabase

supabaseBot commented Jul 6, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project sjrfecxgysukkwxsowpy because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@BigSimmo
BigSimmo marked this pull request as ready for review July 6, 2026 12:38
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@BigSimmo
BigSimmo merged commit 822ce32 into mainJul 6, 2026
6 checks passed

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Auth change stale typeahead
    • When authorizationHeader changes, the hook now clears cached result groups so fresh becomes false and the dropdown shows loading until the auth-scoped refetch completes.

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 716b596. Configure here.


if (!active) return { groups: [], loading: false, query: "" };
const fresh = result.query === trimmedQuery;
return { groups: fresh ? result.groups : [], loading: !fresh, query: result.query };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Auth change stale typeahead

Medium Severity

When authorizationHeader changes (sign-in, sign-out, or token refresh), the hook refetches but keeps the prior result state. While result.query still equals the current input, fresh stays true and loading false, so the dropdown can keep showing the previous access tier’s universal results (e.g. public fixtures) with no spinner until the debounced request finishes.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 716b596. Configure here.

BigSimmo pushed a commit that referenced this pull request Jul 6, 2026
…ankers
Main's #325 added its own snapshot-only rankDifferentialRecords(query)
on the shared rankCatalogRecords primitive while this branch shipped a
records-parameter variant that the /api/differentials route uses to
rank live owner rows. Resolution keeps one ranker built on the shared
primitive with the records-first signature (matching the medication and
service rankers): title x8 + hinge/safety x3 fields, alias expansion
surfaced as a "symptom alias" reason via a new `expanded` signal,
exact/compact/phrase bonuses, and urgency as a tie-break only. The
primitive also gains an optional prefixValues/prefixBonus so the
medication ranker keeps this branch's name-prefix boost, and the
universal-search differentials domain passes the snapshot explicitly.
Test suites from both sides are retained on the unified signature.
Verified post-merge: verify:cheap (1152 tests), format:check, chromium
ui-tools 36/36 and ui-universal-search 3/3 against the live dev server.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015UuyyMMegXxTeEJsyR741t
Sign up for freeto 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.

2 participants

@BigSimmo@claude