Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions .gitleaksignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,3 +8,5 @@
# generic-api-key rule on historical commit content scanned in PR history.
27121756b10df6d93841ebf48b0f1991e0144a1f:data/medications-snapshot.json:generic-api-key:22253
27121756b10df6d93841ebf48b0f1991e0144a1f:data/medications-snapshot.json:generic-api-key:47690
b65acddaa13e69e0ad2a49783116056b3a1a3639:data/medications-snapshot.json:generic-api-key:21520
b65acddaa13e69e0ad2a49783116056b3a1a3639:data/medications-snapshot.json:generic-api-key:46330
6 changes: 3 additions & 3 deletions docs/site-map.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
## Main product pages

- `/` - Main Clinical KB shell. Source: `src/app/page.tsx`.
- `/applications` - Application and tool launcher. Source: `src/app/applications/page.tsx`.
- `/applications` - Route discovered from app directory Source: `src/app/applications/page.tsx`.
- `/differentials` - Differentials home and search surface. Source: `src/app/differentials/page.tsx`.
- `/differentials/diagnoses` - Diagnosis stream. Source: `src/app/differentials/diagnoses/page.tsx`.
- `/differentials/presentations` - Presentation workflow stream. Source: `src/app/differentials/presentations/page.tsx`.
Expand DownExpand Up@@ -40,7 +40,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
| 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` | `/applications` launcher and tool detail panels inside tools 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

Expand DownExpand Up@@ -601,5 +601,5 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
| Differentials | `src/app/differentials, src/lib/differentials.ts` |
| Medications | `src/app/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx` |
| Documents | `src/app/documents, src/lib/document-flow-routes.ts` |
| Applications and tools | `src/app/applications, src/components/applications-launcher-page.tsx` |
| Tools | `src/components/applications-launcher-page.tsx` |
| Mockups | `src/app/mockups` |
16 changes: 15 additions & 1 deletion scripts/eval-quality.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ type EvalQualityArgs = {
retrievalOnly: boolean;
ragOnly: boolean;
skipPreflight: boolean;
forceEmbedding: boolean;
};

export type RagQualityResult = {
Expand DownExpand Up@@ -150,6 +151,7 @@ function parseArgs(argv: string[]): EvalQualityArgs {
retrievalOnly: false,
ragOnly: false,
skipPreflight: false,
forceEmbedding: false,
};

for (let index = 0; index < argv.length; index += 1) {
Expand All@@ -176,6 +178,10 @@ function parseArgs(argv: string[]): EvalQualityArgs {
args.skipPreflight = true;
continue;
}
if (token === "--force-embedding") {
args.forceEmbedding = true;
continue;
}

const value = argv[index + 1];
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${token}`);
Expand DownExpand Up@@ -476,6 +482,11 @@ export function buildEvalQualityReport(args: {
`retrieval content_recall_at_5 ${retrievalSummary.content_recall_at_5} below ${qualityThresholds.retrievalContentRecallAt5}`,
);
}
if (retrievalSummary.force_embedding_failure_count > 0) {
thresholdFailures.push(
`retrieval force_embedding_failure_count ${retrievalSummary.force_embedding_failure_count} above 0`,
);
}
if (governance.stale_rate > qualityThresholds.staleTopResultRate) {
thresholdFailures.push(
`top-result stale_rate ${governance.stale_rate} above ${qualityThresholds.staleTopResultRate}`,
Expand DownExpand Up@@ -664,6 +675,8 @@ ${markdownTable([
## Retrieval Decision Metrics

${markdownTable([
["Force-embedding cases", retrieval.force_embedding_case_count],
["Force-embedding failures", retrieval.force_embedding_failure_count],
["Embedding skipped rate", retrieval.embedding_skipped_rate],
["Median text candidate budget", retrieval.median_text_candidate_budget],
["Second-stage rerank rate", retrieval.second_stage_rerank_rate],
Expand DownExpand Up@@ -728,6 +741,7 @@ async function runRetrievalQualityCases(args: {
ownerId?: string;
limit?: number;
query?: string;
forceEmbedding?: boolean;
supabase: Awaited<ReturnType<typeof loadAdminClient>>;
}) {
const [{ searchChunksWithTelemetry }, capturedCases] = await Promise.all([
Expand DownExpand Up@@ -756,7 +770,7 @@ async function runRetrievalQualityCases(args: {
topK: retrievalLimitForGoldenCase(testCase),
minSimilarity: 0.12,
skipCache: true,
forceEmbedding: testCase.forceEmbedding,
forceEmbedding: testCase.forceEmbedding || args.forceEmbedding,
}),
);
const latencyMs =
Expand Down
24 changes: 24 additions & 0 deletions scripts/eval-retrieval.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,6 +47,7 @@ type EvalArgs = {
export type GoldenRetrievalResult = {
id: string;
query: string;
forceEmbedding: boolean;
expectedQueryClass: string;
actualQueryClass: string | null;
expectedDocumentSubstrings: string[];
Expand DownExpand Up@@ -516,6 +517,11 @@ export function evaluateGoldenRetrievalCase(args: {
const tableEvidenceFoundAtK = hasTableEvidence(args.results, topK);
const actualQueryClass = args.telemetry.query_class ?? null;
const failures: string[] = [];
const vectorLayerCount = Object.entries(args.telemetry.retrieval_layer_counts ?? {}).reduce(
(sum, [layer, count]) =>
["embedding_fields", "index_units", "hybrid_vector", "vector_fallback"].includes(layer) ? sum + count : sum,
0,
);
const hitAtK =
documentHitsAtK.missing.length === 0 &&
contentHitsAtK.missing.length === 0 &&
Expand All@@ -533,10 +539,23 @@ export function evaluateGoldenRetrievalCase(args: {
if (args.testCase.expectTableEvidence && !tableEvidenceFound) {
failures.push("expected table evidence in top 5");
}
if (args.testCase.forceEmbedding) {
if (args.telemetry.embedding_skipped) failures.push("forceEmbedding expected embedding to run");
if (args.telemetry.retrieval_strategy === "search_cache") failures.push("forceEmbedding served search cache");
if (
args.telemetry.retrieval_strategy === "text_fast_path" ||
args.telemetry.retrieval_strategy === "document_lookup_fast_path"
) {
failures.push(`forceEmbedding returned lexical strategy ${args.telemetry.retrieval_strategy}`);
}
if (args.telemetry.coverage_gate_decision === "accepted") failures.push("forceEmbedding returned coverage gate");
if (vectorLayerCount <= 0) failures.push("forceEmbedding found no vector-layer candidates");
}

return {
id: args.testCase.id,
query: args.testCase.query,
forceEmbedding: args.testCase.forceEmbedding ?? false,
expectedQueryClass: args.testCase.expectedQueryClass,
actualQueryClass,
expectedDocumentSubstrings: args.testCase.expectedDocumentSubstrings,
Expand DownExpand Up@@ -612,6 +631,7 @@ export function summarizeGoldenRetrievalResults(results: GoldenRetrievalResult[]
}
return counts;
}, {});
const forceEmbeddingResults = results.filter((result) => result.forceEmbedding);
return {
case_count: results.length,
document_recall_at_5: Number(
Expand DownExpand Up@@ -647,6 +667,8 @@ export function summarizeGoldenRetrievalResults(results: GoldenRetrievalResult[]
embedding_skip_reason_counts: embeddingSkipReasonCounts,
text_fast_path_reason_counts: textFastPathReasonCounts,
retrieval_layer_counts: layerCounts,
force_embedding_case_count: forceEmbeddingResults.length,
force_embedding_failure_count: forceEmbeddingResults.filter((result) => result.failures.length > 0).length,
median_text_candidate_budget: percentile(textCandidateBudgets, 50),
second_stage_rerank_rate: Number(
(results.filter((result) => result.secondStageRerankUsed).length / Math.max(results.length, 1)).toFixed(4),
Expand DownExpand Up@@ -723,6 +745,8 @@ function printHumanSummary(summary: ReturnType<typeof summarizeGoldenRetrievalRe
console.log(` retrieval_strategy_counts=${JSON.stringify(summary.retrieval_strategy_counts)}`);
console.log(` retrieval_plan_counts=${JSON.stringify(summary.retrieval_plan_counts)}`);
console.log(` retrieval_layer_counts=${JSON.stringify(summary.retrieval_layer_counts)}`);
console.log(` force_embedding_case_count=${summary.force_embedding_case_count}`);
console.log(` force_embedding_failure_count=${summary.force_embedding_failure_count}`);
console.log(` embedding_skipped_rate=${summary.embedding_skipped_rate}`);
console.log(` embedding_skip_reason_counts=${JSON.stringify(summary.embedding_skip_reason_counts)}`);
console.log(` text_fast_path_reason_counts=${JSON.stringify(summary.text_fast_path_reason_counts)}`);
Expand Down
5 changes: 2 additions & 3 deletions scripts/generate-site-map.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,7 +40,6 @@ type SiteMapData = {

const routeDescriptions: Record<string, string> = {
"/": "Main Clinical KB shell.",
"/applications": "Application and tool launcher.",
"/differentials": "Differentials home and search surface.",
"/differentials/diagnoses": "Diagnosis stream.",
"/differentials/diagnoses/[slug]": "Differential diagnosis detail.",
Expand DownExpand Up@@ -97,7 +96,7 @@ const routeOwnershipRows = [
["Differentials", "src/app/differentials, src/lib/differentials.ts"],
["Medications", "src/app/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx"],
["Documents", "src/app/documents, src/lib/document-flow-routes.ts"],
["Applications and tools", "src/app/applications, src/components/applications-launcher-page.tsx"],
["Tools", "src/components/applications-launcher-page.tsx"],
["Mockups", "src/app/mockups"],
] as const;

Expand DownExpand Up@@ -279,7 +278,7 @@ function renderModePageIndex() {
mode: "Tools",
home: appModeHomeHref("tools"),
search: appModeHomeHref("tools", { query: "medications", focus: true, run: true }),
detail: "`/applications` launcher and tool detail panels inside tools mode.",
detail: "Tool launcher and detail panels inside dashboard tools mode (`/?mode=tools`).",
},
]);
}
Expand Down
13 changes: 6 additions & 7 deletions src/app/api/documents/[id]/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@ import { invalidateRagCachesForDocumentMutation } from "@/lib/rag";
import { committedIndexGeneration, isCommittedGenerationMetadata } from "@/lib/reindex-pipeline";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth";
import { publicAccessContext, withOwnerReadScope } from "@/lib/public-api-access";
import { writeAuditLog } from "@/lib/audit";
import { parseJsonBody } from "@/lib/validation/body";
import { parseRouteParams } from "@/lib/validation/params";
Expand DownExpand Up@@ -280,13 +281,11 @@ export async function GET(request: Request, { params }: { params: Promise<{ id:

const { id } = parseRouteParams({ id: rawId }, documentRouteParamsSchema, "Invalid document id.");
const supabase = createAdminClient();
const user = await requireAuthenticatedUser(request, supabase);
const { data: document, error } = await supabase
.from("documents")
.select("*")
.eq("id", id)
.eq("owner_id", user.id)
.maybeSingle();
const access = await publicAccessContext(request, supabase);
const { data: document, error } = await withOwnerReadScope(
supabase.from("documents").select("*").eq("id", id),
access.ownerId,
).maybeSingle();

if (error) throw new Error(error.message);
if (!document) return NextResponse.json({ error: "Document not found." }, { status: 404 });
Expand Down
17 changes: 8 additions & 9 deletions src/app/api/documents/[id]/search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,8 @@ import { isDemoMode } from "@/lib/env";
import { jsonError } from "@/lib/http";
import { committedIndexGeneration, isCommittedGenerationMetadata } from "@/lib/reindex-pipeline";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth";
import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth";
import { publicAccessContext, withOwnerReadScope } from "@/lib/public-api-access";
import { parseRouteParams } from "@/lib/validation/params";
import { parseRequestQuery, queryInteger } from "@/lib/validation/query";

Expand DownExpand Up@@ -181,13 +182,11 @@ export async function GET(request: Request, { params }: { params: Promise<{ id:

const { id } = parseRouteParams({ id: rawId }, documentSearchParamsSchema, "Invalid document id.");
const supabase = createAdminClient();
const user = await requireAuthenticatedUser(request, supabase);
const { data: document, error: documentError } = await supabase
.from("documents")
.select("id,metadata")
.eq("id", id)
.eq("owner_id", user.id)
.maybeSingle();
const access = await publicAccessContext(request, supabase);
const { data: document, error: documentError } = await withOwnerReadScope(
supabase.from("documents").select("id,metadata").eq("id", id),
access.ownerId,
).maybeSingle();

if (documentError) throw new Error(documentError.message);
if (!document) return NextResponse.json({ error: "Document not found." }, { status: 404 });
Expand All@@ -197,7 +196,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ id:
p_document_id: id,
p_query: query,
match_count: limit,
p_owner_id: user.id,
p_owner_id: access.ownerId,
});

if (!rpcError) {
Expand Down
15 changes: 7 additions & 8 deletions src/app/api/documents/[id]/signed-url/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,8 @@ import { env } from "@/lib/env";
import { isDemoMode } from "@/lib/env";
import { jsonError, PublicApiError } from "@/lib/http";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth";
import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth";
import { publicAccessContext, withOwnerReadScope } from "@/lib/public-api-access";

export const runtime = "nodejs";

Expand All@@ -31,13 +32,11 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id:
if (!routeIdSchema.safeParse(id).success) throw new PublicApiError("Invalid document id.");

const supabase = createAdminClient();
const user = await requireAuthenticatedUser(_request, supabase);
const { data: document, error } = await supabase
.from("documents")
.select("storage_path,file_type")
.eq("id", id)
.eq("owner_id", user.id)
.maybeSingle();
const access = await publicAccessContext(_request, supabase);
const { data: document, error } = await withOwnerReadScope(
supabase.from("documents").select("storage_path,file_type").eq("id", id),
access.ownerId,
).maybeSingle();

if (error) throw new Error(error.message);
if (!document) return NextResponse.json({ error: "Document not found." }, { status: 404 });
Expand Down
13 changes: 7 additions & 6 deletions src/app/api/documents/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,8 @@ import { demoDocuments } from "@/lib/demo-data";
import { isDemoMode } from "@/lib/env";
import { jsonError } from "@/lib/http";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth";
import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth";
import { publicAccessContext, withOwnerReadScope } from "@/lib/public-api-access";
import { parseRequestQuery, queryBoolean, queryInteger } from "@/lib/validation/query";

export const runtime = "nodejs";
Expand DownExpand Up@@ -130,11 +131,11 @@ export async function GET(request: Request) {
} = parseRequestQuery(request, documentListQuerySchema, "Invalid document list query.");

const supabase = createAdminClient();
const user = await requireAuthenticatedUser(request, supabase);
let query = supabase
.from("documents")
.select(DOCUMENT_LIST_COLUMNS, { count: "exact" })
.eq("owner_id", user.id)
const access = await publicAccessContext(request, supabase);
let query = withOwnerReadScope(
supabase.from("documents").select(DOCUMENT_LIST_COLUMNS, { count: "exact" }),
access.ownerId,
)
.order("created_at", { ascending: false })
.range(offset, offset + limit - 1);

Expand Down
15 changes: 7 additions & 8 deletions src/app/api/images/[id]/signed-url/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,8 @@ import { isDemoMode } from "@/lib/env";
import { jsonError, PublicApiError } from "@/lib/http";
import { committedIndexGeneration, isCommittedGenerationMetadata } from "@/lib/reindex-pipeline";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth";
import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth";
import { publicAccessContext, withOwnerReadScope } from "@/lib/public-api-access";

export const runtime = "nodejs";

Expand All@@ -31,7 +32,7 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id:
if (!routeIdSchema.safeParse(id).success) throw new PublicApiError("Invalid image id.");

const supabase = createAdminClient();
const user = await requireAuthenticatedUser(_request, supabase);
const access = await publicAccessContext(_request, supabase);
const { data: image, error } = await supabase
.from("document_images")
.select("document_id,storage_path,mime_type,caption,metadata")
Expand All@@ -41,12 +42,10 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id:
if (error) throw new Error(error.message);
if (!image) return NextResponse.json({ error: "Image not found." }, { status: 404 });

const { data: document, error: documentError } = await supabase
.from("documents")
.select("id,metadata")
.eq("id", image.document_id)
.eq("owner_id", user.id)
.maybeSingle();
const { data: document, error: documentError } = await withOwnerReadScope(
supabase.from("documents").select("id,metadata").eq("id", image.document_id),
access.ownerId,
).maybeSingle();

if (documentError) throw new Error(documentError.message);
if (!document) return NextResponse.json({ error: "Image not found." }, { status: 404 });
Expand Down
11 changes: 10 additions & 1 deletion src/app/api/registry/records/[slug]/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { z } from "zod";
import { consumeSubjectApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit";
import { isDemoMode, isLocalNoAuthMode } from "@/lib/env";
import { jsonError } from "@/lib/http";
import { publicAccessContext } from "@/lib/public-api-access";
import { hasPublicApiAuthSignal, publicAccessContext } from "@/lib/public-api-access";
import { getFormRecord } from "@/lib/forms";
import {
deriveGovernanceColumns,
Expand DownExpand Up@@ -62,6 +62,15 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
});
}

if (!hasPublicApiAuthSignal(request)) {
const payload = publicRegistryDetailPayload(kind, normalizedSlug);
if (!payload) return notFoundResponse(normalizedSlug);
return registryResponse({
...payload,
publicAccess: true,
});
}

const supabase = createAdminClient();
const access = await publicAccessContext(request, supabase);

Expand Down
Loading