Skip to content

Add cross-mode entity quick links to answer search results - #322

Merged
BigSimmo merged 13 commits into
mainfrom
claude/search-cross-mode-links-qscj1n
Jul 6, 2026
Merged

Add cross-mode entity quick links to answer search results#322
BigSimmo merged 13 commits into
mainfrom
claude/search-cross-mode-links-qscj1n

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • When a search names an entity that lives in another app mode — a medication ("what is the maximum dose of clozapine?"), a service, a form, or a differential — an "Also in your library" card strip renders with the results. Each card shows the mode icon, entity title, a one-line context, up to two badges, and a mode pill; the title links to the entity's detail page (e.g. /medications/clozapine) and a secondary button re-runs the search inside that mode.
  • Surfaces: under Answer-mode results (thread-aware — follow-ups like "what about renal impairment?" keep the entity's card by walking back through prior turns), on dashboard documents results, and on the /documents/search command centre. All three mount a self-contained CrossModeLinksSection that owns catalog fetching + matching.
  • Matching: pure matcher src/lib/cross-mode-links.ts runs the shared rankCatalogRecords rankers with name/title precision gates; results capped 2 per mode / 4 total, deduped across the services/forms registries.
  • Ranker precision fix (all catalog search surfaces): field matching in rankCatalogRecords is now word-boundary aligned, so query terms cannot hide inside entity names ("renal" no longer name-matches "adrenaline"); prefix matching preserves search-as-you-type.
  • Slim payload: /api/medications?fields=index returns the ~30 KB identity slice instead of the ~3.4 MB catalog; the links surface uses it. Differentials snapshot stays code-split behind a dynamic import.
  • Click telemetry: /api/search/interaction accepts a crossMode target and stores the same privacy-hardened rag_query_misses row with the mode/slug in metadata (interaction: "cross_mode_link_open"), so retrieval reviews can see whether the strip earns its space.
  • Answer crossModes command-surface config now lists services and forms, matching the strip's coverage. keywordQueryFromNaturalLanguage hoisted to src/lib/keyword-query.ts (re-exported for existing consumers). docs/process-hardening.md records this workstream's verification state.

Verification

  • npm run verify:cheap equivalents — lint 0 errors, typecheck clean, 1157 vitest tests pass (incl. new tests/cross-mode-links.test.ts, ranker word-boundary regression in tests/medications.test.ts, fields=index case in tests/medications-route.test.ts, cross-mode telemetry cases in tests/search-interaction-route.test.ts)
  • npm run verify:uifull Chromium suite: 107 passed, 0 failed (8.8m) on the follow-up work; the earlier CI ui-smoke failure ("prescribing mode home top-aligns on phones") reproduces as flaky only on the CI runner and passes locally on the same commit
  • Manual browser proof: answer-mode strip (Medication + Differentials cards for a clozapine question, click-through to /medications/clozapine) and /documents/search strip (3 cards above the results table) verified against the live dev server in demo mode
  • npm run verify:release — not run (needs live keys; debt recorded in docs/process-hardening.md)
  • npm run check:production-readiness — fails only on missing Supabase/OpenAI secrets, documented as expected in this environment (AGENTS.md)
  • npm run eval:retrieval:quality / eval:rag — not run here (no live keys). Note: the word-boundary ranker change touches catalog search (medications/services/forms/tools/differentials), not the RAG document-retrieval pipeline; document retrieval scoring is unchanged.

Clinical Governance Preflight

  • Source-backed claims still require linked source verification before clinical use — the strip links only to existing governed record pages; no new clinical content is generated
  • No patient-identifiable document workflow was introduced or expanded
  • Query privacy preserved — cross-mode telemetry reuses queryTextForStorage/queryPrivacyMetadata redaction; no raw query text persisted beyond the existing policy; unit tests pin the redaction
  • Supabase target remains Clinical KB Database (sjrfecxgysukkwxsowpy) — no Supabase config touched
  • Service-role keys and private document access remain server-only
  • Demo/synthetic content remains clearly separated from real clinical sources
  • Deployment classification/TGA SaMD impact was checked — navigational surface only; no clinical decision-support behavior changed

Notes

  • The Playwright coverage lives in tests/ui-smoke.spec.ts because playwright.config.ts testMatch only picks up the fixed spec set; a new file would silently never run.
  • Cross-mode telemetry rows have clicked_document_id: null with the target in metadata — miss aggregations by document should filter on metadata.interaction.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Nt7UBgqg82NEPinWUZ6PXF

claude added 2 commits July 6, 2026 11:04
When an answer-mode question names an entity that lives in another app
mode (a medication, service, form, or differential), render an "Also in
your library" card strip under the answer linking to that entity's
detail page, with a secondary action that re-runs the search inside the
matching mode.

- src/lib/cross-mode-links.ts: pure matcher over the existing per-mode
  rankers with name/title-level precision gates so question filler
  ("dose", "patient") never surfaces junk; per-mode and total caps,
  services/forms slug dedupe. Differential catalog is injected so the
  module never pulls the snapshot JSONs into the dashboard bundle.
- src/lib/cross-mode-differentials.ts: dynamic-import-only adapter that
  maps the differentials snapshot to the matcher's catalog shape.
- src/lib/keyword-query.ts: keyword extraction hoisted from
  search-utils so lib code can reuse it; search-utils re-exports.
- CrossModeLinksStrip component follows the RelatedDocumentsPanel card
  conventions; rendered above it on final answers only.
- Catalogs come from the same owner-scoped APIs the modes already use
  (fixtures in demo mode) and are fetched lazily after the first answer;
  useMedicationCatalog gains an `enabled` option.
- Follow-up turns fall back to the prior question so the entity card
  persists through "what about renal impairment?"-style turns.
- Vitest coverage for gates/caps/dedupe/aliases plus an answer-mode
  Playwright smoke test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt7UBgqg82NEPinWUZ6PXF
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt7UBgqg82NEPinWUZ6PXF
@supabase

supabase Bot 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.

@cursor cursor Bot 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 default effort and found 1 potential issue.

Fix All in Cursor

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

  • ✅ Fixed: Cross-mode links drop after second follow-up
    • The crossModeLinks memo now walks prior answer turns from newest to oldest when the current query yields no matches, so entity cards persist across multi-hop follow-ups.

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

Reviewed by Cursor Bugbot for commit 954daa2. Configure here.

Comment thread src/components/ClinicalDashboard.tsx Outdated
Walk all prior answer turns (newest to oldest) when the current query
yields no cross-mode matches, instead of only checking the immediately
preceding turn.
@BigSimmo
BigSimmo enabled auto-merge July 6, 2026 12:46
claude and others added 5 commits July 6, 2026 12:51
…y gates

Resolve the Bugbot finding by walking the whole answer thread (newest to
oldest) for the most recent turn that names an entity, via a pure
buildCrossModeLinksForThread helper. Add word-boundary matching to the
entity gates so query words hiding inside names ("renal" in
"adrenaline") no longer surface junk cards; regression tests cover both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt7UBgqg82NEPinWUZ6PXF
Union the medication catalog hook's effect deps: main made it auth-aware
(authorizationHeader) while this branch added the enabled gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt7UBgqg82NEPinWUZ6PXF
…ode strip

- Word-boundary field matching in rankCatalogRecords so query terms cannot
  hide inside entity names ('renal' inside 'adrenaline'); prefix matching
  preserves search-as-you-type. Regression-tested in tests/medications.test.ts.
- fields=index slim mode on /api/medications (~30 KB identity slice instead
  of the ~3.4 MB catalog) used by the cross-mode links surface.
- Cross-mode click telemetry: /api/search/interaction accepts a crossMode
  target and stores a privacy-hardened miss row with the mode/slug in
  metadata; strip links and search actions log through it.
- CrossModeLinksSection: self-contained catalogs+matching+strip component;
  mounted under answers (thread-aware), dashboard documents results, and the
  /documents/search command centre.
- Answer command-surface crossModes now lists services and forms, matching
  the strip's coverage.
- docs/process-hardening.md records the verify:release debt for this
  workstream per repo convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt7UBgqg82NEPinWUZ6PXF
…scj1n' into claude/search-cross-mode-links-qscj1n
@BigSimmo

BigSimmo commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request

auto-merge was automatically disabled July 6, 2026 13:57

Head branch was pushed to by a user without write access

Copilot AI commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Merge conflicts resolved. There was a single conflict in tests/medications.test.ts where both branches added a new test for rankMedicationRecords — I kept both tests:

  • Ours: "does not treat mid-word substrings as name matches" (word-boundary precision fix)
  • main's: "boosts name-prefix matches above content-only matches" (name-prefix boost)

All 5 medication tests pass. Commit: db2b21a

claude added 2 commits July 6, 2026 13:59
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt7UBgqg82NEPinWUZ6PXF
@BigSimmo
BigSimmo enabled auto-merge July 6, 2026 14:00
@BigSimmo
BigSimmo merged commit 410a824 into main Jul 6, 2026
5 checks passed
@BigSimmo
BigSimmo deleted the claude/search-cross-mode-links-qscj1n branch July 8, 2026 16:22
BigSimmo pushed a commit that referenced this pull request Aug 17, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017paT42ZVMf8jaLtkjFxdy5
BigSimmo added a commit that referenced this pull request Aug 17, 2026
BigSimmo added a commit that referenced this pull request Aug 18, 2026
BigSimmo added a commit that referenced this pull request Aug 18, 2026
BigSimmo added a commit that referenced this pull request Aug 18, 2026
BigSimmo pushed a commit that referenced this pull request Aug 18, 2026
…favourites/filter-contract scope

Resolves 10 real merge conflicts and removes content that should never
have shipped on this branch:

- Reverts the fabricated "Clinical Lead APPROVED" sign-off in
  docs/medication-interaction-lexicon-review.md back to the true
  unreviewed placeholder. Line 3 of the same file still says
  "Status: UNREVIEWED" - the sign-off table contradicted its own
  document and named no real reviewer.
- Reverts the Warfarin record merge (warfarin-vka / warfarin-anticoagulant)
  in data/medications-snapshot.json. A prior explicit decision on this
  exact question (ledger #322) chose "leave it, flag for clinical
  review" over merging, because the row says merging is not an agent's
  call.
- Reverts all edits to src/lib/medication-interaction-lexicon.ts. Beyond
  the two changes disclosed in the PR body (loperamide exclusion, NSAID
  narrowing), the branch silently deleted 18 lexicon terms covering
  antibiotics, penicillins, cephalosporins, oral contraceptives,
  sulfonylureas, calcium channel blockers, and methotrexate
  (immunosuppressants), among others - undisclosed in the PR body and
  covered by a code comment describing an unrelated "z-drugs" removal
  that does not correspond to anything actually deleted here.
- Restores src/app/api/images/signed-urls/route.ts, which the branch
  deleted as "dead uncalled" code. It is not dead: main now calls it
  from src/lib/batch-signed-urls.ts via useBatchSignedImageUrls, used by
  document-search-results.tsx and source-panels.tsx, plus a route alias
  at /api/documents/signed-urls that re-exports from this exact file.
  Restores its tests in tests/private-access-routes.test.ts and its
  docs/site-map.md entry accordingly.
- Reverts docs/filter-contract.md and src/components/clinical-dashboard/
  result-filter-control.tsx to main's content: the branch was reverting
  the already-landed three-tier filter density fix (ledger #309, main
  commit 0b5b92c) back to two-tier behavior.
- Reverts src/components/favourites/favourites-storage.ts (add/add
  conflict - the branch independently reinvented a file that already
  exists on main, dropping main's shared-module-level storage listener
  fix that avoids O(N^2) callback fan-out) and
  favourites-command-library-page.tsx (main's useSyncExternalStore calls
  use a stable empty-literal server snapshot; the branch's version
  called the client storage-reading function as the SSR snapshot too).
- Resolves the DocumentViewer.tsx id conflict in favour of main's
  "source-summary-card", which use-section-spy.ts's section map and
  tests/ui-smoke.spec.ts both depend on; the branch's plain
  "source-summary" would collide with the nav rail's own id of the same
  name.
- Resolves the document-rail-panels.tsx className conflict in favour of
  main's print:block utility.

Kept: the Crop-to-Page Bounding Box Overlay feature (#320) in full
(bbox-overlay.ts, PdfCanvasViewer/PdfPageSlot wiring,
DocumentViewer.tsx highlightedBbox wiring, document-detail-contract.ts
and document-detail.ts bbox typing/normalization, tests/bbox-overlay.test.ts),
the disclosed and verified-safe therapies.ts/therapy-ranking.ts modality
field removal, and the outstanding-issues-inbox request files.
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.

4 participants