Skip to content
4 changes: 2 additions & 2 deletions docs/codebase-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ 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
- **App shell:** `src/components/clinical-dashboard/global-search-shell.tsx` — route-aware standalone shell and lazy dashboard dispatch via `global-mockup-search-shell.tsx`
- **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`.
- **Home:** `src/app/page.tsx` — dashboard rendered by shell
- **Dashboard:** `src/components/ClinicalDashboard.tsx` + `src/components/clinical-dashboard/`
- **Modes (8):** `src/lib/app-modes.ts` — answer, documents, services, forms, favourites, differentials, prescribing, tools
Expand Down Expand Up @@ -291,7 +291,7 @@ One shared composer (`master-search-header.tsx`) serves every mode. Placement:
| Reindex operations | `docs/reindex-runbook.md` |
| Production readiness | `docs/production-readiness-checklist.md` |
| Capacity / scale-up | `docs/capacity-review.md`, `docs/auth-connection-cap-runbook.md` |
| Frontend refactor | `docs/frontend-architecture-refactor-plan.md` |
| Frontend architecture | `docs/frontend-architecture.md` |
| Repo audit (2026-07-01) | `docs/audit/repo-audit-2026-07-01.md` |

---
Expand Down
22 changes: 22 additions & 0 deletions docs/frontend-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Frontend architecture

## Route ownership

- `GlobalSearchShell` owns shared navigation, responsive chrome and URL-backed mode/query/filter state.
- `ClinicalDashboard` owns submitted Answer, Documents and Prescribing workflows.
- `/documents/search` renders live `/api/search` results for submitted queries; `/documents/[id]` is the canonical viewer.
- `/documents/source*` are compatibility redirects. Fixture document journeys live only below `/mockups/document-search/**`.

## Client state boundaries

- Query mode and non-sensitive filters are validated by `search-navigation-context` and serialized in the URL.
- Selected private document IDs stay in session storage behind a short-lived opaque `scopeRef`; an unavailable reference blocks automatic execution rather than broadening scope.
- `AuthProvider` owns the authentication epoch and abort registry. User-scoped requests capture the epoch and must verify it before committing state.
- `answer-lifecycle` distinguishes loading, streaming, revision, completion, cancellation and failure. Cancelled provisional text is removed and cannot be copied as a final answer.

## Server and safety boundaries

- Pages and layouts remain Server Components unless they require browser state or event handlers.
- Route handlers enforce public/private document scope; client filters are never authorization controls.
- Pre-stream API failures use the public JSON error envelope. SSE error events are reserved for failures after a successful stream begins.
- Production routes must not import fixture/mockup modules.
31 changes: 17 additions & 14 deletions docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/differentials/presentations` - Presentation workflow stream. Source: `src/app/differentials/presentations/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` - Master document reader with demo PDF content and evidence navigation. Source: `src/app/documents/source/page.tsx`.
- `/documents/source/evidence` - Evidence detail page for document flow. Source: `src/app/documents/source/evidence/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`.
- `/favourites` - Saved clinical items and sets. Source: `src/app/favourites/page.tsx`.
- `/forms` - Forms home and search surface. Source: `src/app/forms/page.tsx`.
- `/medications` - Medication index redirect. Source: `src/app/medications/page.tsx`.
Expand All @@ -33,23 +33,23 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`

## Mode page index

| Mode | Home page | Search/results page | Information/detail pages |
| ------------- | -------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Answer | `/?mode=answer` | `/?mode=answer&q=example+question&focus=1&run=1` | Answer, citations, evidence, and source panels render inside the root dashboard shell. |
| Documents | `/?mode=documents` | `/documents/search?mode=documents&q=lithium+monitoring&focus=1&run=1` | `/documents/source` master reader, `/documents/source/evidence` evidence detail, `/documents/search` results, and `/documents/[id]` live viewer. |
| Services | `/services` | `/services?q=13YARN&focus=1&run=1` | `/services/[slug]` service record pages. |
| Forms | `/forms` | `/forms?q=transport+forms&focus=1&run=1` | `/forms/[slug]` form record pages. |
| Favourites | `/favourites` | `/favourites?q=clozapine+set&focus=1&run=1` | Saved set and saved item detail render inside the favourites page surface. |
| Differentials | `/differentials` | `/differentials?q=acute+confusion&focus=1&run=1` | `/differentials/diagnoses`, `/differentials/diagnoses/[slug]`, and `/differentials/presentations`. |
| Medication | `/?mode=prescribing` | `/?mode=prescribing&q=acamprosate+renal+dose&focus=1&run=1` | `/medications/[slug]`; `/medications` redirects to medication mode. |
| Tools | `/?mode=tools` | `/?mode=tools&q=medications&focus=1&run=1` | Tool launcher and detail panels inside dashboard tools mode (`/?mode=tools`). |
| Mode | Home page | Search/results page | Information/detail pages |
| ------------- | -------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Answer | `/?mode=answer` | `/?mode=answer&q=example+question&focus=1&run=1` | Answer, citations, evidence, and source panels render inside the root dashboard shell. |
| Documents | `/?mode=documents` | `/documents/search?mode=documents&q=lithium+monitoring&focus=1&run=1` | `/documents/search` live results and `/documents/[id]` canonical viewer; `/documents/source*` are compatibility redirects. |
| Services | `/services` | `/services?q=13YARN&focus=1&run=1` | `/services/[slug]` service record pages. |
| Forms | `/forms` | `/forms?q=transport+forms&focus=1&run=1` | `/forms/[slug]` form record pages. |
| Favourites | `/favourites` | `/favourites?q=clozapine+set&focus=1&run=1` | Saved set and saved item detail render inside the favourites page surface. |
| Differentials | `/differentials` | `/differentials?q=acute+confusion&focus=1&run=1` | `/differentials/diagnoses`, `/differentials/diagnoses/[slug]`, and `/differentials/presentations`. |
| Medication | `/?mode=prescribing` | `/?mode=prescribing&q=acamprosate+renal+dose&focus=1&run=1` | `/medications/[slug]`; `/medications` redirects to medication mode. |
| Tools | `/?mode=tools` | `/?mode=tools&q=medications&focus=1&run=1` | Tool launcher and detail panels inside dashboard tools mode (`/?mode=tools`). |

## Documents flow index

- `/?mode=documents` - Documents mode home. Stays as the no-query home surface for document mode.
- `/documents/search?mode=documents&q=clozapine+monitoring+table&focus=1&run=1` - Documents search command centre used after submitting a search in Documents mode.
- `/documents/source?mode=documents&document=clozapine-monitoring&q=clozapine+monitoring+table&page=12&chunk=monitoring-table` - Standalone master document reader for a selected result, with bundled demo PDF content and evidence navigation.
- `/documents/source/evidence?mode=documents&document=clozapine-monitoring&evidence=monitoring-table&q=clozapine+monitoring+table&page=12&chunk=monitoring-table` - Reusable evidence detail page opened from the search tray or document reader evidence cards.
- `/documents/source?id=11111111-1111-4111-8111-111111111111&page=12&chunk=monitoring-table` - Legacy source handoff; valid document IDs redirect to the canonical live viewer and invalid IDs return to Documents search.
- `/documents/source/evidence?id=11111111-1111-4111-8111-111111111111&page=12&chunk=monitoring-table` - Legacy evidence handoff redirected to the canonical live document viewer.
- `/documents/[id]` - Live document viewer route remains available for real document records.

## Registry-backed routes
Expand Down Expand Up @@ -513,6 +513,8 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/mockups/document-search` - Route discovered from app directory Source: `src/app/mockups/document-search/page.tsx`.
- `/mockups/document-search-evidence-lens` - Route discovered from app directory Source: `src/app/mockups/document-search-evidence-lens/page.tsx`.
- `/mockups/document-search-triage-board` - Route discovered from app directory Source: `src/app/mockups/document-search-triage-board/page.tsx`.
- `/mockups/document-search/search` - Route discovered from app directory Source: `src/app/mockups/document-search/search/page.tsx`.
- `/mockups/document-search/source` - Route discovered from app directory Source: `src/app/mockups/document-search/source/page.tsx`.
- `/mockups/document-search/source-overlays` - Route discovered from app directory Source: `src/app/mockups/document-search/source-overlays/page.tsx`.
- `/mockups/document-search/source/evidence` - Route discovered from app directory Source: `src/app/mockups/document-search/source/evidence/page.tsx`.
- `/mockups/favourites-command-console` - Route discovered from app directory Source: `src/app/mockups/favourites-command-console/page.tsx`.
Expand Down Expand Up @@ -580,6 +582,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`

## Redirects

- `/documents/source` - Redirects to `/documents/search`. Source: `src/app/documents/source/page.tsx`.
- `/medications` - Redirects to `/?mode=prescribing`. Source: `src/app/medications/page.tsx`.
- `/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`.
Expand Down
32 changes: 8 additions & 24 deletions scripts/generate-site-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ import { pathToFileURL } from "node:url";
import { format } from "prettier";

import { appModeDefinitions, appModeHomeHref, type AppModeId } from "@/lib/app-modes";
import {
documentEvidenceHref,
documentReaderHref,
documentsSearchHref,
DOCUMENTS_MODE_HOME_ROUTE,
} from "@/lib/document-flow-routes";
import { documentsSearchHref, DOCUMENTS_MODE_HOME_ROUTE } from "@/lib/document-flow-routes";
import { differentialRecords } from "@/lib/differentials";
import { formRecords } from "@/lib/forms";
import { serviceRecords } from "@/lib/services";
Expand Down Expand Up @@ -46,8 +41,8 @@ const routeDescriptions: Record<string, string> = {
"/differentials/presentations": "Presentation workflow stream.",
"/documents/[id]": "Document viewer/detail page.",
"/documents/search": "Documents search command centre.",
"/documents/source": "Master document reader with demo PDF content and evidence navigation.",
"/documents/source/evidence": "Evidence detail page for document flow.",
"/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.",
"/favourites": "Saved clinical items and sets.",
"/forms": "Forms home and search surface.",
"/forms/[slug]": "Registry-backed form detail.",
Expand Down Expand Up @@ -242,7 +237,7 @@ function renderModePageIndex() {
home: DOCUMENTS_MODE_HOME_ROUTE,
search: documentsSearchHref({ query: "lithium monitoring", focus: true, run: true }),
detail:
"`/documents/source` master reader, `/documents/source/evidence` evidence detail, `/documents/search` results, and `/documents/[id]` live viewer.",
"`/documents/search` live results and `/documents/[id]` canonical viewer; `/documents/source*` are compatibility redirects.",
},
{
mode: "Services",
Expand Down Expand Up @@ -291,23 +286,12 @@ function renderDocumentFlowIndex() {
"Documents search command centre used after submitting a search in Documents mode.",
),
bullet(
documentReaderHref({
document: "clozapine-monitoring",
query: "clozapine monitoring table",
page: 12,
chunk: "monitoring-table",
}),
"Standalone master document reader for a selected result, with bundled demo PDF content and evidence navigation.",
"/documents/source?id=11111111-1111-4111-8111-111111111111&page=12&chunk=monitoring-table",
"Legacy source handoff; valid document IDs redirect to the canonical live viewer and invalid IDs return to Documents search.",
),
bullet(
documentEvidenceHref({
document: "clozapine-monitoring",
evidence: "monitoring-table",
query: "clozapine monitoring table",
page: 12,
chunk: "monitoring-table",
}),
"Reusable evidence detail page opened from the search tray or document reader evidence cards.",
"/documents/source/evidence?id=11111111-1111-4111-8111-111111111111&page=12&chunk=monitoring-table",
"Legacy evidence handoff redirected to the canonical live document viewer.",
),
bullet("/documents/[id]", "Live document viewer route remains available for real document records."),
];
Expand Down
79 changes: 66 additions & 13 deletions scripts/verify-pr-local.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,46 @@
import { spawnSync } from "node:child_process";

const isWindows = process.platform === "win32";
const baseScripts = ["check:runtime", "format:check", "lint", "typecheck", "test"];

function parseArgs(args) {
const options = { dryRun: false, extended: false, files: undefined };

for (let index = 0; index < args.length; index += 1) {
const token = args[index];
if (token === "--dry-run") {
options.dryRun = true;
continue;
}
if (token === "--extended") {
options.extended = true;
continue;
}
if (token === "--files") {
const value = args[index + 1];
if (!value || value.startsWith("--")) throw new Error("--files requires a comma-separated path list.");
options.files = value;
index += 1;
continue;
}
if (token === "--help" || token === "-h") {
console.log(
"Usage: npm run verify:pr-local -- [--dry-run] [--files pathA,pathB] [--extended]\n" +
" --dry-run Print the selected checks without running them.\n" +
" --files Classify an explicit comma-separated changed-file list.\n" +
" --extended Add the local Chromium UI gate when UI files changed.",
);
process.exit(0);
}
throw new Error(`Unknown option: ${token}`);
}

if (options.extended && !options.dryRun && process.env.ALLOW_EXTENDED_PR_LOCAL !== "true") {
throw new Error("--extended execution requires ALLOW_EXTENDED_PR_LOCAL=true; use --dry-run to inspect the plan.");
}

return options;
}

function runNpmScript(script) {
console.log(`\n> npm run ${script}`);
Expand All @@ -13,28 +53,41 @@ function runNpmScript(script) {
}
}

function readScope() {
const result = spawnSync(process.execPath, ["scripts/ci-change-scope.mjs", "--json"], {
function readScope(files) {
const args = ["scripts/ci-change-scope.mjs", "--json"];
if (files) args.push("--files", files);
const result = spawnSync(process.execPath, args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "inherit"],
});
if (result.status !== 0) process.exit(result.status ?? 1);
return JSON.parse(result.stdout);
}

const scope = readScope();
function selectedScripts(scope, extended) {
const scripts = [...baseScripts];
if (scope.build_changed) scripts.push("build");
if (scope.rag_eval_changed) scripts.push("eval:rag:offline");
if (extended && scope.ui_changed) scripts.push("verify:ui");
return scripts;
}

const options = parseArgs(process.argv.slice(2));
const scope = readScope(options.files);
const scripts = selectedScripts(scope, options.extended);
console.log(`Changed files: ${scope.files.length > 0 ? scope.files.join(", ") : "(none detected)"}`);

for (const script of ["check:runtime", "format:check", "lint", "typecheck", "test"]) {
runNpmScript(script);
if (options.dryRun) {
console.log("\nPR-local verification plan (dry run):");
for (const script of scripts) console.log(`- npm run ${script}`);
if (!scope.build_changed) console.log("- build skipped: no build-affecting changes detected");
if (!scope.rag_eval_changed) console.log("- offline RAG evaluation skipped: no RAG-affecting changes detected");
if (options.extended && !scope.ui_changed)
console.log("- Chromium UI gate skipped: no UI-affecting changes detected");
process.exit(0);
}

if (scope.build_changed) {
runNpmScript("build");
} else {
console.log("\nSkipping build: no build-affecting source, config, package, or container changes detected.");
}
for (const script of scripts) runNpmScript(script);

if (scope.rag_eval_changed) {
runNpmScript("eval:rag:offline");
}
if (!scope.build_changed)
console.log("\nSkipping build: no build-affecting source, config, package, or container changes detected.");
Loading
Loading