Skip to content
Merged
8 changes: 4 additions & 4 deletions docs/site-map.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,9 +14,9 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/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` - Patient information sheet library. Source: `src/app/factsheets/page.tsx`.
- `/factsheets/[slug]` - Patient information sheet detail. Source: `src/app/factsheets/[slug]/page.tsx`.
- `/factsheets/search` - Patient information sheet search. Source: `src/app/factsheets/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`.
Expand DownExpand Up@@ -849,7 +849,7 @@ 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/[slug]`. Source: `src/app/differentials/presentations/route.ts`.
- `/differentials/presentations` - Redirects to `/differentials/presentations/[workflow-slug]`. Source: `src/app/differentials/presentations/route.ts`.
- `/documents/source` - Redirects to `/documents/search`. Source: `src/app/documents/source/page.tsx`.
- `/medications` - Redirects to `/?mode=prescribing`. Source: `src/app/medications/route.ts`.
- `/mockups/favourites-hub` - Redirects to `/favourites`. Source: `src/app/mockups/favourites-hub/page.tsx`.
Expand Down
7 changes: 4 additions & 3 deletions playwright.config.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,10 +43,11 @@ export default defineConfig({
trace: "retain-on-failure",
screenshot: "only-on-failure",
// Disable CSS/web animations suite-wide so a click can't land mid-transition
// on a moving target (documented races in ui-stress/ui-smoke). Set via
// contextOptions (the supported form in this Playwright build); the dedicated
// on a moving target (documented races in ui-stress/ui-smoke). The dedicated
// reduced-motion a11y spec emulates it per-test too, so it is unaffected.
contextOptions: { reducedMotion: "reduce" },
contextOptions: {
reducedMotion: "reduce",
},
},
projects: [
{
Expand Down
5 changes: 1 addition & 4 deletions scripts/generate-site-map.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,7 +41,7 @@ const productRouteHandlerPaths = new Set(["/applications", "/differentials/prese

const documentedRedirectTargets: Record<string, string> = {
"/applications": "/tools",
"/differentials/presentations": "/differentials/presentations/[slug]",
"/differentials/presentations": "/differentials/presentations/[workflow-slug]",
"/medications": "/?mode=prescribing",
};

Expand All@@ -61,9 +61,6 @@ const routeDescriptions: Record<string, string> = {
"/documents/search": "Documents search command centre.",
"/documents/source": "Compatibility redirect to the canonical live document viewer when a valid id is supplied.",
"/documents/source/evidence": "Compatibility redirect sharing the canonical live document viewer handoff.",
"/factsheets": "Patient information sheet library.",
"/factsheets/[slug]": "Patient information sheet detail.",
"/factsheets/search": "Patient information sheet search.",
"/favourites": "Saved clinical items and sets.",
"/forms": "Forms home and search surface.",
"/forms/[slug]": "Registry-backed form detail.",
Expand Down
5 changes: 1 addition & 4 deletions src/app/differentials/presentations/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,7 @@ import { type NextRequest, NextResponse } from "next/server";
import { getPresentationWorkflowSelectionForDiagnosisIds } from "@/lib/differentials";

export function GET(request: NextRequest) {
const rawQuery = request.nextUrl.searchParams.get("query");
const legacyQuery = request.nextUrl.searchParams.get("q");
// Prefer `query`, but fall through on empty/whitespace so `q` remains usable.
const query = (rawQuery?.trim() || legacyQuery?.trim())?.trim();
const query = (request.nextUrl.searchParams.get("query") ?? request.nextUrl.searchParams.get("q"))?.trim();
Comment thread
BigSimmo marked this conversation as resolved.
const selectedIds = (request.nextUrl.searchParams.get("ids") ?? "")
.split(",")
.map((id) => id.trim())
Expand Down
7 changes: 6 additions & 1 deletion src/app/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,12 @@ import { HomePageClient } from "@/app/home-page-client";
import { appModeHomeHref, isAppModeId, isAppModeVisible, type AppModeId } from "@/lib/app-modes";

type HomeProps = {
searchParams?: Promise<Record<string, string | string[] | undefined>>;
searchParams?: Promise<{
mode?: string | string[];
q?: string | string[];
focus?: string | string[];
run?: string | string[];
}>;
};

function firstSearchParam(value: string | string[] | undefined) {
Expand Down
8 changes: 6 additions & 2 deletions src/components/ClinicalDashboard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3648,7 +3648,7 @@ export function ClinicalDashboard({
) : error ? (
<EmptyState
icon={CircleAlert}
title={activeModeResultKind === "answer" ? "Answer unavailable" : "Search unavailable"}
title="Answer unavailable"
Comment thread
BigSimmo marked this conversation as resolved.
body={error}
live="assertive"
tone="danger"
Expand DownExpand Up@@ -3876,7 +3876,11 @@ export function ClinicalDashboard({
/>
) : null}

{showUniversalAlsoMatches ? (
{showUniversalAlsoMatches && activeModeResultKind !== "answer" ? (
<UniversalSearchAlsoMatches modeId={searchMode} query={universalAlsoMatchesQuery} />
) : null}

{showUniversalAlsoMatches && activeModeResultKind === "answer" ? (
<UniversalSearchAlsoMatches modeId={searchMode} query={universalAlsoMatchesQuery} />
) : null}
</section>
Expand Down
18 changes: 3 additions & 15 deletions src/components/clinical-dashboard/master-search-header.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -1440,7 +1440,7 @@ export function MasterSearchHeader({
closeButtonClassName="grid h-11 w-11 shrink-0 place-items-center rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
contentClassName={cn(
"bg-[color:var(--surface-lux)]",
scopeSheetFullscreen ? "max-h-full" : "max-h-[min(84dvh,42rem)]",
scopeSheetFullscreen ? "max-h-dvh" : "max-h-[min(84dvh,42rem)]",
"sm:max-h-[min(88dvh,44rem)] sm:max-w-xl",
)}
bodyClassName={cn(
Expand DownExpand Up@@ -1583,13 +1583,7 @@ export function MasterSearchHeader({
onBlur={(event) => {
const nextFocusedElement = event.relatedTarget;
if (nextFocusedElement instanceof Node && event.currentTarget.contains(nextFocusedElement)) return;
// Defer dismiss so a pointer activation on a menuitem can land before
// unmount; keyboard leave (Tab/Shift+Tab) still closes on the next frame.
const menuRoot = event.currentTarget;
window.requestAnimationFrame(() => {
if (menuRoot.contains(document.activeElement)) return;
setModeMenuOpen(false);
});
setModeMenuOpen(false);
}}
className={cn("relative z-[60] min-w-0", isWorkflowHeader ? "justify-self-start" : "justify-self-center")}
>
Expand All@@ -1598,7 +1592,6 @@ export function MasterSearchHeader({
type="button"
onClick={() => {
setActionMenuOpen(false);
setCommandDropdownOpen(false);
closeScope(false);
setModeMenuOpen((open) => !open);
}}
Expand DownExpand Up@@ -1644,11 +1637,6 @@ export function MasterSearchHeader({
id="app-mode-menu"
role="menu"
aria-label="Choose app mode"
onMouseDown={(event) => {
// Keep focus on the mode trigger so blur-to-dismiss does not
// unmount options before the click lands.
event.preventDefault();
}}
className="polished-scroll fixed left-[max(0.5rem,var(--safe-area-left))] right-[max(0.5rem,var(--safe-area-right))] top-[calc(4.25rem+env(safe-area-inset-top))] z-50 max-h-[min(20rem,calc(100dvh-5.5rem))] overflow-y-auto rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] p-1.5 text-[color:var(--text)] shadow-[var(--shadow-lux)] ring-1 ring-white/25 backdrop-blur-md dark:ring-white/10 sm:absolute sm:left-0 sm:right-auto sm:top-[calc(100%+0.5rem)] sm:w-[min(21rem,calc(100vw-2rem))]"
>
{visibleAppModeOptions.map((mode, index) => {
Expand DownExpand Up@@ -1702,7 +1690,7 @@ export function MasterSearchHeader({
) : null}
</div>

<div className="relative flex min-w-0 shrink-0 items-center justify-end gap-2 justify-self-end">
<div className="relative flex min-w-0 shrink-0 items-center justify-end gap-1.5 justify-self-end sm:gap-2">
{isWorkflowHeader ? (
<>
<button
Expand Down
7 changes: 2 additions & 5 deletions src/components/services/services-navigator-page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -301,10 +301,6 @@ function RightRail({
const comparisonExpanded = showComparison && comparisonAvailable;
const confidenceTotal = counts.high + counts.medium + counts.low + counts.unknown;

// Keep rail toggles mounted across selection-count changes. Derived expanded
// flags below already hide checklist/comparison when the current selection
// cannot support them, so no effect-driven setState is required.

const rows: Array<[string, number, LucideIcon, string]> = [
["Meets", counts.meets, CircleCheck, "text-[color:var(--success)]"],
["Caution", counts.cautions, CircleAlert, "text-[color:var(--warning)]"],
Expand DownExpand Up@@ -609,6 +605,7 @@ export function ServicesNavigatorPage() {
}
sidebar={
<RightRail
key={selected.length === 0 ? "empty" : selected.length === 1 ? "single" : "multiple"}
matches={displayedMatches}
selected={selected}
onClearSelected={() => setSelectedSlugs([])}
Expand DownExpand Up@@ -731,9 +728,9 @@ export function ServicesNavigatorPage() {
/>
))}
</div>
<UniversalSearchAlsoMatches modeId="services" query={query} />
</>
)}
<UniversalSearchAlsoMatches modeId="services" query={query} />
</SearchResultsLayout>
);
}
22 changes: 5 additions & 17 deletions src/lib/rag-candidate-sources.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,13 +44,6 @@ import type { DocumentIndexUnitMatch, DocumentMemoryCard, SearchResult } from "@
// the floor, which is how the live schema drift (42702) went unnoticed. Log it structurally and,
// where telemetry is in scope, record the failing RPC + code so it shows up in rag_retrieval_logs.
export type SupabaseRpcError = { message?: string; code?: string; details?: string; hint?: string } | null;
type RpcResult<T> = Promise<{ data: T | null; error: SupabaseRpcError }>;
type AbortableRpc<T> = RpcResult<T> & {
abortSignal?: (signal: AbortSignal) => RpcResult<T>;
};
type SupabaseRpcClient = {
rpc: (name: string, rpcArgs: Record<string, unknown>) => AbortableRpc<unknown[]> | PromiseLike<unknown>;
};

function legacyRankFields(versionedName: string) {
if (versionedName === "match_document_chunks_v2") return ["similarity"];
Expand DownExpand Up@@ -88,20 +81,15 @@ export async function callVersionedRetrievalRpc<T extends unknown[] = unknown[]>
versionedName: string,
legacyName: string,
args: Record<string, unknown>,
signal?: AbortSignal,
): Promise<{ data: T | null; error: SupabaseRpcError }> {
const client = supabase as unknown as SupabaseRpcClient;
const executeRpc = async (name: string, rpcArgs: Record<string, unknown>) => {
const pending = client.rpc(name, rpcArgs) as AbortableRpc<T>;
const pendingWithAbort =
signal && typeof pending.abortSignal === "function" ? pending.abortSignal(signal) : pending;
return await pendingWithAbort;
const client = supabase as unknown as {
rpc: (name: string, rpcArgs: Record<string, unknown>) => Promise<{ data: T | null; error: SupabaseRpcError }>;
};
const versioned = await executeRpc(versionedName, args);
const versioned = await client.rpc(versionedName, args);
if (versioned && !isMissingRetrievalRpcError(versioned.error)) return versioned;
const legacyArgs = { ...args };
delete legacyArgs.include_public;
const ownerResult = await executeRpc(legacyName, legacyArgs);
const ownerResult = await client.rpc(legacyName, legacyArgs);
const ownerFilter = String(args.owner_filter ?? "");
if (
ownerResult.error ||
Expand All@@ -111,7 +99,7 @@ export async function callVersionedRetrievalRpc<T extends unknown[] = unknown[]>
) {
return ownerResult;
}
const publicResult = await executeRpc(legacyName, {
const publicResult = await client.rpc(legacyName, {
...legacyArgs,
owner_filter: PUBLIC_OWNER_FILTER_SENTINEL,
});
Expand Down
49 changes: 4 additions & 45 deletions src/lib/rag.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,6 @@ import { createAdminClient } from "@/lib/supabase/admin";
import { retrievalAccessScopeForArgs, retrievalRpcScopeArgs } from "@/lib/owner-scope";
import {
callVersionedRetrievalRpc,
createChunkLoadCache,
memoryCardChunkScore,
mergeSearchResults,
recordHybridRpcError,
Expand All@@ -12,6 +11,7 @@ import {
searchTableFactCandidates,
searchTextChunkCandidates,
withMemoryBoostedCandidates,
createChunkLoadCache,
type MemoryCardCache,
} from "@/lib/rag-candidate-sources";
export {
Expand DownExpand Up@@ -120,11 +120,6 @@ export {
retrievalPlanCacheQuery,
} from "@/lib/rag-cache";
import { classifySearchCacheOutcome, recordCacheLookup } from "@/lib/observability/cache-metrics";
import {
recordAnswerOrigination,
recordAnswerOriginationFinished,
recordCoalescedAnswerWaiter,
} from "@/lib/observability/answer-coalescing-metrics";
import { buildRagSourceBlock, compactContextText, neutralizeIdentityField } from "@/lib/rag-source-block";
export { buildRagSourceBlock, truncateForModel } from "@/lib/rag-source-block";
import {
Expand DownExpand Up@@ -428,26 +423,6 @@ function throwIfAborted(signal?: AbortSignal) {
}
}

function awaitWithCallerSignal<T>(pending: Promise<T>, signal?: AbortSignal): Promise<T> {
if (!signal) return pending;
if (signal.aborted) throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");

return new Promise<T>((resolve, reject) => {
const onAbort = () => reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
signal.addEventListener("abort", onAbort, { once: true });
pending.then(
(value) => {
signal.removeEventListener("abort", onAbort);
resolve(value);
},
(error) => {
signal.removeEventListener("abort", onAbort);
reject(error);
},
);
});
}

export type AnswerProgressEvent = {
stage:
| "retrieved"
Expand DownExpand Up@@ -1315,7 +1290,6 @@ export async function analyzeQueryWithClassifierFallback(
// owner_filter retrieval will use so grounding can never see documents retrieval cannot.
corpusGrounding?: { supabase: ReturnType<typeof createAdminClient>; ownerFilter: string | null };
ownerId?: string | null;
signal?: AbortSignal;
},
) {
if (
Expand DownExpand Up@@ -1387,16 +1361,10 @@ export async function analyzeQueryWithClassifierFallback(
}

try {
const verdict = await awaitWithCallerSignal(pending, opts?.signal);
const verdict = await pending;
storeClassifierVerdictMemo(memoKey, verdict);
return applyClassifierVerdict(analysis, verdict);
} catch (error) {
if (
error &&
(error instanceof DOMException || typeof error === "object") &&
(error as { name?: string }).name === "AbortError"
)
throw error;
} catch {
// Transport/parse failures are deliberately NOT memoized: fall back to the deterministic
// analysis for this request only, and let the next request retry the classifier.
return analysis;
Expand DownExpand Up@@ -2430,7 +2398,6 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) {
const queryAnalysis = await analyzeQueryWithClassifierFallback(retrievalQuery, analyzeClinicalQuery(retrievalQuery), {
corpusGrounding: corpusGroundingScope,
ownerId: args.ownerId,
signal: args.signal,
});
throwIfAborted(args.signal);
if (modeQueryClass) queryAnalysis.queryClass = modeQueryClass;
Expand DownExpand Up@@ -3156,7 +3123,6 @@ export async function answerQuestionWithScope(args: AnswerQuestionWithScopeArgs)
let existing = inflightKey ? answerInflight.get(inflightKey) : undefined;

while (existing) {
recordCoalescedAnswerWaiter();
await args.onProgress?.({
stage: "cached",
message: "Waiting for an identical cited answer request already in progress.",
Expand DownExpand Up@@ -3188,15 +3154,8 @@ export async function answerQuestionWithScope(args: AnswerQuestionWithScopeArgs)
}
}

// Only coalescible requests belong in this process-local signal. Requests
// that intentionally bypass cache/coalescing must not make a replica look
// ineffective, and neither keys nor clinical content leave this function.
if (inflightKey) recordAnswerOrigination();
const pending = answerQuestionWithScopeUncoalesced(args, startedAt).finally(() => {
if (inflightKey) {
answerInflight.delete(inflightKey);
recordAnswerOriginationFinished();
}
if (inflightKey) answerInflight.delete(inflightKey);
});
if (inflightKey) answerInflight.set(inflightKey, pending);
return pending;
Expand Down
4 changes: 2 additions & 2 deletions supabase/drift-manifest.json
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
{
"generated_at": "2026-07-17T22:10:14.366Z",
"generated_at": "2026-07-18T08:58:41.495Z",
"generator": "scripts/generate-drift-manifest.ts",
"postgres_image": "supabase/postgres:17.6.1.127",
"schema_sha256": "302a9ecec8e69564d50035be8480fa5e5686ca6136d96978f2188ecf5fb58be1",
"replay_seconds": 13,
"replay_seconds": 18,
"snapshot": {
"views": [
{
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,14 +60,12 @@ security definer
set search_path = public, pg_catalog, pg_temp
as $$
begin
if TG_OP = 'DELETE' or
(TG_OP = 'UPDATE' and OLD.status = 'indexed' and NEW.status is distinct from 'indexed') then
if TG_OP = 'DELETE' or (TG_OP = 'UPDATE' and OLD.status = 'indexed' and NEW.status <> 'indexed') then
delete from public.document_title_words where document_id = OLD.id;
end if;

if (TG_OP = 'INSERT' and NEW.status = 'indexed') or
(TG_OP = 'UPDATE' and NEW.status = 'indexed' and
(OLD.status is distinct from 'indexed' or OLD.title is distinct from NEW.title)) then
if (TG_OP = 'INSERT' and NEW.status = 'indexed') or
(TG_OP = 'UPDATE' and NEW.status = 'indexed' and (OLD.status <> 'indexed' or OLD.title <> NEW.title)) then

if TG_OP = 'UPDATE' then
delete from public.document_title_words where document_id = NEW.id;
Expand Down
Loading
Loading