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
11 changes: 6 additions & 5 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,10 +843,6 @@ 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`.
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
93 changes: 44 additions & 49 deletions scripts/generate-site-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const appDir = path.join(process.cwd(), "src", "app");
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,23 @@ type RedirectRoute = {

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/[slug]",
"/medications": "/?mode=prescribing",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

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 +81,11 @@ const routeDescriptions: Record<string, string> = {
"/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 +142,16 @@ function routeSegment(segment: string) {
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 +172,22 @@ function collectFiles(root: string, targetFileName: string): string[] {
}

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 +204,13 @@ function discoverNonRoutedMockupArtifacts() {

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 +391,17 @@ function renderSiteMapRaw(data = collectSiteMapData()) {
].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,23 +478,21 @@ function renderSiteMapRaw(data = collectSiteMapData()) {
]
: []),
]),
...section(
"Public utility route handlers",
publicUtilityRouteHandlers.map((route) => routeLine(route, publicRouteHandlerDescriptions)),
),
...section(
"API routes",
data.apiRoutes.map((route) => routeLine(route, apiDescriptions)),
),
...(data.appRouteHandlers.length
? section(
"App route handlers",
data.appRouteHandlers.map((route) => routeLine(route, routeDescriptions)),
)
: []),
...section(
"Redirects",
data.redirects.length
? data.redirects.map((redirect) =>
bullet(redirect.route, `Redirects to \`${redirect.target}\`. Source: \`${redirect.file}\`.`),
)
: ["- No redirects discovered."],
: ["- No page-level redirects discovered."],
),
...section("Known caveats and stale-path flags", [
"- `/mockups/*` prototype routes are development-only; production returns 404 and `robots.txt` disallows indexing.",
Expand Down
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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 { PwaLifecycle } from "@/components/pwa-lifecycle";
import { WebVitalsReporter } from "@/components/web-vitals-reporter";
import { resolveMetadataBase } from "@/lib/metadata-base";
import { APP_THEME_COLORS } from "@/lib/theme";
Expand Down
7 changes: 1 addition & 6 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import { HomePageClient } from "@/app/home-page-client";
import { appModeHomeHref, isAppModeId, isAppModeVisible, type AppModeId } from "@/lib/app-modes";

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

function firstSearchParam(value: string | string[] | undefined) {
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