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
16 changes: 10 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 All @@ -29,6 +29,9 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/specifiers/map` - Psychiatric specifier family map. Source: `src/app/specifiers/map/page.tsx`.
- `/therapy-compass` - Route discovered from app directory Source: `src/app/therapy-compass/page.tsx`.
- `/tools` - Route discovered from app directory Source: `src/app/tools/page.tsx`.
- `/applications` - Legacy application launcher redirect to Tools. Source: `src/app/applications/route.ts`.
- `/differentials/presentations` - Presentation workflow stream. Source: `src/app/differentials/presentations/route.ts`.
- `/medications` - Medication index redirect. Source: `src/app/medications/route.ts`.

## Mode/query routes

Expand Down Expand Up @@ -799,6 +802,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 +846,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"],
contextOptions: { 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
102 changes: 50 additions & 52 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,24 @@ type RedirectRoute = {

type SiteMapData = {
pageRoutes: DiscoveredRoute[];
apiRoutes: DiscoveredRoute[];
appRouteHandlers: DiscoveredRoute[];
publicRouteHandlers: DiscoveredRoute[];
apiRoutes: 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 @@ 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 +143,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 +173,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 +205,14 @@ 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,
appRouteHandlers: routeHandlers,
publicRouteHandlers,
apiRoutes: routeHandlers.filter((route) => isApiRoute(route.route)),
redirects: discoverRedirects([...pageRoutes, ...publicRouteHandlers]),
nonRoutedMockupArtifacts: discoverNonRoutedMockupArtifacts(),
};
}
Expand Down Expand Up @@ -397,16 +393,20 @@ 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 productRouteHandlers = 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",
productRoutes.map((route) => routeLine(route, routeDescriptions)),
),
...section("Main product routes", [
...productRoutes.map((route) => routeLine(route, routeDescriptions)),
...productRouteHandlers.map((route) => routeLine(route, routeDescriptions)),
]),
...section("Mode/query routes", renderModeRoutes()),
...section("Mode page index", renderModePageIndex()),
...section("Documents flow index", renderDocumentFlowIndex()),
Expand Down Expand Up @@ -481,23 +481,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,9 +1,9 @@
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 { PwaLifecycle } from "@/components/pwa-lifecycle";
import { resolveMetadataBase } from "@/lib/metadata-base";
import { APP_THEME_COLORS } from "@/lib/theme";
import "./globals.css";
Expand Down
Loading
Loading