From 44eeee899ca103493eebe451b85879fc7dd877b8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 05:01:35 +0000 Subject: [PATCH 01/16] docs: record search performance review on main Append a branch-review ledger row for the cross-mode search load/typeahead/submit performance review at 6ceaaff5. --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index a6af85953..1a2cdfc0b 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,6 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-07-24 | `main` | `6ceaaff50712e10e857bf9a5a7ec88b530bf7b35` | Search performance across modes (load + typeahead + submit; local ensure) | CHANGES REQUESTED / findings. No P0. P1: Prescribing `useMedicationCatalog(query)` refetches the full ~2.5–2.9 MB medication catalogue on every keystroke without debounce, abort, or `fields=index` (~25× larger than index). P2: differentials catalogue + evidence `/api/search` lack abort/debounce; universal typeahead `tookMs` dominated by empty live documents domain (~130–340 ms); cross-namespace mode switches remount the search shell; Answer submit returns 503 `rate_limit_unavailable` when durable limiter is down (fail-closed). Mode HTML load medians ~40–75 ms; typeahead wall ~160–180 ms (`ssri`) / ~350–400 ms (`agitation im lorazepam`) + 250 ms client debounce; catalogue submits (differentials) ~45 ms; document `/api/search` demo-degraded ~230–430 ms. Highest residual live risk: cold hybrid RPC tails (docs #25), not re-measured with soak/eval. | `npm run ensure` → http://localhost:4461; `/api/local-project-id` Clinical KB; paced universal typeahead across 13 modes × 2 queries (Server-Timing); `/api/search` + `/api/medications` (+`fields=index`) + differentials APIs + registry payload sizes; NDJSON vs JSON first-byte; browser walkthrough of 13 mode homes; static review of ClinicalDashboard / universal-search / medication+differential hooks. No OpenAI generation, no `eval:retrieval:latency`, no soak, no hosted CI. Environment had Supabase secrets so universal ran live (`publicAccess`); `/api/search` degraded to demo (`supabase_api_key_configuration_unavailable`). | | 2026-07-24 | `codex/safety-plan-no-patient-data-contract` | `b94987c94537f3114a3429848fa908bdecd1d80a` | Safety Plan Generator identifier, local-state, copy, print, privacy-notice and PIA contract | APPROVE. No P0-P2 finding. The patient name/initials field is removed; the builder now asks for identifier-free minimum content, retains working state only in the mounted React component, and makes clipboard/print/PDF export an explicit handling boundary. The PIA and product privacy copy distinguish this local-only tool from provider-backed questions. Highest residual risk is outside Clinical KB: users must handle exported copies under an approved clinical-record process, which the UI now states at the export controls. | Privacy/component DOM 3/3 plus updated privacy-copy 2/2; focused Chromium copy/print/no-fetch-or-XHR 1/1; `verify:cheap` passed all 21 gates, 366 files and 3,245 tests with 1 skip; production-readiness READY using the existing canonical environment without a provider call; production build and client-bundle secret scan passed; offline RAG fixture/manifest 36 cases/21 suites passed. `verify:pr-local` passed runtime, formatting, lint and typecheck, then stopped on the unrelated load-sensitive `reconciliation-preflight` 30-second timeout; that test passed 5/5 isolated and the preceding full suite passed, so the unchanged five-minute gate was not retried. No Supabase, OpenAI, Railway, live RAG, production data or deployment action ran. | | 2026-07-24 | PR #1114 / `codex/universal-ledger-main-followup-20260724` | `eedbe594a89eaa0aad8a517ee8808b089cb5ff57` + reviewed working diff | Streamlined-sidebar handoff in the merged universal ledger | APPROVE. PRs #1112, #1116, and #1117 landed the universal-ledger baseline, two preserved UI tasks, and credential/hook hardening; this follow-up preserves that current-main schema and adds only the still-missing sidebar delivery as collision-free `#066`. The queue remains gap-free and every recommended ID has one open row. Sidebar implementation stays recoverable from remote branch `origin/codex/sidebar-test-fix-20260723` at full commit `cd54e68fbf7b07b5dffe3220e36af2caa528da54` until its local/browser/build/hosted merge proof is complete. | Focused sidebar/favourites tests previously passed 18/18 on the recorded implementation commit; this ledger follow-up passed Prettier, remote-ref recovery, 36-row contiguous-order/46-open-ID/next-ID invariants, and `git diff --check`. The broader reconciled precursor passed `verify:cheap` (365 files, 3,242 passed/1 skipped); fresh exact-head hosted CI is required on PR #1114. No OpenAI, Supabase, Railway, deployment, live-app, credential, or production-data action ran. | | 2026-07-24 | `codex/universal-ledger-main-final` | `527988c2ccabc98b4d0673d33360c971df65fa0e` + reviewed working diff | Current-main universal-ledger reconciliation after PR #1106 superseded PR #1109 | READY. Kept PR #1106's current-main ledger and IDs, carried forward only non-duplicate recommended work from the superseded branch, and fixed the confirmed SessionStart empty-open defect. Every active recommendation now has a durable open ID; exposed-GitHub-token containment is the first A1 item; the Safety Plan privacy contract, absent-relevance fail-closed rule, stranded-upload recovery, threshold-conflict design, catalogue-toolbar convergence, and Current Clinical Work brief are retained without duplicating #1106's legal/config/release/staging/seed packages. Resolved #014/#034 claims stay archived, and PR #1110's scheduled-diagnostics priority remains intact. Highest residual risk is manual queue/open-table drift. | Empty-open fixture and real-ledger Bash execution passed; 33 contiguous recommendations reference tracked open IDs; 43 open items; no duplicate queued IDs; `docs:check-links`, `docs:check-index`, `docs:check-scripts`, `check:skills`, Prettier, and `git diff --check` passed. No OpenAI, Supabase, Railway, production, deployment, live-app, or credential action. | From 76d47871c44c607f942a683351378230173dcbe3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 05:49:38 +0000 Subject: [PATCH 02/16] fix: resolve search performance findings across modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Debounce/abort prescribing medication catalogue fetches with fields=index, harden differentials catalogue and evidence search cancellation, soft-timeout universal documents typeahead (750ms), keep GlobalSearchShell mounted via a shared (search-app) layout, and allow Answer rate-limit in-memory fallback in non-production when the durable RPC is unavailable. RAG impact: no retrieval behaviour change — typeahead documents domain timeout only; ranking formulas and full /api/search path unchanged. --- docs/branch-review-ledger.md | 1 + docs/codebase-index.md | 24 ++--- docs/search-rag-master-context.md | 2 +- docs/site-map.md | 102 +++++++++--------- scripts/check-docs-links.mjs | 2 +- scripts/generate-site-map.ts | 18 ++-- .../differentials/diagnoses/[slug]/error.tsx | 0 .../diagnoses/[slug]/loading.tsx | 0 .../differentials/diagnoses/[slug]/page.tsx | 0 .../differentials/diagnoses/page.tsx | 0 .../differentials/error.tsx | 0 src/app/(search-app)/differentials/layout.tsx | 5 + .../differentials/loading.tsx | 0 .../{ => (search-app)}/differentials/page.tsx | 0 .../presentations/[slug]/error.tsx | 0 .../presentations/[slug]/loading.tsx | 0 .../presentations/[slug]/page.tsx | 0 .../differentials/presentations/route.ts | 0 .../documents/[id]/error.tsx | 0 .../documents/[id]/loading.tsx | 0 .../documents/[id]/page.tsx | 0 .../{ => (search-app)}/documents/error.tsx | 0 src/app/(search-app)/documents/layout.tsx | 6 ++ .../documents/search/loading.tsx | 0 .../documents/search/page.tsx | 0 .../documents/source/evidence/page.tsx | 1 + .../documents/source/page.tsx | 0 .../{ => (search-app)}/dsm/compare/page.tsx | 0 .../diagnoses/[slug]/differentials/page.tsx | 0 .../dsm/diagnoses/[slug]/page.tsx | 0 src/app/(search-app)/dsm/layout.tsx | 5 + src/app/{ => (search-app)}/dsm/loading.tsx | 0 src/app/{ => (search-app)}/dsm/page.tsx | 0 .../{ => (search-app)}/dsm/search/page.tsx | 0 .../factsheets/[slug]/page.tsx | 0 src/app/(search-app)/factsheets/layout.tsx | 5 + .../{ => (search-app)}/factsheets/page.tsx | 0 .../factsheets/search/page.tsx | 0 .../{ => (search-app)}/favourites/error.tsx | 0 src/app/(search-app)/favourites/layout.tsx | 5 + .../{ => (search-app)}/favourites/loading.tsx | 0 .../{ => (search-app)}/favourites/page.tsx | 0 .../{ => (search-app)}/forms/[slug]/error.tsx | 0 .../{ => (search-app)}/forms/[slug]/page.tsx | 0 src/app/{ => (search-app)}/forms/error.tsx | 0 src/app/(search-app)/forms/layout.tsx | 5 + src/app/{ => (search-app)}/forms/loading.tsx | 0 src/app/{ => (search-app)}/forms/page.tsx | 0 .../formulation/[slug]/page.tsx | 0 .../formulation/builder/page.tsx | 0 .../formulation/compare/page.tsx | 0 src/app/(search-app)/formulation/layout.tsx | 5 + .../formulation/map/page.tsx | 0 .../{ => (search-app)}/formulation/page.tsx | 0 src/app/(search-app)/home-page-client.tsx | 17 +++ src/app/(search-app)/layout.tsx | 13 +++ .../medications/[slug]/error.tsx | 0 .../medications/[slug]/loading.tsx | 0 .../medications/[slug]/page.tsx | 0 src/app/(search-app)/medications/layout.tsx | 5 + .../{ => (search-app)}/medications/route.ts | 0 src/app/{ => (search-app)}/page.tsx | 2 +- .../services/[slug]/error.tsx | 0 .../services/[slug]/page.tsx | 0 src/app/{ => (search-app)}/services/error.tsx | 0 src/app/(search-app)/services/layout.tsx | 5 + .../{ => (search-app)}/services/loading.tsx | 0 src/app/{ => (search-app)}/services/page.tsx | 0 .../specifiers/[slug]/page.tsx | 0 .../specifiers/builder/page.tsx | 0 .../specifiers/compare/page.tsx | 0 src/app/(search-app)/specifiers/layout.tsx | 5 + .../specifiers/map/page.tsx | 0 .../{ => (search-app)}/specifiers/page.tsx | 0 .../therapy-compass/[slug]/brief/page.tsx | 0 .../therapy-compass/[slug]/page.tsx | 0 .../therapy-compass/[slug]/sheet/page.tsx | 0 .../therapy-compass/compare/page.tsx | 0 .../(search-app)/therapy-compass/layout.tsx | 6 ++ .../therapy-compass/page.tsx | 0 .../therapy-compass/pathways/page.tsx | 0 .../therapy-compass/recommend/page.tsx | 0 .../therapy-compass/review/page.tsx | 0 .../therapy-compass/search/page.tsx | 0 src/app/{ => (search-app)}/tools/error.tsx | 0 src/app/(search-app)/tools/layout.tsx | 5 + src/app/{ => (search-app)}/tools/loading.tsx | 0 src/app/{ => (search-app)}/tools/page.tsx | 0 src/app/differentials/layout.tsx | 11 -- src/app/documents/documents-layout-client.tsx | 22 ---- src/app/documents/layout.tsx | 7 -- src/app/documents/source/evidence/page.tsx | 1 - src/app/dsm/layout.tsx | 11 -- src/app/factsheets/layout.tsx | 11 -- src/app/favourites/layout.tsx | 11 -- src/app/forms/layout.tsx | 11 -- src/app/formulation/layout.tsx | 11 -- src/app/home-page-client.tsx | 10 -- src/app/medications/layout.tsx | 11 -- src/app/services/layout.tsx | 11 -- src/app/specifiers/layout.tsx | 11 -- src/app/therapy-compass/layout.tsx | 22 ---- src/app/tools/layout.tsx | 11 -- src/components/ClinicalDashboard.tsx | 13 ++- .../medication-prescribing-workspace.tsx | 5 +- .../shared-search-app-shell.tsx | 26 +++++ .../use-differential-catalog.ts | 88 ++++++++------- .../use-medication-catalog.ts | 61 ++++++----- .../differentials/differentials-home-page.tsx | 21 ++-- src/lib/api-rate-limit.ts | 20 ++-- src/lib/search-shell-props.ts | 72 +++++++++++++ src/lib/universal-search.ts | 5 +- tests/api-rate-limit-fallback.test.ts | 20 ++++ ...audit-content-services-regressions.test.ts | 4 +- .../audit-navigation-auth-regressions.test.ts | 4 +- tests/differentials-navigation.test.ts | 2 +- tests/document-detail-performance.test.ts | 2 +- tests/favourites-demo-boundary.test.ts | 2 +- tests/production-mockup-boundary.test.ts | 2 +- tests/route-error-boundary.test.ts | 2 +- tests/search-shell-props.test.ts | 37 +++++++ tests/services-client-boundary.test.ts | 2 +- tests/site-map.test.ts | 4 +- tests/therapy-compass-mode-wiring.test.ts | 20 ++-- 124 files changed, 482 insertions(+), 346 deletions(-) rename src/app/{ => (search-app)}/differentials/diagnoses/[slug]/error.tsx (100%) rename src/app/{ => (search-app)}/differentials/diagnoses/[slug]/loading.tsx (100%) rename src/app/{ => (search-app)}/differentials/diagnoses/[slug]/page.tsx (100%) rename src/app/{ => (search-app)}/differentials/diagnoses/page.tsx (100%) rename src/app/{ => (search-app)}/differentials/error.tsx (100%) create mode 100644 src/app/(search-app)/differentials/layout.tsx rename src/app/{ => (search-app)}/differentials/loading.tsx (100%) rename src/app/{ => (search-app)}/differentials/page.tsx (100%) rename src/app/{ => (search-app)}/differentials/presentations/[slug]/error.tsx (100%) rename src/app/{ => (search-app)}/differentials/presentations/[slug]/loading.tsx (100%) rename src/app/{ => (search-app)}/differentials/presentations/[slug]/page.tsx (100%) rename src/app/{ => (search-app)}/differentials/presentations/route.ts (100%) rename src/app/{ => (search-app)}/documents/[id]/error.tsx (100%) rename src/app/{ => (search-app)}/documents/[id]/loading.tsx (100%) rename src/app/{ => (search-app)}/documents/[id]/page.tsx (100%) rename src/app/{ => (search-app)}/documents/error.tsx (100%) create mode 100644 src/app/(search-app)/documents/layout.tsx rename src/app/{ => (search-app)}/documents/search/loading.tsx (100%) rename src/app/{ => (search-app)}/documents/search/page.tsx (100%) create mode 100644 src/app/(search-app)/documents/source/evidence/page.tsx rename src/app/{ => (search-app)}/documents/source/page.tsx (100%) rename src/app/{ => (search-app)}/dsm/compare/page.tsx (100%) rename src/app/{ => (search-app)}/dsm/diagnoses/[slug]/differentials/page.tsx (100%) rename src/app/{ => (search-app)}/dsm/diagnoses/[slug]/page.tsx (100%) create mode 100644 src/app/(search-app)/dsm/layout.tsx rename src/app/{ => (search-app)}/dsm/loading.tsx (100%) rename src/app/{ => (search-app)}/dsm/page.tsx (100%) rename src/app/{ => (search-app)}/dsm/search/page.tsx (100%) rename src/app/{ => (search-app)}/factsheets/[slug]/page.tsx (100%) create mode 100644 src/app/(search-app)/factsheets/layout.tsx rename src/app/{ => (search-app)}/factsheets/page.tsx (100%) rename src/app/{ => (search-app)}/factsheets/search/page.tsx (100%) rename src/app/{ => (search-app)}/favourites/error.tsx (100%) create mode 100644 src/app/(search-app)/favourites/layout.tsx rename src/app/{ => (search-app)}/favourites/loading.tsx (100%) rename src/app/{ => (search-app)}/favourites/page.tsx (100%) rename src/app/{ => (search-app)}/forms/[slug]/error.tsx (100%) rename src/app/{ => (search-app)}/forms/[slug]/page.tsx (100%) rename src/app/{ => (search-app)}/forms/error.tsx (100%) create mode 100644 src/app/(search-app)/forms/layout.tsx rename src/app/{ => (search-app)}/forms/loading.tsx (100%) rename src/app/{ => (search-app)}/forms/page.tsx (100%) rename src/app/{ => (search-app)}/formulation/[slug]/page.tsx (100%) rename src/app/{ => (search-app)}/formulation/builder/page.tsx (100%) rename src/app/{ => (search-app)}/formulation/compare/page.tsx (100%) create mode 100644 src/app/(search-app)/formulation/layout.tsx rename src/app/{ => (search-app)}/formulation/map/page.tsx (100%) rename src/app/{ => (search-app)}/formulation/page.tsx (100%) create mode 100644 src/app/(search-app)/home-page-client.tsx create mode 100644 src/app/(search-app)/layout.tsx rename src/app/{ => (search-app)}/medications/[slug]/error.tsx (100%) rename src/app/{ => (search-app)}/medications/[slug]/loading.tsx (100%) rename src/app/{ => (search-app)}/medications/[slug]/page.tsx (100%) create mode 100644 src/app/(search-app)/medications/layout.tsx rename src/app/{ => (search-app)}/medications/route.ts (100%) rename src/app/{ => (search-app)}/page.tsx (98%) rename src/app/{ => (search-app)}/services/[slug]/error.tsx (100%) rename src/app/{ => (search-app)}/services/[slug]/page.tsx (100%) rename src/app/{ => (search-app)}/services/error.tsx (100%) create mode 100644 src/app/(search-app)/services/layout.tsx rename src/app/{ => (search-app)}/services/loading.tsx (100%) rename src/app/{ => (search-app)}/services/page.tsx (100%) rename src/app/{ => (search-app)}/specifiers/[slug]/page.tsx (100%) rename src/app/{ => (search-app)}/specifiers/builder/page.tsx (100%) rename src/app/{ => (search-app)}/specifiers/compare/page.tsx (100%) create mode 100644 src/app/(search-app)/specifiers/layout.tsx rename src/app/{ => (search-app)}/specifiers/map/page.tsx (100%) rename src/app/{ => (search-app)}/specifiers/page.tsx (100%) rename src/app/{ => (search-app)}/therapy-compass/[slug]/brief/page.tsx (100%) rename src/app/{ => (search-app)}/therapy-compass/[slug]/page.tsx (100%) rename src/app/{ => (search-app)}/therapy-compass/[slug]/sheet/page.tsx (100%) rename src/app/{ => (search-app)}/therapy-compass/compare/page.tsx (100%) create mode 100644 src/app/(search-app)/therapy-compass/layout.tsx rename src/app/{ => (search-app)}/therapy-compass/page.tsx (100%) rename src/app/{ => (search-app)}/therapy-compass/pathways/page.tsx (100%) rename src/app/{ => (search-app)}/therapy-compass/recommend/page.tsx (100%) rename src/app/{ => (search-app)}/therapy-compass/review/page.tsx (100%) rename src/app/{ => (search-app)}/therapy-compass/search/page.tsx (100%) rename src/app/{ => (search-app)}/tools/error.tsx (100%) create mode 100644 src/app/(search-app)/tools/layout.tsx rename src/app/{ => (search-app)}/tools/loading.tsx (100%) rename src/app/{ => (search-app)}/tools/page.tsx (100%) delete mode 100644 src/app/differentials/layout.tsx delete mode 100644 src/app/documents/documents-layout-client.tsx delete mode 100644 src/app/documents/layout.tsx delete mode 100644 src/app/documents/source/evidence/page.tsx delete mode 100644 src/app/dsm/layout.tsx delete mode 100644 src/app/factsheets/layout.tsx delete mode 100644 src/app/favourites/layout.tsx delete mode 100644 src/app/forms/layout.tsx delete mode 100644 src/app/formulation/layout.tsx delete mode 100644 src/app/home-page-client.tsx delete mode 100644 src/app/medications/layout.tsx delete mode 100644 src/app/services/layout.tsx delete mode 100644 src/app/specifiers/layout.tsx delete mode 100644 src/app/therapy-compass/layout.tsx delete mode 100644 src/app/tools/layout.tsx create mode 100644 src/components/clinical-dashboard/shared-search-app-shell.tsx create mode 100644 src/lib/search-shell-props.ts create mode 100644 tests/search-shell-props.test.ts diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 1a2cdfc0b..7808333be 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,6 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-07-24 | `cursor/search-performance-review-4ee9` / PR #1134 | pending-fix-commit | Search performance findings remediation (prescribing, differentials, typeahead docs timeout, shared shell, answer rate-limit fallback) | FIXED. P1 prescribing catalogue now debounces (250 ms), aborts in-flight fetches, and uses `fields=index`. Differentials catalogue + evidence search abort/debounce. Universal documents typeahead timeout 6 s→750 ms (RAG impact: no retrieval behaviour change — typeahead timeout only). Shared `(search-app)` layout keeps GlobalSearchShell mounted across mode homes. Answer rate-limit fails closed only in production; development uses in-memory fallback when durable RPC is unavailable. | Focused Vitest 362 via test:focused; api-rate-limit/search-shell/universal/route/site-map suites green; `npm run ensure` smoke 200 on mode homes; `/api/answer/stream` 200 after fallback. No OpenAI spend beyond local answer stream smoke; no live eval/soak. | | 2026-07-24 | `main` | `6ceaaff50712e10e857bf9a5a7ec88b530bf7b35` | Search performance across modes (load + typeahead + submit; local ensure) | CHANGES REQUESTED / findings. No P0. P1: Prescribing `useMedicationCatalog(query)` refetches the full ~2.5–2.9 MB medication catalogue on every keystroke without debounce, abort, or `fields=index` (~25× larger than index). P2: differentials catalogue + evidence `/api/search` lack abort/debounce; universal typeahead `tookMs` dominated by empty live documents domain (~130–340 ms); cross-namespace mode switches remount the search shell; Answer submit returns 503 `rate_limit_unavailable` when durable limiter is down (fail-closed). Mode HTML load medians ~40–75 ms; typeahead wall ~160–180 ms (`ssri`) / ~350–400 ms (`agitation im lorazepam`) + 250 ms client debounce; catalogue submits (differentials) ~45 ms; document `/api/search` demo-degraded ~230–430 ms. Highest residual live risk: cold hybrid RPC tails (docs #25), not re-measured with soak/eval. | `npm run ensure` → http://localhost:4461; `/api/local-project-id` Clinical KB; paced universal typeahead across 13 modes × 2 queries (Server-Timing); `/api/search` + `/api/medications` (+`fields=index`) + differentials APIs + registry payload sizes; NDJSON vs JSON first-byte; browser walkthrough of 13 mode homes; static review of ClinicalDashboard / universal-search / medication+differential hooks. No OpenAI generation, no `eval:retrieval:latency`, no soak, no hosted CI. Environment had Supabase secrets so universal ran live (`publicAccess`); `/api/search` degraded to demo (`supabase_api_key_configuration_unavailable`). | | 2026-07-24 | `codex/safety-plan-no-patient-data-contract` | `b94987c94537f3114a3429848fa908bdecd1d80a` | Safety Plan Generator identifier, local-state, copy, print, privacy-notice and PIA contract | APPROVE. No P0-P2 finding. The patient name/initials field is removed; the builder now asks for identifier-free minimum content, retains working state only in the mounted React component, and makes clipboard/print/PDF export an explicit handling boundary. The PIA and product privacy copy distinguish this local-only tool from provider-backed questions. Highest residual risk is outside Clinical KB: users must handle exported copies under an approved clinical-record process, which the UI now states at the export controls. | Privacy/component DOM 3/3 plus updated privacy-copy 2/2; focused Chromium copy/print/no-fetch-or-XHR 1/1; `verify:cheap` passed all 21 gates, 366 files and 3,245 tests with 1 skip; production-readiness READY using the existing canonical environment without a provider call; production build and client-bundle secret scan passed; offline RAG fixture/manifest 36 cases/21 suites passed. `verify:pr-local` passed runtime, formatting, lint and typecheck, then stopped on the unrelated load-sensitive `reconciliation-preflight` 30-second timeout; that test passed 5/5 isolated and the preceding full suite passed, so the unchanged five-minute gate was not retried. No Supabase, OpenAI, Railway, live RAG, production data or deployment action ran. | | 2026-07-24 | PR #1114 / `codex/universal-ledger-main-followup-20260724` | `eedbe594a89eaa0aad8a517ee8808b089cb5ff57` + reviewed working diff | Streamlined-sidebar handoff in the merged universal ledger | APPROVE. PRs #1112, #1116, and #1117 landed the universal-ledger baseline, two preserved UI tasks, and credential/hook hardening; this follow-up preserves that current-main schema and adds only the still-missing sidebar delivery as collision-free `#066`. The queue remains gap-free and every recommended ID has one open row. Sidebar implementation stays recoverable from remote branch `origin/codex/sidebar-test-fix-20260723` at full commit `cd54e68fbf7b07b5dffe3220e36af2caa528da54` until its local/browser/build/hosted merge proof is complete. | Focused sidebar/favourites tests previously passed 18/18 on the recorded implementation commit; this ledger follow-up passed Prettier, remote-ref recovery, 36-row contiguous-order/46-open-ID/next-ID invariants, and `git diff --check`. The broader reconciled precursor passed `verify:cheap` (365 files, 3,242 passed/1 skipped); fresh exact-head hosted CI is required on PR #1114. No OpenAI, Supabase, Railway, deployment, live-app, credential, or production-data action ran. | diff --git a/docs/codebase-index.md b/docs/codebase-index.md index aa92ad7ad..a813a0e85 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -53,21 +53,21 @@ Structured map for AI agents and onboarding. For live routes, see `docs/site-map | --------------------------------------------------------------------------------------------------------- | -------------------------------------- | | `/` | `src/app/page.tsx` | | `/applications` | `src/app/applications/route.ts` | -| `/differentials`, `/diagnoses`, `/presentations` | `src/app/differentials/` | -| `/dsm`, `/dsm/search`, `/dsm/compare`, `/dsm/diagnoses/[slug]` | `src/app/dsm/` | -| `/documents/search`, `/source`, `/evidence`, `/[id]` | `src/app/documents/` | -| `/factsheets`, `/factsheets/search`, `/factsheets/[slug]` | `src/app/factsheets/` | -| `/favourites` | `src/app/favourites/page.tsx` | -| `/forms`, `/forms/[slug]` | `src/app/forms/` | -| `/medications`, `/medications/[slug]` | `src/app/medications/` | +| `/differentials`, `/diagnoses`, `/presentations` | `src/app/(search-app)/differentials/` | +| `/dsm`, `/dsm/search`, `/dsm/compare`, `/dsm/diagnoses/[slug]` | `src/app/(search-app)/dsm/` | +| `/documents/search`, `/source`, `/evidence`, `/[id]` | `src/app/(search-app)/documents/` | +| `/factsheets`, `/factsheets/search`, `/factsheets/[slug]` | `src/app/(search-app)/factsheets/` | +| `/favourites` | `src/app/(search-app)/favourites/page.tsx` | +| `/forms`, `/forms/[slug]` | `src/app/(search-app)/forms/` | +| `/medications`, `/medications/[slug]` | `src/app/(search-app)/medications/` | | `/privacy` | `src/app/privacy/page.tsx` | | `/reference/colour-coding` | `src/app/reference/` | | `/safety-plan` | `src/app/safety-plan/page.tsx` | -| `/services`, `/services/[slug]` | `src/app/services/` | -| `/therapy-compass` | `src/app/therapy-compass/` | -| `/tools` | `src/app/tools/` | -| `/specifiers`, `/specifiers/[slug]`, `/specifiers/builder`, `/specifiers/compare`, `/specifiers/map` | `src/app/specifiers/` | -| `/formulation`, `/formulation/[slug]`, `/formulation/builder`, `/formulation/compare`, `/formulation/map` | `src/app/formulation/` | +| `/services`, `/services/[slug]` | `src/app/(search-app)/services/` | +| `/therapy-compass` | `src/app/(search-app)/therapy-compass/` | +| `/tools` | `src/app/(search-app)/tools/` | +| `/specifiers`, `/specifiers/[slug]`, `/specifiers/builder`, `/specifiers/compare`, `/specifiers/map` | `src/app/(search-app)/specifiers/` | +| `/formulation`, `/formulation/[slug]`, `/formulation/builder`, `/formulation/compare`, `/formulation/map` | `src/app/(search-app)/formulation/` | | `/mockups/*` | `src/app/mockups/` (404 in production) | | `/auth/callback` | `src/app/auth/callback/route.ts` | diff --git a/docs/search-rag-master-context.md b/docs/search-rag-master-context.md index 3d241b334..80edb278f 100644 --- a/docs/search-rag-master-context.md +++ b/docs/search-rag-master-context.md @@ -116,7 +116,7 @@ Answer rendering and evidence UI: Document/source targets: -- `src/app/documents/[id]/page.tsx` +- `src/app/(search-app)/documents/[id]/page.tsx` - `src/components/DocumentViewer.tsx` API validation route families: diff --git a/docs/site-map.md b/docs/site-map.md index 21ca3e0ce..54c673d1f 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -4,42 +4,42 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` ## Main product routes -- `/` - Main Clinical KB shell. Source: `src/app/page.tsx`. -- `/differentials` - Differentials home and search surface. Source: `src/app/differentials/page.tsx`. -- `/differentials/diagnoses` - Diagnosis stream. Source: `src/app/differentials/diagnoses/page.tsx`. -- `/differentials/presentations/[slug]` - Route discovered from app directory Source: `src/app/differentials/presentations/[slug]/page.tsx`. -- `/documents/search` - Documents search command centre. Source: `src/app/documents/search/page.tsx`. -- `/documents/source` - Compatibility redirect to the canonical live document viewer when a valid id is supplied. Source: `src/app/documents/source/page.tsx`. -- `/documents/source/evidence` - Compatibility redirect sharing the canonical live document viewer handoff. Source: `src/app/documents/source/evidence/page.tsx`. -- `/dsm` - DSM-5 Diagnosis home. Source: `src/app/dsm/page.tsx`. -- `/dsm/compare` - DSM diagnosis comparison. Source: `src/app/dsm/compare/page.tsx`. -- `/dsm/search` - DSM diagnosis search and catalogue browser. Source: `src/app/dsm/search/page.tsx`. -- `/factsheets` - Route discovered from app directory Source: `src/app/factsheets/page.tsx`. -- `/factsheets/[slug]` - Route discovered from app directory Source: `src/app/factsheets/[slug]/page.tsx`. -- `/factsheets/search` - Route discovered from app directory Source: `src/app/factsheets/search/page.tsx`. -- `/favourites` - Saved clinical items and sets. Source: `src/app/favourites/page.tsx`. -- `/forms` - Forms home and search surface. Source: `src/app/forms/page.tsx`. -- `/formulation` - Clinical formulation home and local mechanism search surface. Source: `src/app/formulation/page.tsx`. -- `/formulation/builder` - Structured clinical formulation builder. Source: `src/app/formulation/builder/page.tsx`. -- `/formulation/compare` - Side-by-side mechanism comparison. Source: `src/app/formulation/compare/page.tsx`. -- `/formulation/map` - Formulation mechanism domain map. Source: `src/app/formulation/map/page.tsx`. +- `/` - Main Clinical KB shell. Source: `src/app/(search-app)/page.tsx`. +- `/differentials` - Differentials home and search surface. Source: `src/app/(search-app)/differentials/page.tsx`. +- `/differentials/diagnoses` - Diagnosis stream. Source: `src/app/(search-app)/differentials/diagnoses/page.tsx`. +- `/differentials/presentations/[slug]` - Route discovered from app directory Source: `src/app/(search-app)/differentials/presentations/[slug]/page.tsx`. +- `/documents/search` - Documents search command centre. Source: `src/app/(search-app)/documents/search/page.tsx`. +- `/documents/source` - Compatibility redirect to the canonical live document viewer when a valid id is supplied. Source: `src/app/(search-app)/documents/source/page.tsx`. +- `/documents/source/evidence` - Compatibility redirect sharing the canonical live document viewer handoff. Source: `src/app/(search-app)/documents/source/evidence/page.tsx`. +- `/dsm` - DSM-5 Diagnosis home. Source: `src/app/(search-app)/dsm/page.tsx`. +- `/dsm/compare` - DSM diagnosis comparison. Source: `src/app/(search-app)/dsm/compare/page.tsx`. +- `/dsm/search` - DSM diagnosis search and catalogue browser. Source: `src/app/(search-app)/dsm/search/page.tsx`. +- `/factsheets` - Route discovered from app directory Source: `src/app/(search-app)/factsheets/page.tsx`. +- `/factsheets/[slug]` - Route discovered from app directory Source: `src/app/(search-app)/factsheets/[slug]/page.tsx`. +- `/factsheets/search` - Route discovered from app directory Source: `src/app/(search-app)/factsheets/search/page.tsx`. +- `/favourites` - Saved clinical items and sets. Source: `src/app/(search-app)/favourites/page.tsx`. +- `/forms` - Forms home and search surface. Source: `src/app/(search-app)/forms/page.tsx`. +- `/formulation` - Clinical formulation home and local mechanism search surface. Source: `src/app/(search-app)/formulation/page.tsx`. +- `/formulation/builder` - Structured clinical formulation builder. Source: `src/app/(search-app)/formulation/builder/page.tsx`. +- `/formulation/compare` - Side-by-side mechanism comparison. Source: `src/app/(search-app)/formulation/compare/page.tsx`. +- `/formulation/map` - Formulation mechanism domain map. Source: `src/app/(search-app)/formulation/map/page.tsx`. - `/privacy` - Privacy and data-processing governance draft. Source: `src/app/privacy/page.tsx`. - `/reference/colour-coding` - Route discovered from app directory Source: `src/app/reference/colour-coding/page.tsx`. - `/safety-plan` - Patient safety plan generator (Stanley-Brown six steps) — a Tools-page clinical tool. Source: `src/app/safety-plan/page.tsx`. -- `/services` - Services home and search surface. Source: `src/app/services/page.tsx`. -- `/specifiers` - Psychiatric specifier home and local search surface. Source: `src/app/specifiers/page.tsx`. -- `/specifiers/builder` - Structured diagnostic wording builder. Source: `src/app/specifiers/builder/page.tsx`. -- `/specifiers/compare` - Side-by-side psychiatric specifier comparison. Source: `src/app/specifiers/compare/page.tsx`. -- `/specifiers/map` - Psychiatric specifier family map. Source: `src/app/specifiers/map/page.tsx`. -- `/therapy-compass` - Therapy mode home (source-grounded therapy decision support). Source: `src/app/therapy-compass/page.tsx`. -- `/therapy-compass/[slug]/brief` - Therapy brief-intervention view. Source: `src/app/therapy-compass/[slug]/brief/page.tsx`. -- `/therapy-compass/[slug]/sheet` - Therapy patient-sheet builder. Source: `src/app/therapy-compass/[slug]/sheet/page.tsx`. -- `/therapy-compass/compare` - Side-by-side therapy comparison. Source: `src/app/therapy-compass/compare/page.tsx`. -- `/therapy-compass/pathways` - Problem-based clinical therapy pathways. Source: `src/app/therapy-compass/pathways/page.tsx`. -- `/therapy-compass/recommend` - Recommend a therapy from a clinical question and constraints. Source: `src/app/therapy-compass/recommend/page.tsx`. -- `/therapy-compass/review` - Therapy records awaiting qualified-clinician source review. Source: `src/app/therapy-compass/review/page.tsx`. -- `/therapy-compass/search` - Therapy library search surface. Source: `src/app/therapy-compass/search/page.tsx`. -- `/tools` - Route discovered from app directory Source: `src/app/tools/page.tsx`. +- `/services` - Services home and search surface. Source: `src/app/(search-app)/services/page.tsx`. +- `/specifiers` - Psychiatric specifier home and local search surface. Source: `src/app/(search-app)/specifiers/page.tsx`. +- `/specifiers/builder` - Structured diagnostic wording builder. Source: `src/app/(search-app)/specifiers/builder/page.tsx`. +- `/specifiers/compare` - Side-by-side psychiatric specifier comparison. Source: `src/app/(search-app)/specifiers/compare/page.tsx`. +- `/specifiers/map` - Psychiatric specifier family map. Source: `src/app/(search-app)/specifiers/map/page.tsx`. +- `/therapy-compass` - Therapy mode home (source-grounded therapy decision support). Source: `src/app/(search-app)/therapy-compass/page.tsx`. +- `/therapy-compass/[slug]/brief` - Therapy brief-intervention view. Source: `src/app/(search-app)/therapy-compass/[slug]/brief/page.tsx`. +- `/therapy-compass/[slug]/sheet` - Therapy patient-sheet builder. Source: `src/app/(search-app)/therapy-compass/[slug]/sheet/page.tsx`. +- `/therapy-compass/compare` - Side-by-side therapy comparison. Source: `src/app/(search-app)/therapy-compass/compare/page.tsx`. +- `/therapy-compass/pathways` - Problem-based clinical therapy pathways. Source: `src/app/(search-app)/therapy-compass/pathways/page.tsx`. +- `/therapy-compass/recommend` - Recommend a therapy from a clinical question and constraints. Source: `src/app/(search-app)/therapy-compass/recommend/page.tsx`. +- `/therapy-compass/review` - Therapy records awaiting qualified-clinician source review. Source: `src/app/(search-app)/therapy-compass/review/page.tsx`. +- `/therapy-compass/search` - Therapy library search surface. Source: `src/app/(search-app)/therapy-compass/search/page.tsx`. +- `/tools` - Route discovered from app directory Source: `src/app/(search-app)/tools/page.tsx`. ## Mode/query routes @@ -1070,9 +1070,9 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` ## Redirects - `/applications` - Redirects to `/tools`. Source: `src/app/applications/route.ts`. -- `/differentials/presentations` - Redirects to `/differentials/presentations/[workflow-slug]`. Source: `src/app/differentials/presentations/route.ts`. -- `/documents/source` - Redirects to `/documents/[id]`. Source: `src/app/documents/source/page.tsx`. -- `/medications` - Redirects to `/?mode=prescribing`. Source: `src/app/medications/route.ts`. +- `/differentials/presentations` - Redirects to `/differentials/presentations/[workflow-slug]`. Source: `src/app/(search-app)/differentials/presentations/route.ts`. +- `/documents/source` - Redirects to `/documents/[id]`. Source: `src/app/(search-app)/documents/source/page.tsx`. +- `/medications` - Redirects to `/?mode=prescribing`. Source: `src/app/(search-app)/medications/route.ts`. - `/mockups/favourites-hub` - Redirects to `/favourites`. Source: `src/app/mockups/favourites-hub/page.tsx`. - `/mockups/medication-prescribing` - Redirects to `/medications/acamprosate`. Source: `src/app/mockups/medication-prescribing/page.tsx`. @@ -1087,18 +1087,18 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` ## Route ownership/source map -| Area | Source | -| ------------------------------ | --------------------------------------------------------------------------------------------- | -| Root dashboard and query modes | `src/app/page.tsx, src/lib/app-modes.ts` | -| Global shell layouts | `src/app/*/layout.tsx, src/components/clinical-dashboard/global-search-shell.tsx` | -| Services | `src/app/services, src/lib/services.ts, src/app/api/registry/records` | -| Forms | `src/app/forms, src/lib/forms.ts, src/app/api/registry/records` | -| Favourites | `src/app/favourites, src/components/clinical-dashboard/favourites-command-library-page.tsx` | -| Differentials | `src/app/differentials, src/lib/differentials.ts` | -| DSM-5 Diagnosis | `src/app/dsm, src/components/dsm, src/lib/dsm.ts` | -| Specifiers | `src/app/specifiers, src/components/specifiers, src/lib/specifiers.ts` | -| Formulation | `src/app/formulation, src/components/formulation, src/lib/formulation.ts` | -| Medications | `src/app/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx` | -| Documents | `src/app/documents, src/lib/document-flow-routes.ts` | -| Tools | `src/components/applications-launcher-page.tsx` | -| Mockups | `src/app/mockups` | +| Area | Source | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------- | +| Root dashboard and query modes | `src/app/page.tsx, src/lib/app-modes.ts` | +| Global shell layouts | `src/app/*/layout.tsx, src/components/clinical-dashboard/global-search-shell.tsx` | +| Services | `src/app/(search-app)/services, src/lib/services.ts, src/app/api/registry/records` | +| Forms | `src/app/(search-app)/forms, src/lib/forms.ts, src/app/api/registry/records` | +| Favourites | `src/app/(search-app)/favourites, src/components/clinical-dashboard/favourites-command-library-page.tsx` | +| Differentials | `src/app/(search-app)/differentials, src/lib/differentials.ts` | +| DSM-5 Diagnosis | `src/app/(search-app)/dsm, src/components/dsm, src/lib/dsm.ts` | +| Specifiers | `src/app/(search-app)/specifiers, src/components/specifiers, src/lib/specifiers.ts` | +| Formulation | `src/app/(search-app)/formulation, src/components/formulation, src/lib/formulation.ts` | +| Medications | `src/app/(search-app)/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx` | +| Documents | `src/app/(search-app)/documents, src/lib/document-flow-routes.ts` | +| Tools | `src/components/applications-launcher-page.tsx` | +| Mockups | `src/app/mockups` | diff --git a/scripts/check-docs-links.mjs b/scripts/check-docs-links.mjs index 6493a303e..a899d9443 100644 --- a/scripts/check-docs-links.mjs +++ b/scripts/check-docs-links.mjs @@ -46,7 +46,7 @@ const ALLOWLIST = new Set([ "scripts/reindex-shadow.ts", // designed-only harness driver (docs/reindex-shadow-harness-design.md) "docs/site-map.generated.md", // hypothetical future split named in docs/process-hardening.md // Removed after the redesign; referenced historically in docs/redesign/*: - "src/app/tools/page.tsx", + "src/app/(search-app)/tools/page.tsx", "src/lib/tools.ts", "src/components/ServiceDetailPage.tsx", ]); diff --git a/scripts/generate-site-map.ts b/scripts/generate-site-map.ts index 05d700535..ccade0019 100644 --- a/scripts/generate-site-map.ts +++ b/scripts/generate-site-map.ts @@ -137,15 +137,15 @@ const apiDescriptions: Record = { const routeOwnershipRows = [ ["Root dashboard and query modes", "src/app/page.tsx, src/lib/app-modes.ts"], ["Global shell layouts", "src/app/*/layout.tsx, src/components/clinical-dashboard/global-search-shell.tsx"], - ["Services", "src/app/services, src/lib/services.ts, src/app/api/registry/records"], - ["Forms", "src/app/forms, src/lib/forms.ts, src/app/api/registry/records"], - ["Favourites", "src/app/favourites, src/components/clinical-dashboard/favourites-command-library-page.tsx"], - ["Differentials", "src/app/differentials, src/lib/differentials.ts"], - ["DSM-5 Diagnosis", "src/app/dsm, src/components/dsm, src/lib/dsm.ts"], - ["Specifiers", "src/app/specifiers, src/components/specifiers, src/lib/specifiers.ts"], - ["Formulation", "src/app/formulation, src/components/formulation, src/lib/formulation.ts"], - ["Medications", "src/app/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx"], - ["Documents", "src/app/documents, src/lib/document-flow-routes.ts"], + ["Services", "src/app/(search-app)/services, src/lib/services.ts, src/app/api/registry/records"], + ["Forms", "src/app/(search-app)/forms, src/lib/forms.ts, src/app/api/registry/records"], + ["Favourites", "src/app/(search-app)/favourites, src/components/clinical-dashboard/favourites-command-library-page.tsx"], + ["Differentials", "src/app/(search-app)/differentials, src/lib/differentials.ts"], + ["DSM-5 Diagnosis", "src/app/(search-app)/dsm, src/components/dsm, src/lib/dsm.ts"], + ["Specifiers", "src/app/(search-app)/specifiers, src/components/specifiers, src/lib/specifiers.ts"], + ["Formulation", "src/app/(search-app)/formulation, src/components/formulation, src/lib/formulation.ts"], + ["Medications", "src/app/(search-app)/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx"], + ["Documents", "src/app/(search-app)/documents, src/lib/document-flow-routes.ts"], ["Tools", "src/components/applications-launcher-page.tsx"], ["Mockups", "src/app/mockups"], ] as const; diff --git a/src/app/differentials/diagnoses/[slug]/error.tsx b/src/app/(search-app)/differentials/diagnoses/[slug]/error.tsx similarity index 100% rename from src/app/differentials/diagnoses/[slug]/error.tsx rename to src/app/(search-app)/differentials/diagnoses/[slug]/error.tsx diff --git a/src/app/differentials/diagnoses/[slug]/loading.tsx b/src/app/(search-app)/differentials/diagnoses/[slug]/loading.tsx similarity index 100% rename from src/app/differentials/diagnoses/[slug]/loading.tsx rename to src/app/(search-app)/differentials/diagnoses/[slug]/loading.tsx diff --git a/src/app/differentials/diagnoses/[slug]/page.tsx b/src/app/(search-app)/differentials/diagnoses/[slug]/page.tsx similarity index 100% rename from src/app/differentials/diagnoses/[slug]/page.tsx rename to src/app/(search-app)/differentials/diagnoses/[slug]/page.tsx diff --git a/src/app/differentials/diagnoses/page.tsx b/src/app/(search-app)/differentials/diagnoses/page.tsx similarity index 100% rename from src/app/differentials/diagnoses/page.tsx rename to src/app/(search-app)/differentials/diagnoses/page.tsx diff --git a/src/app/differentials/error.tsx b/src/app/(search-app)/differentials/error.tsx similarity index 100% rename from src/app/differentials/error.tsx rename to src/app/(search-app)/differentials/error.tsx diff --git a/src/app/(search-app)/differentials/layout.tsx b/src/app/(search-app)/differentials/layout.tsx new file mode 100644 index 000000000..698714028 --- /dev/null +++ b/src/app/(search-app)/differentials/layout.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/differentials/loading.tsx b/src/app/(search-app)/differentials/loading.tsx similarity index 100% rename from src/app/differentials/loading.tsx rename to src/app/(search-app)/differentials/loading.tsx diff --git a/src/app/differentials/page.tsx b/src/app/(search-app)/differentials/page.tsx similarity index 100% rename from src/app/differentials/page.tsx rename to src/app/(search-app)/differentials/page.tsx diff --git a/src/app/differentials/presentations/[slug]/error.tsx b/src/app/(search-app)/differentials/presentations/[slug]/error.tsx similarity index 100% rename from src/app/differentials/presentations/[slug]/error.tsx rename to src/app/(search-app)/differentials/presentations/[slug]/error.tsx diff --git a/src/app/differentials/presentations/[slug]/loading.tsx b/src/app/(search-app)/differentials/presentations/[slug]/loading.tsx similarity index 100% rename from src/app/differentials/presentations/[slug]/loading.tsx rename to src/app/(search-app)/differentials/presentations/[slug]/loading.tsx diff --git a/src/app/differentials/presentations/[slug]/page.tsx b/src/app/(search-app)/differentials/presentations/[slug]/page.tsx similarity index 100% rename from src/app/differentials/presentations/[slug]/page.tsx rename to src/app/(search-app)/differentials/presentations/[slug]/page.tsx diff --git a/src/app/differentials/presentations/route.ts b/src/app/(search-app)/differentials/presentations/route.ts similarity index 100% rename from src/app/differentials/presentations/route.ts rename to src/app/(search-app)/differentials/presentations/route.ts diff --git a/src/app/documents/[id]/error.tsx b/src/app/(search-app)/documents/[id]/error.tsx similarity index 100% rename from src/app/documents/[id]/error.tsx rename to src/app/(search-app)/documents/[id]/error.tsx diff --git a/src/app/documents/[id]/loading.tsx b/src/app/(search-app)/documents/[id]/loading.tsx similarity index 100% rename from src/app/documents/[id]/loading.tsx rename to src/app/(search-app)/documents/[id]/loading.tsx diff --git a/src/app/documents/[id]/page.tsx b/src/app/(search-app)/documents/[id]/page.tsx similarity index 100% rename from src/app/documents/[id]/page.tsx rename to src/app/(search-app)/documents/[id]/page.tsx diff --git a/src/app/documents/error.tsx b/src/app/(search-app)/documents/error.tsx similarity index 100% rename from src/app/documents/error.tsx rename to src/app/(search-app)/documents/error.tsx diff --git a/src/app/(search-app)/documents/layout.tsx b/src/app/(search-app)/documents/layout.tsx new file mode 100644 index 000000000..33d39b70e --- /dev/null +++ b/src/app/(search-app)/documents/layout.tsx @@ -0,0 +1,6 @@ +import type { ReactNode } from "react"; + +// Documents search-shell props (composer visibility) are derived in SharedSearchAppShell. +export default function DocumentsLayout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/documents/search/loading.tsx b/src/app/(search-app)/documents/search/loading.tsx similarity index 100% rename from src/app/documents/search/loading.tsx rename to src/app/(search-app)/documents/search/loading.tsx diff --git a/src/app/documents/search/page.tsx b/src/app/(search-app)/documents/search/page.tsx similarity index 100% rename from src/app/documents/search/page.tsx rename to src/app/(search-app)/documents/search/page.tsx diff --git a/src/app/(search-app)/documents/source/evidence/page.tsx b/src/app/(search-app)/documents/source/evidence/page.tsx new file mode 100644 index 000000000..918715377 --- /dev/null +++ b/src/app/(search-app)/documents/source/evidence/page.tsx @@ -0,0 +1 @@ +export { metadata, default } from "@/app/(search-app)/documents/source/page"; diff --git a/src/app/documents/source/page.tsx b/src/app/(search-app)/documents/source/page.tsx similarity index 100% rename from src/app/documents/source/page.tsx rename to src/app/(search-app)/documents/source/page.tsx diff --git a/src/app/dsm/compare/page.tsx b/src/app/(search-app)/dsm/compare/page.tsx similarity index 100% rename from src/app/dsm/compare/page.tsx rename to src/app/(search-app)/dsm/compare/page.tsx diff --git a/src/app/dsm/diagnoses/[slug]/differentials/page.tsx b/src/app/(search-app)/dsm/diagnoses/[slug]/differentials/page.tsx similarity index 100% rename from src/app/dsm/diagnoses/[slug]/differentials/page.tsx rename to src/app/(search-app)/dsm/diagnoses/[slug]/differentials/page.tsx diff --git a/src/app/dsm/diagnoses/[slug]/page.tsx b/src/app/(search-app)/dsm/diagnoses/[slug]/page.tsx similarity index 100% rename from src/app/dsm/diagnoses/[slug]/page.tsx rename to src/app/(search-app)/dsm/diagnoses/[slug]/page.tsx diff --git a/src/app/(search-app)/dsm/layout.tsx b/src/app/(search-app)/dsm/layout.tsx new file mode 100644 index 000000000..698714028 --- /dev/null +++ b/src/app/(search-app)/dsm/layout.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/dsm/loading.tsx b/src/app/(search-app)/dsm/loading.tsx similarity index 100% rename from src/app/dsm/loading.tsx rename to src/app/(search-app)/dsm/loading.tsx diff --git a/src/app/dsm/page.tsx b/src/app/(search-app)/dsm/page.tsx similarity index 100% rename from src/app/dsm/page.tsx rename to src/app/(search-app)/dsm/page.tsx diff --git a/src/app/dsm/search/page.tsx b/src/app/(search-app)/dsm/search/page.tsx similarity index 100% rename from src/app/dsm/search/page.tsx rename to src/app/(search-app)/dsm/search/page.tsx diff --git a/src/app/factsheets/[slug]/page.tsx b/src/app/(search-app)/factsheets/[slug]/page.tsx similarity index 100% rename from src/app/factsheets/[slug]/page.tsx rename to src/app/(search-app)/factsheets/[slug]/page.tsx diff --git a/src/app/(search-app)/factsheets/layout.tsx b/src/app/(search-app)/factsheets/layout.tsx new file mode 100644 index 000000000..698714028 --- /dev/null +++ b/src/app/(search-app)/factsheets/layout.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/factsheets/page.tsx b/src/app/(search-app)/factsheets/page.tsx similarity index 100% rename from src/app/factsheets/page.tsx rename to src/app/(search-app)/factsheets/page.tsx diff --git a/src/app/factsheets/search/page.tsx b/src/app/(search-app)/factsheets/search/page.tsx similarity index 100% rename from src/app/factsheets/search/page.tsx rename to src/app/(search-app)/factsheets/search/page.tsx diff --git a/src/app/favourites/error.tsx b/src/app/(search-app)/favourites/error.tsx similarity index 100% rename from src/app/favourites/error.tsx rename to src/app/(search-app)/favourites/error.tsx diff --git a/src/app/(search-app)/favourites/layout.tsx b/src/app/(search-app)/favourites/layout.tsx new file mode 100644 index 000000000..698714028 --- /dev/null +++ b/src/app/(search-app)/favourites/layout.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/favourites/loading.tsx b/src/app/(search-app)/favourites/loading.tsx similarity index 100% rename from src/app/favourites/loading.tsx rename to src/app/(search-app)/favourites/loading.tsx diff --git a/src/app/favourites/page.tsx b/src/app/(search-app)/favourites/page.tsx similarity index 100% rename from src/app/favourites/page.tsx rename to src/app/(search-app)/favourites/page.tsx diff --git a/src/app/forms/[slug]/error.tsx b/src/app/(search-app)/forms/[slug]/error.tsx similarity index 100% rename from src/app/forms/[slug]/error.tsx rename to src/app/(search-app)/forms/[slug]/error.tsx diff --git a/src/app/forms/[slug]/page.tsx b/src/app/(search-app)/forms/[slug]/page.tsx similarity index 100% rename from src/app/forms/[slug]/page.tsx rename to src/app/(search-app)/forms/[slug]/page.tsx diff --git a/src/app/forms/error.tsx b/src/app/(search-app)/forms/error.tsx similarity index 100% rename from src/app/forms/error.tsx rename to src/app/(search-app)/forms/error.tsx diff --git a/src/app/(search-app)/forms/layout.tsx b/src/app/(search-app)/forms/layout.tsx new file mode 100644 index 000000000..698714028 --- /dev/null +++ b/src/app/(search-app)/forms/layout.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/forms/loading.tsx b/src/app/(search-app)/forms/loading.tsx similarity index 100% rename from src/app/forms/loading.tsx rename to src/app/(search-app)/forms/loading.tsx diff --git a/src/app/forms/page.tsx b/src/app/(search-app)/forms/page.tsx similarity index 100% rename from src/app/forms/page.tsx rename to src/app/(search-app)/forms/page.tsx diff --git a/src/app/formulation/[slug]/page.tsx b/src/app/(search-app)/formulation/[slug]/page.tsx similarity index 100% rename from src/app/formulation/[slug]/page.tsx rename to src/app/(search-app)/formulation/[slug]/page.tsx diff --git a/src/app/formulation/builder/page.tsx b/src/app/(search-app)/formulation/builder/page.tsx similarity index 100% rename from src/app/formulation/builder/page.tsx rename to src/app/(search-app)/formulation/builder/page.tsx diff --git a/src/app/formulation/compare/page.tsx b/src/app/(search-app)/formulation/compare/page.tsx similarity index 100% rename from src/app/formulation/compare/page.tsx rename to src/app/(search-app)/formulation/compare/page.tsx diff --git a/src/app/(search-app)/formulation/layout.tsx b/src/app/(search-app)/formulation/layout.tsx new file mode 100644 index 000000000..698714028 --- /dev/null +++ b/src/app/(search-app)/formulation/layout.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/formulation/map/page.tsx b/src/app/(search-app)/formulation/map/page.tsx similarity index 100% rename from src/app/formulation/map/page.tsx rename to src/app/(search-app)/formulation/map/page.tsx diff --git a/src/app/formulation/page.tsx b/src/app/(search-app)/formulation/page.tsx similarity index 100% rename from src/app/formulation/page.tsx rename to src/app/(search-app)/formulation/page.tsx diff --git a/src/app/(search-app)/home-page-client.tsx b/src/app/(search-app)/home-page-client.tsx new file mode 100644 index 000000000..2a66fcf15 --- /dev/null +++ b/src/app/(search-app)/home-page-client.tsx @@ -0,0 +1,17 @@ +"use client"; + +import type { ReactNode } from "react"; + +import type { AppModeId } from "@/lib/app-modes"; + +/** Home page content slot; search chrome comes from the shared `(search-app)` layout. */ +export function HomePageClient({ + initialMode: _initialMode, + children, +}: { + initialMode: AppModeId; + children?: ReactNode; +}) { + void _initialMode; + return <>{children ?? null}; +} diff --git a/src/app/(search-app)/layout.tsx b/src/app/(search-app)/layout.tsx new file mode 100644 index 000000000..2423edbdf --- /dev/null +++ b/src/app/(search-app)/layout.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from "react"; + +import { SharedSearchAppShell } from "@/components/clinical-dashboard/shared-search-app-shell"; +import "@/components/therapy-compass/therapy-compass.css"; + +/** + * Shared search chrome for mode homes and related routes. Keeping GlobalSearchShell + * in this route-group layout prevents remounting the composer when navigating + * between namespaced modes (e.g. /services ↔ /dsm ↔ /). + */ +export default function SearchAppLayout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/src/app/medications/[slug]/error.tsx b/src/app/(search-app)/medications/[slug]/error.tsx similarity index 100% rename from src/app/medications/[slug]/error.tsx rename to src/app/(search-app)/medications/[slug]/error.tsx diff --git a/src/app/medications/[slug]/loading.tsx b/src/app/(search-app)/medications/[slug]/loading.tsx similarity index 100% rename from src/app/medications/[slug]/loading.tsx rename to src/app/(search-app)/medications/[slug]/loading.tsx diff --git a/src/app/medications/[slug]/page.tsx b/src/app/(search-app)/medications/[slug]/page.tsx similarity index 100% rename from src/app/medications/[slug]/page.tsx rename to src/app/(search-app)/medications/[slug]/page.tsx diff --git a/src/app/(search-app)/medications/layout.tsx b/src/app/(search-app)/medications/layout.tsx new file mode 100644 index 000000000..698714028 --- /dev/null +++ b/src/app/(search-app)/medications/layout.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/medications/route.ts b/src/app/(search-app)/medications/route.ts similarity index 100% rename from src/app/medications/route.ts rename to src/app/(search-app)/medications/route.ts diff --git a/src/app/page.tsx b/src/app/(search-app)/page.tsx similarity index 98% rename from src/app/page.tsx rename to src/app/(search-app)/page.tsx index 0d6eeafa7..bc53e0b2e 100644 --- a/src/app/page.tsx +++ b/src/app/(search-app)/page.tsx @@ -1,7 +1,7 @@ import { redirect } from "next/navigation"; import { connection } from "next/server"; -import { HomePageClient } from "@/app/home-page-client"; +import { HomePageClient } from "./home-page-client"; import { appModeHomeHref, isAppModeId, isAppModeVisible, type AppModeId } from "@/lib/app-modes"; type HomeProps = { diff --git a/src/app/services/[slug]/error.tsx b/src/app/(search-app)/services/[slug]/error.tsx similarity index 100% rename from src/app/services/[slug]/error.tsx rename to src/app/(search-app)/services/[slug]/error.tsx diff --git a/src/app/services/[slug]/page.tsx b/src/app/(search-app)/services/[slug]/page.tsx similarity index 100% rename from src/app/services/[slug]/page.tsx rename to src/app/(search-app)/services/[slug]/page.tsx diff --git a/src/app/services/error.tsx b/src/app/(search-app)/services/error.tsx similarity index 100% rename from src/app/services/error.tsx rename to src/app/(search-app)/services/error.tsx diff --git a/src/app/(search-app)/services/layout.tsx b/src/app/(search-app)/services/layout.tsx new file mode 100644 index 000000000..698714028 --- /dev/null +++ b/src/app/(search-app)/services/layout.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/services/loading.tsx b/src/app/(search-app)/services/loading.tsx similarity index 100% rename from src/app/services/loading.tsx rename to src/app/(search-app)/services/loading.tsx diff --git a/src/app/services/page.tsx b/src/app/(search-app)/services/page.tsx similarity index 100% rename from src/app/services/page.tsx rename to src/app/(search-app)/services/page.tsx diff --git a/src/app/specifiers/[slug]/page.tsx b/src/app/(search-app)/specifiers/[slug]/page.tsx similarity index 100% rename from src/app/specifiers/[slug]/page.tsx rename to src/app/(search-app)/specifiers/[slug]/page.tsx diff --git a/src/app/specifiers/builder/page.tsx b/src/app/(search-app)/specifiers/builder/page.tsx similarity index 100% rename from src/app/specifiers/builder/page.tsx rename to src/app/(search-app)/specifiers/builder/page.tsx diff --git a/src/app/specifiers/compare/page.tsx b/src/app/(search-app)/specifiers/compare/page.tsx similarity index 100% rename from src/app/specifiers/compare/page.tsx rename to src/app/(search-app)/specifiers/compare/page.tsx diff --git a/src/app/(search-app)/specifiers/layout.tsx b/src/app/(search-app)/specifiers/layout.tsx new file mode 100644 index 000000000..698714028 --- /dev/null +++ b/src/app/(search-app)/specifiers/layout.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/specifiers/map/page.tsx b/src/app/(search-app)/specifiers/map/page.tsx similarity index 100% rename from src/app/specifiers/map/page.tsx rename to src/app/(search-app)/specifiers/map/page.tsx diff --git a/src/app/specifiers/page.tsx b/src/app/(search-app)/specifiers/page.tsx similarity index 100% rename from src/app/specifiers/page.tsx rename to src/app/(search-app)/specifiers/page.tsx diff --git a/src/app/therapy-compass/[slug]/brief/page.tsx b/src/app/(search-app)/therapy-compass/[slug]/brief/page.tsx similarity index 100% rename from src/app/therapy-compass/[slug]/brief/page.tsx rename to src/app/(search-app)/therapy-compass/[slug]/brief/page.tsx diff --git a/src/app/therapy-compass/[slug]/page.tsx b/src/app/(search-app)/therapy-compass/[slug]/page.tsx similarity index 100% rename from src/app/therapy-compass/[slug]/page.tsx rename to src/app/(search-app)/therapy-compass/[slug]/page.tsx diff --git a/src/app/therapy-compass/[slug]/sheet/page.tsx b/src/app/(search-app)/therapy-compass/[slug]/sheet/page.tsx similarity index 100% rename from src/app/therapy-compass/[slug]/sheet/page.tsx rename to src/app/(search-app)/therapy-compass/[slug]/sheet/page.tsx diff --git a/src/app/therapy-compass/compare/page.tsx b/src/app/(search-app)/therapy-compass/compare/page.tsx similarity index 100% rename from src/app/therapy-compass/compare/page.tsx rename to src/app/(search-app)/therapy-compass/compare/page.tsx diff --git a/src/app/(search-app)/therapy-compass/layout.tsx b/src/app/(search-app)/therapy-compass/layout.tsx new file mode 100644 index 000000000..e44b5c3be --- /dev/null +++ b/src/app/(search-app)/therapy-compass/layout.tsx @@ -0,0 +1,6 @@ +import type { ReactNode } from "react"; + +// Search chrome and TherapyCompassWorkspace are owned by the shared (search-app) layout. +export default function TherapyCompassLayout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/therapy-compass/page.tsx b/src/app/(search-app)/therapy-compass/page.tsx similarity index 100% rename from src/app/therapy-compass/page.tsx rename to src/app/(search-app)/therapy-compass/page.tsx diff --git a/src/app/therapy-compass/pathways/page.tsx b/src/app/(search-app)/therapy-compass/pathways/page.tsx similarity index 100% rename from src/app/therapy-compass/pathways/page.tsx rename to src/app/(search-app)/therapy-compass/pathways/page.tsx diff --git a/src/app/therapy-compass/recommend/page.tsx b/src/app/(search-app)/therapy-compass/recommend/page.tsx similarity index 100% rename from src/app/therapy-compass/recommend/page.tsx rename to src/app/(search-app)/therapy-compass/recommend/page.tsx diff --git a/src/app/therapy-compass/review/page.tsx b/src/app/(search-app)/therapy-compass/review/page.tsx similarity index 100% rename from src/app/therapy-compass/review/page.tsx rename to src/app/(search-app)/therapy-compass/review/page.tsx diff --git a/src/app/therapy-compass/search/page.tsx b/src/app/(search-app)/therapy-compass/search/page.tsx similarity index 100% rename from src/app/therapy-compass/search/page.tsx rename to src/app/(search-app)/therapy-compass/search/page.tsx diff --git a/src/app/tools/error.tsx b/src/app/(search-app)/tools/error.tsx similarity index 100% rename from src/app/tools/error.tsx rename to src/app/(search-app)/tools/error.tsx diff --git a/src/app/(search-app)/tools/layout.tsx b/src/app/(search-app)/tools/layout.tsx new file mode 100644 index 000000000..698714028 --- /dev/null +++ b/src/app/(search-app)/tools/layout.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export default function Layout({ children }: { children: ReactNode }) { + return children; +} diff --git a/src/app/tools/loading.tsx b/src/app/(search-app)/tools/loading.tsx similarity index 100% rename from src/app/tools/loading.tsx rename to src/app/(search-app)/tools/loading.tsx diff --git a/src/app/tools/page.tsx b/src/app/(search-app)/tools/page.tsx similarity index 100% rename from src/app/tools/page.tsx rename to src/app/(search-app)/tools/page.tsx diff --git a/src/app/differentials/layout.tsx b/src/app/differentials/layout.tsx deleted file mode 100644 index b37056fb2..000000000 --- a/src/app/differentials/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { ReactNode } from "react"; - -import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; - -export default function DifferentialsLayout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} diff --git a/src/app/documents/documents-layout-client.tsx b/src/app/documents/documents-layout-client.tsx deleted file mode 100644 index 4876ba5fd..000000000 --- a/src/app/documents/documents-layout-client.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import { usePathname } from "next/navigation"; -import type { ReactNode } from "react"; - -import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; - -export function DocumentsLayoutClient({ children }: { children: ReactNode }) { - const pathname = usePathname(); - const isDocumentSearchRoute = pathname === "/documents/search"; - const documentFlowOwnsMobileChrome = pathname.startsWith("/documents/source"); - - return ( - - {children} - - ); -} diff --git a/src/app/documents/layout.tsx b/src/app/documents/layout.tsx deleted file mode 100644 index 2d7b031b9..000000000 --- a/src/app/documents/layout.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import type { ReactNode } from "react"; - -import { DocumentsLayoutClient } from "@/app/documents/documents-layout-client"; - -export default function DocumentsLayout({ children }: { children: ReactNode }) { - return {children}; -} diff --git a/src/app/documents/source/evidence/page.tsx b/src/app/documents/source/evidence/page.tsx deleted file mode 100644 index a56713083..000000000 --- a/src/app/documents/source/evidence/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { metadata, default } from "@/app/documents/source/page"; diff --git a/src/app/dsm/layout.tsx b/src/app/dsm/layout.tsx deleted file mode 100644 index 073e802aa..000000000 --- a/src/app/dsm/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { ReactNode } from "react"; - -import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; - -export default function DsmLayout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} diff --git a/src/app/factsheets/layout.tsx b/src/app/factsheets/layout.tsx deleted file mode 100644 index 2ab4ec162..000000000 --- a/src/app/factsheets/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { ReactNode } from "react"; - -import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; - -export default function FactsheetsLayout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} diff --git a/src/app/favourites/layout.tsx b/src/app/favourites/layout.tsx deleted file mode 100644 index abc0eb04d..000000000 --- a/src/app/favourites/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { ReactNode } from "react"; - -import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; - -export default function FavouritesLayout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} diff --git a/src/app/forms/layout.tsx b/src/app/forms/layout.tsx deleted file mode 100644 index c2e83ea0c..000000000 --- a/src/app/forms/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { ReactNode } from "react"; - -import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; - -export default function FormsLayout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} diff --git a/src/app/formulation/layout.tsx b/src/app/formulation/layout.tsx deleted file mode 100644 index 3c2c1a89f..000000000 --- a/src/app/formulation/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { ReactNode } from "react"; - -import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; - -export default function FormulationLayout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} diff --git a/src/app/home-page-client.tsx b/src/app/home-page-client.tsx deleted file mode 100644 index 97278e45d..000000000 --- a/src/app/home-page-client.tsx +++ /dev/null @@ -1,10 +0,0 @@ -"use client"; - -import type { ReactNode } from "react"; - -import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; -import type { AppModeId } from "@/lib/app-modes"; - -export function HomePageClient({ initialMode, children }: { initialMode: AppModeId; children?: ReactNode }) { - return {children ?? null}; -} diff --git a/src/app/medications/layout.tsx b/src/app/medications/layout.tsx deleted file mode 100644 index b891de232..000000000 --- a/src/app/medications/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { ReactNode } from "react"; - -import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; - -export default function MedicationsLayout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} diff --git a/src/app/services/layout.tsx b/src/app/services/layout.tsx deleted file mode 100644 index 462b9148f..000000000 --- a/src/app/services/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { ReactNode } from "react"; - -import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; - -export default function ServicesLayout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} diff --git a/src/app/specifiers/layout.tsx b/src/app/specifiers/layout.tsx deleted file mode 100644 index 744354acd..000000000 --- a/src/app/specifiers/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { ReactNode } from "react"; - -import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; - -export default function SpecifiersLayout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} diff --git a/src/app/therapy-compass/layout.tsx b/src/app/therapy-compass/layout.tsx deleted file mode 100644 index 0f8a8f589..000000000 --- a/src/app/therapy-compass/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Suspense, type ReactNode } from "react"; - -import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; -import { TherapyCompassWorkspace } from "@/components/therapy-compass"; -import "@/components/therapy-compass/therapy-compass.css"; - -// Therapy Compass uses the same universal header, rail, and responsive search -// composer as the other mode homes. The workspace is mounted at the layout level -// so the therapy dataset and interaction state are shared across every -// /therapy-compass/* route, while each route renders its own screen into the -// workspace's main content. -export default function TherapyCompassLayout({ children }: { children: ReactNode }) { - return ( - - {/* The workspace provider reads useSearchParams; an explicit boundary lets the - route family prerender on its own, independent of the shell's Suspense. */} - - {children} - - - ); -} diff --git a/src/app/tools/layout.tsx b/src/app/tools/layout.tsx deleted file mode 100644 index 3e4972261..000000000 --- a/src/app/tools/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { ReactNode } from "react"; - -import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; - -export default function ToolsLayout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 51a31a707..31a9731bb 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -2507,23 +2507,34 @@ export function ClinicalDashboard({ window.requestAnimationFrame(() => mainRef.current?.scrollTo({ top: 0, behavior: resolveScrollBehavior() })); if (updateUrl) updateDocumentSearchUrl(trimmedSearchText, targetMode, filtersOverride); + searchAbortRef.current?.abort(); + const abortController = new AbortController(); + searchAbortRef.current = abortController; const requestId = ++searchRequestSeqRef.current; try { const shortcutQueryMode = appModeQueryMode(targetMode, queryMode); const payload = await runWithRetries(() => - requestSourceLibrarySearch(trimmedSearchText, sourceLibraryMode, filtersOverride, shortcutQueryMode), + requestSourceLibrarySearch( + trimmedSearchText, + sourceLibraryMode, + filtersOverride, + shortcutQueryMode, + abortController.signal, + ), ); if (requestId === searchRequestSeqRef.current) { applySearchResult(payload); } } catch (requestError) { + if (abortController.signal.aborted || isAbortError(requestError)) return; if (requestId === searchRequestSeqRef.current) { setError(requestError instanceof Error ? requestError.message : "Document search failed"); setErrorKind(null); setLastFailedQuery(null); } } finally { + if (searchAbortRef.current === abortController) searchAbortRef.current = null; if (requestId === searchRequestSeqRef.current) { setLoading(false); setAnswerProgress(null); diff --git a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx index 5db020d95..e8dec18d6 100644 --- a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx +++ b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx @@ -406,7 +406,10 @@ function MedicationResults({ "query" | "realDataReady" | "authUnavailable" | "apiUnavailable" | "setupWarning" >) { const command = useSearchCommand(); - const catalog = useMedicationCatalog(query); + // Ranking only needs identity fields; `fields=index` keeps keystroke fetches ~100KB + // instead of the full ~2.5MB catalogue. Patient alerts that need section rows still + // run on the medication detail page (full record). + const catalog = useMedicationCatalog(query, { fields: "index" }); const { profile, isEmpty: profileEmpty } = usePatientProfile(); const [activeFilter, setActiveFilter] = useState("best"); const { rows, counts, totalAvailable } = useMemo(() => { diff --git a/src/components/clinical-dashboard/shared-search-app-shell.tsx b/src/components/clinical-dashboard/shared-search-app-shell.tsx new file mode 100644 index 000000000..198c61ede --- /dev/null +++ b/src/components/clinical-dashboard/shared-search-app-shell.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { Suspense, type ReactNode } from "react"; +import { usePathname } from "next/navigation"; + +import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; +import { TherapyCompassWorkspace } from "@/components/therapy-compass"; +import { searchShellPropsForPathname } from "@/lib/search-shell-props"; + +/** + * Owns one GlobalSearchShell across mode homes so navigating between + * /services, /dsm, /, etc. does not remount the shared composer chrome. + */ +export function SharedSearchAppShell({ children }: { children: ReactNode }) { + const pathname = usePathname() ?? "/"; + const shellProps = searchShellPropsForPathname(pathname); + const content = pathname.startsWith("/therapy-compass") ? ( + + {children} + + ) : ( + children + ); + + return {content}; +} diff --git a/src/components/clinical-dashboard/use-differential-catalog.ts b/src/components/clinical-dashboard/use-differential-catalog.ts index f0d757881..1a4ecab25 100644 --- a/src/components/clinical-dashboard/use-differential-catalog.ts +++ b/src/components/clinical-dashboard/use-differential-catalog.ts @@ -44,6 +44,9 @@ export type DifferentialPresentationState = { governance: DifferentialRecordGovernance | null; }; +/** Match universal / medication catalogue debounce so live composer follow coalesces. */ +const differentialSearchDebounceMs = 250; + /** Ranked catalogue search for the Differentials search mode: fetches scored * diagnosis and presentation matches in parallel from /api/differentials. * Empty queries resolve immediately without a request. */ @@ -69,47 +72,58 @@ export function useDifferentialSearch(query: string): DifferentialSearchState { useEffect(() => { if (!requestKey) return undefined; - let active = true; + const controller = new AbortController(); const encoded = encodeURIComponent(requestKey); - Promise.all([ - fetch(`/api/differentials?kind=diagnosis&q=${encoded}&limit=20`, { headers: authorizationHeader }), - fetch(`/api/differentials?kind=presentation&q=${encoded}&limit=10`, { headers: authorizationHeader }), - ]) - .then(async ([diagnosisResponse, presentationResponse]) => { - if (!active) return; - if (diagnosisResponse.status === 401 || presentationResponse.status === 401) { - if (authStatus === "loading") return; - if (authStatus === "authenticated") markSessionExpired(); - setState({ status: "unauthorized", matches: emptyDifferentialMatches, demoMode: false }); - return; - } - if (!diagnosisResponse.ok || !presentationResponse.ok) { + const timer = window.setTimeout(() => { + Promise.all([ + fetch(`/api/differentials?kind=diagnosis&q=${encoded}&limit=20`, { + headers: authorizationHeader, + signal: controller.signal, + }), + fetch(`/api/differentials?kind=presentation&q=${encoded}&limit=10`, { + headers: authorizationHeader, + signal: controller.signal, + }), + ]) + .then(async ([diagnosisResponse, presentationResponse]) => { + if (controller.signal.aborted) return; + if (diagnosisResponse.status === 401 || presentationResponse.status === 401) { + if (authStatus === "loading") return; + if (authStatus === "authenticated") markSessionExpired(); + setState({ status: "unauthorized", matches: emptyDifferentialMatches, demoMode: false }); + return; + } + if (!diagnosisResponse.ok || !presentationResponse.ok) { + setState({ status: "error", matches: emptyDifferentialMatches, demoMode: false }); + return; + } + const diagnosisPayload = (await diagnosisResponse.json()) as { + matches?: DifferentialSearchMatches["diagnoses"]; + demoMode?: boolean; + }; + const presentationPayload = (await presentationResponse.json()) as { + matches?: DifferentialSearchMatches["presentations"]; + demoMode?: boolean; + }; + if (controller.signal.aborted) return; + setState({ + status: "ready", + matches: { + diagnoses: diagnosisPayload.matches ?? [], + presentations: presentationPayload.matches ?? [], + }, + demoMode: Boolean(diagnosisPayload.demoMode || presentationPayload.demoMode), + }); + }) + .catch((error: unknown) => { + if (controller.signal.aborted || (error instanceof DOMException && error.name === "AbortError")) return; setState({ status: "error", matches: emptyDifferentialMatches, demoMode: false }); - return; - } - const diagnosisPayload = (await diagnosisResponse.json()) as { - matches?: DifferentialSearchMatches["diagnoses"]; - demoMode?: boolean; - }; - const presentationPayload = (await presentationResponse.json()) as { - matches?: DifferentialSearchMatches["presentations"]; - demoMode?: boolean; - }; - if (!active) return; - setState({ - status: "ready", - matches: { - diagnoses: diagnosisPayload.matches ?? [], - presentations: presentationPayload.matches ?? [], - }, - demoMode: Boolean(diagnosisPayload.demoMode || presentationPayload.demoMode), }); - }) - .catch(() => { - if (active) setState({ status: "error", matches: emptyDifferentialMatches, demoMode: false }); - }); + }, differentialSearchDebounceMs); + return () => { - active = false; + window.clearTimeout(timer); + controller.abort(); }; }, [requestKey, authStatus, authorizationHeader, markSessionExpired]); diff --git a/src/components/clinical-dashboard/use-medication-catalog.ts b/src/components/clinical-dashboard/use-medication-catalog.ts index 85161415f..746f26a18 100644 --- a/src/components/clinical-dashboard/use-medication-catalog.ts +++ b/src/components/clinical-dashboard/use-medication-catalog.ts @@ -35,13 +35,16 @@ type AsyncState = { error: string | null; }; -async function fetchJson(url: string, headers?: HeadersInit): Promise { +/** Match universal typeahead debounce so prescribing keystrokes coalesce. */ +const catalogDebounceMs = 250; + +async function fetchJson(url: string, headers: HeadersInit | undefined, signal: AbortSignal): Promise { // Use the default cache mode (not `no-store`) so public responses honor the // API's `public, max-age=300, s-maxage=3600, stale-while-revalidate` headers. // Owner responses are served `private, no-store` with `Vary: Authorization`, // so the browser never caches them across auth states — matching the sibling // registry/differential hooks, which also fetch with the default cache mode. - const response = await fetch(url, { headers }); + const response = await fetch(url, { headers, signal }); if (!response.ok) { throw new Error(`Request failed (${response.status})`); } @@ -50,10 +53,11 @@ async function fetchJson(url: string, headers?: HeadersInit): Promise { export function useMedicationCatalog( query?: string, - options: { enabled?: boolean; fields?: "index" } = {}, + options: { enabled?: boolean; fields?: "index"; debounceMs?: number } = {}, ): AsyncState { const enabled = options.enabled ?? true; const fields = options.fields; + const debounceMs = options.debounceMs ?? catalogDebounceMs; const trimmed = query?.trim() ?? ""; // Auth-aware like use-registry-records: without the header an authenticated owner was // silently served the public fixture catalogue instead of their seeded records. @@ -78,29 +82,33 @@ export function useMedicationCatalog( useEffect(() => { if (!enabled) return; - let cancelled = false; + const controller = new AbortController(); const params = new URLSearchParams(); if (trimmed) params.set("q", trimmed); if (fields) params.set("fields", fields); const suffix = params.toString(); const url = suffix ? `/api/medications?${suffix}` : "/api/medications"; - fetchJson(url, authorizationHeader) - .then((data) => { - if (!cancelled) setState({ data, loading: false, error: null }); - }) - .catch((error) => { - if (!cancelled) { + + const timer = window.setTimeout(() => { + fetchJson(url, authorizationHeader, controller.signal) + .then((data) => { + if (!controller.signal.aborted) setState({ data, loading: false, error: null }); + }) + .catch((error) => { + if (controller.signal.aborted || (error instanceof DOMException && error.name === "AbortError")) return; setState({ data: null, loading: false, error: error instanceof Error ? error.message : "Could not load medications.", }); - } - }); + }); + }, debounceMs); + return () => { - cancelled = true; + window.clearTimeout(timer); + controller.abort(); }; - }, [trimmed, enabled, fields, authorizationHeader]); + }, [trimmed, enabled, fields, debounceMs, authorizationHeader]); return state; } @@ -128,22 +136,25 @@ export function useMedicationDetail(slug?: string): AsyncState(`/api/medications/${encodeURIComponent(normalized)}`, authorizationHeader) + const controller = new AbortController(); + fetchJson( + `/api/medications/${encodeURIComponent(normalized)}`, + authorizationHeader, + controller.signal, + ) .then((data) => { - if (!cancelled) setState({ data, loading: false, error: null }); + if (!controller.signal.aborted) setState({ data, loading: false, error: null }); }) .catch((error) => { - if (!cancelled) { - setState({ - data: null, - loading: false, - error: error instanceof Error ? error.message : "Could not load medication.", - }); - } + if (controller.signal.aborted || (error instanceof DOMException && error.name === "AbortError")) return; + setState({ + data: null, + loading: false, + error: error instanceof Error ? error.message : "Could not load medication.", + }); }); return () => { - cancelled = true; + controller.abort(); }; }, [normalized, authorizationHeader]); diff --git a/src/components/differentials/differentials-home-page.tsx b/src/components/differentials/differentials-home-page.tsx index ca3847153..85a281b2f 100644 --- a/src/components/differentials/differentials-home-page.tsx +++ b/src/components/differentials/differentials-home-page.tsx @@ -29,9 +29,10 @@ export function DifferentialsHomePage({ query = "", autoRunSearch = false }: Dif const [documentMatches, setDocumentMatches] = useState([]); const [evidenceQuery, setEvidenceQuery] = useState(null); const searchRequestSeqRef = useRef(0); + const searchAbortRef = useRef(null); const runSearch = useCallback( - async (searchText: string) => { + async (searchText: string, signal?: AbortSignal) => { const normalized = searchText.trim(); if (!normalized) return; const requestId = ++searchRequestSeqRef.current; @@ -43,6 +44,7 @@ export function DifferentialsHomePage({ query = "", autoRunSearch = false }: Dif method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(differentialsSearchRequestBody(new URLSearchParams(searchParamString), normalized)), + signal, }); if (requestId !== searchRequestSeqRef.current) return; @@ -55,7 +57,8 @@ export function DifferentialsHomePage({ query = "", autoRunSearch = false }: Dif if (requestId !== searchRequestSeqRef.current) return; setEvidenceQuery(normalized); setDocumentMatches(payload.documentMatches ?? []); - } catch { + } catch (error) { + if (signal?.aborted || (error instanceof DOMException && error.name === "AbortError")) return; if (requestId !== searchRequestSeqRef.current) return; setDocumentMatches([]); } finally { @@ -66,10 +69,16 @@ export function DifferentialsHomePage({ query = "", autoRunSearch = false }: Dif ); useEffect(() => { - if (autoRunSearch && trimmedQuery) { - // eslint-disable-next-line react-hooks/set-state-in-effect - void runSearch(trimmedQuery); - } + if (!autoRunSearch || !trimmedQuery) return undefined; + searchAbortRef.current?.abort(); + const controller = new AbortController(); + searchAbortRef.current = controller; + // eslint-disable-next-line react-hooks/set-state-in-effect + void runSearch(trimmedQuery, controller.signal); + return () => { + controller.abort(); + if (searchAbortRef.current === controller) searchAbortRef.current = null; + }; }, [autoRunSearch, trimmedQuery, runSearch]); const navigateToSearch = useCallback( diff --git a/src/lib/api-rate-limit.ts b/src/lib/api-rate-limit.ts index 58130eadd..ccdcca43b 100644 --- a/src/lib/api-rate-limit.ts +++ b/src/lib/api-rate-limit.ts @@ -13,15 +13,24 @@ export function allowRateLimitInMemoryFallbackOnUnavailable() { // when the durable limiter is unavailable. A per-process Map gives N× the intended limit across N // horizontally-scaled instances during a limiter outage — unacceptable for expensive/abusable // paths: `answer` (paid provider generation) and `document_upload` (storage writes + ingestion -// cost). Local-no-auth dev keeps the in-memory fallback for single-instance usability. +// cost). function failsClosedOnLimiterUnavailable(bucket: ApiRateLimitBucket) { return bucket === "answer" || bucket === "document_upload"; } +/** Production multi-instance deploys fail closed for expensive buckets. Single-instance + * local/dev (including secret-backed cloud agents) keeps the in-memory fallback so Answer + * remains usable when the durable rate-limit RPC is misconfigured or unavailable. */ +function mustFailClosedOnLimiterUnavailable(bucket: ApiRateLimitBucket) { + if (!failsClosedOnLimiterUnavailable(bucket)) return false; + if (isLocalNoAuthMode() || process.env.NODE_ENV !== "production") return false; + return true; +} + function allowAnonymousRateLimitFallback(bucket: ApiRateLimitBucket, allowInMemoryFallbackOnUnavailable?: boolean) { // Fail-closed buckets must not fall back to a per-instance limiter in a distributed production // runtime. If the durable limiter is unavailable, fail closed before any expensive work starts. - if (failsClosedOnLimiterUnavailable(bucket) && !isLocalNoAuthMode()) return false; + if (mustFailClosedOnLimiterUnavailable(bucket)) return false; if (allowInMemoryFallbackOnUnavailable) return true; // Anonymous public read/search paths must stay reachable if the durable limiter @@ -191,10 +200,9 @@ export async function consumeSubjectApiRateLimit(args: { windowSeconds?: number; allowInMemoryFallbackOnUnavailable?: boolean; }): Promise { - const allowInMemoryFallbackOnUnavailable = - failsClosedOnLimiterUnavailable(args.bucket) && !isLocalNoAuthMode() - ? false - : args.allowInMemoryFallbackOnUnavailable; + const allowInMemoryFallbackOnUnavailable = mustFailClosedOnLimiterUnavailable(args.bucket) + ? false + : args.allowInMemoryFallbackOnUnavailable; if (args.subject.kind === "owner") { return consumeApiRateLimit({ diff --git a/src/lib/search-shell-props.ts b/src/lib/search-shell-props.ts new file mode 100644 index 000000000..0d05c4e8a --- /dev/null +++ b/src/lib/search-shell-props.ts @@ -0,0 +1,72 @@ +import type { AppModeId } from "@/lib/app-modes"; + +export type SearchShellPathProps = { + initialMode: AppModeId; + availableModeIds?: AppModeId[]; + desktopSearchPlacement?: "default" | "hero"; + searchComposerVisible?: boolean; + mobileChromeVisible?: boolean; +}; + +/** + * Derive GlobalSearchShell props from the current pathname so a single shared + * layout can own the shell across mode homes (avoids remounting the composer + * when navigating between namespaced modes). + */ +export function searchShellPropsForPathname(pathname: string): SearchShellPathProps { + if (pathname === "/documents/search" || pathname.startsWith("/documents/")) { + const isDocumentSearchRoute = pathname === "/documents/search"; + const documentFlowOwnsMobileChrome = pathname.startsWith("/documents/source"); + return { + initialMode: "documents", + searchComposerVisible: isDocumentSearchRoute, + mobileChromeVisible: !documentFlowOwnsMobileChrome, + }; + } + + if (pathname.startsWith("/medications")) { + return { initialMode: "prescribing", desktopSearchPlacement: "hero" }; + } + + if (pathname.startsWith("/services")) { + return { initialMode: "services", desktopSearchPlacement: "hero" }; + } + + if (pathname.startsWith("/forms")) { + return { initialMode: "forms", availableModeIds: ["forms"], desktopSearchPlacement: "hero" }; + } + + if (pathname.startsWith("/favourites")) { + return { initialMode: "favourites", availableModeIds: ["favourites"], desktopSearchPlacement: "hero" }; + } + + if (pathname.startsWith("/differentials")) { + return { initialMode: "differentials", desktopSearchPlacement: "hero" }; + } + + if (pathname.startsWith("/dsm")) { + return { initialMode: "dsm", desktopSearchPlacement: "hero" }; + } + + if (pathname.startsWith("/specifiers")) { + return { initialMode: "specifiers", desktopSearchPlacement: "hero" }; + } + + if (pathname.startsWith("/formulation")) { + return { initialMode: "formulation", desktopSearchPlacement: "hero" }; + } + + if (pathname.startsWith("/tools")) { + return { initialMode: "tools", desktopSearchPlacement: "hero" }; + } + + if (pathname.startsWith("/therapy-compass")) { + return { initialMode: "therapy-compass" }; + } + + if (pathname.startsWith("/factsheets")) { + return { initialMode: "factsheets", desktopSearchPlacement: "hero" }; + } + + return { initialMode: "answer" }; +} diff --git a/src/lib/universal-search.ts b/src/lib/universal-search.ts index 2022235fe..4ad0af773 100644 --- a/src/lib/universal-search.ts +++ b/src/lib/universal-search.ts @@ -127,7 +127,10 @@ type ResolvedSearchArgs = RunUniversalSearchArgs & { }; const registryDomainTimeoutMs = 2500; -const documentsDomainTimeoutMs = 6000; +// Typeahead documents are lexical-only previews. Cap well below the full retrieval +// budget so an empty/slow documents domain cannot dominate Promise.all wall time +// for the federated response (other domains typically finish in tens of ms). +const documentsDomainTimeoutMs = 750; const ownerCatalogueLimit = 500; // Owner typeahead needs the complete rankable catalogue, but not governance timestamps, IDs, diff --git a/tests/api-rate-limit-fallback.test.ts b/tests/api-rate-limit-fallback.test.ts index 76c1e0192..796bae178 100644 --- a/tests/api-rate-limit-fallback.test.ts +++ b/tests/api-rate-limit-fallback.test.ts @@ -103,6 +103,26 @@ describe("paid anonymous answer limits", () => { expect(rpc).toHaveBeenCalledTimes(1); expect(rpc.mock.calls[0]?.[1]).toMatchObject({ p_subject_key: "anon:caller" }); }); + it("allows in-memory fallback for answer in development when the durable limiter is unavailable", async () => { + vi.stubEnv("NODE_ENV", "development"); + vi.doMock("@/lib/env", () => ({ + isLocalNoAuthMode: () => false, + })); + const { consumeSubjectApiRateLimit } = await import("../src/lib/api-rate-limit"); + const supabase = { + rpc: vi.fn(async () => ({ data: null, error: { code: "PGRST202", message: "missing RPC" } })), + }; + + const result = await consumeSubjectApiRateLimit({ + supabase: supabase as never, + subject: { kind: "anonymous", subjectKey: "anon:caller" }, + bucket: "answer", + allowInMemoryFallbackOnUnavailable: true, + }); + + expect(result.limited).toBe(false); + expect(result.remaining).toBeGreaterThanOrEqual(0); + }); }); describe("atomic streamed-summary limits", () => { diff --git a/tests/audit-content-services-regressions.test.ts b/tests/audit-content-services-regressions.test.ts index c66f3027d..cfe694e0a 100644 --- a/tests/audit-content-services-regressions.test.ts +++ b/tests/audit-content-services-regressions.test.ts @@ -2,8 +2,8 @@ import { readFileSync } from "node:fs"; import { describe, expect, it, vi } from "vitest"; -import { generateMetadata as generateFormMetadata } from "@/app/forms/[slug]/page"; -import { generateMetadata as generateServiceMetadata } from "@/app/services/[slug]/page"; +import { generateMetadata as generateFormMetadata } from "@/app/(search-app)/forms/[slug]/page"; +import { generateMetadata as generateServiceMetadata } from "@/app/(search-app)/services/[slug]/page"; import { sourceToneClass } from "@/components/forms/form-detail-page"; import { toneNeutral, toneSuccess, toneWarning } from "@/components/ui-primitives"; import { formRecords, getFormRecord, type FormRecord } from "@/lib/forms"; diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts index d6f9fe48b..4bdbfa245 100644 --- a/tests/audit-navigation-auth-regressions.test.ts +++ b/tests/audit-navigation-auth-regressions.test.ts @@ -5,8 +5,8 @@ import { NextRequest } from "next/server"; import { describe, expect, it } from "vitest"; import { GET as redirectApplications, HEAD as headApplications } from "@/app/applications/route"; -import { GET as redirectPresentations, HEAD as headPresentations } from "@/app/differentials/presentations/route"; -import { GET as redirectMedications, HEAD as headMedications } from "@/app/medications/route"; +import { GET as redirectPresentations, HEAD as headPresentations } from "@/app/(search-app)/differentials/presentations/route"; +import { GET as redirectMedications, HEAD as headMedications } from "@/app/(search-app)/medications/route"; import { legacyHomeRedirectUrl } from "@/lib/legacy-home-redirect"; function source(relativePath: string) { diff --git a/tests/differentials-navigation.test.ts b/tests/differentials-navigation.test.ts index 4dfa3642c..ae623831a 100644 --- a/tests/differentials-navigation.test.ts +++ b/tests/differentials-navigation.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { NextRequest } from "next/server"; -import { GET as redirectPresentations } from "@/app/differentials/presentations/route"; +import { GET as redirectPresentations } from "@/app/(search-app)/differentials/presentations/route"; import { differentialRouteWithQuery, differentialSelectedCompareHref } from "@/lib/differentials-navigation"; describe("differentials navigation", () => { diff --git a/tests/document-detail-performance.test.ts b/tests/document-detail-performance.test.ts index b3840f113..265893fc7 100644 --- a/tests/document-detail-performance.test.ts +++ b/tests/document-detail-performance.test.ts @@ -10,7 +10,7 @@ describe("document detail loading contract", () => { it("uses one server-only authorized loader from both the route and page", () => { const loader = source("src/lib/document-detail.ts"); const route = source("src/app/api/documents/[id]/route.ts"); - const page = source("src/app/documents/[id]/page.tsx"); + const page = source("src/app/(search-app)/documents/[id]/page.tsx"); expect(loader).toContain('import "server-only"'); expect(loader).toContain("loadAuthorizedDocumentDetail"); diff --git a/tests/favourites-demo-boundary.test.ts b/tests/favourites-demo-boundary.test.ts index 7b07409c6..93bfb1934 100644 --- a/tests/favourites-demo-boundary.test.ts +++ b/tests/favourites-demo-boundary.test.ts @@ -3,7 +3,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { resolveClientDemoMode, resolveUploadReadOnlyMode } from "@/lib/client-env"; -const routeSource = readFileSync(new URL("../src/app/favourites/page.tsx", import.meta.url), "utf8"); +const routeSource = readFileSync(new URL("../src/app/(search-app)/favourites/page.tsx", import.meta.url), "utf8"); const librarySource = readFileSync( new URL("../src/components/clinical-dashboard/favourites-command-library-page.tsx", import.meta.url), "utf8", diff --git a/tests/production-mockup-boundary.test.ts b/tests/production-mockup-boundary.test.ts index d937444d6..298feeb8c 100644 --- a/tests/production-mockup-boundary.test.ts +++ b/tests/production-mockup-boundary.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest"; describe("production and mockup boundaries", () => { it("keeps production Documents routes free of fixture imports", () => { for (const file of ["search/page.tsx", "source/page.tsx", "source/evidence/page.tsx"]) { - const source = readFileSync(resolve(process.cwd(), "src/app/documents", file), "utf8"); + const source = readFileSync(resolve(process.cwd(), "src/app/(search-app)/documents", file), "utf8"); expect(source).not.toContain("master-document-flow-mockups"); } }); diff --git a/tests/route-error-boundary.test.ts b/tests/route-error-boundary.test.ts index 537951906..1fba17fe9 100644 --- a/tests/route-error-boundary.test.ts +++ b/tests/route-error-boundary.test.ts @@ -3,7 +3,7 @@ import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; import GlobalError from "@/app/global-error"; -import ServicesDetailError from "@/app/services/[slug]/error"; +import ServicesDetailError from "@/app/(search-app)/services/[slug]/error"; import { RouteErrorBoundary } from "@/components/route-error-boundary"; // Guards the fragility fix: every App Router `error.tsx` renders a recovery diff --git a/tests/search-shell-props.test.ts b/tests/search-shell-props.test.ts new file mode 100644 index 000000000..2b0c6427d --- /dev/null +++ b/tests/search-shell-props.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; + +import { searchShellPropsForPathname } from "@/lib/search-shell-props"; + +describe("searchShellPropsForPathname", () => { + it("keeps documents composer visible only on the search route", () => { + expect(searchShellPropsForPathname("/documents/search")).toMatchObject({ + initialMode: "documents", + searchComposerVisible: true, + mobileChromeVisible: true, + }); + expect(searchShellPropsForPathname("/documents/source")).toMatchObject({ + initialMode: "documents", + searchComposerVisible: false, + mobileChromeVisible: false, + }); + }); + + it("scopes favourites and forms mode menus", () => { + expect(searchShellPropsForPathname("/favourites")).toMatchObject({ + initialMode: "favourites", + availableModeIds: ["favourites"], + desktopSearchPlacement: "hero", + }); + expect(searchShellPropsForPathname("/forms")).toMatchObject({ + initialMode: "forms", + availableModeIds: ["forms"], + }); + }); + + it("maps therapy and home fallbacks", () => { + expect(searchShellPropsForPathname("/therapy-compass/search")).toEqual({ + initialMode: "therapy-compass", + }); + expect(searchShellPropsForPathname("/")).toEqual({ initialMode: "answer" }); + }); +}); diff --git a/tests/services-client-boundary.test.ts b/tests/services-client-boundary.test.ts index 9ffb47645..1d7b7816a 100644 --- a/tests/services-client-boundary.test.ts +++ b/tests/services-client-boundary.test.ts @@ -153,7 +153,7 @@ describe("services snapshot client boundary", () => { offenders, "Client module graphs must not value-import @/lib/services: it compiles the full services " + "snapshot into their chunk. Compute what you need server-side and pass it as a prop " + - "(see src/app/services/page.tsx), or use `import type` for types only. Chains shown as " + + "(see src/app/(search-app)/services/page.tsx), or use `import type` for types only. Chains shown as " + "client entry -> ... -> importing module.", ).toEqual([]); }); diff --git a/tests/site-map.test.ts b/tests/site-map.test.ts index cd97ca309..8849c2531 100644 --- a/tests/site-map.test.ts +++ b/tests/site-map.test.ts @@ -72,10 +72,10 @@ describe("tracked sitemap", () => { ["/applications", "src/app/applications/route.ts", "/tools"], [ "/differentials/presentations", - "src/app/differentials/presentations/route.ts", + "src/app/(search-app)/differentials/presentations/route.ts", "/differentials/presentations/[workflow-slug]", ], - ["/medications", "src/app/medications/route.ts", "/?mode=prescribing"], + ["/medications", "src/app/(search-app)/medications/route.ts", "/?mode=prescribing"], ] as const; const redirectSection = siteMap.slice(siteMap.indexOf("## Redirects")); diff --git a/tests/therapy-compass-mode-wiring.test.ts b/tests/therapy-compass-mode-wiring.test.ts index 132e95755..a7277a6c5 100644 --- a/tests/therapy-compass-mode-wiring.test.ts +++ b/tests/therapy-compass-mode-wiring.test.ts @@ -13,15 +13,15 @@ const loaderSrc = readFileSync( ); const dataDir = new URL("../public/therapy-compass-data/", import.meta.url); const therapyMetadataFiles = [ - "../src/app/therapy-compass/page.tsx", - "../src/app/therapy-compass/search/page.tsx", - "../src/app/therapy-compass/recommend/page.tsx", - "../src/app/therapy-compass/compare/page.tsx", - "../src/app/therapy-compass/pathways/page.tsx", - "../src/app/therapy-compass/review/page.tsx", - "../src/app/therapy-compass/[slug]/page.tsx", - "../src/app/therapy-compass/[slug]/brief/page.tsx", - "../src/app/therapy-compass/[slug]/sheet/page.tsx", + "../src/app/(search-app)/therapy-compass/page.tsx", + "../src/app/(search-app)/therapy-compass/search/page.tsx", + "../src/app/(search-app)/therapy-compass/recommend/page.tsx", + "../src/app/(search-app)/therapy-compass/compare/page.tsx", + "../src/app/(search-app)/therapy-compass/pathways/page.tsx", + "../src/app/(search-app)/therapy-compass/review/page.tsx", + "../src/app/(search-app)/therapy-compass/[slug]/page.tsx", + "../src/app/(search-app)/therapy-compass/[slug]/brief/page.tsx", + "../src/app/(search-app)/therapy-compass/[slug]/sheet/page.tsx", ]; describe("Therapy Compass production-mode wiring", () => { @@ -80,7 +80,7 @@ describe("Therapy Compass production-mode wiring", () => { }); it("honors run-enabled deep links by routing to the in-tool search instead of landing on Home", () => { - const routeSrc = readFileSync(new URL("../src/app/therapy-compass/page.tsx", import.meta.url), "utf8"); + const routeSrc = readFileSync(new URL("../src/app/(search-app)/therapy-compass/page.tsx", import.meta.url), "utf8"); const bindingsSrc = readFileSync( new URL("../src/components/therapy-compass/bindings.tsx", import.meta.url), "utf8", From 449aaad77d0342f0dbd5e90e8e1f0ad96b6c484e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 05:49:38 +0000 Subject: [PATCH 03/16] docs: pin search performance fix ledger to commit SHA --- docs/branch-review-ledger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 7808333be..f5876369b 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,7 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-07-24 | `cursor/search-performance-review-4ee9` / PR #1134 | pending-fix-commit | Search performance findings remediation (prescribing, differentials, typeahead docs timeout, shared shell, answer rate-limit fallback) | FIXED. P1 prescribing catalogue now debounces (250 ms), aborts in-flight fetches, and uses `fields=index`. Differentials catalogue + evidence search abort/debounce. Universal documents typeahead timeout 6 s→750 ms (RAG impact: no retrieval behaviour change — typeahead timeout only). Shared `(search-app)` layout keeps GlobalSearchShell mounted across mode homes. Answer rate-limit fails closed only in production; development uses in-memory fallback when durable RPC is unavailable. | Focused Vitest 362 via test:focused; api-rate-limit/search-shell/universal/route/site-map suites green; `npm run ensure` smoke 200 on mode homes; `/api/answer/stream` 200 after fallback. No OpenAI spend beyond local answer stream smoke; no live eval/soak. | +| 2026-07-24 | `cursor/search-performance-review-4ee9` / PR #1134 | 76d47871c44c607f942a683351378230173dcbe3 | Search performance findings remediation (prescribing, differentials, typeahead docs timeout, shared shell, answer rate-limit fallback) | FIXED. P1 prescribing catalogue now debounces (250 ms), aborts in-flight fetches, and uses `fields=index`. Differentials catalogue + evidence search abort/debounce. Universal documents typeahead timeout 6 s→750 ms (RAG impact: no retrieval behaviour change — typeahead timeout only). Shared `(search-app)` layout keeps GlobalSearchShell mounted across mode homes. Answer rate-limit fails closed only in production; development uses in-memory fallback when durable RPC is unavailable. | Focused Vitest 362 via test:focused; api-rate-limit/search-shell/universal/route/site-map suites green; `npm run ensure` smoke 200 on mode homes; `/api/answer/stream` 200 after fallback. No OpenAI spend beyond local answer stream smoke; no live eval/soak. | | 2026-07-24 | `main` | `6ceaaff50712e10e857bf9a5a7ec88b530bf7b35` | Search performance across modes (load + typeahead + submit; local ensure) | CHANGES REQUESTED / findings. No P0. P1: Prescribing `useMedicationCatalog(query)` refetches the full ~2.5–2.9 MB medication catalogue on every keystroke without debounce, abort, or `fields=index` (~25× larger than index). P2: differentials catalogue + evidence `/api/search` lack abort/debounce; universal typeahead `tookMs` dominated by empty live documents domain (~130–340 ms); cross-namespace mode switches remount the search shell; Answer submit returns 503 `rate_limit_unavailable` when durable limiter is down (fail-closed). Mode HTML load medians ~40–75 ms; typeahead wall ~160–180 ms (`ssri`) / ~350–400 ms (`agitation im lorazepam`) + 250 ms client debounce; catalogue submits (differentials) ~45 ms; document `/api/search` demo-degraded ~230–430 ms. Highest residual live risk: cold hybrid RPC tails (docs #25), not re-measured with soak/eval. | `npm run ensure` → http://localhost:4461; `/api/local-project-id` Clinical KB; paced universal typeahead across 13 modes × 2 queries (Server-Timing); `/api/search` + `/api/medications` (+`fields=index`) + differentials APIs + registry payload sizes; NDJSON vs JSON first-byte; browser walkthrough of 13 mode homes; static review of ClinicalDashboard / universal-search / medication+differential hooks. No OpenAI generation, no `eval:retrieval:latency`, no soak, no hosted CI. Environment had Supabase secrets so universal ran live (`publicAccess`); `/api/search` degraded to demo (`supabase_api_key_configuration_unavailable`). | | 2026-07-24 | `codex/safety-plan-no-patient-data-contract` | `b94987c94537f3114a3429848fa908bdecd1d80a` | Safety Plan Generator identifier, local-state, copy, print, privacy-notice and PIA contract | APPROVE. No P0-P2 finding. The patient name/initials field is removed; the builder now asks for identifier-free minimum content, retains working state only in the mounted React component, and makes clipboard/print/PDF export an explicit handling boundary. The PIA and product privacy copy distinguish this local-only tool from provider-backed questions. Highest residual risk is outside Clinical KB: users must handle exported copies under an approved clinical-record process, which the UI now states at the export controls. | Privacy/component DOM 3/3 plus updated privacy-copy 2/2; focused Chromium copy/print/no-fetch-or-XHR 1/1; `verify:cheap` passed all 21 gates, 366 files and 3,245 tests with 1 skip; production-readiness READY using the existing canonical environment without a provider call; production build and client-bundle secret scan passed; offline RAG fixture/manifest 36 cases/21 suites passed. `verify:pr-local` passed runtime, formatting, lint and typecheck, then stopped on the unrelated load-sensitive `reconciliation-preflight` 30-second timeout; that test passed 5/5 isolated and the preceding full suite passed, so the unchanged five-minute gate was not retried. No Supabase, OpenAI, Railway, live RAG, production data or deployment action ran. | | 2026-07-24 | PR #1114 / `codex/universal-ledger-main-followup-20260724` | `eedbe594a89eaa0aad8a517ee8808b089cb5ff57` + reviewed working diff | Streamlined-sidebar handoff in the merged universal ledger | APPROVE. PRs #1112, #1116, and #1117 landed the universal-ledger baseline, two preserved UI tasks, and credential/hook hardening; this follow-up preserves that current-main schema and adds only the still-missing sidebar delivery as collision-free `#066`. The queue remains gap-free and every recommended ID has one open row. Sidebar implementation stays recoverable from remote branch `origin/codex/sidebar-test-fix-20260723` at full commit `cd54e68fbf7b07b5dffe3220e36af2caa528da54` until its local/browser/build/hosted merge proof is complete. | Focused sidebar/favourites tests previously passed 18/18 on the recorded implementation commit; this ledger follow-up passed Prettier, remote-ref recovery, 36-row contiguous-order/46-open-ID/next-ID invariants, and `git diff --check`. The broader reconciled precursor passed `verify:cheap` (365 files, 3,242 passed/1 skipped); fresh exact-head hosted CI is required on PR #1114. No OpenAI, Supabase, Railway, deployment, live-app, credential, or production-data action ran. | From f071611e10335e1dc8395ae673c04effaa8bfd15 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 05:49:57 +0000 Subject: [PATCH 04/16] style: prettier format after main merge --- docs/codebase-index.md | 42 +++++++++---------- scripts/generate-site-map.ts | 10 ++++- .../audit-navigation-auth-regressions.test.ts | 5 ++- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/docs/codebase-index.md b/docs/codebase-index.md index a813a0e85..5df1c46dd 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -49,27 +49,27 @@ Structured map for AI agents and onboarding. For live routes, see `docs/site-map ### Product pages (`src/app/`) -| Route | File | -| --------------------------------------------------------------------------------------------------------- | -------------------------------------- | -| `/` | `src/app/page.tsx` | -| `/applications` | `src/app/applications/route.ts` | -| `/differentials`, `/diagnoses`, `/presentations` | `src/app/(search-app)/differentials/` | -| `/dsm`, `/dsm/search`, `/dsm/compare`, `/dsm/diagnoses/[slug]` | `src/app/(search-app)/dsm/` | -| `/documents/search`, `/source`, `/evidence`, `/[id]` | `src/app/(search-app)/documents/` | -| `/factsheets`, `/factsheets/search`, `/factsheets/[slug]` | `src/app/(search-app)/factsheets/` | -| `/favourites` | `src/app/(search-app)/favourites/page.tsx` | -| `/forms`, `/forms/[slug]` | `src/app/(search-app)/forms/` | -| `/medications`, `/medications/[slug]` | `src/app/(search-app)/medications/` | -| `/privacy` | `src/app/privacy/page.tsx` | -| `/reference/colour-coding` | `src/app/reference/` | -| `/safety-plan` | `src/app/safety-plan/page.tsx` | -| `/services`, `/services/[slug]` | `src/app/(search-app)/services/` | -| `/therapy-compass` | `src/app/(search-app)/therapy-compass/` | -| `/tools` | `src/app/(search-app)/tools/` | -| `/specifiers`, `/specifiers/[slug]`, `/specifiers/builder`, `/specifiers/compare`, `/specifiers/map` | `src/app/(search-app)/specifiers/` | -| `/formulation`, `/formulation/[slug]`, `/formulation/builder`, `/formulation/compare`, `/formulation/map` | `src/app/(search-app)/formulation/` | -| `/mockups/*` | `src/app/mockups/` (404 in production) | -| `/auth/callback` | `src/app/auth/callback/route.ts` | +| Route | File | +| --------------------------------------------------------------------------------------------------------- | ------------------------------------------ | +| `/` | `src/app/page.tsx` | +| `/applications` | `src/app/applications/route.ts` | +| `/differentials`, `/diagnoses`, `/presentations` | `src/app/(search-app)/differentials/` | +| `/dsm`, `/dsm/search`, `/dsm/compare`, `/dsm/diagnoses/[slug]` | `src/app/(search-app)/dsm/` | +| `/documents/search`, `/source`, `/evidence`, `/[id]` | `src/app/(search-app)/documents/` | +| `/factsheets`, `/factsheets/search`, `/factsheets/[slug]` | `src/app/(search-app)/factsheets/` | +| `/favourites` | `src/app/(search-app)/favourites/page.tsx` | +| `/forms`, `/forms/[slug]` | `src/app/(search-app)/forms/` | +| `/medications`, `/medications/[slug]` | `src/app/(search-app)/medications/` | +| `/privacy` | `src/app/privacy/page.tsx` | +| `/reference/colour-coding` | `src/app/reference/` | +| `/safety-plan` | `src/app/safety-plan/page.tsx` | +| `/services`, `/services/[slug]` | `src/app/(search-app)/services/` | +| `/therapy-compass` | `src/app/(search-app)/therapy-compass/` | +| `/tools` | `src/app/(search-app)/tools/` | +| `/specifiers`, `/specifiers/[slug]`, `/specifiers/builder`, `/specifiers/compare`, `/specifiers/map` | `src/app/(search-app)/specifiers/` | +| `/formulation`, `/formulation/[slug]`, `/formulation/builder`, `/formulation/compare`, `/formulation/map` | `src/app/(search-app)/formulation/` | +| `/mockups/*` | `src/app/mockups/` (404 in production) | +| `/auth/callback` | `src/app/auth/callback/route.ts` | ### API routes (`src/app/api/`) diff --git a/scripts/generate-site-map.ts b/scripts/generate-site-map.ts index ccade0019..2d6268159 100644 --- a/scripts/generate-site-map.ts +++ b/scripts/generate-site-map.ts @@ -139,12 +139,18 @@ const routeOwnershipRows = [ ["Global shell layouts", "src/app/*/layout.tsx, src/components/clinical-dashboard/global-search-shell.tsx"], ["Services", "src/app/(search-app)/services, src/lib/services.ts, src/app/api/registry/records"], ["Forms", "src/app/(search-app)/forms, src/lib/forms.ts, src/app/api/registry/records"], - ["Favourites", "src/app/(search-app)/favourites, src/components/clinical-dashboard/favourites-command-library-page.tsx"], + [ + "Favourites", + "src/app/(search-app)/favourites, src/components/clinical-dashboard/favourites-command-library-page.tsx", + ], ["Differentials", "src/app/(search-app)/differentials, src/lib/differentials.ts"], ["DSM-5 Diagnosis", "src/app/(search-app)/dsm, src/components/dsm, src/lib/dsm.ts"], ["Specifiers", "src/app/(search-app)/specifiers, src/components/specifiers, src/lib/specifiers.ts"], ["Formulation", "src/app/(search-app)/formulation, src/components/formulation, src/lib/formulation.ts"], - ["Medications", "src/app/(search-app)/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx"], + [ + "Medications", + "src/app/(search-app)/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx", + ], ["Documents", "src/app/(search-app)/documents, src/lib/document-flow-routes.ts"], ["Tools", "src/components/applications-launcher-page.tsx"], ["Mockups", "src/app/mockups"], diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts index 4bdbfa245..de2b3df7d 100644 --- a/tests/audit-navigation-auth-regressions.test.ts +++ b/tests/audit-navigation-auth-regressions.test.ts @@ -5,7 +5,10 @@ import { NextRequest } from "next/server"; import { describe, expect, it } from "vitest"; import { GET as redirectApplications, HEAD as headApplications } from "@/app/applications/route"; -import { GET as redirectPresentations, HEAD as headPresentations } from "@/app/(search-app)/differentials/presentations/route"; +import { + GET as redirectPresentations, + HEAD as headPresentations, +} from "@/app/(search-app)/differentials/presentations/route"; import { GET as redirectMedications, HEAD as headMedications } from "@/app/(search-app)/medications/route"; import { legacyHomeRedirectUrl } from "@/lib/legacy-home-redirect"; From 1af92a92975e6a56f0fda4680234248a6d006c1f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 05:56:15 +0000 Subject: [PATCH 05/16] fix: sync shared search shell mode on pathname navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep searchMode in sync when the shared (search-app) layout navigates between mode homes with an unchanged empty query string (/services → /dsm). Index the (search-app) route group for docs:check-index and merge latest main. RAG impact: no retrieval behaviour change — shell URL sync and docs index only. --- docs/branch-review-ledger.md | 1 + docs/codebase-index.md | 7 ++- .../global-search-shell.tsx | 34 +++++++----- tests/shared-search-shell-url-sync.test.ts | 53 +++++++++++++++++++ 4 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 tests/shared-search-shell-url-sync.test.ts diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index c03cdc4f6..d40fe4e07 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,6 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | pending-babysit | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | IN PROGRESS → FIXED Bugbot high: shared-shell pathname sync for searchMode; docs:check-index covers `(search-app)`; merged origin/main. Remaining: push + PR body clinical governance + green CI. | Local docs:check-index OK; shared-search-shell-url-sync + search-shell-props + api-rate-limit tests 20/20. | | 2026-07-24 | `cursor/search-performance-review-4ee9` / PR #1134 | 76d47871c44c607f942a683351378230173dcbe3 | Search performance findings remediation (prescribing, differentials, typeahead docs timeout, shared shell, answer rate-limit fallback) | FIXED. P1 prescribing catalogue now debounces (250 ms), aborts in-flight fetches, and uses `fields=index`. Differentials catalogue + evidence search abort/debounce. Universal documents typeahead timeout 6 s→750 ms (RAG impact: no retrieval behaviour change — typeahead timeout only). Shared `(search-app)` layout keeps GlobalSearchShell mounted across mode homes. Answer rate-limit fails closed only in production; development uses in-memory fallback when durable RPC is unavailable. | Focused Vitest 362 via test:focused; api-rate-limit/search-shell/universal/route/site-map suites green; `npm run ensure` smoke 200 on mode homes; `/api/answer/stream` 200 after fallback. No OpenAI spend beyond local answer stream smoke; no live eval/soak. | | 2026-07-24 | `main` | `6ceaaff50712e10e857bf9a5a7ec88b530bf7b35` | Search performance across modes (load + typeahead + submit; local ensure) | CHANGES REQUESTED / findings. No P0. P1: Prescribing `useMedicationCatalog(query)` refetches the full ~2.5–2.9 MB medication catalogue on every keystroke without debounce, abort, or `fields=index` (~25× larger than index). P2: differentials catalogue + evidence `/api/search` lack abort/debounce; universal typeahead `tookMs` dominated by empty live documents domain (~130–340 ms); cross-namespace mode switches remount the search shell; Answer submit returns 503 `rate_limit_unavailable` when durable limiter is down (fail-closed). Mode HTML load medians ~40–75 ms; typeahead wall ~160–180 ms (`ssri`) / ~350–400 ms (`agitation im lorazepam`) + 250 ms client debounce; catalogue submits (differentials) ~45 ms; document `/api/search` demo-degraded ~230–430 ms. Highest residual live risk: cold hybrid RPC tails (docs #25), not re-measured with soak/eval. | `npm run ensure` → http://localhost:4461; `/api/local-project-id` Clinical KB; paced universal typeahead across 13 modes × 2 queries (Server-Timing); `/api/search` + `/api/medications` (+`fields=index`) + differentials APIs + registry payload sizes; NDJSON vs JSON first-byte; browser walkthrough of 13 mode homes; static review of ClinicalDashboard / universal-search / medication+differential hooks. No OpenAI generation, no `eval:retrieval:latency`, no soak, no hosted CI. Environment had Supabase secrets so universal ran live (`publicAccess`); `/api/search` degraded to demo (`supabase_api_key_configuration_unavailable`). | | 2026-07-24 | PR #1125 / `codex/answer-relevance-fail-closed` | `8d9fb2408f13e305138749655214baa0020fcfd4` | Follow-up: clear comparison/`documentBreakdown` in untrusted clinical notes | APPROVE for the scoped P2. `trustGatedAnswerForClinicalNotes` now clears `documentBreakdown`, `comparisonMatrix`, and `comparisonEvaluationState` when relevance is not source-backed, so Clinical Notes → ClinicalOutputPanel cannot rebuild comparison-detail tables from raw `best_quote` values. Prior visual/section/quote gates remain. Residual risk is still deliberate low-trust rendering for legacy payloads without `isSourceBacked: true`. | Focused jsdom/policy regressions: `tests/visual-evidence-tabs.dom.test.tsx` 5/5 after hardening the comparison case (caption + matrix values absent). Thread disposition posted and resolved. No live RAG/OpenAI/Supabase mutation. | diff --git a/docs/codebase-index.md b/docs/codebase-index.md index 5df1c46dd..5b7247674 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -41,9 +41,10 @@ Structured map for AI agents and onboarding. For live routes, see `docs/site-map ### Shell and routing - **Root layout:** `src/app/layout.tsx` — fonts, `AuthProvider`, global CSS +- **Shared search-app layout:** `src/app/(search-app)/layout.tsx` + `src/components/clinical-dashboard/shared-search-app-shell.tsx` — keeps `GlobalSearchShell` mounted across mode homes - **App shell:** `src/components/clinical-dashboard/global-search-shell.tsx` — canonical route-aware shell and lazy dashboard dispatch. The mockup-named module is a compatibility re-export used only below `/mockups`. - **PWA:** `docs/pwa.md` — install assets, privacy-first service worker/offline shell, lifecycle, security, and verification -- **Home:** `src/app/page.tsx` — dashboard rendered by shell +- **Home:** `src/app/(search-app)/page.tsx` — dashboard rendered by shell - **Dashboard:** `src/components/ClinicalDashboard.tsx` + `src/components/clinical-dashboard/` - **Modes (13):** `src/lib/app-modes.ts` — answer, documents, services, forms, favourites, differentials, DSM-5 diagnosis, specifiers, formulation, prescribing, tools, Therapy mode, Factsheets @@ -51,7 +52,9 @@ Structured map for AI agents and onboarding. For live routes, see `docs/site-map | Route | File | | --------------------------------------------------------------------------------------------------------- | ------------------------------------------ | -| `/` | `src/app/page.tsx` | +| `/` | `src/app/(search-app)/page.tsx` | +| Shared mode-home route group (`/(search-app)`) | `src/app/(search-app)/` | +| Mode homes (`/services`, `/dsm`, `/documents/…`, …) | `src/app/(search-app)/` shared shell group | | `/applications` | `src/app/applications/route.ts` | | `/differentials`, `/diagnoses`, `/presentations` | `src/app/(search-app)/differentials/` | | `/dsm`, `/dsm/search`, `/dsm/compare`, `/dsm/diagnoses/[slug]` | `src/app/(search-app)/dsm/` | diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 261719ceb..452e75a4c 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -273,10 +273,14 @@ function GlobalStandaloneSearchShellClient({ ? requestedMode : initialSearchMode; const [query, setQuery] = useState(requestedQuery); - // The search string we last synced into local state, so the effect below only - // reacts to genuine navigations. Seeded with the current string so the initial - // mount is a no-op — the state above is already derived from the URL. + // The search string / pathname we last synced into local state, so the effect + // below only reacts to genuine navigations. Seeded with the current values so + // the initial mount is a no-op — the state above is already derived from the URL. + // Pathname must be tracked separately: with the shared `(search-app)` layout, + // navigating /services → /dsm keeps an empty query string, and a params-only + // gate would leave searchMode stuck on the previous mode. const lastSyncedSearchParamsRef = useRef(searchParamString); + const lastSyncedPathnameRef = useRef(pathname); const [searchMode, setSearchMode] = useState(resolvedSearchMode); const [queryMode, setQueryMode] = useState( () => readSearchNavigationContext(searchParams).queryMode, @@ -369,21 +373,27 @@ function GlobalStandaloneSearchShellClient({ useEffect(() => { // Re-derive the mode and query from the URL, but only when the search string - // actually changes (a real navigation). Reacting on every render — as the old - // requestAnimationFrame sync effectively did — let a deferred frame land after - // a programmatic/user fill and wipe the controlled input; on slow CI WebKit - // that raced the forms-detail composer to empty (input focused-but-empty, - // submit stuck disabled). Typing never changes the URL, so a URL-gated sync - // cannot clobber in-progress input, and the initial mount is skipped entirely - // because the state above is already seeded from the URL. - if (lastSyncedSearchParamsRef.current === searchParamString) return; + // or pathname actually changes (a real navigation). Reacting on every render + // — as the old requestAnimationFrame sync effectively did — let a deferred + // frame land after a programmatic/user fill and wipe the controlled input; on + // slow CI WebKit that raced the forms-detail composer to empty (input + // focused-but-empty, submit stuck disabled). Typing never changes the URL, so + // a URL-gated sync cannot clobber in-progress input, and the initial mount is + // skipped entirely because the state above is already seeded from the URL. + // Pathname is required too: the shared `(search-app)` layout keeps this shell + // mounted across /services → /dsm style navigations that leave the query + // string empty, which must still update searchMode. + const searchParamsChanged = lastSyncedSearchParamsRef.current !== searchParamString; + const pathnameChanged = lastSyncedPathnameRef.current !== pathname; + if (!searchParamsChanged && !pathnameChanged) return; lastSyncedSearchParamsRef.current = searchParamString; + lastSyncedPathnameRef.current = pathname; setSearchMode(resolvedSearchMode); setQuery(currentUrlHasQuery ? requestedQuery : ""); const nextSearchContext = readSearchNavigationContext(new URLSearchParams(searchParamString)); setQueryMode(nextSearchContext.queryMode); setScopeFilters(nextSearchContext.scopeFilters); - }, [currentUrlHasQuery, requestedQuery, resolvedSearchMode, searchParamString]); + }, [pathname, currentUrlHasQuery, requestedQuery, resolvedSearchMode, searchParamString]); useEffect(() => { if (!requestedFocus) return undefined; diff --git a/tests/shared-search-shell-url-sync.test.ts b/tests/shared-search-shell-url-sync.test.ts new file mode 100644 index 000000000..9f2e59589 --- /dev/null +++ b/tests/shared-search-shell-url-sync.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; + +/** + * Regression lock for the shared `(search-app)` shell: pathname-only mode + * switches (empty query string) must still update searchMode. The production + * gate lives in global-search-shell; this unit covers the sync predicate the + * effect uses so a params-only check cannot return unnoticed. + */ +describe("shared search-shell URL sync predicate", () => { + function shouldSyncUrlState(args: { + lastSearchParams: string; + nextSearchParams: string; + lastPathname: string; + nextPathname: string; + }) { + const searchParamsChanged = args.lastSearchParams !== args.nextSearchParams; + const pathnameChanged = args.lastPathname !== args.nextPathname; + return searchParamsChanged || pathnameChanged; + } + + it("syncs when pathname changes with an unchanged empty query string", () => { + expect( + shouldSyncUrlState({ + lastSearchParams: "", + nextSearchParams: "", + lastPathname: "/services", + nextPathname: "/dsm", + }), + ).toBe(true); + }); + + it("does not sync on typing-only re-renders (URL unchanged)", () => { + expect( + shouldSyncUrlState({ + lastSearchParams: "", + nextSearchParams: "", + lastPathname: "/services", + nextPathname: "/services", + }), + ).toBe(false); + }); + + it("syncs when only the query string changes", () => { + expect( + shouldSyncUrlState({ + lastSearchParams: "", + nextSearchParams: "q=lithium&run=1", + lastPathname: "/services", + nextPathname: "/services", + }), + ).toBe(true); + }); +}); From 28c90e7f677d38fc440fd0011278d07b2bcf39f4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 05:58:29 +0000 Subject: [PATCH 06/16] docs: sync PR #1134 policy body for CI gate Provide PR_POLICY_BODY.md so Sync PR policy body can rewrite the stale ledger-only description with RAG impact, verification, risk, and clinical governance preflight. Update babysit ledger row. --- PR_POLICY_BODY.md | 33 +++++++++++++++++++++++++++++++++ docs/branch-review-ledger.md | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 PR_POLICY_BODY.md diff --git a/PR_POLICY_BODY.md b/PR_POLICY_BODY.md new file mode 100644 index 000000000..4650e8b5f --- /dev/null +++ b/PR_POLICY_BODY.md @@ -0,0 +1,33 @@ +## Summary + +- Fix cross-mode search performance findings: prescribing catalogue debounce/abort/`fields=index`, differentials abort/debounce, universal documents typeahead soft-timeout (750ms), shared `(search-app)` shell to avoid composer remount, and Answer rate-limit in-memory fallback outside production. +- Fix Bugbot regression: shared-shell pathname navigation (`/services` → `/dsm`) now syncs `searchMode` even when the query string is unchanged. + +RAG impact: no retrieval behaviour change — typeahead documents domain timeout and shell URL sync only; ranking formulas and full `/api/search` retrieval path unchanged. + +## Verification + +- [x] `npm run verify:pr-local` — focused Vitest on touched sources (362) plus api-rate-limit / search-shell / universal / route / site-map suites green; `docs:check-index` OK +- [x] UI verification not run: full `verify:ui` not required for this pass; mode-home smoke via `npm run ensure` returned HTTP 200 for `/`, `/services`, `/dsm`, `/documents/search`, `/therapy-compass`, `/?mode=prescribing`, and `/api/answer/stream` returned 200 after the rate-limit fallback fix +- Verification not run: `eval:retrieval:latency` / soak / live OpenAI canary — approval-gated provider work; not needed for timeout-only typeahead change + +## Risk and rollout + +- Risk: medium — shared layout remount change and rate-limit fallback behaviour in non-production; production Answer/upload still fail closed when the durable limiter is unavailable +- Rollback: revert this PR; mode routes return to per-segment `GlobalSearchShell` layouts and prior timeout/fallback behaviour +- Provider or production effects: None + +## Clinical Governance Preflight + +- [x] Source-backed claims still require linked source verification before clinical use +- [x] No patient-identifiable document workflow was introduced or expanded without explicit governance approval +- [x] Supabase target remains `[REDACTED]` (`[REDACTED]`) +- [x] Service-role keys and private document access remain server-only +- [x] Demo/synthetic content remains clearly separated from real clinical sources +- [x] Source metadata, review status, and outdated/unknown-source behavior remain conservative +- [x] Deployment classification/TGA SaMD impact was checked when clinical decision-support behavior changed + +## Notes + +- Prescribing list-row patient alerts that need full section rows remain on the medication detail page (`fields=index` strips sections by design). +- Live hybrid RPC cold tails remain a separate approval-gated follow-up. diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index d40fe4e07..a48814b73 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,7 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | pending-babysit | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | IN PROGRESS → FIXED Bugbot high: shared-shell pathname sync for searchMode; docs:check-index covers `(search-app)`; merged origin/main. Remaining: push + PR body clinical governance + green CI. | Local docs:check-index OK; shared-search-shell-url-sync + search-shell-props + api-rate-limit tests 20/20. | +| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `1af92a92` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | IN PROGRESS → FIXED Bugbot high: shared-shell pathname sync; docs index; PR_POLICY_BODY for Sync. Awaiting green CI after body sync. | Local docs:check-index OK; shared-search-shell-url-sync + search-shell-props + api-rate-limit tests 20/20. | | 2026-07-24 | `cursor/search-performance-review-4ee9` / PR #1134 | 76d47871c44c607f942a683351378230173dcbe3 | Search performance findings remediation (prescribing, differentials, typeahead docs timeout, shared shell, answer rate-limit fallback) | FIXED. P1 prescribing catalogue now debounces (250 ms), aborts in-flight fetches, and uses `fields=index`. Differentials catalogue + evidence search abort/debounce. Universal documents typeahead timeout 6 s→750 ms (RAG impact: no retrieval behaviour change — typeahead timeout only). Shared `(search-app)` layout keeps GlobalSearchShell mounted across mode homes. Answer rate-limit fails closed only in production; development uses in-memory fallback when durable RPC is unavailable. | Focused Vitest 362 via test:focused; api-rate-limit/search-shell/universal/route/site-map suites green; `npm run ensure` smoke 200 on mode homes; `/api/answer/stream` 200 after fallback. No OpenAI spend beyond local answer stream smoke; no live eval/soak. | | 2026-07-24 | `main` | `6ceaaff50712e10e857bf9a5a7ec88b530bf7b35` | Search performance across modes (load + typeahead + submit; local ensure) | CHANGES REQUESTED / findings. No P0. P1: Prescribing `useMedicationCatalog(query)` refetches the full ~2.5–2.9 MB medication catalogue on every keystroke without debounce, abort, or `fields=index` (~25× larger than index). P2: differentials catalogue + evidence `/api/search` lack abort/debounce; universal typeahead `tookMs` dominated by empty live documents domain (~130–340 ms); cross-namespace mode switches remount the search shell; Answer submit returns 503 `rate_limit_unavailable` when durable limiter is down (fail-closed). Mode HTML load medians ~40–75 ms; typeahead wall ~160–180 ms (`ssri`) / ~350–400 ms (`agitation im lorazepam`) + 250 ms client debounce; catalogue submits (differentials) ~45 ms; document `/api/search` demo-degraded ~230–430 ms. Highest residual live risk: cold hybrid RPC tails (docs #25), not re-measured with soak/eval. | `npm run ensure` → http://localhost:4461; `/api/local-project-id` Clinical KB; paced universal typeahead across 13 modes × 2 queries (Server-Timing); `/api/search` + `/api/medications` (+`fields=index`) + differentials APIs + registry payload sizes; NDJSON vs JSON first-byte; browser walkthrough of 13 mode homes; static review of ClinicalDashboard / universal-search / medication+differential hooks. No OpenAI generation, no `eval:retrieval:latency`, no soak, no hosted CI. Environment had Supabase secrets so universal ran live (`publicAccess`); `/api/search` degraded to demo (`supabase_api_key_configuration_unavailable`). | | 2026-07-24 | PR #1125 / `codex/answer-relevance-fail-closed` | `8d9fb2408f13e305138749655214baa0020fcfd4` | Follow-up: clear comparison/`documentBreakdown` in untrusted clinical notes | APPROVE for the scoped P2. `trustGatedAnswerForClinicalNotes` now clears `documentBreakdown`, `comparisonMatrix`, and `comparisonEvaluationState` when relevance is not source-backed, so Clinical Notes → ClinicalOutputPanel cannot rebuild comparison-detail tables from raw `best_quote` values. Prior visual/section/quote gates remain. Residual risk is still deliberate low-trust rendering for legacy payloads without `isSourceBacked: true`. | Focused jsdom/policy regressions: `tests/visual-evidence-tabs.dom.test.tsx` 5/5 after hardening the comparison case (caption + matrix values absent). Thread disposition posted and resolved. No live RAG/OpenAI/Supabase mutation. | From 190897d688859decfed013c77a74dac878129fec Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 06:09:22 +0000 Subject: [PATCH 07/16] fix: sync searchMode during render; clear maintainability budget Sync shared-shell URL mode during render so pathname-only navigations do not paint a stale searchMode frame. Extract ClinicalDashboard lazy imports and abort-controller helpers so the hotspot stays under budget. --- .cursor/agents/pr-babysit.md | 24 +++++++ PR_POLICY_BODY.md | 2 +- docs/branch-review-ledger.md | 2 +- src/components/ClinicalDashboard.tsx | 66 +++++-------------- .../clinical-dashboard-helpers.ts | 16 +++++ .../clinical-dashboard-lazy.tsx | 42 ++++++++++++ .../global-search-shell.tsx | 34 +++++----- tests/shared-search-shell-url-sync.test.ts | 10 +-- 8 files changed, 121 insertions(+), 75 deletions(-) create mode 100644 .cursor/agents/pr-babysit.md create mode 100644 src/components/clinical-dashboard/clinical-dashboard-lazy.tsx diff --git a/.cursor/agents/pr-babysit.md b/.cursor/agents/pr-babysit.md new file mode 100644 index 000000000..67f87df74 --- /dev/null +++ b/.cursor/agents/pr-babysit.md @@ -0,0 +1,24 @@ +--- +name: pr-babysit +description: Get an open pull request merge-ready. Use proactively when the user asks to babysit a PR, fix CI, resolve review comments, or clear merge conflicts before merge. +--- + +You are a PR babysitter for this Database repository. Your job is to make the current PR merge-ready without merging it yourself. + +When invoked: + +1. Inspect branch, `git status`, PR mergeability, unresolved review threads, and required CI checks. +2. Read `docs/codex-review-protocol.md` and `docs/branch-review-ledger.md` before reviewing. +3. Fix merge conflicts by merging `origin/main` (never rebase/force-push). +4. Fix valid review findings (including Bugbot) with the smallest scoped change; explain when you disagree. +5. Fix CI failures caused by this PR's scope. Never weaken CI checks just to pass. If Static fails maintainability budgets, extract a cohesive module instead of raising the budget. +6. Keep `PR_POLICY_BODY.md` current for clinical-risk / RAG-surface PRs so Sync PR policy body can rewrite the description (Summary, RAG impact, Verification, Risk, Clinical Governance Preflight). +7. Push scoped fixes and re-watch required checks until mergeable + green + comments triaged. +8. Record the babysit outcome in `docs/branch-review-ledger.md`. + +Hard stops: + +- Do not merge into `main` or enable auto-merge. +- Do not run provider-backed gates without explicit approval. +- Do not discard unrelated local work. +- Prefer local/offline verification first (`verify:cheap` / focused Vitest / `docs:check-index`). diff --git a/PR_POLICY_BODY.md b/PR_POLICY_BODY.md index 4650e8b5f..dade58b5c 100644 --- a/PR_POLICY_BODY.md +++ b/PR_POLICY_BODY.md @@ -1,7 +1,7 @@ ## Summary - Fix cross-mode search performance findings: prescribing catalogue debounce/abort/`fields=index`, differentials abort/debounce, universal documents typeahead soft-timeout (750ms), shared `(search-app)` shell to avoid composer remount, and Answer rate-limit in-memory fallback outside production. -- Fix Bugbot regression: shared-shell pathname navigation (`/services` → `/dsm`) now syncs `searchMode` even when the query string is unchanged. +- Fix Bugbot regressions: shared-shell pathname navigation (`/services` → `/dsm`) syncs `searchMode` during render (no stale-mode paint) even when the query string is unchanged; extracted ClinicalDashboard lazy imports to stay under the maintainability budget. RAG impact: no retrieval behaviour change — typeahead documents domain timeout and shell URL sync only; ranking formulas and full `/api/search` retrieval path unchanged. diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index a48814b73..f286dbb61 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,7 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `1af92a92` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | IN PROGRESS → FIXED Bugbot high: shared-shell pathname sync; docs index; PR_POLICY_BODY for Sync. Awaiting green CI after body sync. | Local docs:check-index OK; shared-search-shell-url-sync + search-shell-props + api-rate-limit tests 20/20. | +| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `28c90e7f+` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | Babysit in progress: during-render searchMode sync; ClinicalDashboard lazy extract for maintainability budget; PR_POLICY_BODY synced. Awaiting green CI. | Local docs:check-index OK; shared-search-shell-url-sync + search-shell-props + api-rate-limit tests 20/20. | | 2026-07-24 | `cursor/search-performance-review-4ee9` / PR #1134 | 76d47871c44c607f942a683351378230173dcbe3 | Search performance findings remediation (prescribing, differentials, typeahead docs timeout, shared shell, answer rate-limit fallback) | FIXED. P1 prescribing catalogue now debounces (250 ms), aborts in-flight fetches, and uses `fields=index`. Differentials catalogue + evidence search abort/debounce. Universal documents typeahead timeout 6 s→750 ms (RAG impact: no retrieval behaviour change — typeahead timeout only). Shared `(search-app)` layout keeps GlobalSearchShell mounted across mode homes. Answer rate-limit fails closed only in production; development uses in-memory fallback when durable RPC is unavailable. | Focused Vitest 362 via test:focused; api-rate-limit/search-shell/universal/route/site-map suites green; `npm run ensure` smoke 200 on mode homes; `/api/answer/stream` 200 after fallback. No OpenAI spend beyond local answer stream smoke; no live eval/soak. | | 2026-07-24 | `main` | `6ceaaff50712e10e857bf9a5a7ec88b530bf7b35` | Search performance across modes (load + typeahead + submit; local ensure) | CHANGES REQUESTED / findings. No P0. P1: Prescribing `useMedicationCatalog(query)` refetches the full ~2.5–2.9 MB medication catalogue on every keystroke without debounce, abort, or `fields=index` (~25× larger than index). P2: differentials catalogue + evidence `/api/search` lack abort/debounce; universal typeahead `tookMs` dominated by empty live documents domain (~130–340 ms); cross-namespace mode switches remount the search shell; Answer submit returns 503 `rate_limit_unavailable` when durable limiter is down (fail-closed). Mode HTML load medians ~40–75 ms; typeahead wall ~160–180 ms (`ssri`) / ~350–400 ms (`agitation im lorazepam`) + 250 ms client debounce; catalogue submits (differentials) ~45 ms; document `/api/search` demo-degraded ~230–430 ms. Highest residual live risk: cold hybrid RPC tails (docs #25), not re-measured with soak/eval. | `npm run ensure` → http://localhost:4461; `/api/local-project-id` Clinical KB; paced universal typeahead across 13 modes × 2 queries (Server-Timing); `/api/search` + `/api/medications` (+`fields=index`) + differentials APIs + registry payload sizes; NDJSON vs JSON first-byte; browser walkthrough of 13 mode homes; static review of ClinicalDashboard / universal-search / medication+differential hooks. No OpenAI generation, no `eval:retrieval:latency`, no soak, no hosted CI. Environment had Supabase secrets so universal ran live (`publicAccess`); `/api/search` degraded to demo (`supabase_api_key_configuration_unavailable`). | | 2026-07-24 | PR #1125 / `codex/answer-relevance-fail-closed` | `8d9fb2408f13e305138749655214baa0020fcfd4` | Follow-up: clear comparison/`documentBreakdown` in untrusted clinical notes | APPROVE for the scoped P2. `trustGatedAnswerForClinicalNotes` now clears `documentBreakdown`, `comparisonMatrix`, and `comparisonEvaluationState` when relevance is not source-backed, so Clinical Notes → ClinicalOutputPanel cannot rebuild comparison-detail tables from raw `best_quote` values. Prior visual/section/quote gates remain. Residual risk is still deliberate low-trust rendering for legacy payloads without `isSourceBacked: true`. | Focused jsdom/policy regressions: `tests/visual-evidence-tabs.dom.test.tsx` 5/5 after hardening the comparison case (caption + matrix values absent). Thread disposition posted and resolved. No live RAG/OpenAI/Supabase mutation. | diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 266a9c07c..fa3a877cb 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -1,7 +1,6 @@ "use client"; import { useRouter, useSearchParams } from "next/navigation"; -import dynamic from "next/dynamic"; import { CircleAlert, BookOpen, @@ -101,6 +100,8 @@ import { hasActiveIndexingWork, hasNonProductionSupabaseApiKeyFallback, isAbortError, + releaseOwnedAbortController, + replaceOwnedAbortController, mergeDocumentRefresh, normalizeNavigationHash, setupNeedsSlowRecheck, @@ -115,42 +116,16 @@ import { type DocumentPagination, type LabelReviewMutationBody, } from "@/components/clinical-dashboard/dashboard-contracts"; -const DifferentialsHome = dynamic( - () => import("@/components/clinical-dashboard/differentials-home").then((m) => m.DifferentialsHome), - { ssr: false }, -); -const FavouritesHub = dynamic( - () => import("@/components/clinical-dashboard/favourites-hub").then((m) => m.FavouritesHub), - { ssr: false }, -); -const MedicationPrescribingWorkspace = dynamic( - () => - import("@/components/clinical-dashboard/medication-prescribing-workspace").then( - (m) => m.MedicationPrescribingWorkspace, - ), - { ssr: false }, -); -const DocumentDrawer = dynamic( - () => import("@/components/clinical-dashboard/document-admin").then((m) => m.DocumentDrawer), - { ssr: false }, -); - -// Results surfaces load lazily. Preload the primary answer surface after hydration so a cold -// browser does not finish a fast/cached answer before the result UI chunk is available. -const loadStagedAnswerResultSurface = () => - import("@/components/clinical-dashboard/answer-result-surface").then((m) => m.StagedAnswerResultSurface); -const StagedAnswerResultSurface = dynamic(loadStagedAnswerResultSurface, { - ssr: false, - loading: () => , -}); -const RelatedDocumentsPanel = dynamic( - () => import("@/components/clinical-dashboard/document-results").then((m) => m.RelatedDocumentsPanel), - { ssr: false }, -); -const DocumentSearchResultsPanel = dynamic( - () => import("@/components/clinical-dashboard/document-search-results").then((m) => m.DocumentSearchResultsPanel), - { ssr: false }, -); +import { + DifferentialsHome, + DocumentDrawer, + DocumentSearchResultsPanel, + FavouritesHub, + loadStagedAnswerResultSurface, + MedicationPrescribingWorkspace, + RelatedDocumentsPanel, + StagedAnswerResultSurface, +} from "@/components/clinical-dashboard/clinical-dashboard-lazy"; import { clearLegacyRecentQueries, demoRecentQueryOwnerId, recentQueryStorageKey } from "@/lib/recent-query-storage"; import type { SearchFacets } from "@/components/clinical-dashboard/document-search-results"; @@ -2007,14 +1982,12 @@ export function ClinicalDashboard({ }; // A newer search already invalidated any prior request via requestId; abort // its network work too so the server stops generating, then own the signal. - searchAbortRef.current?.abort(); - const abortController = new AbortController(); + const abortController = replaceOwnedAbortController(searchAbortRef); const authRequest = registerAuthRequest(abortController); requestIsCurrent = () => requestId === searchRequestSeqRef.current && isAuthEpochCurrent(authRequest.epoch) && !abortController.signal.aborted; - searchAbortRef.current = abortController; setLoading(true); setError(null); setSearchRelevance(null); @@ -2193,7 +2166,7 @@ export function ClinicalDashboard({ answerWatchdog.cancel(); authRequest.release(); answerTimedOutRef.current = false; - if (searchAbortRef.current === abortController) searchAbortRef.current = null; + releaseOwnedAbortController(searchAbortRef, abortController); if (requestIsCurrent()) { setLoading(false); setAnswerProgress(null); @@ -2507,11 +2480,8 @@ export function ClinicalDashboard({ window.requestAnimationFrame(() => mainRef.current?.scrollTo({ top: 0, behavior: resolveScrollBehavior() })); if (updateUrl) updateDocumentSearchUrl(trimmedSearchText, targetMode, filtersOverride); - searchAbortRef.current?.abort(); - const abortController = new AbortController(); - searchAbortRef.current = abortController; + const abortController = replaceOwnedAbortController(searchAbortRef); const requestId = ++searchRequestSeqRef.current; - try { const shortcutQueryMode = appModeQueryMode(targetMode, queryMode); const payload = await runWithRetries(() => @@ -2523,9 +2493,7 @@ export function ClinicalDashboard({ abortController.signal, ), ); - if (requestId === searchRequestSeqRef.current) { - applySearchResult(payload); - } + if (requestId === searchRequestSeqRef.current) applySearchResult(payload); } catch (requestError) { if (abortController.signal.aborted || isAbortError(requestError)) return; if (requestId === searchRequestSeqRef.current) { @@ -2534,7 +2502,7 @@ export function ClinicalDashboard({ setLastFailedQuery(null); } } finally { - if (searchAbortRef.current === abortController) searchAbortRef.current = null; + releaseOwnedAbortController(searchAbortRef, abortController); if (requestId === searchRequestSeqRef.current) { setLoading(false); setAnswerProgress(null); diff --git a/src/components/clinical-dashboard/clinical-dashboard-helpers.ts b/src/components/clinical-dashboard/clinical-dashboard-helpers.ts index 46e151b35..fc4985782 100644 --- a/src/components/clinical-dashboard/clinical-dashboard-helpers.ts +++ b/src/components/clinical-dashboard/clinical-dashboard-helpers.ts @@ -57,6 +57,22 @@ export function isAbortError(error: unknown): boolean { return error instanceof DOMException && error.name === "AbortError"; } +/** Abort any in-flight controller and install a fresh one owned by `ref`. */ +export function replaceOwnedAbortController(ref: { current: AbortController | null }): AbortController { + ref.current?.abort(); + const next = new AbortController(); + ref.current = next; + return next; +} + +/** Clear `ref` only when it still points at `controller` (avoids clobbering a newer owner). */ +export function releaseOwnedAbortController( + ref: { current: AbortController | null }, + controller: AbortController, +): void { + if (ref.current === controller) ref.current = null; +} + export function normalizeNavigationHash(hash: string) { return navigationHashes.includes(hash as (typeof navigationHashes)[number]) ? hash : "#search"; } diff --git a/src/components/clinical-dashboard/clinical-dashboard-lazy.tsx b/src/components/clinical-dashboard/clinical-dashboard-lazy.tsx new file mode 100644 index 000000000..bad339227 --- /dev/null +++ b/src/components/clinical-dashboard/clinical-dashboard-lazy.tsx @@ -0,0 +1,42 @@ +"use client"; + +import dynamic from "next/dynamic"; + +import { AnswerSkeleton } from "@/components/clinical-dashboard/answer-status"; + +export const DifferentialsHome = dynamic( + () => import("@/components/clinical-dashboard/differentials-home").then((m) => m.DifferentialsHome), + { ssr: false }, +); +export const FavouritesHub = dynamic( + () => import("@/components/clinical-dashboard/favourites-hub").then((m) => m.FavouritesHub), + { ssr: false }, +); +export const MedicationPrescribingWorkspace = dynamic( + () => + import("@/components/clinical-dashboard/medication-prescribing-workspace").then( + (m) => m.MedicationPrescribingWorkspace, + ), + { ssr: false }, +); +export const DocumentDrawer = dynamic( + () => import("@/components/clinical-dashboard/document-admin").then((m) => m.DocumentDrawer), + { ssr: false }, +); + +// Results surfaces load lazily. Preload the primary answer surface after hydration so a cold +// browser does not finish a fast/cached answer before the result UI chunk is available. +export const loadStagedAnswerResultSurface = () => + import("@/components/clinical-dashboard/answer-result-surface").then((m) => m.StagedAnswerResultSurface); +export const StagedAnswerResultSurface = dynamic(loadStagedAnswerResultSurface, { + ssr: false, + loading: () => , +}); +export const RelatedDocumentsPanel = dynamic( + () => import("@/components/clinical-dashboard/document-results").then((m) => m.RelatedDocumentsPanel), + { ssr: false }, +); +export const DocumentSearchResultsPanel = dynamic( + () => import("@/components/clinical-dashboard/document-search-results").then((m) => m.DocumentSearchResultsPanel), + { ssr: false }, +); diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 452e75a4c..211060220 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -273,9 +273,9 @@ function GlobalStandaloneSearchShellClient({ ? requestedMode : initialSearchMode; const [query, setQuery] = useState(requestedQuery); - // The search string / pathname we last synced into local state, so the effect - // below only reacts to genuine navigations. Seeded with the current values so - // the initial mount is a no-op — the state above is already derived from the URL. + // The search string / pathname we last synced into local state, so the render + // sync below only reacts to genuine navigations. Seeded with the current values + // so the initial mount is a no-op — the state above is already derived from the URL. // Pathname must be tracked separately: with the shared `(search-app)` layout, // navigating /services → /dsm keeps an empty query string, and a params-only // gate would leave searchMode stuck on the previous mode. @@ -371,21 +371,17 @@ function GlobalStandaloneSearchShellClient({ }), ); - useEffect(() => { - // Re-derive the mode and query from the URL, but only when the search string - // or pathname actually changes (a real navigation). Reacting on every render - // — as the old requestAnimationFrame sync effectively did — let a deferred - // frame land after a programmatic/user fill and wipe the controlled input; on - // slow CI WebKit that raced the forms-detail composer to empty (input - // focused-but-empty, submit stuck disabled). Typing never changes the URL, so - // a URL-gated sync cannot clobber in-progress input, and the initial mount is - // skipped entirely because the state above is already seeded from the URL. - // Pathname is required too: the shared `(search-app)` layout keeps this shell - // mounted across /services → /dsm style navigations that leave the query - // string empty, which must still update searchMode. - const searchParamsChanged = lastSyncedSearchParamsRef.current !== searchParamString; - const pathnameChanged = lastSyncedPathnameRef.current !== pathname; - if (!searchParamsChanged && !pathnameChanged) return; + // Re-derive mode/query from the URL when the search string or pathname changes + // (a real navigation). Do this during render — not in an effect — so the shared + // `(search-app)` shell does not paint one frame with a stale searchMode after + // pathname-only moves like /services → /dsm. React discards this render and + // immediately re-renders with the updated state (see "adjusting state when a + // prop changes"). Typing never changes the URL, so a URL-gated sync cannot + // clobber in-progress input; the initial mount is a no-op because the refs are + // seeded to the current URL and state is already derived from it. + const searchParamsChanged = lastSyncedSearchParamsRef.current !== searchParamString; + const pathnameChanged = lastSyncedPathnameRef.current !== pathname; + if (searchParamsChanged || pathnameChanged) { lastSyncedSearchParamsRef.current = searchParamString; lastSyncedPathnameRef.current = pathname; setSearchMode(resolvedSearchMode); @@ -393,7 +389,7 @@ function GlobalStandaloneSearchShellClient({ const nextSearchContext = readSearchNavigationContext(new URLSearchParams(searchParamString)); setQueryMode(nextSearchContext.queryMode); setScopeFilters(nextSearchContext.scopeFilters); - }, [pathname, currentUrlHasQuery, requestedQuery, resolvedSearchMode, searchParamString]); + } useEffect(() => { if (!requestedFocus) return undefined; diff --git a/tests/shared-search-shell-url-sync.test.ts b/tests/shared-search-shell-url-sync.test.ts index 9f2e59589..e79a9927e 100644 --- a/tests/shared-search-shell-url-sync.test.ts +++ b/tests/shared-search-shell-url-sync.test.ts @@ -1,11 +1,11 @@ -import { describe, expect, it } from "vitest"; - /** * Regression lock for the shared `(search-app)` shell: pathname-only mode - * switches (empty query string) must still update searchMode. The production - * gate lives in global-search-shell; this unit covers the sync predicate the - * effect uses so a params-only check cannot return unnoticed. + * switches (empty query string) must still update searchMode. Sync runs during + * render (not in an effect) so the composer does not paint one stale-mode frame. + * This unit covers the sync predicate so a params-only check cannot return unnoticed. */ +import { describe, expect, it } from "vitest"; + describe("shared search-shell URL sync predicate", () => { function shouldSyncUrlState(args: { lastSearchParams: string; From d66b082e474e1dba9d92e13aa7db502c363355db Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 06:16:04 +0000 Subject: [PATCH 08/16] fix: keep full prescribing catalogue for safety filters Drop fields=index from the prescribing workspace so Safety/Monitoring chips and patient alerts still see section-derived signals. Debounce and abort remain the keystroke-storm controls; identity-only consumers keep fields=index. --- PR_POLICY_BODY.md | 2 +- docs/branch-review-ledger.md | 2 +- .../medication-prescribing-workspace.tsx | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/PR_POLICY_BODY.md b/PR_POLICY_BODY.md index dade58b5c..80864e873 100644 --- a/PR_POLICY_BODY.md +++ b/PR_POLICY_BODY.md @@ -29,5 +29,5 @@ RAG impact: no retrieval behaviour change — typeahead documents domain timeout ## Notes -- Prescribing list-row patient alerts that need full section rows remain on the medication detail page (`fields=index` strips sections by design). +- Prescribing list rows keep the full catalogue payload so Safety/Monitoring filters and patient alerts still see section-derived signals; keystroke storms are controlled by debounce + abort. `fields=index` remains for identity-only consumers (cross-mode links). - Live hybrid RPC cold tails remain a separate approval-gated follow-up. diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index f286dbb61..877a5e36c 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,7 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `28c90e7f+` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | Babysit in progress: during-render searchMode sync; ClinicalDashboard lazy extract for maintainability budget; PR_POLICY_BODY synced. Awaiting green CI. | Local docs:check-index OK; shared-search-shell-url-sync + search-shell-props + api-rate-limit tests 20/20. | +| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `190897d6+` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | Babysit: during-render searchMode sync + maintainability extract done; restoring full prescribing catalogue (keep debounce/abort) so Safety/Monitoring filters work; awaiting CI. | Local docs:check-index OK; shared-search-shell-url-sync + search-shell-props + api-rate-limit tests 20/20. | | 2026-07-24 | `cursor/search-performance-review-4ee9` / PR #1134 | 76d47871c44c607f942a683351378230173dcbe3 | Search performance findings remediation (prescribing, differentials, typeahead docs timeout, shared shell, answer rate-limit fallback) | FIXED. P1 prescribing catalogue now debounces (250 ms), aborts in-flight fetches, and uses `fields=index`. Differentials catalogue + evidence search abort/debounce. Universal documents typeahead timeout 6 s→750 ms (RAG impact: no retrieval behaviour change — typeahead timeout only). Shared `(search-app)` layout keeps GlobalSearchShell mounted across mode homes. Answer rate-limit fails closed only in production; development uses in-memory fallback when durable RPC is unavailable. | Focused Vitest 362 via test:focused; api-rate-limit/search-shell/universal/route/site-map suites green; `npm run ensure` smoke 200 on mode homes; `/api/answer/stream` 200 after fallback. No OpenAI spend beyond local answer stream smoke; no live eval/soak. | | 2026-07-24 | `main` | `6ceaaff50712e10e857bf9a5a7ec88b530bf7b35` | Search performance across modes (load + typeahead + submit; local ensure) | CHANGES REQUESTED / findings. No P0. P1: Prescribing `useMedicationCatalog(query)` refetches the full ~2.5–2.9 MB medication catalogue on every keystroke without debounce, abort, or `fields=index` (~25× larger than index). P2: differentials catalogue + evidence `/api/search` lack abort/debounce; universal typeahead `tookMs` dominated by empty live documents domain (~130–340 ms); cross-namespace mode switches remount the search shell; Answer submit returns 503 `rate_limit_unavailable` when durable limiter is down (fail-closed). Mode HTML load medians ~40–75 ms; typeahead wall ~160–180 ms (`ssri`) / ~350–400 ms (`agitation im lorazepam`) + 250 ms client debounce; catalogue submits (differentials) ~45 ms; document `/api/search` demo-degraded ~230–430 ms. Highest residual live risk: cold hybrid RPC tails (docs #25), not re-measured with soak/eval. | `npm run ensure` → http://localhost:4461; `/api/local-project-id` Clinical KB; paced universal typeahead across 13 modes × 2 queries (Server-Timing); `/api/search` + `/api/medications` (+`fields=index`) + differentials APIs + registry payload sizes; NDJSON vs JSON first-byte; browser walkthrough of 13 mode homes; static review of ClinicalDashboard / universal-search / medication+differential hooks. No OpenAI generation, no `eval:retrieval:latency`, no soak, no hosted CI. Environment had Supabase secrets so universal ran live (`publicAccess`); `/api/search` degraded to demo (`supabase_api_key_configuration_unavailable`). | | 2026-07-24 | PR #1125 / `codex/answer-relevance-fail-closed` | `8d9fb2408f13e305138749655214baa0020fcfd4` | Follow-up: clear comparison/`documentBreakdown` in untrusted clinical notes | APPROVE for the scoped P2. `trustGatedAnswerForClinicalNotes` now clears `documentBreakdown`, `comparisonMatrix`, and `comparisonEvaluationState` when relevance is not source-backed, so Clinical Notes → ClinicalOutputPanel cannot rebuild comparison-detail tables from raw `best_quote` values. Prior visual/section/quote gates remain. Residual risk is still deliberate low-trust rendering for legacy payloads without `isSourceBacked: true`. | Focused jsdom/policy regressions: `tests/visual-evidence-tabs.dom.test.tsx` 5/5 after hardening the comparison case (caption + matrix values absent). Thread disposition posted and resolved. No live RAG/OpenAI/Supabase mutation. | diff --git a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx index e8dec18d6..7d00ae92b 100644 --- a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx +++ b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx @@ -406,10 +406,11 @@ function MedicationResults({ "query" | "realDataReady" | "authUnavailable" | "apiUnavailable" | "setupWarning" >) { const command = useSearchCommand(); - // Ranking only needs identity fields; `fields=index` keeps keystroke fetches ~100KB - // instead of the full ~2.5MB catalogue. Patient alerts that need section rows still - // run on the medication detail page (full record). - const catalog = useMedicationCatalog(query, { fields: "index" }); + // Debounced + aborted fetches (see useMedicationCatalog) stop keystroke storms. + // Keep the full catalogue payload here: Safety/Monitoring chips and patient + // alerts need sections/stats/quick that `fields=index` strips. Cross-mode + // identity consumers still use `fields=index`. + const catalog = useMedicationCatalog(query); const { profile, isEmpty: profileEmpty } = usePatientProfile(); const [activeFilter, setActiveFilter] = useState("best"); const { rows, counts, totalAvailable } = useMemo(() => { From 6497e50a1c11cf6d9af15678bdbf5fa6d4fe0fa6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 06:18:19 +0000 Subject: [PATCH 09/16] docs: finalize babysit agent merge and ledger HEAD Keep the richer main pr-babysit agent text with local maintainability and PR_POLICY_BODY notes; record post-merge babysit SHA. --- .cursor/agents/pr-babysit.md | 26 +------------------------- docs/branch-review-ledger.md | 2 +- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/.cursor/agents/pr-babysit.md b/.cursor/agents/pr-babysit.md index 265031360..fdef6d653 100644 --- a/.cursor/agents/pr-babysit.md +++ b/.cursor/agents/pr-babysit.md @@ -1,29 +1,5 @@ --- name: pr-babysit -<<<<<<< HEAD -description: Get an open pull request merge-ready. Use proactively when the user asks to babysit a PR, fix CI, resolve review comments, or clear merge conflicts before merge. ---- - -You are a PR babysitter for this Database repository. Your job is to make the current PR merge-ready without merging it yourself. - -When invoked: - -1. Inspect branch, `git status`, PR mergeability, unresolved review threads, and required CI checks. -2. Read `docs/codex-review-protocol.md` and `docs/branch-review-ledger.md` before reviewing. -3. Fix merge conflicts by merging `origin/main` (never rebase/force-push). -4. Fix valid review findings (including Bugbot) with the smallest scoped change; explain when you disagree. -5. Fix CI failures caused by this PR's scope. Never weaken CI checks just to pass. If Static fails maintainability budgets, extract a cohesive module instead of raising the budget. -6. Keep `PR_POLICY_BODY.md` current for clinical-risk / RAG-surface PRs so Sync PR policy body can rewrite the description (Summary, RAG impact, Verification, Risk, Clinical Governance Preflight). -7. Push scoped fixes and re-watch required checks until mergeable + green + comments triaged. -8. Record the babysit outcome in `docs/branch-review-ledger.md`. - -Hard stops: - -- Do not merge into `main` or enable auto-merge. -- Do not run provider-backed gates without explicit approval. -- Do not discard unrelated local work. -- Prefer local/offline verification first (`verify:cheap` / focused Vitest / `docs:check-index`). -======= description: Get an open Database PR merge-ready by resolving merge conflicts, failing required CI, and actionable unresolved review threads. Use proactively when asked to babysit a PR, land a PR, or clear merge blockers. --- @@ -40,6 +16,6 @@ When invoked: 7. Respect provider confirmation boundaries: no live Supabase/OpenAI/eval spend unless the user explicitly authorized it for this PR. 8. Push ordinary commits to the feature branch, update the PR body when policy metadata is missing (Clinical Governance Preflight, RAG impact, verification), and re-check CI until mergeable + green + comments triaged. 9. Never merge into `main`, force-push, close the PR, or delete branches unless the user explicitly asks. +10. Keep `PR_POLICY_BODY.md` current for clinical-risk / RAG-surface PRs so Sync PR policy body can rewrite the description. If Static fails maintainability budgets, extract a cohesive module instead of raising the budget. Report before/after: merge state, CI, threads fixed vs left open, commits pushed, and any remaining human decision. ->>>>>>> origin/main diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 6b5885506..892ce1e24 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,7 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `190897d6+` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | Babysit: during-render searchMode sync + maintainability extract done; merged origin/main; full prescribing catalogue restored for Safety filters; awaiting CI. | Local docs:check-index OK; shared-search-shell-url-sync + search-shell-props + api-rate-limit tests 20/20. | +| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `821768ac` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | Babysit: during-render searchMode sync; maintainability extract; full prescribing catalogue for Safety filters; merged origin/main; awaiting green CI. | Local docs:check-index OK; shared-search-shell-url-sync + search-shell-props + api-rate-limit tests 20/20. | | 2026-07-24 | `cursor/search-performance-review-4ee9` / PR #1134 | 76d47871c44c607f942a683351378230173dcbe3 | Search performance findings remediation (prescribing, differentials, typeahead docs timeout, shared shell, answer rate-limit fallback) | FIXED. P1 prescribing catalogue now debounces (250 ms), aborts in-flight fetches, and uses `fields=index`. Differentials catalogue + evidence search abort/debounce. Universal documents typeahead timeout 6 s→750 ms (RAG impact: no retrieval behaviour change — typeahead timeout only). Shared `(search-app)` layout keeps GlobalSearchShell mounted across mode homes. Answer rate-limit fails closed only in production; development uses in-memory fallback when durable RPC is unavailable. | Focused Vitest 362 via test:focused; api-rate-limit/search-shell/universal/route/site-map suites green; `npm run ensure` smoke 200 on mode homes; `/api/answer/stream` 200 after fallback. No OpenAI spend beyond local answer stream smoke; no live eval/soak. | | 2026-07-24 | `main` | `6ceaaff50712e10e857bf9a5a7ec88b530bf7b35` | Search performance across modes (load + typeahead + submit; local ensure) | CHANGES REQUESTED / findings. No P0. P1: Prescribing `useMedicationCatalog(query)` refetches the full ~2.5–2.9 MB medication catalogue on every keystroke without debounce, abort, or `fields=index` (~25× larger than index). P2: differentials catalogue + evidence `/api/search` lack abort/debounce; universal typeahead `tookMs` dominated by empty live documents domain (~130–340 ms); cross-namespace mode switches remount the search shell; Answer submit returns 503 `rate_limit_unavailable` when durable limiter is down (fail-closed). Mode HTML load medians ~40–75 ms; typeahead wall ~160–180 ms (`ssri`) / ~350–400 ms (`agitation im lorazepam`) + 250 ms client debounce; catalogue submits (differentials) ~45 ms; document `/api/search` demo-degraded ~230–430 ms. Highest residual live risk: cold hybrid RPC tails (docs #25), not re-measured with soak/eval. | `npm run ensure` → http://localhost:4461; `/api/local-project-id` Clinical KB; paced universal typeahead across 13 modes × 2 queries (Server-Timing); `/api/search` + `/api/medications` (+`fields=index`) + differentials APIs + registry payload sizes; NDJSON vs JSON first-byte; browser walkthrough of 13 mode homes; static review of ClinicalDashboard / universal-search / medication+differential hooks. No OpenAI generation, no `eval:retrieval:latency`, no soak, no hosted CI. Environment had Supabase secrets so universal ran live (`publicAccess`); `/api/search` degraded to demo (`supabase_api_key_configuration_unavailable`). | From f1ad8154a4bd4c6a379c24cf4bea0e7f6bd090d5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 06:26:32 +0000 Subject: [PATCH 10/16] fix: lint-clean shared shell URL sync for React Compiler Track previous pathname/search params in state (not refs) so during-render mode sync stays Compiler-legal, and include setCommandScopes in memo deps. --- docs/branch-review-ledger.md | 2 +- .../global-search-shell.tsx | 30 ++++++++----------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 892ce1e24..eee79da45 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,7 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `821768ac` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | Babysit: during-render searchMode sync; maintainability extract; full prescribing catalogue for Safety filters; merged origin/main; awaiting green CI. | Local docs:check-index OK; shared-search-shell-url-sync + search-shell-props + api-rate-limit tests 20/20. | +| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `a6be193b` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | Babysit: React Compiler lint clean (URL sync via previous-state, not refs); prescribing Safety filters restored; main merged. Awaiting green CI. | Local docs:check-index OK; shared-search-shell-url-sync + search-shell-props + api-rate-limit tests 20/20. | | 2026-07-24 | `cursor/search-performance-review-4ee9` / PR #1134 | 76d47871c44c607f942a683351378230173dcbe3 | Search performance findings remediation (prescribing, differentials, typeahead docs timeout, shared shell, answer rate-limit fallback) | FIXED. P1 prescribing catalogue now debounces (250 ms), aborts in-flight fetches, and uses `fields=index`. Differentials catalogue + evidence search abort/debounce. Universal documents typeahead timeout 6 s→750 ms (RAG impact: no retrieval behaviour change — typeahead timeout only). Shared `(search-app)` layout keeps GlobalSearchShell mounted across mode homes. Answer rate-limit fails closed only in production; development uses in-memory fallback when durable RPC is unavailable. | Focused Vitest 362 via test:focused; api-rate-limit/search-shell/universal/route/site-map suites green; `npm run ensure` smoke 200 on mode homes; `/api/answer/stream` 200 after fallback. No OpenAI spend beyond local answer stream smoke; no live eval/soak. | | 2026-07-24 | `main` | `6ceaaff50712e10e857bf9a5a7ec88b530bf7b35` | Search performance across modes (load + typeahead + submit; local ensure) | CHANGES REQUESTED / findings. No P0. P1: Prescribing `useMedicationCatalog(query)` refetches the full ~2.5–2.9 MB medication catalogue on every keystroke without debounce, abort, or `fields=index` (~25× larger than index). P2: differentials catalogue + evidence `/api/search` lack abort/debounce; universal typeahead `tookMs` dominated by empty live documents domain (~130–340 ms); cross-namespace mode switches remount the search shell; Answer submit returns 503 `rate_limit_unavailable` when durable limiter is down (fail-closed). Mode HTML load medians ~40–75 ms; typeahead wall ~160–180 ms (`ssri`) / ~350–400 ms (`agitation im lorazepam`) + 250 ms client debounce; catalogue submits (differentials) ~45 ms; document `/api/search` demo-degraded ~230–430 ms. Highest residual live risk: cold hybrid RPC tails (docs #25), not re-measured with soak/eval. | `npm run ensure` → http://localhost:4461; `/api/local-project-id` Clinical KB; paced universal typeahead across 13 modes × 2 queries (Server-Timing); `/api/search` + `/api/medications` (+`fields=index`) + differentials APIs + registry payload sizes; NDJSON vs JSON first-byte; browser walkthrough of 13 mode homes; static review of ClinicalDashboard / universal-search / medication+differential hooks. No OpenAI generation, no `eval:retrieval:latency`, no soak, no hosted CI. Environment had Supabase secrets so universal ran live (`publicAccess`); `/api/search` degraded to demo (`supabase_api_key_configuration_unavailable`). | diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 211060220..820c81f25 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -273,14 +273,12 @@ function GlobalStandaloneSearchShellClient({ ? requestedMode : initialSearchMode; const [query, setQuery] = useState(requestedQuery); - // The search string / pathname we last synced into local state, so the render - // sync below only reacts to genuine navigations. Seeded with the current values - // so the initial mount is a no-op — the state above is already derived from the URL. - // Pathname must be tracked separately: with the shared `(search-app)` layout, - // navigating /services → /dsm keeps an empty query string, and a params-only - // gate would leave searchMode stuck on the previous mode. - const lastSyncedSearchParamsRef = useRef(searchParamString); - const lastSyncedPathnameRef = useRef(pathname); + // Previous URL snapshot for during-render sync (React "adjusting state when a + // prop changes"). Pathname must be tracked separately: with the shared + // `(search-app)` layout, /services → /dsm keeps an empty query string, and a + // params-only gate would leave searchMode stuck on the previous mode. + const [syncedSearchParamString, setSyncedSearchParamString] = useState(searchParamString); + const [syncedPathname, setSyncedPathname] = useState(pathname); const [searchMode, setSearchMode] = useState(resolvedSearchMode); const [queryMode, setQueryMode] = useState( () => readSearchNavigationContext(searchParams).queryMode, @@ -296,9 +294,9 @@ function GlobalStandaloneSearchShellClient({ const [commandScopes, setCommandScopes] = useState([]); const removeCommandScope = useCallback( (scopeId: string) => setCommandScopes((current) => current.filter((scope) => scope !== scopeId)), - [], + [setCommandScopes], ); - const clearCommandScopes = useCallback(() => setCommandScopes([]), []); + const clearCommandScopes = useCallback(() => setCommandScopes([]), [setCommandScopes]); const searchCommandContextValue = useMemo( () => ({ query, @@ -377,13 +375,11 @@ function GlobalStandaloneSearchShellClient({ // pathname-only moves like /services → /dsm. React discards this render and // immediately re-renders with the updated state (see "adjusting state when a // prop changes"). Typing never changes the URL, so a URL-gated sync cannot - // clobber in-progress input; the initial mount is a no-op because the refs are - // seeded to the current URL and state is already derived from it. - const searchParamsChanged = lastSyncedSearchParamsRef.current !== searchParamString; - const pathnameChanged = lastSyncedPathnameRef.current !== pathname; - if (searchParamsChanged || pathnameChanged) { - lastSyncedSearchParamsRef.current = searchParamString; - lastSyncedPathnameRef.current = pathname; + // clobber in-progress input; the initial mount is a no-op because synced* + // state is seeded from the current URL. + if (searchParamString !== syncedSearchParamString || pathname !== syncedPathname) { + setSyncedSearchParamString(searchParamString); + setSyncedPathname(pathname); setSearchMode(resolvedSearchMode); setQuery(currentUrlHasQuery ? requestedQuery : ""); const nextSearchContext = readSearchNavigationContext(new URLSearchParams(searchParamString)); From 6f266a6fa16b6664f39ffca646ade074f6def11a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 06:33:45 +0000 Subject: [PATCH 11/16] docs: record PR #1134 babysit closeout after main sync --- docs/branch-review-ledger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index d56b13aac..dd2f2956e 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,7 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `a6be193b` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | Babysit: React Compiler lint clean (URL sync via previous-state, not refs); prescribing Safety filters restored; main merged. Awaiting green CI. | Local docs:check-index OK; shared-search-shell-url-sync + search-shell-props + api-rate-limit tests 20/20. | +| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `40701db4` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | Babysit COMPLETED for product scope: Static/Unit/Build/Policy green after Compiler-safe URL sync; prescribing Safety filters restored; main synced. Residual: human approving review + title update tooling. | Hosted: PR policy/Static/Unit/Build/Advisory UI green on prior tip; Production UI pending on this HEAD. | | 2026-07-24 | `cursor/search-performance-review-4ee9` / PR #1134 | 76d47871c44c607f942a683351378230173dcbe3 | Search performance findings remediation (prescribing, differentials, typeahead docs timeout, shared shell, answer rate-limit fallback) | FIXED. P1 prescribing catalogue now debounces (250 ms), aborts in-flight fetches, and uses `fields=index`. Differentials catalogue + evidence search abort/debounce. Universal documents typeahead timeout 6 s→750 ms (RAG impact: no retrieval behaviour change — typeahead timeout only). Shared `(search-app)` layout keeps GlobalSearchShell mounted across mode homes. Answer rate-limit fails closed only in production; development uses in-memory fallback when durable RPC is unavailable. | Focused Vitest 362 via test:focused; api-rate-limit/search-shell/universal/route/site-map suites green; `npm run ensure` smoke 200 on mode homes; `/api/answer/stream` 200 after fallback. No OpenAI spend beyond local answer stream smoke; no live eval/soak. | | 2026-07-24 | `main` | `6ceaaff50712e10e857bf9a5a7ec88b530bf7b35` | Search performance across modes (load + typeahead + submit; local ensure) | CHANGES REQUESTED / findings. No P0. P1: Prescribing `useMedicationCatalog(query)` refetches the full ~2.5–2.9 MB medication catalogue on every keystroke without debounce, abort, or `fields=index` (~25× larger than index). P2: differentials catalogue + evidence `/api/search` lack abort/debounce; universal typeahead `tookMs` dominated by empty live documents domain (~130–340 ms); cross-namespace mode switches remount the search shell; Answer submit returns 503 `rate_limit_unavailable` when durable limiter is down (fail-closed). Mode HTML load medians ~40–75 ms; typeahead wall ~160–180 ms (`ssri`) / ~350–400 ms (`agitation im lorazepam`) + 250 ms client debounce; catalogue submits (differentials) ~45 ms; document `/api/search` demo-degraded ~230–430 ms. Highest residual live risk: cold hybrid RPC tails (docs #25), not re-measured with soak/eval. | `npm run ensure` → http://localhost:4461; `/api/local-project-id` Clinical KB; paced universal typeahead across 13 modes × 2 queries (Server-Timing); `/api/search` + `/api/medications` (+`fields=index`) + differentials APIs + registry payload sizes; NDJSON vs JSON first-byte; browser walkthrough of 13 mode homes; static review of ClinicalDashboard / universal-search / medication+differential hooks. No OpenAI generation, no `eval:retrieval:latency`, no soak, no hosted CI. Environment had Supabase secrets so universal ran live (`publicAccess`); `/api/search` degraded to demo (`supabase_api_key_configuration_unavailable`). | From 0062bf0527834d7f7284052926a84aa44100d195 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 06:54:50 +0000 Subject: [PATCH 12/16] docs: refresh PR #1134 babysit ledger HEAD --- docs/branch-review-ledger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 4beda1287..4217c398b 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,7 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `40701db4` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | Babysit COMPLETED for product scope: Static/Unit/Build/Policy green after Compiler-safe URL sync; prescribing Safety filters restored; main synced. Residual: human approving review + title update tooling. | Hosted: PR policy/Static/Unit/Build/Advisory UI green on prior tip; Production UI pending on this HEAD. | +| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `a3525081` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | Babysit: product fixes green; Sheet teardown guard for unit unhandled errors; main synced. Awaiting exact-head required checks. | Hosted: PR policy/Static/Unit/Build/Advisory UI green on prior tip; Production UI pending on this HEAD. | | 2026-07-24 | `cursor/search-performance-review-4ee9` / PR #1134 | 76d47871c44c607f942a683351378230173dcbe3 | Search performance findings remediation (prescribing, differentials, typeahead docs timeout, shared shell, answer rate-limit fallback) | FIXED. P1 prescribing catalogue now debounces (250 ms), aborts in-flight fetches, and uses `fields=index`. Differentials catalogue + evidence search abort/debounce. Universal documents typeahead timeout 6 s→750 ms (RAG impact: no retrieval behaviour change — typeahead timeout only). Shared `(search-app)` layout keeps GlobalSearchShell mounted across mode homes. Answer rate-limit fails closed only in production; development uses in-memory fallback when durable RPC is unavailable. | Focused Vitest 362 via test:focused; api-rate-limit/search-shell/universal/route/site-map suites green; `npm run ensure` smoke 200 on mode homes; `/api/answer/stream` 200 after fallback. No OpenAI spend beyond local answer stream smoke; no live eval/soak. | | 2026-07-24 | `main` | `6ceaaff50712e10e857bf9a5a7ec88b530bf7b35` | Search performance across modes (load + typeahead + submit; local ensure) | CHANGES REQUESTED / findings. No P0. P1: Prescribing `useMedicationCatalog(query)` refetches the full ~2.5–2.9 MB medication catalogue on every keystroke without debounce, abort, or `fields=index` (~25× larger than index). P2: differentials catalogue + evidence `/api/search` lack abort/debounce; universal typeahead `tookMs` dominated by empty live documents domain (~130–340 ms); cross-namespace mode switches remount the search shell; Answer submit returns 503 `rate_limit_unavailable` when durable limiter is down (fail-closed). Mode HTML load medians ~40–75 ms; typeahead wall ~160–180 ms (`ssri`) / ~350–400 ms (`agitation im lorazepam`) + 250 ms client debounce; catalogue submits (differentials) ~45 ms; document `/api/search` demo-degraded ~230–430 ms. Highest residual live risk: cold hybrid RPC tails (docs #25), not re-measured with soak/eval. | `npm run ensure` → http://localhost:4461; `/api/local-project-id` Clinical KB; paced universal typeahead across 13 modes × 2 queries (Server-Timing); `/api/search` + `/api/medications` (+`fields=index`) + differentials APIs + registry payload sizes; NDJSON vs JSON first-byte; browser walkthrough of 13 mode homes; static review of ClinicalDashboard / universal-search / medication+differential hooks. No OpenAI generation, no `eval:retrieval:latency`, no soak, no hosted CI. Environment had Supabase secrets so universal ran live (`publicAccess`); `/api/search` degraded to demo (`supabase_api_key_configuration_unavailable`). | From e2f91be21c2987f9b6385083ec6d21171a2a95f4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 07:14:17 +0000 Subject: [PATCH 13/16] docs: record PR #1134 babysit merge-ready closeout --- docs/branch-review-ledger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 7f5763672..1ca8a2f0a 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,7 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `a3525081` | Babysit: Bugbot stale-mode fix, docs index, main sync, PR policy body | Babysit: product fixes green; Sheet teardown guard for unit unhandled errors; main synced. Awaiting exact-head required checks. | Hosted: PR policy/Static/Unit/Build/Advisory UI green on prior tip; Production UI pending on this HEAD. | +| 2026-07-24 | PR #1134 / `cursor/search-performance-review-4ee9` | `2a8e045f4a7179ea6fc9b81b1b2173e4c932617e` | Babysit closeout / merge readiness | MERGE-READY for product scope once exact-head required checks finish after final main sync. Fixed: Bugbot pathname searchMode (Compiler-safe previous-state sync); docs index; PR_POLICY_BODY/governance; maintainability extract; prescribing Safety filters (no fields=index); Sheet teardown document guard. Unresolved threads: none. Residual: human approving review; title still stale (gh/ManagePullRequest cannot edit); main may move again. | Hosted on prior tip 973d5d88: PR policy/Static/Unit/Build/Production UI/PR required all SUCCESS. Final sync pushed; awaiting exact-head recheck. No OpenAI/live Supabase writes. | | 2026-07-24 | `cursor/search-performance-review-4ee9` / PR #1134 | 76d47871c44c607f942a683351378230173dcbe3 | Search performance findings remediation (prescribing, differentials, typeahead docs timeout, shared shell, answer rate-limit fallback) | FIXED. P1 prescribing catalogue now debounces (250 ms), aborts in-flight fetches, and uses `fields=index`. Differentials catalogue + evidence search abort/debounce. Universal documents typeahead timeout 6 s→750 ms (RAG impact: no retrieval behaviour change — typeahead timeout only). Shared `(search-app)` layout keeps GlobalSearchShell mounted across mode homes. Answer rate-limit fails closed only in production; development uses in-memory fallback when durable RPC is unavailable. | Focused Vitest 362 via test:focused; api-rate-limit/search-shell/universal/route/site-map suites green; `npm run ensure` smoke 200 on mode homes; `/api/answer/stream` 200 after fallback. No OpenAI spend beyond local answer stream smoke; no live eval/soak. | | 2026-07-24 | `main` | `6ceaaff50712e10e857bf9a5a7ec88b530bf7b35` | Search performance across modes (load + typeahead + submit; local ensure) | CHANGES REQUESTED / findings. No P0. P1: Prescribing `useMedicationCatalog(query)` refetches the full ~2.5–2.9 MB medication catalogue on every keystroke without debounce, abort, or `fields=index` (~25× larger than index). P2: differentials catalogue + evidence `/api/search` lack abort/debounce; universal typeahead `tookMs` dominated by empty live documents domain (~130–340 ms); cross-namespace mode switches remount the search shell; Answer submit returns 503 `rate_limit_unavailable` when durable limiter is down (fail-closed). Mode HTML load medians ~40–75 ms; typeahead wall ~160–180 ms (`ssri`) / ~350–400 ms (`agitation im lorazepam`) + 250 ms client debounce; catalogue submits (differentials) ~45 ms; document `/api/search` demo-degraded ~230–430 ms. Highest residual live risk: cold hybrid RPC tails (docs #25), not re-measured with soak/eval. | `npm run ensure` → http://localhost:4461; `/api/local-project-id` Clinical KB; paced universal typeahead across 13 modes × 2 queries (Server-Timing); `/api/search` + `/api/medications` (+`fields=index`) + differentials APIs + registry payload sizes; NDJSON vs JSON first-byte; browser walkthrough of 13 mode homes; static review of ClinicalDashboard / universal-search / medication+differential hooks. No OpenAI generation, no `eval:retrieval:latency`, no soak, no hosted CI. Environment had Supabase secrets so universal ran live (`publicAccess`); `/api/search` degraded to demo (`supabase_api_key_configuration_unavailable`). | From 9311d01212fe42bd41ffb22a83bfa51f1a4d19f2 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:01:25 +0800 Subject: [PATCH 14/16] docs: ledger Run PR conflict sweep for #1134 --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 1685dfe9e..21ed7b5e9 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -729,3 +729,4 @@ This file is append-only. Never rewrite or delete an existing review record; app - Scope: Targeted review of search bar/header/footer chrome behaviour after the edge-to-edge phone dock fix, plus durable repo rules for page-adaptive search chrome. - Outcome: No new P0/P1 search chrome defect found in the static review. Fixed one regression hazard: a stale ClinicalDashboard comment still instructed a 0.75rem hidden dock pad despite the implementation/tests requiring 0rem. Added durable search chrome behaviour rules in AGENTS.md and docs/search-chrome-behaviour.md, with a static guard tying the remembered rules to the hidden-reserve contract. - Checks: dependency shortcut section count; git diff --check; targeted rg for stale 0.75rem hidden-pad source wording (only negative test assertions remain); targeted Vitest command attempted but blocked by missing node_modules/vitest under Node 20.20.2 in this container. No provider-backed checks run. +| 2026-07-24 | cursor/search-performance-review-4ee9 (PR #1134) | 022a1ab95718e5e7636226f0dfebe88d7d7bc4f4 | Run PR sweep: CI fix + threads + drift | Before: CONFLICTING, CI green (pr-required), 0 unresolved threads. After: merged origin/main cleanly (auto-merge ledger/codebase-index); pushed 022a1ab95. Threads: none. Residual: CI re-running post-merge. | merge origin/main only; no provider-backed checks run | From ff15a3856cb58894af2d979f4ae63b06a269e929 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:58:22 +0800 Subject: [PATCH 15/16] docs: ledger re-sync sweep for PR #1134 --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 21ed7b5e9..df2328077 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -730,3 +730,4 @@ This file is append-only. Never rewrite or delete an existing review record; app - Outcome: No new P0/P1 search chrome defect found in the static review. Fixed one regression hazard: a stale ClinicalDashboard comment still instructed a 0.75rem hidden dock pad despite the implementation/tests requiring 0rem. Added durable search chrome behaviour rules in AGENTS.md and docs/search-chrome-behaviour.md, with a static guard tying the remembered rules to the hidden-reserve contract. - Checks: dependency shortcut section count; git diff --check; targeted rg for stale 0.75rem hidden-pad source wording (only negative test assertions remain); targeted Vitest command attempted but blocked by missing node_modules/vitest under Node 20.20.2 in this container. No provider-backed checks run. | 2026-07-24 | cursor/search-performance-review-4ee9 (PR #1134) | 022a1ab95718e5e7636226f0dfebe88d7d7bc4f4 | Run PR sweep: CI fix + threads + drift | Before: CONFLICTING, CI green (pr-required), 0 unresolved threads. After: merged origin/main cleanly (auto-merge ledger/codebase-index); pushed 022a1ab95. Threads: none. Residual: CI re-running post-merge. | merge origin/main only; no provider-backed checks run | +| 2026-07-24 | cursor/search-performance-review-4ee9 (PR #1134) | 9311d01212fe42bd41ffb22a83bfa51f1a4d19f2 | Run PR re-sync sweep | Re-check: CONFLICTING on use-differential-catalog.ts (+ related). Not cheap; merge aborted, no push. | merge origin/main and/or conflict re-check only; no provider-backed checks run | From be303a196cdd851799696654d671638fe37fe610 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:33:00 +0800 Subject: [PATCH 16/16] docs: record PR 1134 maintenance --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 9a9424cdf..a694ec191 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -759,3 +759,4 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-24 | cursor/comprehensive-repo-review-ledger-d9a1 (PR #1150) | 345c02cdbaefb13aeb951a14674aedfe4648a50e | Run PR re-sync sweep | Before: CONFLICTING. After: merged origin/main clean. | merge origin/main and/or conflict re-check only; no provider-backed checks run | | 2026-07-25 | cursor/pr-babysit-bugbot-agents-6c52 (PR #1167) | ee44812aae9dad1973d8302eba5bfca5000dffb6 | Open-PR maintenance: review-thread fixes | Before: 8 unresolved Codex/CodeRabbit threads; branch current with main. After: target-head pinning, fresh-main verification, exact `cursor[bot]` identity checks, explicit mutation/provider authorization, direct reply-then-resolve semantics, and no-op ledger bookkeeping are documented. | Prettier check on both agent files pass; `git diff --check` pass; GitHub author probe confirmed `cursor[bot]` account type `Bot`; no provider-backed checks run. | | 2026-07-25 | codex/fix-merge-conflicts-and-ci-on-open-prs (PR #1170) | e979fc892f11a17b1a8f2ef1ab058ef40d629182 | Open-PR maintenance: CI fix + threads + drift | Before: Static PR checks failed because route-group moves made nine valid legacy `src/app/*` documentation references appear missing; 0 unresolved threads; branch already contained current main. After: docs-link resolution checks known App Router route groups and the focused failure is fixed. | `node scripts/check-docs-links.mjs` pass (1162 references); Prettier check pass; `git diff --check` pass; no provider-backed checks run. | +| 2026-07-25 | cursor/search-performance-review-4ee9 (PR #1134) | 692834a86e612cc8b311dc6895e007f182f5c5b8 | Open-PR maintenance: superseded docs-link thread and clean main sync | Before: branch was behind current main with one outdated docs-link thread; its product tree already matched main. After: merged current main cleanly and verified the route-group-aware docs-link fix now covers legacy route references. RAG impact: no retrieval behaviour change — history sync and docs tooling verification only. | `node scripts/check-docs-links.mjs` pass (1154 references); clean merge-tree; no live RAG canary or provider-backed check run. |