Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions docs/codebase-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,19 @@ Structured map for AI agents and onboarding. For live routes, see `docs/site-map

### API routes (`src/app/api/`)

| Area | Routes | Entry files |
| ----------- | ----------------------------------------------------------------------- | --------------------------------------------------------------- |
| Answers | `/api/answer`, `/api/answer/stream`, `/api/answer-feedback` | `answer/route.ts`, `answer/stream/route.ts`, `answer-feedback/` |
| Search | `/api/search`, `/api/search/interaction` | `search/` |
| Upload | `/api/upload` | `upload/route.ts` |
| Documents | CRUD, bulk, reindex, labels, search, summarize, table-facts, signed-url | `documents/` |
| Ingestion | batches, jobs, retry, quality | `ingestion/` |
| Registry | records CRUD | `registry/records/` |
| Images | signed URLs | `images/[id]/signed-url/route.ts` |
| Ops | health, setup-status, local-project-id | `health/`, `setup-status/`, `local-project-id/` |
| Eval / jobs | eval cases, job state | `eval-cases/`, `jobs/` |
| Area | Routes | Entry files |
| ------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Answers | `/api/answer`, `/api/answer/stream`, `/api/answer-feedback` | `answer/route.ts`, `answer/stream/route.ts`, `answer-feedback/` |
| Search | `/api/search`, `/api/search/interaction`, `/api/search/universal` | `search/` |
| Upload | `/api/upload` | `upload/route.ts` |
| Documents | `/api/documents`, `/api/documents/[id]`, bulk/reindex, labels, reviews, search, signed URLs, summaries, table facts | `documents/` |
| Differentials | `/api/differentials`, `/api/differentials/[slug]`, `/api/differentials/presentations/[slug]` | `differentials/` |
| Medications | `/api/medications`, `/api/medications/[slug]` | `medications/` |
| Ingestion | `/api/ingestion/batches`, `/api/ingestion/jobs`, retry, quality | `ingestion/` |
| Registry | `/api/registry/records`, `/api/registry/records/[slug]` | `registry/records/` |
| Images | `/api/images/[id]/signed-url` | `images/[id]/signed-url/route.ts` |
| Ops | `/api/health`, `/api/health/ready`, `/api/setup-status`, `/api/local-project-id` | `health/`, `setup-status/`, `local-project-id/` |
| Eval / jobs | `/api/eval-cases`, `/api/jobs` | `eval-cases/`, `jobs/` |

---

Expand Down Expand Up @@ -151,12 +153,12 @@ Structured map for AI agents and onboarding. For live routes, see `docs/site-map

- **CLI:** `supabase/config.toml` — `indexing-v3-agent` function, `verify_jwt = false`
- **Schema mirror:** `supabase/schema.sql` (reference; migrations are source of truth)
- **Migrations:** `supabase/migrations/*.sql` (~90 files, May–Jul 2026)
- **Migrations:** `supabase/migrations/*.sql` (chronological source of truth; do not hardcode a count)
- **Drift policy:** `docs/supabase-migration-reconciliation.md`

### Core tables
### Schema tables

`documents`, `document_pages`, `document_images`, `document_chunks`, `document_embedding_fields`, `document_index_units`, `document_table_facts`, `document_labels`, `document_summaries`, `document_sections`, `document_memory_cards`, `document_index_quality`, `ingestion_jobs`, `ingestion_job_stages`, `indexing_v3_agent_jobs`, `import_batches`, `rag_queries`, `rag_query_misses`, `rag_aliases`, `rag_response_cache`, `rag_retrieval_logs`, `clinical_registry_records`, `api_rate_limits`, `audit_logs`, `storage_cleanup_jobs`
`documents`, `document_pages`, `document_images`, `document_chunks`, `document_embedding_fields`, `document_index_units`, `document_table_facts`, `document_labels`, `document_summaries`, `document_sections`, `document_memory_cards`, `document_index_quality`, `document_title_words`, `ingestion_jobs`, `ingestion_job_stages`, `indexing_v3_agent_jobs`, `import_batches`, `image_caption_cache`, `rag_queries`, `rag_query_misses`, `rag_aliases`, `rag_response_cache`, `rag_retrieval_logs`, `rag_visual_eval_cases`, `rag_visual_eval_runs`, `rag_answer_feedback`, `clinical_registry_records`, `clinical_registry_record_sources`, `medication_records`, `differential_records`, `source_review_events`, `api_rate_limits`, `api_rate_limit_subjects`, `audit_logs`, `storage_cleanup_jobs`

**Storage buckets:** `clinical-documents`, `clinical-images` (private)

Expand Down
13 changes: 7 additions & 6 deletions docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` to verify it is current.

## Main product pages
## Main product routes

- `/` - Main Clinical KB shell. Source: `src/app/page.tsx`.
- `/differentials` - Differentials home and search surface. Source: `src/app/differentials/page.tsx`.
Expand Down Expand Up @@ -799,6 +799,11 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/mockups/universal-search-command` - Route discovered from app directory Source: `src/app/mockups/universal-search-command/page.tsx`.
- `/mockups/universal-search-redesign` - Route discovered from app directory Source: `src/app/mockups/universal-search-redesign/page.tsx`.

## Public utility route handlers

- `/auth/callback` - Authentication callback handler. Source: `src/app/auth/callback/route.ts`.
- `/icons/[variant]` - Dynamically generated application icon handler. Source: `src/app/icons/[variant]/route.tsx`.

## API routes

- `/api/answer` - Generate answer response. Source: `src/app/api/answer/route.ts`.
Expand Down Expand Up @@ -838,14 +843,10 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/api/setup-status` - Setup status. Source: `src/app/api/setup-status/route.ts`.
- `/api/upload` - Upload endpoint. Source: `src/app/api/upload/route.ts`.

## App route handlers

- `/auth/callback` - Route discovered from app directory Source: `src/app/auth/callback/route.ts`.

## 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
1 change: 1 addition & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default defineConfig({
grepInvert: mockupTag,
use: {
...devices["Desktop Chrome"],
reducedMotion: "no-preference",
...(chromiumExecutablePath ? { launchOptions: { executablePath: chromiumExecutablePath } } : {}),
},
},
Expand Down
16 changes: 11 additions & 5 deletions scripts/check-github-action-pins.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { readdirSync, readFileSync } from "node:fs";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import path from "node:path";
import { validateActionReference } from "./github-action-pins.mjs";
import { yamlBlock } from "./yaml-contract.mjs";
Expand All @@ -10,10 +10,16 @@ const failures = [];
const expectedSupabaseCliVersion = "2.108.0";
const expectedSupabaseCliVersionPattern = expectedSupabaseCliVersion.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

for (const fileName of readdirSync(workflowDir)
.filter((name) => /\.ya?ml$/i.test(name))
.sort()) {
const filePath = path.join(workflowDir, fileName);
function discoverGitHubActionFiles(workflowRoot) {
const workflowDir = path.join(workflowRoot, ".github", "workflows");
if (!existsSync(workflowDir)) return [];
return readdirSync(workflowDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name))
.map((entry) => path.join(workflowDir, entry.name));
}

for (const filePath of discoverGitHubActionFiles(process.cwd())) {
const fileName = path.relative(process.cwd(), filePath).replaceAll("\\", "/");
const lines = readFileSync(filePath, "utf8").split(/\r?\n/);

lines.forEach((line, index) => {
Expand Down
84 changes: 43 additions & 41 deletions scripts/generate-site-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
const siteMapPath = path.join(process.cwd(), "docs", "site-map.md");
const medicationSlugs = ["acamprosate"] as const;

type RouteKind = "page" | "api";
type RouteKind = "page" | "handler";

type DiscoveredRoute = {
route: string;
Expand All @@ -31,14 +31,24 @@

type SiteMapData = {
pageRoutes: DiscoveredRoute[];
publicRouteHandlers: DiscoveredRoute[];
apiRoutes: DiscoveredRoute[];
appRouteHandlers: DiscoveredRoute[];
redirects: RedirectRoute[];
nonRoutedMockupArtifacts: string[];
};

const productRouteHandlerPaths = new Set(["/applications", "/differentials/presentations", "/medications"]);

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

const routeDescriptions: Record<string, string> = {
"/": "Main Clinical KB shell.",
"/applications": "Legacy application launcher redirect to Tools.",
"/differentials": "Differentials home and search surface.",
"/differentials/diagnoses": "Diagnosis stream.",
"/differentials/diagnoses/[slug]": "Differential diagnosis detail.",
Expand Down Expand Up @@ -72,6 +82,11 @@
"/specifiers/map": "Psychiatric specifier family map.",
};

const publicRouteHandlerDescriptions: Record<string, string> = {
"/auth/callback": "Authentication callback handler.",
"/icons/[variant]": "Dynamically generated application icon handler.",
};

const apiDescriptions: Record<string, string> = {
"/api/answer": "Generate answer response.",
"/api/answer/stream": "Streaming answer response.",
Expand Down Expand Up @@ -128,8 +143,16 @@
return segment;
}

function isApiRoute(route: string) {
return route === "/api" || route.startsWith("/api/");
}

function fileToRoute(filePath: string, kind: RouteKind) {
const suffix = kind === "page" ? "page.tsx" : "route.ts";
const suffix = path.basename(filePath);
const expectedSuffixes = kind === "page" ? ["page.tsx"] : ["route.ts", "route.tsx"];
if (!expectedSuffixes.includes(suffix)) {
throw new Error(`Unsupported ${kind} route file: ${filePath}`);
}
const relative = toPosixPath(path.relative(appDir, filePath));
const withoutFile = relative.slice(0, -suffix.length).replace(/\/$/, "");
const segments = withoutFile.split("/").filter(Boolean).map(routeSegment).filter(Boolean);
Expand All @@ -150,43 +173,22 @@
}

function discoverRoutes(kind: RouteKind): DiscoveredRoute[] {
const targetFile = kind === "page" ? "page.tsx" : "route.ts";
return collectFiles(appDir, targetFile)
const targetFiles = kind === "page" ? ["page.tsx"] : ["route.ts", "route.tsx"];
return targetFiles
.flatMap((targetFile) => collectFiles(appDir, targetFile))
.map((file) => ({
route: fileToRoute(file, kind),
file: toPosixPath(path.relative(process.cwd(), file)),
}))
.sort((left, right) => left.route.localeCompare(right.route) || left.file.localeCompare(right.file));
}

function isApiRoute(route: string) {
return route === "/api" || route.startsWith("/api/");
}

function extractRedirectTarget(source: string): string | null {
const pageRedirect = source.match(/\bredirect\(\s*["']([^"']+)["']\s*\)/)?.[1];
if (pageRedirect) return pageRedirect;

const urlRedirect = source.match(/NextResponse\.redirect\(\s*new URL\(\s*["']([^"']+)["']/)?.[1];
if (urlRedirect) return urlRedirect;

const pathnameRedirect = source.match(/\.pathname\s*=\s*["']([^"']+)["']/)?.[1];
if (pathnameRedirect) return pathnameRedirect;

// Template-literal destination builders with a stable route prefix.
const presentationsRedirect = source.match(/`(\/differentials\/presentations\/)\$\{/)?.[1];
if (presentationsRedirect && source.includes("NextResponse.redirect")) {
return `${presentationsRedirect}[slug]`;
}

return null;
}

function discoverRedirects(routes: DiscoveredRoute[]): RedirectRoute[] {
return routes
.map((route) => {
const source = readFileSync(path.join(process.cwd(), route.file), "utf8");
const target = extractRedirectTarget(source);
const target =
documentedRedirectTargets[route.route] ?? source.match(/\bredirect\(\s*["']([^"']+)["']\s*\)/)?.[1];
return target ? { ...route, target } : null;
})
.filter((value): value is RedirectRoute => Boolean(value))
Expand All @@ -203,20 +205,13 @@

export function collectSiteMapData(): SiteMapData {
const pageRoutes = discoverRoutes("page");
const routeHandlers = discoverRoutes("api");
const apiRoutes = routeHandlers.filter((route) => isApiRoute(route.route));
const nonApiHandlers = routeHandlers.filter((route) => !isApiRoute(route.route));
const redirects = [...discoverRedirects(pageRoutes), ...discoverRedirects(nonApiHandlers)].sort(
(left, right) => left.route.localeCompare(right.route) || left.file.localeCompare(right.file),
);
const redirectRoutes = new Set(redirects.map((redirect) => redirect.route));
const appRouteHandlers = nonApiHandlers.filter((route) => !redirectRoutes.has(route.route));

const routeHandlers = discoverRoutes("handler");
const publicRouteHandlers = routeHandlers.filter((route) => !isApiRoute(route.route));
return {
pageRoutes,
apiRoutes,
appRouteHandlers,
redirects,
publicRouteHandlers,
apiRoutes: routeHandlers.filter((route) => isApiRoute(route.route)),
redirects: discoverRedirects([...pageRoutes, ...publicRouteHandlers]),
nonRoutedMockupArtifacts: discoverNonRoutedMockupArtifacts(),
};
}
Expand Down Expand Up @@ -397,14 +392,17 @@
].includes(route.route),
);
const mockupRoutes = data.pageRoutes.filter((route) => route.route.startsWith("/mockups"));
const publicUtilityRouteHandlers = data.publicRouteHandlers.filter(
(route) => !productRouteHandlerPaths.has(route.route),
);

const lines = [
"# Clinical KB Site Map",
"",
"This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` to verify it is current.",
"",
...section(
"Main product pages",
"Main product routes",
productRoutes.map((route) => routeLine(route, routeDescriptions)),
),
...section("Mode/query routes", renderModeRoutes()),
Expand Down Expand Up @@ -481,11 +479,15 @@
]
: []),
]),
...section(
"Public utility route handlers",
publicUtilityRouteHandlers.map((route) => routeLine(route, publicRouteHandlerDescriptions)),
),
...section(
"API routes",
data.apiRoutes.map((route) => routeLine(route, apiDescriptions)),
),
...(data.appRouteHandlers.length

Check failure on line 490 in scripts/generate-site-map.ts

View workflow job for this annotation

GitHub Actions / Unit coverage

[node] tests/site-map.test.ts > tracked sitemap > matches the generated sitemap output

TypeError: Cannot read properties of undefined (reading 'length') ❯ renderSiteMapRaw scripts/generate-site-map.ts:490:31 ❯ renderSiteMap scripts/generate-site-map.ts:526:17 ❯ tests/site-map.test.ts:52:32
? section(
"App route handlers",
data.appRouteHandlers.map((route) => routeLine(route, routeDescriptions)),
Expand Down
1 change: 0 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { headers } from "next/headers";
import { PwaLifecycle } from "@/components/pwa-lifecycle";
import { AuthProvider } from "@/lib/supabase/client";
import { WebVitalsReporter } from "@/components/web-vitals-reporter";
import { resolveMetadataBase } from "@/lib/metadata-base";
Expand Down Expand Up @@ -98,7 +97,7 @@
Skip to main content
</a>
<WebVitalsReporter />
<PwaLifecycle />

Check failure on line 100 in src/app/layout.tsx

View workflow job for this annotation

GitHub Actions / Static PR checks

'PwaLifecycle' is not defined
<AuthProvider>{children}</AuthProvider>
</body>
</html>
Expand Down
3 changes: 0 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import { appModeHomeHref, isAppModeId, isAppModeVisible, type AppModeId } from "
type HomeProps = {
searchParams?: Promise<{
mode?: string | string[];
q?: string | string[];
focus?: string | string[];
run?: string | string[];
}>;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1581,13 +1581,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 Down
17 changes: 7 additions & 10 deletions src/components/forms/forms-home-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
FileQuestion,
FileText,
Loader2,
Route,
Search,
ShieldAlert,
ShieldCheck,
Expand All @@ -30,22 +29,22 @@ import { countVerifiedRegistryRecords, useRegistryRecords } from "@/lib/use-regi
const taskCards: ModeHomeAction[] = [
{
title: "Find a form",
description: "Number, pathway, clock, keyword.",
description: "Title, purpose, or workflow detail.",
icon: Search,
href: appModeHomeHref("forms", { focus: true }),
},
{
title: "Readiness checks",
description: "Maker, clock, copies, source.",
description: "Review status, source, and local confirmation.",
icon: ClipboardCheck,
href: `/forms/${defaultFormSlug() ?? ""}`,
},
{
title: "Browse pathways",
description: "Before, current, parallel, after.",
icon: Route,
title: "Check source status",
description: "Find records that still need local confirmation.",
icon: ShieldAlert,
href: appModeHomeHref("forms", {
query: "forms pathway before current parallel after",
query: "local confirmation required",
focus: true,
run: true,
}),
Expand Down Expand Up @@ -91,9 +90,7 @@ export function FormsHomePage() {
<ModeHomeStatusNotice
icon={ShieldAlert}
title="Session expired"
body="Your session expired. Sign in again to open private form records and pathways."
actionHref="/"
actionLabel="Open account setup"
body="Your session expired. Use the account control in the header to sign in again."
/>
) : registry.status === "error" ? (
<ModeHomeStatusNotice
Expand Down
Loading
Loading