diff --git a/.gitleaksignore b/.gitleaksignore
index 8b3bd8187..16fe1ab80 100644
--- a/.gitleaksignore
+++ b/.gitleaksignore
@@ -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
diff --git a/package.json b/package.json
index c38c76383..aaaeefd4a 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,8 @@
"reindex:cleanup-staged": "tsx scripts/cleanup-abandoned-reindex-generations.ts",
"supabase:recovery-status": "tsx scripts/supabase-recovery-status.ts",
"promote:query-misses": "tsx scripts/promote-query-misses.ts",
+ "promote:public-documents": "tsx scripts/promote-public-documents.ts",
+ "promote:public-documents:batch": "tsx scripts/promote-public-documents-batch.ts",
"eval:rag": "node scripts/run-eval-safe.mjs scripts/eval-rag.ts",
"eval:answer-quality": "node scripts/run-eval-safe.mjs scripts/eval-answer-quality.ts",
"eval:quality": "node scripts/run-eval-safe.mjs scripts/eval-quality.ts",
diff --git a/scripts/eval-quality.ts b/scripts/eval-quality.ts
index 0bde64faf..0c1749ceb 100644
--- a/scripts/eval-quality.ts
+++ b/scripts/eval-quality.ts
@@ -45,6 +45,7 @@ type EvalQualityArgs = {
retrievalOnly: boolean;
ragOnly: boolean;
skipPreflight: boolean;
+ forceEmbedding: boolean;
};
export type RagQualityResult = {
@@ -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) {
@@ -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}`);
@@ -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}`,
@@ -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],
@@ -728,6 +741,7 @@ async function runRetrievalQualityCases(args: {
ownerId?: string;
limit?: number;
query?: string;
+ forceEmbedding?: boolean;
supabase: Awaited>;
}) {
const [{ searchChunksWithTelemetry }, capturedCases] = await Promise.all([
@@ -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 =
diff --git a/scripts/eval-retrieval.ts b/scripts/eval-retrieval.ts
index edeb89564..2a46d96f2 100644
--- a/scripts/eval-retrieval.ts
+++ b/scripts/eval-retrieval.ts
@@ -47,6 +47,7 @@ type EvalArgs = {
export type GoldenRetrievalResult = {
id: string;
query: string;
+ forceEmbedding: boolean;
expectedQueryClass: string;
actualQueryClass: string | null;
expectedDocumentSubstrings: string[];
@@ -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 &&
@@ -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,
@@ -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(
@@ -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),
@@ -723,6 +745,8 @@ function printHumanSummary(summary: ReturnType>) {
+ const { count, error } = await supabase
+ .from("documents")
+ .select("id", { count: "exact", head: true })
+ .eq("status", "indexed")
+ .is("owner_id", null);
+ if (error) throw new Error(error.message);
+ return count ?? 0;
+}
+
+async function fetchBatchIds(supabase: Awaited>) {
+ const { data, error } = await supabase
+ .from("documents")
+ .select("id")
+ .eq("status", "indexed")
+ .not("owner_id", "is", null)
+ .in("metadata->>clinical_validation_status", ["locally_reviewed", "approved"])
+ .limit(BATCH_SIZE);
+ if (error) throw new Error(error.message);
+ return (data ?? []).map((row) => row.id as string);
+}
+
+async function promoteBatch(supabase: Awaited>, ids: string[]) {
+ if (ids.length === 0) return;
+
+ const { error } = await supabase
+ .from("documents")
+ .update({
+ owner_id: null,
+ updated_at: new Date().toISOString(),
+ })
+ .in("id", ids);
+ if (error) throw new Error(error.message);
+}
+
+async function main() {
+ const supabase = await loadAdminClient();
+ let batches = 0;
+
+ while (true) {
+ const ids = await fetchBatchIds(supabase);
+ if (ids.length === 0) break;
+ await promoteBatch(supabase, ids);
+ batches += 1;
+ const publicCount = await countPublic(supabase);
+ console.log(
+ `[public-documents:promote] batch ${batches}: promoted ${ids.length}; public indexed total ${publicCount}`,
+ );
+ }
+
+ console.log(`[public-documents:promote] complete. indexed public documents: ${await countPublic(supabase)}`);
+}
+
+main().catch((error: unknown) => {
+ console.error(error instanceof Error ? error.message : error);
+ process.exit(1);
+});
diff --git a/scripts/promote-public-documents.ts b/scripts/promote-public-documents.ts
new file mode 100644
index 000000000..e7b1b606d
--- /dev/null
+++ b/scripts/promote-public-documents.ts
@@ -0,0 +1,80 @@
+import { loadEnvConfig } from "@next/env";
+
+loadEnvConfig(process.cwd());
+
+type PromoteArgs = {
+ ownerId?: string;
+};
+
+async function loadAdminClient() {
+ const { createAdminClient } = await import("@/lib/supabase/admin");
+ return createAdminClient();
+}
+
+function parseArgs(argv: string[]): PromoteArgs {
+ const args: PromoteArgs = {
+ ownerId: process.env.PUBLIC_WORKSPACE_OWNER_ID ?? process.env.LOCAL_NO_AUTH_OWNER_ID,
+ };
+
+ for (let index = 0; index < argv.length; index += 1) {
+ const token = argv[index];
+ if (token === "--owner-id") {
+ args.ownerId = argv[index + 1];
+ index += 1;
+ continue;
+ }
+ throw new Error(`Unknown argument: ${token}`);
+ }
+
+ return args;
+}
+
+async function countCandidates(supabase: Awaited>, ownerId?: string) {
+ let query = supabase
+ .from("documents")
+ .select("id", { count: "exact", head: true })
+ .eq("status", "indexed")
+ .not("owner_id", "is", null)
+ .in("metadata->>clinical_validation_status", ["locally_reviewed", "approved"]);
+
+ if (ownerId) query = query.eq("owner_id", ownerId);
+
+ const { count, error } = await query;
+ if (error) throw new Error(error.message);
+ return count ?? 0;
+}
+
+async function countPublic(supabase: Awaited>) {
+ const { count, error } = await supabase
+ .from("documents")
+ .select("id", { count: "exact", head: true })
+ .eq("status", "indexed")
+ .is("owner_id", null);
+
+ if (error) throw new Error(error.message);
+ return count ?? 0;
+}
+
+async function main() {
+ const args = parseArgs(process.argv.slice(2));
+ const supabase = await loadAdminClient();
+
+ const [candidateCount, publicCount] = await Promise.all([
+ countCandidates(supabase, args.ownerId),
+ countPublic(supabase),
+ ]);
+
+ console.log("[public-documents:promote] indexed public documents:", publicCount);
+ console.log(
+ `[public-documents:promote] pending promotion${args.ownerId ? ` for owner ${args.ownerId}` : ""}:`,
+ candidateCount,
+ );
+ console.log(
+ "Apply migration 20260705220000_promote_locally_reviewed_documents_public.sql with `npx supabase db push --linked`.",
+ );
+}
+
+main().catch((error: unknown) => {
+ console.error(error instanceof Error ? error.message : error);
+ process.exit(1);
+});
diff --git a/src/app/api/answer/route.ts b/src/app/api/answer/route.ts
index 8d4f3b2a9..55baf040b 100644
--- a/src/app/api/answer/route.ts
+++ b/src/app/api/answer/route.ts
@@ -4,7 +4,11 @@ import { demoAnswer } from "@/lib/demo-data";
import { isDemoMode, isLocalNoAuthMode } from "@/lib/env";
import { answerQuestionWithScope } from "@/lib/rag";
import { jsonError, PublicApiError } from "@/lib/http";
-import { consumeSubjectApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit";
+import {
+ allowRateLimitInMemoryFallbackOnUnavailable,
+ consumeSubjectApiRateLimit,
+ rateLimitJsonResponse,
+} from "@/lib/api-rate-limit";
import { publicAccessContext } from "@/lib/public-api-access";
import { classifyRagQuery } from "@/lib/clinical-search";
import { buildSmartRagApiPlan } from "@/lib/smart-rag-api";
@@ -70,7 +74,7 @@ export async function POST(request: Request) {
supabase,
subject: access.rateLimitSubject,
bucket: "answer",
- allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
+ allowInMemoryFallbackOnUnavailable: allowRateLimitInMemoryFallbackOnUnavailable(),
});
if (rateLimit.limited) {
return rateLimitJsonResponse("Too many answer requests. Retry shortly.", rateLimit);
diff --git a/src/app/api/answer/stream/route.ts b/src/app/api/answer/stream/route.ts
index 4741c2287..b8509d102 100644
--- a/src/app/api/answer/stream/route.ts
+++ b/src/app/api/answer/stream/route.ts
@@ -2,7 +2,11 @@ import { z } from "zod";
import { demoAnswer } from "@/lib/demo-data";
import { isDemoMode, isLocalNoAuthMode } from "@/lib/env";
import { PublicApiError, jsonError } from "@/lib/http";
-import { consumeSubjectApiRateLimit, type ApiRateLimitResult } from "@/lib/api-rate-limit";
+import {
+ allowRateLimitInMemoryFallbackOnUnavailable,
+ consumeSubjectApiRateLimit,
+ type ApiRateLimitResult,
+} from "@/lib/api-rate-limit";
import { publicAccessContext } from "@/lib/public-api-access";
import { answerQuestionWithScope, type AnswerProgressEvent } from "@/lib/rag";
import { classifyRagQuery } from "@/lib/clinical-search";
@@ -232,7 +236,7 @@ export async function POST(request: Request) {
supabase,
subject: access.rateLimitSubject,
bucket: "answer",
- allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
+ allowInMemoryFallbackOnUnavailable: allowRateLimitInMemoryFallbackOnUnavailable(),
});
if (rateLimit.limited) return rateLimitStream(rateLimit);
diff --git a/src/app/api/differentials/[slug]/route.ts b/src/app/api/differentials/[slug]/route.ts
index eaed5d200..abe35bbb2 100644
--- a/src/app/api/differentials/[slug]/route.ts
+++ b/src/app/api/differentials/[slug]/route.ts
@@ -1,7 +1,11 @@
import { NextResponse } from "next/server";
import { z } from "zod";
-import { consumeSubjectApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit";
+import {
+ allowRateLimitInMemoryFallbackOnUnavailable,
+ consumeSubjectApiRateLimit,
+ rateLimitJsonResponse,
+} from "@/lib/api-rate-limit";
import {
deriveGovernanceFromSnapshot,
normalizeDifferentialSlug,
@@ -14,7 +18,7 @@ import { ensureDifferentialsSeeded, loadDifferentialSnapshot } from "@/lib/diffe
import { getDifferentialRecord, getPresentationWorkflow } from "@/lib/differentials";
import { isDemoMode, isLocalNoAuthMode } from "@/lib/env";
import { jsonError } from "@/lib/http";
-import { hasPublicApiAuthSignal, publicAccessContext } from "@/lib/public-api-access";
+import { publicAccessContext, shouldResolvePublicCatalogAccess } from "@/lib/public-api-access";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth";
import { parseRequestQuery } from "@/lib/validation/query";
@@ -63,7 +67,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
});
}
- if (!hasPublicApiAuthSignal(request)) {
+ if (!shouldResolvePublicCatalogAccess(request)) {
const snapshot = loadDifferentialSnapshot();
const governance = deriveGovernanceFromSnapshot(snapshot);
if (kind === "presentation") {
@@ -91,7 +95,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
supabase,
subject: access.rateLimitSubject,
bucket: "registry",
- allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
+ allowInMemoryFallbackOnUnavailable: allowRateLimitInMemoryFallbackOnUnavailable(),
});
if (rateLimit.limited) {
return rateLimitJsonResponse("Differential requests are rate limited. Try again shortly.", rateLimit);
diff --git a/src/app/api/differentials/presentations/[slug]/route.ts b/src/app/api/differentials/presentations/[slug]/route.ts
index 239579032..85574d8fe 100644
--- a/src/app/api/differentials/presentations/[slug]/route.ts
+++ b/src/app/api/differentials/presentations/[slug]/route.ts
@@ -1,6 +1,10 @@
import { NextResponse } from "next/server";
-import { consumeSubjectApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit";
+import {
+ allowRateLimitInMemoryFallbackOnUnavailable,
+ consumeSubjectApiRateLimit,
+ rateLimitJsonResponse,
+} from "@/lib/api-rate-limit";
import {
deriveGovernanceFromSnapshot,
normalizeDifferentialSlug,
@@ -13,7 +17,7 @@ import { ensureDifferentialsSeeded, loadDifferentialSnapshot } from "@/lib/diffe
import { getDifferentialRecord, getPresentationWorkflow } from "@/lib/differentials";
import { isDemoMode, isLocalNoAuthMode } from "@/lib/env";
import { jsonError } from "@/lib/http";
-import { hasPublicApiAuthSignal, publicAccessContext } from "@/lib/public-api-access";
+import { publicAccessContext, shouldResolvePublicCatalogAccess } from "@/lib/public-api-access";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth";
@@ -53,7 +57,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
});
}
- if (!hasPublicApiAuthSignal(request)) {
+ if (!shouldResolvePublicCatalogAccess(request)) {
const snapshot = loadDifferentialSnapshot();
const workflow = getPresentationWorkflow(normalizedSlug);
if (!workflow) return notFoundResponse(normalizedSlug);
@@ -78,7 +82,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
supabase,
subject: access.rateLimitSubject,
bucket: "registry",
- allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
+ allowInMemoryFallbackOnUnavailable: allowRateLimitInMemoryFallbackOnUnavailable(),
});
if (rateLimit.limited) {
return rateLimitJsonResponse("Differential requests are rate limited. Try again shortly.", rateLimit);
diff --git a/src/app/api/differentials/route.ts b/src/app/api/differentials/route.ts
index 260e2a90a..2b467a47f 100644
--- a/src/app/api/differentials/route.ts
+++ b/src/app/api/differentials/route.ts
@@ -1,7 +1,11 @@
import { NextResponse } from "next/server";
import { z } from "zod";
-import { consumeSubjectApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit";
+import {
+ allowRateLimitInMemoryFallbackOnUnavailable,
+ consumeSubjectApiRateLimit,
+ rateLimitJsonResponse,
+} from "@/lib/api-rate-limit";
import {
deriveGovernanceFromSnapshot,
rowGovernance,
@@ -14,7 +18,7 @@ import { ensureDifferentialsSeeded, loadDifferentialSnapshot } from "@/lib/diffe
import { differentialRecords, searchDifferentialRecords, searchPresentationWorkflows } from "@/lib/differentials";
import { isDemoMode, isLocalNoAuthMode } from "@/lib/env";
import { jsonError } from "@/lib/http";
-import { hasPublicApiAuthSignal, publicAccessContext } from "@/lib/public-api-access";
+import { publicAccessContext, shouldResolvePublicCatalogAccess } from "@/lib/public-api-access";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth";
import { parseRequestQuery, queryInteger } from "@/lib/validation/query";
@@ -68,7 +72,7 @@ export async function GET(request: Request) {
});
}
- if (!hasPublicApiAuthSignal(request)) {
+ if (!shouldResolvePublicCatalogAccess(request)) {
return differentialResponse({
...publicDifferentialPayload(kind, q, limit),
publicAccess: true,
@@ -82,7 +86,7 @@ export async function GET(request: Request) {
supabase,
subject: access.rateLimitSubject,
bucket: "registry",
- allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
+ allowInMemoryFallbackOnUnavailable: allowRateLimitInMemoryFallbackOnUnavailable(),
});
if (rateLimit.limited) {
return rateLimitJsonResponse("Differential requests are rate limited. Try again shortly.", rateLimit);
diff --git a/src/app/api/documents/[id]/route.ts b/src/app/api/documents/[id]/route.ts
index 55d6a1558..4ec7010ed 100644
--- a/src/app/api/documents/[id]/route.ts
+++ b/src/app/api/documents/[id]/route.ts
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import type { Json } from "@/lib/supabase/database.types";
import { z } from "zod";
+import { rateLimitJsonResponse } from "@/lib/api-rate-limit";
import { getDemoDocumentPayload } from "@/lib/demo-data";
import { env, isDemoMode } from "@/lib/env";
import { jsonError, PublicApiError } from "@/lib/http";
@@ -8,6 +9,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 { enforceDocumentReadRateLimit, withOwnerReadScope } from "@/lib/public-api-access";
import { writeAuditLog } from "@/lib/audit";
import { parseJsonBody } from "@/lib/validation/body";
import { parseRouteParams } from "@/lib/validation/params";
@@ -280,13 +282,14 @@ 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, rateLimit } = await enforceDocumentReadRateLimit(request, supabase);
+ if (rateLimit.limited) {
+ return rateLimitJsonResponse("Document requests are rate limited. Try again shortly.", rateLimit);
+ }
+ 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 });
diff --git a/src/app/api/documents/[id]/search/route.ts b/src/app/api/documents/[id]/search/route.ts
index 4ab60b1ed..80deb18ae 100644
--- a/src/app/api/documents/[id]/search/route.ts
+++ b/src/app/api/documents/[id]/search/route.ts
@@ -1,11 +1,13 @@
import { NextResponse } from "next/server";
import { z } from "zod";
+import { rateLimitJsonResponse } from "@/lib/api-rate-limit";
import { demoChunks, getDemoDocument } from "@/lib/demo-data";
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 { enforceDocumentReadRateLimit, withOwnerReadScope } from "@/lib/public-api-access";
import { parseRouteParams } from "@/lib/validation/params";
import { parseRequestQuery, queryInteger } from "@/lib/validation/query";
@@ -181,13 +183,14 @@ 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, rateLimit } = await enforceDocumentReadRateLimit(request, supabase);
+ if (rateLimit.limited) {
+ return rateLimitJsonResponse("Document requests are rate limited. Try again shortly.", rateLimit);
+ }
+ 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 });
@@ -197,7 +200,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) {
diff --git a/src/app/api/documents/[id]/signed-url/route.ts b/src/app/api/documents/[id]/signed-url/route.ts
index 043d01118..7ecb96f47 100644
--- a/src/app/api/documents/[id]/signed-url/route.ts
+++ b/src/app/api/documents/[id]/signed-url/route.ts
@@ -1,11 +1,13 @@
import { NextResponse } from "next/server";
import { z } from "zod";
+import { rateLimitJsonResponse } from "@/lib/api-rate-limit";
import { getDemoDocument } from "@/lib/demo-data";
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 { enforceDocumentReadRateLimit, withOwnerReadScope } from "@/lib/public-api-access";
export const runtime = "nodejs";
@@ -31,13 +33,14 @@ 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, rateLimit } = await enforceDocumentReadRateLimit(_request, supabase);
+ if (rateLimit.limited) {
+ return rateLimitJsonResponse("Document requests are rate limited. Try again shortly.", rateLimit);
+ }
+ 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 });
diff --git a/src/app/api/documents/route.ts b/src/app/api/documents/route.ts
index 52814c3a6..eb6640817 100644
--- a/src/app/api/documents/route.ts
+++ b/src/app/api/documents/route.ts
@@ -1,14 +1,33 @@
import { NextResponse } from "next/server";
import { z } from "zod";
+import { rateLimitJsonResponse } from "@/lib/api-rate-limit";
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 { enforceDocumentReadRateLimit, withOwnerReadScope } from "@/lib/public-api-access";
import { parseRequestQuery, queryBoolean, queryInteger } from "@/lib/validation/query";
export const runtime = "nodejs";
+const PUBLIC_DOCUMENT_LIST_COLUMNS = [
+ "id",
+ "owner_id",
+ "title",
+ "description",
+ "file_name",
+ "file_type",
+ "file_size",
+ "status",
+ "page_count",
+ "chunk_count",
+ "image_count",
+ "metadata",
+ "created_at",
+ "updated_at",
+].join(",");
+
const DOCUMENT_LIST_COLUMNS = [
"id",
"owner_id",
@@ -31,6 +50,18 @@ const DOCUMENT_LIST_COLUMNS = [
"updated_at",
].join(",");
+const PUBLIC_LABEL_LIST_COLUMNS = [
+ "id",
+ "document_id",
+ "label",
+ "label_type",
+ "source",
+ "confidence",
+ "metadata",
+ "created_at",
+ "updated_at",
+].join(",");
+
const LABEL_LIST_COLUMNS = [
"id",
"document_id",
@@ -44,6 +75,8 @@ const LABEL_LIST_COLUMNS = [
"updated_at",
].join(",");
+const PUBLIC_SUMMARY_LIST_COLUMNS = ["id", "document_id", "summary", "clinical_specifics", "generated_at"].join(",");
+
const SUMMARY_LIST_COLUMNS = [
"id",
"document_id",
@@ -130,11 +163,14 @@ 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, rateLimit } = await enforceDocumentReadRateLimit(request, supabase);
+ if (rateLimit.limited) {
+ return rateLimitJsonResponse("Document requests are rate limited. Try again shortly.", rateLimit);
+ }
+
+ const effectiveIncludeMeta = access.authenticated ? includeMeta : false;
+ const listColumns = access.authenticated ? DOCUMENT_LIST_COLUMNS : PUBLIC_DOCUMENT_LIST_COLUMNS;
+ let query = withOwnerReadScope(supabase.from("documents").select(listColumns, { count: "exact" }), access.ownerId)
.order("created_at", { ascending: false })
.range(offset, offset + limit - 1);
@@ -161,13 +197,15 @@ export async function GET(request: Request) {
hasMore: count === null ? documents.length === limit : offset + documents.length < count,
};
- if (documentIds.length === 0 || !includeMeta) {
+ if (documentIds.length === 0 || !effectiveIncludeMeta) {
return documentsResponse({ documents, pagination }, indexing);
}
+ const labelColumns = access.authenticated ? LABEL_LIST_COLUMNS : PUBLIC_LABEL_LIST_COLUMNS;
+ const summaryColumns = access.authenticated ? SUMMARY_LIST_COLUMNS : PUBLIC_SUMMARY_LIST_COLUMNS;
const [labelsResult, summariesResult] = await Promise.all([
- supabase.from("document_labels").select(LABEL_LIST_COLUMNS).in("document_id", documentIds),
- supabase.from("document_summaries").select(SUMMARY_LIST_COLUMNS).in("document_id", documentIds),
+ supabase.from("document_labels").select(labelColumns).in("document_id", documentIds),
+ supabase.from("document_summaries").select(summaryColumns).in("document_id", documentIds),
]);
if (labelsResult.error) throw new Error(labelsResult.error.message);
diff --git a/src/app/api/images/[id]/signed-url/route.ts b/src/app/api/images/[id]/signed-url/route.ts
index b061aaf8a..c5fac4d63 100644
--- a/src/app/api/images/[id]/signed-url/route.ts
+++ b/src/app/api/images/[id]/signed-url/route.ts
@@ -1,12 +1,14 @@
import { NextResponse } from "next/server";
import { z } from "zod";
+import { rateLimitJsonResponse } from "@/lib/api-rate-limit";
import { getDemoImage } from "@/lib/demo-data";
import { env } from "@/lib/env";
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 { enforceDocumentReadRateLimit, withOwnerReadScope } from "@/lib/public-api-access";
export const runtime = "nodejs";
@@ -31,7 +33,10 @@ 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, rateLimit } = await enforceDocumentReadRateLimit(_request, supabase);
+ if (rateLimit.limited) {
+ return rateLimitJsonResponse("Document requests are rate limited. Try again shortly.", rateLimit);
+ }
const { data: image, error } = await supabase
.from("document_images")
.select("document_id,storage_path,mime_type,caption,metadata")
@@ -41,12 +46,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 });
diff --git a/src/app/api/medications/[slug]/route.ts b/src/app/api/medications/[slug]/route.ts
index a6853c1a9..83ba38e40 100644
--- a/src/app/api/medications/[slug]/route.ts
+++ b/src/app/api/medications/[slug]/route.ts
@@ -1,6 +1,10 @@
import { NextResponse } from "next/server";
-import { consumeSubjectApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit";
+import {
+ allowRateLimitInMemoryFallbackOnUnavailable,
+ consumeSubjectApiRateLimit,
+ rateLimitJsonResponse,
+} from "@/lib/api-rate-limit";
import { isDemoMode, isLocalNoAuthMode } from "@/lib/env";
import { jsonError } from "@/lib/http";
import { getMedicationRecord } from "@/lib/medication-snapshot";
@@ -12,7 +16,7 @@ import {
rowToMedicationRecord,
type MedicationRecordRow,
} from "@/lib/medication-records";
-import { hasPublicApiAuthSignal, publicAccessContext } from "@/lib/public-api-access";
+import { publicAccessContext, shouldResolvePublicCatalogAccess } from "@/lib/public-api-access";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth";
@@ -56,7 +60,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
});
}
- if (!hasPublicApiAuthSignal(request)) {
+ if (!shouldResolvePublicCatalogAccess(request)) {
const payload = publicMedicationDetailPayload(normalizedSlug);
if (!payload) return notFoundResponse(normalizedSlug);
return medicationResponse({
@@ -72,7 +76,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
supabase,
subject: access.rateLimitSubject,
bucket: "registry",
- allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
+ allowInMemoryFallbackOnUnavailable: allowRateLimitInMemoryFallbackOnUnavailable(),
});
if (rateLimit.limited) {
return rateLimitJsonResponse("Medication requests are rate limited. Try again shortly.", rateLimit);
diff --git a/src/app/api/medications/route.ts b/src/app/api/medications/route.ts
index 7fc2cb999..88aff92a3 100644
--- a/src/app/api/medications/route.ts
+++ b/src/app/api/medications/route.ts
@@ -1,7 +1,11 @@
import { NextResponse } from "next/server";
import { z } from "zod";
-import { consumeSubjectApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit";
+import {
+ allowRateLimitInMemoryFallbackOnUnavailable,
+ consumeSubjectApiRateLimit,
+ rateLimitJsonResponse,
+} from "@/lib/api-rate-limit";
import { isDemoMode, isLocalNoAuthMode } from "@/lib/env";
import { jsonError } from "@/lib/http";
import { defaultMedicationRecords, ensureMedicationsSeeded } from "@/lib/medication-seed";
@@ -13,7 +17,7 @@ import {
type MedicationRecordRow,
} from "@/lib/medication-records";
import { medicationToSearchResult, rankMedicationRecords, type MedicationSearchMatch } from "@/lib/medications";
-import { hasPublicApiAuthSignal, publicAccessContext } from "@/lib/public-api-access";
+import { publicAccessContext, shouldResolvePublicCatalogAccess } from "@/lib/public-api-access";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth";
import { parseRequestQuery, queryInteger } from "@/lib/validation/query";
@@ -76,7 +80,7 @@ export async function GET(request: Request) {
});
}
- if (!hasPublicApiAuthSignal(request)) {
+ if (!shouldResolvePublicCatalogAccess(request)) {
return medicationResponse({
...publicMedicationPayload(q, limit),
publicAccess: true,
@@ -90,7 +94,7 @@ export async function GET(request: Request) {
supabase,
subject: access.rateLimitSubject,
bucket: "registry",
- allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
+ allowInMemoryFallbackOnUnavailable: allowRateLimitInMemoryFallbackOnUnavailable(),
});
if (rateLimit.limited) {
return rateLimitJsonResponse("Medication requests are rate limited. Try again shortly.", rateLimit);
diff --git a/src/app/api/registry/records/[slug]/route.ts b/src/app/api/registry/records/[slug]/route.ts
index bfa21c873..79eda031e 100644
--- a/src/app/api/registry/records/[slug]/route.ts
+++ b/src/app/api/registry/records/[slug]/route.ts
@@ -1,10 +1,14 @@
import { NextResponse } from "next/server";
import { z } from "zod";
-import { consumeSubjectApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit";
+import {
+ allowRateLimitInMemoryFallbackOnUnavailable,
+ 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 { publicAccessContext, shouldResolvePublicCatalogAccess } from "@/lib/public-api-access";
import { getFormRecord } from "@/lib/forms";
import {
deriveGovernanceColumns,
@@ -62,6 +66,15 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
});
}
+ if (!shouldResolvePublicCatalogAccess(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);
@@ -69,7 +82,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
supabase,
subject: access.rateLimitSubject,
bucket: "registry",
- allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
+ allowInMemoryFallbackOnUnavailable: allowRateLimitInMemoryFallbackOnUnavailable(),
});
if (rateLimit.limited) {
return rateLimitJsonResponse("Registry requests are rate limited. Try again shortly.", rateLimit);
diff --git a/src/app/api/registry/records/route.ts b/src/app/api/registry/records/route.ts
index 5897a1525..373ec658a 100644
--- a/src/app/api/registry/records/route.ts
+++ b/src/app/api/registry/records/route.ts
@@ -1,10 +1,14 @@
import { NextResponse } from "next/server";
import { z } from "zod";
-import { consumeSubjectApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit";
+import {
+ allowRateLimitInMemoryFallbackOnUnavailable,
+ 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 { publicAccessContext, shouldResolvePublicCatalogAccess } from "@/lib/public-api-access";
import { rankFormRecords, formRecords } from "@/lib/forms";
import {
deriveGovernanceColumns,
@@ -78,6 +82,13 @@ export async function GET(request: Request) {
});
}
+ if (!shouldResolvePublicCatalogAccess(request)) {
+ return registryResponse({
+ ...publicRegistryPayload(kind, q, limit),
+ publicAccess: true,
+ });
+ }
+
const supabase = createAdminClient();
const access = await publicAccessContext(request, supabase);
@@ -85,7 +96,7 @@ export async function GET(request: Request) {
supabase,
subject: access.rateLimitSubject,
bucket: "registry",
- allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
+ allowInMemoryFallbackOnUnavailable: allowRateLimitInMemoryFallbackOnUnavailable(),
});
if (rateLimit.limited) {
return rateLimitJsonResponse("Registry requests are rate limited. Try again shortly.", rateLimit);
diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts
index a0474e14d..be077175c 100644
--- a/src/app/api/search/route.ts
+++ b/src/app/api/search/route.ts
@@ -14,7 +14,11 @@ import { buildSmartRagApiPlan } from "@/lib/smart-rag-api";
import { SOURCE_ONLY_EMBEDDING_SKIP_REASON } from "@/lib/rag-provider";
import { createAdminClient } from "@/lib/supabase/admin";
import * as serverAuth from "@/lib/supabase/auth";
-import { consumeSubjectApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit";
+import {
+ allowRateLimitInMemoryFallbackOnUnavailable,
+ consumeSubjectApiRateLimit,
+ rateLimitJsonResponse,
+} from "@/lib/api-rate-limit";
import { publicAccessContext } from "@/lib/public-api-access";
import { clinicalQueryModeSchema, queryClassForClinicalMode, queryForClinicalMode } from "@/lib/clinical-query-mode";
import { parseJsonBody } from "@/lib/validation/body";
@@ -893,7 +897,7 @@ export async function POST(request: Request) {
supabase,
subject: access.rateLimitSubject,
bucket: "search",
- allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
+ allowInMemoryFallbackOnUnavailable: allowRateLimitInMemoryFallbackOnUnavailable(),
});
if (rateLimit.limited) {
return rateLimitJsonResponse(
diff --git a/src/app/api/setup-status/route.ts b/src/app/api/setup-status/route.ts
index bf0551c94..ba34855b8 100644
--- a/src/app/api/setup-status/route.ts
+++ b/src/app/api/setup-status/route.ts
@@ -2,7 +2,6 @@ import { NextResponse } from "next/server";
import { env, isDemoMode } from "@/lib/env";
import { localProjectRequestIdentityPayload, unsafeLocalProjectResponse } from "@/lib/local-project-guard";
import { createAdminClient } from "@/lib/supabase/admin";
-import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth";
import { formatSupabaseUnavailableError, isSupabaseUnavailableError, probeSupabaseHealth } from "@/lib/supabase/health";
import { checkSupabaseProjectConfig, formatSupabaseProjectCheck } from "@/lib/supabase/project";
@@ -409,27 +408,11 @@ async function readSetupStatusPayload() {
}
}
-async function requireProductionSetupStatusAuth(request: Request) {
+export async function GET(request: Request) {
const identity = localProjectRequestIdentityPayload(request);
- if (process.env.NODE_ENV !== "production" || identity.localServer.currentUrl) {
- return identity;
+ if (!identity.localServer.safeLocalOrigin) {
+ return unsafeLocalProjectResponse(identity);
}
- await requireAuthenticatedUser(request, createAdminClient());
- return identity;
-}
-export async function GET(request: Request) {
- try {
- const identity = await requireProductionSetupStatusAuth(request);
- if (!identity.localServer.safeLocalOrigin) {
- return unsafeLocalProjectResponse(identity);
- }
-
- return setupStatusResponse(await readSetupStatusPayload());
- } catch (error) {
- if (error instanceof AuthenticationError) {
- return unauthorizedResponse(error);
- }
- throw error;
- }
+ return setupStatusResponse(await readSetupStatusPayload());
}
diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts
index 72ddb6023..a2df002f9 100644
--- a/src/app/api/upload/route.ts
+++ b/src/app/api/upload/route.ts
@@ -2,13 +2,14 @@ import { randomUUID } from "node:crypto";
import { createHash } from "node:crypto";
import { NextResponse } from "next/server";
import { z } from "zod";
-import { env } from "@/lib/env";
+import { env, publicUploadsEnabled, publicWorkspaceOwnerId } from "@/lib/env";
import { assertAllowedFile, assertFileContentSignature, jsonError, PublicApiError } from "@/lib/http";
import { logger } from "@/lib/logger";
import { writeAuditLog } from "@/lib/audit";
import { planDocumentName, type DocumentNameSupabase } from "@/lib/document-naming";
import { createAdminClient } from "@/lib/supabase/admin";
-import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth";
+import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth";
+import { publicAccessContext } from "@/lib/public-api-access";
import { probeSupabaseHealth } from "@/lib/supabase/health";
import { optionalFormText, parseFormDataFields } from "@/lib/validation/form-data";
@@ -84,7 +85,11 @@ export async function POST(request: Request) {
try {
supabase = createAdminClient();
const adminSupabase = supabase;
- const user = await requireAuthenticatedUser(request, adminSupabase);
+ const access = await publicAccessContext(request, adminSupabase);
+ const uploadOwnerId = access.ownerId ?? (publicUploadsEnabled() ? publicWorkspaceOwnerId() : null);
+ if (!uploadOwnerId) {
+ return NextResponse.json({ error: "Public uploads are not configured for this workspace." }, { status: 503 });
+ }
const formData = await request.formData().catch((cause) => {
throw new PublicApiError("Invalid upload form data.", 400, {
code: "invalid_form_data",
@@ -107,7 +112,7 @@ export async function POST(request: Request) {
const documentId = randomUUID();
const safeName = file.name.replace(/[^\w.\-() ]+/g, "_");
- const storagePath = `${user.id}/documents/${documentId}/${safeName}`;
+ const storagePath = `${uploadOwnerId}/documents/${documentId}/${safeName}`;
const buffer = Buffer.from(await file.arrayBuffer());
// The declared MIME type is client-supplied; verify the real byte signature
// before persisting a clinical document.
@@ -117,7 +122,7 @@ export async function POST(request: Request) {
const { data: duplicate, error: duplicateError } = await adminSupabase
.from("documents")
.select("id,title,file_name,status,page_count,chunk_count,image_count,created_at")
- .eq("owner_id", user.id)
+ .eq("owner_id", uploadOwnerId)
.eq("content_hash", contentHash)
.maybeSingle();
@@ -147,7 +152,7 @@ export async function POST(request: Request) {
};
const namePlan = await planDocumentName({
supabase: namingSupabase,
- ownerId: user.id,
+ ownerId: uploadOwnerId,
fileName: file.name,
requestedTitle: uploadMetadata.title,
contentHash,
@@ -160,7 +165,7 @@ export async function POST(request: Request) {
.from("documents")
.insert({
id: documentId,
- owner_id: user.id,
+ owner_id: uploadOwnerId,
title,
description,
file_name: file.name,
@@ -178,7 +183,7 @@ export async function POST(request: Request) {
review_date: null,
uploaded_at: uploadedAt,
indexed_at: null,
- uploaded_by: user.id,
+ uploaded_by: uploadOwnerId,
original_file_name: namePlan.originalFileName,
original_title: namePlan.originalTitle,
smart_title_base: namePlan.baseTitle,
@@ -202,7 +207,7 @@ export async function POST(request: Request) {
insertedDocumentOwnerId = null;
return duplicateUploadResponse({
supabase,
- ownerId: user.id,
+ ownerId: uploadOwnerId,
contentHash,
storagePath: uploadedPath,
});
@@ -210,7 +215,7 @@ export async function POST(request: Request) {
throw new Error(documentError.message);
}
insertedDocumentId = documentId;
- insertedDocumentOwnerId = user.id;
+ insertedDocumentOwnerId = uploadOwnerId;
const { data: job, error: jobError } = await supabase
.from("ingestion_jobs")
@@ -230,7 +235,7 @@ export async function POST(request: Request) {
.from("documents")
.delete()
.eq("id", documentId)
- .eq("owner_id", user.id);
+ .eq("owner_id", uploadOwnerId);
if (rollbackDocumentError) {
throw new Error(
`Failed to enqueue ingestion job: ${jobError.message}; rollback failed: ${rollbackDocumentError.message}`,
@@ -242,7 +247,7 @@ export async function POST(request: Request) {
}
await writeAuditLog(supabase, {
- ownerId: user.id,
+ ownerId: uploadOwnerId,
action: "document_upload",
resourceType: "document",
resourceId: documentId,
diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx
index 2545d2b24..018ec0c1d 100644
--- a/src/components/ClinicalDashboard.tsx
+++ b/src/components/ClinicalDashboard.tsx
@@ -1,18 +1,15 @@
"use client";
-import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import dynamic from "next/dynamic";
import {
AlertCircle,
Bell,
BookOpen,
- CheckCircle2,
ChevronDown,
ChevronRight,
CircleUserRound,
Clock3,
- Copy,
ExternalLink,
FileImage,
FileText,
@@ -21,7 +18,6 @@ import {
HelpCircle,
Heart,
Keyboard,
- Layers,
ListChecks,
Loader2,
LogOut,
@@ -29,7 +25,6 @@ import {
LockKeyhole,
Palette,
PanelTop,
- Plus,
Quote,
RefreshCw,
Search,
@@ -45,69 +40,33 @@ import {
Wrench,
X,
} from "lucide-react";
-import {
- type CSSProperties,
- type FormEvent,
- type RefObject,
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from "react";
-import { AccessibleTable } from "@/components/AccessibleTable";
-import {
- DocumentOrganizationBadges,
- documentDisplayTitle,
- documentOrganizationProfile,
-} from "@/components/DocumentOrganizationBadges";
-import { DocumentTagCloud } from "@/components/DocumentTagCloud";
-import { DocumentManagementActions, type DocumentDeleteResult } from "@/components/DocumentManagementActions";
+import { type CSSProperties, type FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { type DocumentDeleteResult } from "@/components/DocumentManagementActions";
import { useDismissableLayer } from "@/components/use-dismissable-layer";
-import { formatCompactCitationLabel } from "@/lib/citations";
import { extractSafetyFindings } from "@/lib/clinical-safety";
import { readLocalProjectIdentity, unsafeLocalProjectMessage } from "@/lib/local-project-identity";
-import { isLocalNoAuthMode } from "@/lib/env";
+import { isDeployedClinicalKb } from "@/lib/deployed-app";
+import { isLocalNoAuthMode, publicUploadsEnabled } from "@/lib/env";
import {
appBackdrop,
answerSurface,
- chatMicroAction,
- clinicalDivider,
cn,
- EmptyState,
- fieldControlPlain,
fieldControlWithIcon,
fieldIcon,
floatingControl,
- iconTilePremium,
- metadataPill,
- panelSubtle,
primaryControl,
- SourceProvenance,
- SourceStatusBadge,
- sourceCard,
- subtleStatusPill,
- tableCard,
- tableCardHeader,
- tableMicroActionRow,
textMuted,
- toneDanger,
- toneInfo,
- toneNeutral,
toneSuccess,
toneWarning,
} from "@/components/ui-primitives";
import { useAuthSession } from "@/lib/supabase/client";
-import { SafeBoldText } from "@/components/SafeBoldText";
import { Sheet } from "@/components/ui/sheet";
import { AccountSetupDialog } from "@/components/clinical-dashboard/account-setup-dialog";
import { StagedAnswerResultSurface } from "@/components/clinical-dashboard/answer-result-surface";
import { RelatedDocumentsPanel } from "@/components/clinical-dashboard/document-results";
-import { AnswerFollowUpSuggestions } from "@/components/clinical-dashboard/answer-follow-up-suggestions";
import { AuthPanel } from "@/components/clinical-dashboard/auth-panel";
import { useSidebarCollapsed } from "@/components/clinical-dashboard/use-sidebar-collapsed";
import { useTheme } from "@/components/clinical-dashboard/use-theme";
-import { StatusBadge } from "@/components/clinical-dashboard/badges";
import {
type SidebarIdentity,
deriveSidebarIdentity,
@@ -126,45 +85,18 @@ import {
type SetupCheck,
type IngestionQualityReviewItem,
} from "@/components/clinical-dashboard/DocumentManagerPanel";
-import {
- GuideDialog,
- GuideTrigger,
- SectionHeading,
- UtilityDrawer,
-} from "@/components/clinical-dashboard/dashboard-shell";
-import {
- cleanDisplayTitle,
- sanitizeAnswerDisplayText,
- sanitizeDisplayText,
-} from "@/components/clinical-dashboard/display-text";
+import { GuideDialog, GuideTrigger, UtilityDrawer } from "@/components/clinical-dashboard/dashboard-shell";
+import { sanitizeAnswerDisplayText, sanitizeDisplayText } from "@/components/clinical-dashboard/display-text";
import {
NaturalLanguageAnswer,
ScopeAndGovernanceNotice,
- SourceImage,
UserQuestionBubble,
} from "@/components/clinical-dashboard/answer-content";
import { AnswerEmptyState, AnswerSkeleton } from "@/components/clinical-dashboard/answer-status";
-import {
- AnswerFeedbackPanel,
- AnswerSafetyNotice,
- AnswerSupportSummaryCard,
- answerHasCentralTable,
- answerSupportPriority,
- ClinicalNotesChecklistPanel,
- clinicalNotesCount,
- clinicalNotesDisplayCountForAnswer,
- compactEvidenceSummary,
- type EvidenceTabName,
- simpleClinicalTableProps,
- evidenceMapRowsFromRenderModel,
- evidenceTabCount,
- evidenceTabOrder,
- QuoteCards,
- SafetyFindingsListContent,
-} from "@/components/clinical-dashboard/evidence-panels";
+import { evidenceMapRowsFromRenderModel } from "@/components/clinical-dashboard/evidence-panels";
import { MasterSearchHeader } from "@/components/clinical-dashboard/master-search-header";
import { SearchCommandProvider } from "@/components/clinical-dashboard/search-command-context";
-import { emptyStates, errorCopy } from "@/lib/ui-copy";
+import { errorCopy } from "@/lib/ui-copy";
import { applicationsLauncherItemCount } from "@/components/applications-launcher-page";
import {
DrawerGroupLabel,
@@ -198,7 +130,7 @@ const DocumentDrawer = dynamic(
);
import { DocumentSearchResultsPanel, type SearchFacets } from "@/components/clinical-dashboard/document-search-results";
-import { isWeakRelevance, QueryCoverageChips } from "@/components/clinical-dashboard/relevance";
+import { isWeakRelevance } from "@/components/clinical-dashboard/relevance";
import {
answerPayloadIsUsable,
isRetryableError,
@@ -236,21 +168,13 @@ import {
maxStoredAnswerTurns,
savePersistedAnswerThread,
} from "@/lib/answer-thread-storage";
-import { buildAnswerRenderModel, type AnswerRenderModel } from "@/lib/answer-render-policy";
-import { sourceTextForCompactDisplay } from "@/lib/source-text-sanitizer";
+import { buildAnswerRenderModel } from "@/lib/answer-render-policy";
import {
frontendSourceGovernanceWarnings,
groupSourceGovernanceWarnings,
type SourceGovernanceWarning,
} from "@/lib/source-governance";
-import { smartEvidenceTags } from "@/lib/evidence-tags";
-import {
- tagSearchText,
- type SmartDocumentTag,
- type SmartDocumentTagFacet,
- type SmartDocumentTagTier,
- type SmartDocumentTagQualityIssueKind,
-} from "@/lib/document-tags";
+import { type SmartDocumentTag, type SmartDocumentTagFacet } from "@/lib/document-tags";
import type {
ClinicalDocument,
DocumentMatch,
@@ -263,19 +187,12 @@ import type {
RelatedDocument,
SearchResult,
SearchScopeSummary,
- VisualEvidenceCard,
ClinicalQueryMode,
DocumentLabel,
- DocumentLabelType,
} from "@/lib/types";
import type { SearchScopeFilters } from "@/lib/search-scope";
import { differentialsMobileCompareAddonSlotId, modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
-import {
- createQuoteFollowUp,
- type AnswerEvidenceMapRow,
- type AnswerViewMode,
- shouldPollForUpdates,
-} from "@/lib/ward-output";
+import { createQuoteFollowUp, type AnswerViewMode, shouldPollForUpdates } from "@/lib/ward-output";
export const navigationHashes = ["#search", "#quotes", "#images", "#sources"] as const;
export const mobileSectionFabMediaQuery =
@@ -487,367 +404,6 @@ async function readAnswerStream(response: Response, onProgress: (message: string
function normalizeNavigationHash(hash: string) {
return navigationHashes.includes(hash as (typeof navigationHashes)[number]) ? hash : "#search";
}
-
-function compactClinicalTableCaption(item: VisualEvidenceCard) {
- const raw = item.tableTitle || item.tableLabel || item.caption || "Clinical table";
- const cleaned = sourceTextForCompactDisplay(raw)
- .replace(/\btable\s+\d+\s*[:.-]?\s*/i, "")
- .replace(/\b(?:page|p\.)\s*\d+\b/gi, "")
- .replace(/\s{2,}/g, " ")
- .trim();
- const caption = cleaned || "Clinical table";
- return caption.length <= 72 ? caption : `${caption.slice(0, 69).trim()}...`;
-}
-
-function visualEvidenceHeader(item: VisualEvidenceCard) {
- const titleSource = [item.tableLabel, item.tableTitle].filter(Boolean).join(" · ");
- const titleText = sourceTextForCompactDisplay(titleSource).trim();
- const captionText = sourceTextForCompactDisplay(item.caption ?? "").trim();
- const normalizedTitle = titleText.toLowerCase();
- const normalizedCaption = captionText.toLowerCase();
- const isDuplicateCaption =
- Boolean(normalizedCaption) &&
- (normalizedCaption.startsWith(normalizedTitle) || normalizedCaption === normalizedTitle);
- return {
- title: titleText || captionText || "Visual evidence",
- caption: isDuplicateCaption ? null : captionText,
- };
-}
-
-function VisualEvidenceStrip({
- evidence,
- collapsed = false,
- embedded = false,
-}: {
- evidence: VisualEvidenceCard[];
- collapsed?: boolean;
- embedded?: boolean;
-}) {
- function looksLikeTableText(value?: string | null) {
- return Boolean(value?.includes("|") && value.split("|").filter((cell) => cell.trim()).length >= 3);
- }
-
- if (collapsed) {
- return (
-
- );
- }
-
- const content = (
- <>
-
- {evidence.length === 0 ? (
-
- ) : (
-
- {evidence.map((item) => {
- const tableMarkdown = item.accessibleTableMarkdown?.trim()
- ? item.accessibleTableMarkdown
- : looksLikeTableText(item.tableTextSnippet)
- ? item.tableTextSnippet
- : null;
- const hasStructuredTable = Boolean(tableMarkdown || item.tableRows?.length || item.tableColumns?.length);
- const tableCaption = compactClinicalTableCaption(item);
- const sourceHeader = visualEvidenceHeader(item);
- const displayLabels = smartEvidenceTags(
- item.labels,
- [[item.tableLabel, item.tableTitle].filter(Boolean).join(": "), item.caption, item.tableTextSnippet]
- .filter(Boolean)
- .join(" "),
- );
- return (
-
-
-
-
-
- {!hasStructuredTable ? {sourceHeader.title}
: null}
- {!hasStructuredTable && sourceHeader.caption ? {sourceHeader.caption}
: null}
-
- {!hasStructuredTable && item.tableTextSnippet ? (
-
- {sourceTextForCompactDisplay(item.tableTextSnippet)}
-
- ) : null}
- {displayLabels.length ? (
-
- {displayLabels.map((label) => (
-
- {label}
-
- ))}
-
- ) : null}
-
-
-
- {formatCompactCitationLabel(item)}
-
-
- {cleanDisplayTitle(item.title)}, page {item.page_number ?? "n/a"}
-
- {item.image_type && (
-
- {item.image_type.replaceAll("_", " ")}
-
- )}
- {!hasStructuredTable ? : null}
-
-
- Open source
-
-
-
- );
- })}
-
- )}
- >
- );
-
- if (embedded) return {content}
;
-
- return (
-
- );
-}
-
-const evidenceTabIconMap: Record = {
- Claims: CheckCircle2,
- Quotes: Quote,
- Tables: ListChecks,
- Images: FileImage,
- Gaps: AlertCircle,
-};
-
-function supportDotClass(supportLevel: string) {
- const normalized = supportLevel.toLowerCase();
- if (normalized.includes("unsupported") || normalized.includes("none")) return "bg-[color:var(--danger)]";
- if (normalized.includes("partial") || normalized.includes("limited") || normalized.includes("nearby")) {
- return "bg-[color:var(--warning)]";
- }
- return "bg-[color:var(--clinical-accent)]";
-}
-
-function supportLabel(supportLevel: string) {
- const normalized = supportLevel.toLowerCase();
- if (normalized.includes("unsupported") || normalized.includes("none")) return "Unsupported";
- if (normalized.includes("partial") || normalized.includes("limited") || normalized.includes("nearby"))
- return "Partial";
- return "Direct";
-}
-
-function claimRowsForEvidencePanel(rows: AnswerEvidenceMapRow[], renderModel: AnswerRenderModel) {
- if (rows.length) return rows.slice(0, 6);
- return renderModel.primarySources.slice(0, 6).map((source, index) => ({
- id: source.id,
- section: source.label || cleanDisplayTitle(source.title || source.file_name) || `Source ${index + 1}`,
- detail: source.snippet || source.reason || "Open source passage to review the cited evidence.",
- supportLevel: source.sourceStrength === "none" ? "partial" : source.sourceStrength,
- citationCount: 1,
- sourceStatus:
- source.sourceStrength === "none" ? "Source requires review" : `${source.sourceStrength} source support`,
- bestSourceLabel: source.label,
- bestLinkedPassage: source.snippet || source.reason,
- href: source.href,
- }));
-}
-
-function EvidenceClaimsList({ rows, renderModel }: { rows: AnswerEvidenceMapRow[]; renderModel: AnswerRenderModel }) {
- const claimRows = claimRowsForEvidencePanel(rows, renderModel);
- const directCount = claimRows.filter((row) => supportLabel(row.supportLevel) === "Direct").length;
- const partialCount = claimRows.filter((row) => supportLabel(row.supportLevel) === "Partial").length;
-
- if (!claimRows.length) {
- return ;
- }
-
- return (
-
-
-
-
Claims checked
-
-
-
- Direct
-
-
-
- Partial
-
-
-
- Unsupported
-
-
-
-
- {directCount} direct · {partialCount} partial
-
-
-
-
- {claimRows.map((row, index) => (
-
-
-
-
- {row.section}
-
- {row.detail || row.bestLinkedPassage || row.bestSourceLabel}
-
-
-
-
- ))}
-
-
- );
-}
-
-function EvidenceGapsPanel({ warnings }: { warnings: string[] }) {
- if (!warnings.length) {
- return (
-
- );
- }
-
- return (
-
- {warnings.map((warning, index) => (
-
-
-
-
Gap {index + 1}
-
{warning}
-
-
- ))}
-
- );
-}
-
-function MobileEvidenceTabPanel({
- tab,
- renderModel,
- visualEvidence,
- answerEvidenceMapRows,
- copiedQuotes,
- onCopyQuotes,
- onFollowUpQuote,
- onScopeDocument,
-}: {
- tab: EvidenceTabName;
- renderModel: AnswerRenderModel;
- visualEvidence: VisualEvidenceCard[];
- answerEvidenceMapRows: AnswerEvidenceMapRow[];
- copiedQuotes: boolean;
- onCopyQuotes: () => void;
- onFollowUpQuote?: (quote: QuoteCard) => void;
- onScopeDocument: (documentId: string) => void;
-}) {
- if (tab === "Claims") {
- return ;
- }
-
- if (tab === "Tables") {
- const tableEvidence = visualEvidence.filter((item) => item.accessibleTableMarkdown || item.tableRows?.length);
- return tableEvidence.length ? (
-
- {tableEvidence.slice(0, 4).map((item, index) => (
-
-
-
-
-
-
- {compactClinicalTableCaption(item)}
-
-
- Table {index + 1} · p.{item.page_number ?? "n/a"}
-
-
-
-
-
-
- ))}
-
- ) : (
-
- );
- }
-
- if (tab === "Images") {
- return visualEvidence.length ? (
-
- ) : (
-
- );
- }
-
- if (tab === "Quotes") {
- return (
-
- );
- }
-
- return ;
-}
-
/**
* A completed Q&A exchange kept on screen after a newer answer arrives, so
* Answer mode reads as a conversation thread instead of replacing each result.
@@ -2174,7 +1730,10 @@ export function ClinicalDashboard({
process.env.NODE_ENV !== "production" && localProjectReady && hasReadyRequiredPublicSearchConfig(setupChecks);
const canUsePrivateApis =
localProjectReady && (localNoAuthMode || localDevCanAttemptPrivateApis || authStatus === "authenticated");
- const canRunSearch = explicitDemoMode || canUsePublicSearchApis || canUseDegradedLocalSearchApis;
+ const canUploadDocuments = canUsePrivateApis || (publicUploadsEnabled() && canUsePublicSearchApis);
+ const canAttemptDeployedPublicSearch = isDeployedClinicalKb() && localProjectReady;
+ const canRunSearch =
+ explicitDemoMode || canUsePublicSearchApis || canUseDegradedLocalSearchApis || canAttemptDeployedPublicSearch;
const closeDashboardTransientSurfaces = useCallback(
(except?: "guide" | "settings" | "accountSetup" | "mobileSidebar" | "documents" | "upload") => {
if (except !== "guide") setGuideOpen(false);
@@ -2355,20 +1914,25 @@ export function ClinicalDashboard({
const setupResponse = await fetch("/api/setup-status", { cache: "no-store" }).catch(() => null);
if (!setupResponse) {
- setApiUnavailable(true);
- setSetupWarning("The local API is unavailable.");
- return;
- }
-
- if (setupResponse.ok) {
+ if (isDeployedClinicalKb()) {
+ setSetupWarning("Setup status could not be loaded. You can still try search.");
+ } else {
+ setApiUnavailable(true);
+ setSetupWarning("The local API is unavailable.");
+ return;
+ }
+ } else if (setupResponse.ok) {
const payload = (await setupResponse.json()) as SetupStatusPayload;
setSetupChecks(payload.checks ?? fallbackSetupChecks);
nextDemoMode = Boolean(payload.demoMode);
routeIndexingActive = Boolean(payload.indexingActive);
routePollDelayMs = shorterPollDelay(routePollDelayMs, payload.pollAfterMs);
if (nextDemoMode) setDemoMode(true);
+ } else if (isDeployedClinicalKb()) {
+ setSetupWarning("Setup status could not be loaded. You can still try search.");
} else {
setApiUnavailable(true);
+ return;
}
}
@@ -2922,11 +2486,13 @@ export function ClinicalDashboard({
function searchNetworkFailure(label: string) {
const offline = typeof navigator !== "undefined" && !navigator.onLine;
- const localOrigin = typeof window !== "undefined" ? window.location.origin : "the local Clinical KB server";
+ const origin = typeof window !== "undefined" ? window.location.origin : "Clinical KB";
return makeSearchError(
offline
? `${label} could not run because the browser is offline.`
- : `${label} could not reach Clinical KB at ${localOrigin}. The local server may still be starting or restarting; retry shortly or run npm run ensure.`,
+ : isDeployedClinicalKb()
+ ? `${label} could not reach Clinical KB at ${origin}. Check your connection and try again shortly.`
+ : `${label} could not reach Clinical KB at ${origin}. The local server may still be starting or restarting; retry shortly or run npm run ensure.`,
undefined,
true,
);
@@ -4005,21 +3571,21 @@ export function ClinicalDashboard({
);
- const showAuthPanel = !clientDemoMode && !canUsePrivateApis;
- const showDegradedNotice = !isOnline || apiUnavailable;
+ const showAuthPanel = false;
+ const showDegradedNotice = !isOnline || (apiUnavailable && !canRunSearch);
const hasMobileBottomSearch = searchMode !== "answer";
const showDesktopHomeComposer =
- !loading &&
!error &&
- ((activeModeResultKind === "answer" && !answer && !modeSearchSubmitted) ||
- (searchMode === "documents" &&
- activeModeResultKind === "documents" &&
- documentMatches.length === 0 &&
- !modeSearchSubmitted) ||
- (searchMode === "prescribing" && activeModeResultKind === "documents" && !modeSearchSubmitted) ||
- (activeModeResultKind === "differentials" && !modeSearchSubmitted) ||
+ (activeModeResultKind === "tools" ||
activeModeResultKind === "favourites" ||
- activeModeResultKind === "tools");
+ (!loading &&
+ ((activeModeResultKind === "answer" && !answer && !modeSearchSubmitted) ||
+ (searchMode === "documents" &&
+ activeModeResultKind === "documents" &&
+ documentMatches.length === 0 &&
+ !modeSearchSubmitted) ||
+ (searchMode === "prescribing" && activeModeResultKind === "documents" && !modeSearchSubmitted) ||
+ (activeModeResultKind === "differentials" && !modeSearchSubmitted))));
const desktopHomeComposerSlotId = showDesktopHomeComposer ? modeHomeDesktopComposerSlotId : undefined;
// Favourites and Tools are content-rich hubs: they share the centred hero but
// stay top-aligned so their lists start in a stable position.
@@ -4044,14 +3610,18 @@ export function ClinicalDashboard({
summary={
!isOnline
? "Your browser is offline. Existing content may remain visible, but private search and uploads need network access."
- : "The local API did not respond. Check the app server and setup status before retrying."
+ : isDeployedClinicalKb()
+ ? "The app could not reach its API. Try again in a moment."
+ : "The local API did not respond. Check the app server and setup status before retrying."
}
mobileSummary={!isOnline ? "Offline" : "API unavailable"}
>
{!isOnline
? "Reconnect before uploading documents, refreshing source URLs, or generating answers."
- : "The app will preserve the current view. Retry after confirming the local server, Supabase, OpenAI, and worker setup."}
+ : isDeployedClinicalKb()
+ ? "The app will preserve the current view. If this keeps happening, check your connection and try again shortly."
+ : "The app will preserve the current view. Retry after confirming the local server, Supabase, OpenAI, and worker setup."}
);
@@ -4079,7 +3649,7 @@ export function ClinicalDashboard({
{
id: "upload",
label: "Upload",
- summary: uploadReadOnlyMode || !canUsePrivateApis ? "Locked" : "Ready",
+ summary: uploadReadOnlyMode || !canUploadDocuments ? "Locked" : "Ready",
panelId: "dashboard-upload-section",
icon: UploadCloud,
},
@@ -4659,7 +4229,7 @@ export function ClinicalDashboard({
diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx
index bd4c4d222..630790c43 100644
--- a/src/components/DocumentViewer.tsx
+++ b/src/components/DocumentViewer.tsx
@@ -1914,7 +1914,6 @@ export function DocumentViewer({
const [documentSearchError, setDocumentSearchError] = useState(null);
const [reviewingTableFactId, setReviewingTableFactId] = useState(null);
const [isOnline, setIsOnline] = useState(true);
- const [authLoadingTimedOut, setAuthLoadingTimedOut] = useState(false);
const [localProjectReady, setLocalProjectReady] = useState(true);
const [mobileActionsOpen, setMobileActionsOpen] = useState(false);
const [useNativePdfViewer, setUseNativePdfViewer] = useState(() => getInitialPdfViewerMode().useNativePdfViewer);
@@ -1924,11 +1923,22 @@ export function DocumentViewer({
const [viewerModeInitialized] = useState(true);
const generatedSummaryRef = useRef(null);
const { status: authStatus, isConfigured, authorizationHeader, markSessionExpired } = useAuthSession();
+ const [authLoadingTimedOut, setAuthLoadingTimedOut] = useState(false);
const [serverDemoMode, setServerDemoMode] = useState(process.env.NEXT_PUBLIC_DEMO_MODE === "true");
const localNoAuthMode = isLocalNoAuthMode();
const clientDemoMode = localNoAuthMode || serverDemoMode;
+ const canViewSourceDocuments = localProjectReady;
const canUsePrivateApis = localProjectReady && (clientDemoMode || authStatus === "authenticated");
+ useEffect(() => {
+ if (authStatus !== "loading") {
+ const resetId = window.setTimeout(() => setAuthLoadingTimedOut(false), 0);
+ return () => window.clearTimeout(resetId);
+ }
+ const timeoutId = window.setTimeout(() => setAuthLoadingTimedOut(true), 4_000);
+ return () => window.clearTimeout(timeoutId);
+ }, [authStatus]);
+
useEffect(() => {
if (typeof window === "undefined" || !viewerModeInitialized || hasExplicitPdfViewerMode) return;
@@ -2002,10 +2012,10 @@ export function DocumentViewer({
}, [isConfigured]);
useEffect(() => {
- if (!canUsePrivateApis && authStatus === "loading") {
+ if (!canViewSourceDocuments && authStatus === "loading") {
return () => undefined;
}
- if (!canUsePrivateApis) {
+ if (!canViewSourceDocuments) {
return () => undefined;
}
@@ -2090,9 +2100,17 @@ export function DocumentViewer({
setTableFacts([]);
setChunks([]);
setIndexHealth(null);
- setViewerError(
- detailResult.reason instanceof Error ? detailResult.reason.message : "Document could not be loaded.",
- );
+ const message =
+ detailResult.reason instanceof Error ? detailResult.reason.message : "Document could not be loaded.";
+ if (!canUsePrivateApis && !clientDemoMode && message === "Document not found.") {
+ setViewerError(
+ isConfigured
+ ? "Sign in to open private source documents."
+ : "Supabase browser authentication is not configured for private source documents.",
+ );
+ } else {
+ setViewerError(message);
+ }
}
if (signedUrlResult.status === "fulfilled") {
@@ -2141,7 +2159,7 @@ export function DocumentViewer({
}, [
authStatus,
authorizationHeader,
- canUsePrivateApis,
+ canViewSourceDocuments,
clientDemoMode,
documentId,
chunkId,
@@ -2153,7 +2171,7 @@ export function DocumentViewer({
useEffect(() => {
const query = sourceSearch.trim();
- if (!canUsePrivateApis || query.length < 2) {
+ if (!canViewSourceDocuments || query.length < 2) {
const reset = window.setTimeout(() => {
setDocumentSearchResults([]);
setSearchingDocument(false);
@@ -2195,7 +2213,7 @@ export function DocumentViewer({
window.clearTimeout(timeout);
controller.abort();
};
- }, [authorizationHeader, canUsePrivateApis, clientDemoMode, documentId, markSessionExpired, sourceSearch]);
+ }, [authorizationHeader, canViewSourceDocuments, clientDemoMode, documentId, markSessionExpired, sourceSearch]);
useEffect(() => {
const updateOnline = () => setIsOnline(navigator.onLine);
@@ -2208,15 +2226,6 @@ export function DocumentViewer({
};
}, []);
- useEffect(() => {
- if (canUsePrivateApis || authStatus !== "loading") {
- return () => undefined;
- }
-
- const timeout = window.setTimeout(() => setAuthLoadingTimedOut(true), 3000);
- return () => window.clearTimeout(timeout);
- }, [authStatus, canUsePrivateApis]);
-
async function summarize() {
if (!canSummarizeDocument) {
setSummaryError("Load a source document before summarising.");
@@ -2248,14 +2257,17 @@ export function DocumentViewer({
}
const authViewerError =
- !canUsePrivateApis && (authStatus !== "loading" || authLoadingTimedOut)
- ? isConfigured
- ? "Sign in to open private source documents."
- : "Supabase browser authentication is not configured for private source documents."
+ !canUsePrivateApis &&
+ !clientDemoMode &&
+ !loadingDocument &&
+ !document &&
+ (authStatus !== "loading" || authLoadingTimedOut) &&
+ (viewerError === "Sign in to open private source documents." ||
+ viewerError === "Supabase browser authentication is not configured for private source documents.")
+ ? viewerError
: null;
- const effectiveLoadingDocument = !canUsePrivateApis
- ? authStatus === "loading" && !authLoadingTimedOut && loadingDocument
- : loadingDocument;
+ const effectiveLoadingDocument =
+ !canUsePrivateApis && authStatus === "loading" && !authLoadingTimedOut && loadingDocument ? true : loadingDocument;
const effectiveViewerError = authViewerError ?? viewerError;
const viewerState = effectiveLoadingDocument
? "loading"
diff --git a/src/components/applications-launcher-page.tsx b/src/components/applications-launcher-page.tsx
index b47b1267a..a362bc811 100644
--- a/src/components/applications-launcher-page.tsx
+++ b/src/components/applications-launcher-page.tsx
@@ -906,12 +906,17 @@ export function ApplicationsLauncherWorkspace({
}: ApplicationsLauncherWorkspaceProps) {
const [uncontrolledQuery, setUncontrolledQuery] = useState("");
const [activeFilter, setActiveFilter] = useState("all");
- const [selectedId, setSelectedId] = useState(() => initialToolId(controlledQuery));
const isDashboardTools = variant === "dashboard-tools";
const [detailOpen, setDetailOpen] = useState(!isDashboardTools && showDetailPanel === true);
const copy = isDashboardTools ? dashboardToolsLauncherCopy : standaloneLauncherCopy;
const query = controlledQuery ?? uncontrolledQuery;
const normalizedQuery = query.trim().toLowerCase();
+ const queryDerivedId = useMemo(() => initialToolId(query), [query]);
+ const [selection, setSelection] = useState(() => ({
+ queryKey: (controlledQuery ?? "").trim().toLowerCase(),
+ id: initialToolId(controlledQuery),
+ }));
+ const selectedId = selection.queryKey === normalizedQuery ? selection.id : queryDerivedId;
const filteredApps = useMemo(() => {
return launcherApps.filter((app) => {
@@ -941,7 +946,7 @@ export function ApplicationsLauncherWorkspace({
}
function openTool(id: string) {
- setSelectedId(id);
+ setSelection({ queryKey: normalizedQuery, id });
setDetailOpen(true);
}
diff --git a/src/components/clinical-dashboard/DocumentManagerPanel.tsx b/src/components/clinical-dashboard/DocumentManagerPanel.tsx
index b25fae026..381f0da09 100644
--- a/src/components/clinical-dashboard/DocumentManagerPanel.tsx
+++ b/src/components/clinical-dashboard/DocumentManagerPanel.tsx
@@ -188,7 +188,9 @@ export function UploadPanel({
return;
}
if (!canUpload) {
- changeStatus("Sign in before uploading private guideline files.");
+ changeStatus(
+ demoMode ? demoUploadReadOnlyMessage : "Uploads are unavailable until this public workspace is configured.",
+ );
return;
}
@@ -202,7 +204,7 @@ export function UploadPanel({
setUploading(true);
changeStatus(
files.length === 1
- ? "Uploading private document to Supabase Storage..."
+ ? "Uploading document to Supabase Storage..."
: `Uploading 1 of ${files.length}: ${files[0].name}`,
);
@@ -226,8 +228,8 @@ export function UploadPanel({
if (failures.length === 0) {
changeStatus(
files.length === 1
- ? "Successfully uploaded private document to storage queue."
- : `Successfully uploaded ${files.length} private documents.`,
+ ? "Successfully uploaded document to storage queue."
+ : `Successfully uploaded ${files.length} documents.`,
);
if (input) input.value = "";
onUploaded();
diff --git a/src/components/clinical-dashboard/document-search-results.tsx b/src/components/clinical-dashboard/document-search-results.tsx
index 5efffa0ad..8398bc679 100644
--- a/src/components/clinical-dashboard/document-search-results.tsx
+++ b/src/components/clinical-dashboard/document-search-results.tsx
@@ -24,6 +24,7 @@ import {
import { DocumentTagCloud } from "@/components/DocumentTagCloud";
import { documentDisplayTitle } from "@/components/DocumentOrganizationBadges";
+import { isDeployedClinicalKb } from "@/lib/deployed-app";
import { ModeHomeTemplate } from "@/components/mode-home-template";
import { SearchResultsHeaderBand } from "@/components/clinical-dashboard/search-results-header-band";
import { SafeBoldText } from "@/components/SafeBoldText";
@@ -726,7 +727,12 @@ function RecordRegistryNotice({ status, mode }: { status: RegistryRequestStatus;
status === "loading"
? { Icon: Loader2, spin: true, tone: "info" as const, text: `Loading your ${noun} registry...` }
: status === "unauthorized"
- ? { Icon: Shield, spin: false, tone: "warning" as const, text: `Sign in to search your ${noun} registry.` }
+ ? {
+ Icon: Shield,
+ spin: false,
+ tone: "warning" as const,
+ text: `Your session expired. Sign in again to search your private ${noun} registry.`,
+ }
: {
Icon: ShieldAlert,
spin: false,
@@ -834,9 +840,11 @@ export function DocumentSearchResultsPanel({
}
const unavailableMessage = apiUnavailable
- ? "The local API is unavailable. Check the app server before searching documents."
+ ? isDeployedClinicalKb()
+ ? "Clinical KB could not be reached. Check your connection and try again shortly."
+ : "The local API is unavailable. Check the app server before searching documents."
: authUnavailable
- ? "Sign in or enable local no-auth mode before listing private indexed documents."
+ ? "Your session expired. Sign in again to view private indexed documents."
: !realDataReady
? setupWarning || "Complete the search setup before using Documents mode."
: null;
diff --git a/src/components/clinical-dashboard/favourites-hub.tsx b/src/components/clinical-dashboard/favourites-hub.tsx
index c1b2835a3..68072f029 100644
--- a/src/components/clinical-dashboard/favourites-hub.tsx
+++ b/src/components/clinical-dashboard/favourites-hub.tsx
@@ -22,7 +22,6 @@ import {
favouriteItems,
favouriteSets,
favouriteTabs,
- favouriteTypeCount,
type FavouriteItem,
type FavouriteSet,
type FavouriteTabId,
diff --git a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx
index 05596db09..597d4d459 100644
--- a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx
+++ b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx
@@ -31,6 +31,7 @@ import { SearchResultsHeaderBand } from "@/components/clinical-dashboard/search-
import { useSearchCommand } from "@/components/clinical-dashboard/search-command-context";
import { useMedicationCatalog } from "@/components/clinical-dashboard/use-medication-catalog";
import { medicationMatchesCommandScopes } from "@/lib/search-command-surface";
+import { isDeployedClinicalKb } from "@/lib/deployed-app";
import { cn, toneDanger, toneInfo, toneNeutral, toneSuccess, toneWarning } from "@/components/ui-primitives";
type MedicationPrescribingWorkspaceProps = {
@@ -414,9 +415,13 @@ function StatusNotice({
}: Pick) {
if (realDataReady && !authUnavailable && !apiUnavailable && !setupWarning) return null;
const message = authUnavailable
- ? "Private medication search is waiting for sign-in."
+ ? isDeployedClinicalKb()
+ ? "Sign in to search your private medication library."
+ : "Private medication search is waiting for sign-in."
: apiUnavailable
- ? "Medication search is using the local mockup while the API is unavailable."
+ ? isDeployedClinicalKb()
+ ? "Medication search is temporarily unavailable. Try again shortly."
+ : "Medication search is using the local mockup while the API is unavailable."
: setupWarning || "Medication search setup is still warming up.";
return (
diff --git a/src/components/clinical-dashboard/visual-evidence.tsx b/src/components/clinical-dashboard/visual-evidence.tsx
index 2f84e2134..5dcca9e54 100644
--- a/src/components/clinical-dashboard/visual-evidence.tsx
+++ b/src/components/clinical-dashboard/visual-evidence.tsx
@@ -29,7 +29,6 @@ import {
evidenceTabOrder,
QuoteCards,
simpleClinicalTableProps,
- VerificationWorkspace,
} from "@/components/clinical-dashboard/evidence-panels";
import { QueryCoverageChips } from "@/components/clinical-dashboard/relevance";
import {
diff --git a/src/components/forms/forms-home-page.tsx b/src/components/forms/forms-home-page.tsx
index eb3e040de..03a6495c1 100644
--- a/src/components/forms/forms-home-page.tsx
+++ b/src/components/forms/forms-home-page.tsx
@@ -90,10 +90,10 @@ export function FormsHomePage() {
) : registry.status === "unauthorized" ? (
) : registry.status === "error" ? (
}
- title="Sign in required"
- body={`Sign in to view this ${copy.noun}. Registry records are private to your workspace.`}
- action={{ href: "/", label: "Go to sign in" }}
+ title="Session expired"
+ body={`Your session expired. Sign in again to view your private ${copy.noun}. Public ${copy.noun} records remain available from search.`}
+ action={{ href: "/", label: "Open account setup" }}
/>
);
}
diff --git a/src/components/services/services-home-page.tsx b/src/components/services/services-home-page.tsx
index 132432f57..6624fc2e6 100644
--- a/src/components/services/services-home-page.tsx
+++ b/src/components/services/services-home-page.tsx
@@ -92,10 +92,10 @@ export function ServicesHomePage() {
) : registry.status === "unauthorized" ? (
) : registry.status === "error" ? (
({ urlQuery, value: initialQuery }));
const query = localQuery.urlQuery === urlQuery ? localQuery.value : initialQuery;
const registry = useRegistryRecords("service");
- const searchableRecords =
- registry.status === "ready" ? registry.records : registry.status === "loading" ? [] : serviceRecords;
+ const searchableRecords = useMemo(
+ () => (registry.status === "ready" ? registry.records : registry.status === "loading" ? [] : serviceRecords),
+ [registry.records, registry.status],
+ );
const matches = useMemo(() => {
const ranked = rankServiceRecords(searchableRecords, query);
return ranked.length ? ranked.map((match) => match.service) : query.trim() ? [] : searchableRecords;
diff --git a/src/lib/api-rate-limit.ts b/src/lib/api-rate-limit.ts
index 2a19a21ba..842d99dc8 100644
--- a/src/lib/api-rate-limit.ts
+++ b/src/lib/api-rate-limit.ts
@@ -1,10 +1,16 @@
import { NextResponse } from "next/server";
+import { isLocalNoAuthMode } from "@/lib/env";
import { PublicApiError } from "@/lib/http";
import type { RateLimitSubject } from "@/lib/public-api-access";
import type { createAdminClient } from "@/lib/supabase/admin";
+/** Prefer durable RPC rate limits; fall back to per-instance memory when the DB function is unavailable. */
+export function allowRateLimitInMemoryFallbackOnUnavailable() {
+ return isLocalNoAuthMode() || process.env.NODE_ENV === "production";
+}
+
export type ApiRateLimitBucket =
- "answer" | "search" | "document_summarize" | "document_reindex" | "bulk_reindex" | "registry";
+ "answer" | "search" | "document_read" | "document_summarize" | "document_reindex" | "bulk_reindex" | "registry";
export type ApiRateLimitResult = {
limited: boolean;
@@ -17,6 +23,7 @@ export type ApiRateLimitResult = {
const apiRateLimitDefaults = {
answer: { limit: 30, windowSeconds: 60 },
search: { limit: 240, windowSeconds: 60 },
+ document_read: { limit: 180, windowSeconds: 60 },
document_summarize: { limit: 12, windowSeconds: 60 },
document_reindex: { limit: 6, windowSeconds: 60 },
bulk_reindex: { limit: 2, windowSeconds: 60 },
@@ -26,6 +33,7 @@ const apiRateLimitDefaults = {
const anonymousApiRateLimitDefaults: Partial> = {
answer: { limit: 6, windowSeconds: 60 },
search: { limit: 60, windowSeconds: 60 },
+ document_read: { limit: 45, windowSeconds: 60 },
};
type SupabaseAdmin = ReturnType;
diff --git a/src/lib/deployed-app.ts b/src/lib/deployed-app.ts
new file mode 100644
index 000000000..82bb8f3a7
--- /dev/null
+++ b/src/lib/deployed-app.ts
@@ -0,0 +1,3 @@
+export function isDeployedClinicalKb() {
+ return process.env.NODE_ENV === "production";
+}
diff --git a/src/lib/env.ts b/src/lib/env.ts
index ac5d8dbf5..f6acba54f 100644
--- a/src/lib/env.ts
+++ b/src/lib/env.ts
@@ -11,6 +11,8 @@ const envSchema = z.object({
LOCAL_NO_AUTH: z.enum(["true", "false"]).optional().default("false"),
LOCAL_NO_AUTH_OWNER_EMAIL: z.string().optional(),
LOCAL_NO_AUTH_OWNER_ID: z.string().optional(),
+ PUBLIC_WORKSPACE_OWNER_ID: z.string().uuid().optional(),
+ NEXT_PUBLIC_PUBLIC_UPLOADS_ENABLED: z.enum(["true", "false"]).optional(),
OPENAI_API_KEY: z.string().optional(),
OPENAI_EMBEDDING_MODEL: z.string().default("text-embedding-3-small"),
// Must match the vector(N) dimension in supabase/schema.sql. Changing the embedding
@@ -192,3 +194,11 @@ export function isLocalNoAuthMode() {
return process.env.NODE_ENV !== "production" && (publicNoAuth || serverNoAuth);
}
+
+export function publicWorkspaceOwnerId() {
+ return env.PUBLIC_WORKSPACE_OWNER_ID?.trim() || null;
+}
+
+export function publicUploadsEnabled() {
+ return env.NEXT_PUBLIC_PUBLIC_UPLOADS_ENABLED === "true";
+}
diff --git a/src/lib/public-api-access.ts b/src/lib/public-api-access.ts
index 26b80f3fc..4de54ea73 100644
--- a/src/lib/public-api-access.ts
+++ b/src/lib/public-api-access.ts
@@ -1,5 +1,10 @@
import { createHash } from "node:crypto";
import type { createAdminClient } from "@/lib/supabase/admin";
+import {
+ consumeSubjectApiRateLimit,
+ allowRateLimitInMemoryFallbackOnUnavailable,
+ type ApiRateLimitResult,
+} from "@/lib/api-rate-limit";
import { getOptionalAuthenticatedUser } from "@/lib/supabase/auth";
type AdminClient = ReturnType;
@@ -25,14 +30,38 @@ export function anonymousApiSubjectKey(request: Request) {
return `anon:${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
}
-export function hasPublicApiAuthSignal(request: Request) {
- const authorization = request.headers.get("authorization") ?? "";
- if (/^Bearer\s+\S+/i.test(authorization)) return true;
-
+export function hasSessionCookieSignal(request: Request) {
const cookieHeader = request.headers.get("cookie") ?? "";
return cookieHeader.includes("sb-");
}
+export function hasBearerAuthAttempt(request: Request) {
+ const authorization = request.headers.get("authorization") ?? "";
+ return /^Bearer\s+\S+/i.test(authorization);
+}
+
+/** True when the request may carry a durable Supabase session (cookie), not a bare bearer attempt. */
+export function hasPublicApiAuthSignal(request: Request) {
+ return hasSessionCookieSignal(request);
+}
+
+/** Anonymous callers with no cookie or bearer skip auth resolution and rate limits on curated public catalogs. */
+export function shouldResolvePublicCatalogAccess(request: Request) {
+ return hasSessionCookieSignal(request) || hasBearerAuthAttempt(request);
+}
+
+type OwnerScopedQuery = {
+ eq(column: string, value: unknown): T;
+ is(column: string, value: null): T;
+ or(filters: string): T;
+};
+
+/** Scope reads to public rows (owner_id IS NULL) and, when signed in, the caller's owned rows. */
+export function withOwnerReadScope>(query: T, ownerId: string | undefined): T {
+ if (ownerId) return query.or(`owner_id.eq.${ownerId},owner_id.is.null`);
+ return query.is("owner_id", null);
+}
+
export async function publicAccessContext(request: Request, supabase: AdminClient) {
const user = await getOptionalAuthenticatedUser(request, supabase);
if (user) {
@@ -49,3 +78,17 @@ export async function publicAccessContext(request: Request, supabase: AdminClien
rateLimitSubject: { kind: "anonymous", subjectKey: anonymousApiSubjectKey(request) } satisfies RateLimitSubject,
};
}
+
+export async function enforceDocumentReadRateLimit(
+ request: Request,
+ supabase: AdminClient,
+): Promise<{ access: Awaited>; rateLimit: ApiRateLimitResult }> {
+ const access = await publicAccessContext(request, supabase);
+ const rateLimit = await consumeSubjectApiRateLimit({
+ supabase,
+ subject: access.rateLimitSubject,
+ bucket: "document_read",
+ allowInMemoryFallbackOnUnavailable: allowRateLimitInMemoryFallbackOnUnavailable(),
+ });
+ return { access, rateLimit };
+}
diff --git a/src/lib/rag.ts b/src/lib/rag.ts
index 6adea390f..e43a125e6 100644
--- a/src/lib/rag.ts
+++ b/src/lib/rag.ts
@@ -1456,6 +1456,7 @@ export function retrievalPlanCacheQuery(
`mode:${modeKey(args)}`,
`topK:${args.topK ?? 8}`,
`min:${args.minSimilarity ?? 0.15}`,
+ `forceEmbedding:${args.forceEmbedding ? "1" : "0"}`,
`rag:${ragDeepMemoryVersion}`,
`force:${args.forceEmbedding ? 1 : 0}`,
].join("|");
@@ -1554,7 +1555,7 @@ type SharedCacheMissReason =
function sharedCacheSelector(
supabase: ReturnType,
kind: SharedCacheKind,
- args: Pick,
+ args: Pick,
indexingVersion: string,
normalizedQuery: string = queryCacheKeyForStorage(normalizedCacheQuery(`${modeKey(args)} ${args.query}`)),
) {
@@ -1719,7 +1720,10 @@ async function getSharedCachedSearch(
}
async function getSharedCachedAnswer(
- args: Pick,
+ args: Pick<
+ SearchChunksArgs,
+ "query" | "documentId" | "documentIds" | "ownerId" | "skipCache" | "queryMode" | "forceEmbedding"
+ >,
startedAt: number,
) {
if (args.skipCache || env.RAG_ANSWER_CACHE_TTL_MS <= 0) return null;
@@ -1752,7 +1756,7 @@ async function getSharedCachedAnswer(
async function replaceSharedCacheRow(
kind: SharedCacheKind,
- args: Pick,
+ args: Pick,
payload: unknown,
ttlMs: number,
normalizedQuery: string = queryCacheKeyForStorage(normalizedCacheQuery(`${modeKey(args)} ${args.query}`)),
@@ -1804,7 +1808,10 @@ function setSharedCachedSearch(
}
function setSharedCachedAnswer(
- args: Pick,
+ args: Pick<
+ SearchChunksArgs,
+ "query" | "documentId" | "documentIds" | "ownerId" | "skipCache" | "queryMode" | "forceEmbedding"
+ >,
answer: RagAnswer,
) {
if (args.skipCache || env.RAG_ANSWER_CACHE_TTL_MS <= 0) return;
@@ -5386,6 +5393,9 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) {
// When the provider is source-only (offline mode, or auto mode without a usable key) we must
// never call OpenAI for embeddings; retrieval falls back to the lexical text-fast-path only.
const sourceOnlyRetrieval = isSourceOnlyMode();
+ if (args.forceEmbedding && sourceOnlyRetrieval) {
+ throw new Error("forceEmbedding requires embedding-capable retrieval; source-only mode cannot exercise vectors.");
+ }
// A3: shared across every withMemoryBoostedCandidates call in this request so the same
// owner/query memory cards are fetched at most once per (query, embedding-present, count).
const memoryCardCache: MemoryCardCache = new Map();
@@ -5463,7 +5473,10 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) {
telemetry.shared_cache_miss_reason = sharedCached.reason;
}
- if (shouldApplyUnsupportedSearchShortCircuit(retrievalQuery, queryAnalysis, ragAliasExpansions)) {
+ if (
+ !args.forceEmbedding &&
+ shouldApplyUnsupportedSearchShortCircuit(retrievalQuery, queryAnalysis, ragAliasExpansions)
+ ) {
// Item 10 follow-up (RC6): a typo can make an on-topic query ("schizophrenai management") look
// unsupported and short-circuit before any layer runs. Before giving up, trigram-correct the
// query against the known clinical-term vocabulary; if it changes, re-run the whole retrieval
@@ -5722,7 +5735,7 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) {
telemetry,
});
const coverageGate = evaluateEvidenceCoverageGate(args.query, coverageGateResults, queryClassification.queryClass);
- applyCoverageGateTelemetry(telemetry, coverageGate, coverageGate.accepted);
+ applyCoverageGateTelemetry(telemetry, coverageGate, !args.forceEmbedding && coverageGate.accepted);
if (!args.forceEmbedding && coverageGate.accepted) {
telemetry.retrieval_strategy = coverageGate.strategy;
recordSearchScoreTelemetry(telemetry, coverageGateResults);
@@ -5752,7 +5765,7 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) {
} catch (error) {
// In auto mode a failed embedding call (e.g. quota exhausted) degrades to the lexical
// results already gathered rather than failing the whole search. "openai" mode rethrows.
- if (!allowsAutoDegrade()) throw error;
+ if (args.forceEmbedding || !allowsAutoDegrade()) throw error;
telemetry.embedding_skipped = true;
telemetry.embedding_skip_reason = sourceOnlyReason(error);
telemetry.vector_skipped_reason = classifyProviderFailure(error);
@@ -5854,10 +5867,14 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) {
latencyMs: hybridResult.latencyMs,
topScore: layerTopScore((hybridData ?? []) as SearchResult[]),
});
+ const vectorCandidates = mergeSearchResults(
+ mergeSearchResults((hybridData ?? []) as SearchResult[], embeddingFieldCandidates),
+ indexUnitCandidates,
+ );
if (!hybridError) {
const rerankStartedAt = Date.now();
- const merged = mergeSearchResults((hybridData ?? []) as SearchResult[], textFastResults);
+ const merged = args.forceEmbedding ? vectorCandidates : mergeSearchResults(vectorCandidates, textFastResults);
const mergedWithMetadata = await attachDocumentRankingMetadata(supabase, merged, args.ownerId);
const memoryBoost = await withMemoryBoostedCandidates({
supabase,
@@ -5918,7 +5935,7 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) {
return (data ?? []) as SearchResult[];
}),
).catch((error) => {
- if (textFastResults.length > 0) return [] as SearchResult[][];
+ if (!args.forceEmbedding && textFastResults.length > 0) return [] as SearchResult[][];
throw error;
});
const fallbackLatencyMs = Date.now() - fallbackRpcStartedAt;
@@ -5930,9 +5947,13 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) {
});
const rerankStartedAt = Date.now();
+ const fallbackVectorCandidates = mergeSearchResults(
+ mergeSearchResults(resultSets.flat(), embeddingFieldCandidates),
+ indexUnitCandidates,
+ );
const mergedWithMetadata = await attachDocumentRankingMetadata(
supabase,
- mergeSearchResults(resultSets.flat(), textFastResults),
+ args.forceEmbedding ? fallbackVectorCandidates : mergeSearchResults(fallbackVectorCandidates, textFastResults),
args.ownerId,
);
const memoryBoost = await withMemoryBoostedCandidates({
diff --git a/src/lib/supabase/auth.ts b/src/lib/supabase/auth.ts
index db67f2cbe..8f20205c9 100644
--- a/src/lib/supabase/auth.ts
+++ b/src/lib/supabase/auth.ts
@@ -31,12 +31,14 @@ function readCookies(cookieHeader: string | null): Map {
return cookies;
}
-function extractSessionAccessToken(request: Request): string | null {
+function extractBearerAccessToken(request: Request): string | null {
const authorization = request.headers.get("authorization") ?? "";
const match = authorization.match(/^Bearer\s+(.+)$/i);
const headerToken = match?.[1]?.trim();
- if (headerToken) return headerToken;
+ return headerToken || null;
+}
+function extractCookieSessionAccessToken(request: Request): string | null {
const cookies = readCookies(request.headers.get("cookie"));
const legacyAccessToken = cookies.get("sb-access-token")?.trim();
if (legacyAccessToken) return legacyAccessToken;
@@ -57,6 +59,16 @@ function extractSessionAccessToken(request: Request): string | null {
return null;
}
+function extractSessionAccessToken(request: Request): string | null {
+ return extractBearerAccessToken(request) ?? extractCookieSessionAccessToken(request);
+}
+
+async function getUserFromAccessToken(supabase: AdminClient, token: string): Promise {
+ const { data, error } = await supabase.auth.getUser(token);
+ if (error || !data.user?.id) return null;
+ return { id: data.user.id };
+}
+
export class AuthenticationError extends Error {
constructor(message = "Authentication required.") {
super(message);
@@ -72,7 +84,7 @@ export function unauthorizedResponse(error?: AuthenticationError) {
/**
* Resolve the user from the `@supabase/ssr` cookie session. The
* `sb-[-auth-token` cookie it writes is base64-encoded (and chunked when
- * large), which `extractSessionAccessToken`'s plain-JSON parser cannot read, so
+ * large), which `extractCookieSessionAccessToken`'s plain-JSON parser cannot read, so
* this uses the ssr server client to decode + validate it. Returns null when
* the public env is absent or no `sb-` cookie is present.
*/
@@ -102,22 +114,28 @@ async function getUserFromRequestCookies(request: Request): Promise] {
- // 1. Bearer token / legacy cookie (programmatic callers + current clients).
- const token = extractSessionAccessToken(request);
- if (token) {
- const { data, error } = await supabase.auth.getUser(token);
- if (!error && data.user?.id) {
- return { id: data.user.id };
- }
+async function resolveOptionalAuthenticatedUser(
+ request: Request,
+ supabase: AdminClient,
+): Promise {
+ const bearerToken = extractBearerAccessToken(request);
+ if (bearerToken) {
+ const bearerUser = await getUserFromAccessToken(supabase, bearerToken);
+ if (bearerUser) return bearerUser;
}
- // 2. @supabase/ssr cookie session (persistent cookie logins).
- const cookieUser = await getUserFromRequestCookies(request);
- if (cookieUser) {
- return cookieUser;
+ const cookieToken = extractCookieSessionAccessToken(request);
+ if (cookieToken && cookieToken !== bearerToken) {
+ const cookieTokenUser = await getUserFromAccessToken(supabase, cookieToken);
+ if (cookieTokenUser) return cookieTokenUser;
}
+ return getUserFromRequestCookies(request);
+}
+
+export async function requireAuthenticatedUser(request: Request, supabase: AdminClient): Promise {
+ const user = await resolveOptionalAuthenticatedUser(request, supabase);
+ if (user) return user;
throw new AuthenticationError();
}
@@ -125,14 +143,8 @@ export async function getOptionalAuthenticatedUser(
request: Request,
supabase: AdminClient,
): Promise {
- const token = extractSessionAccessToken(request);
- if (token) {
- const { data, error } = await supabase.auth.getUser(token);
- if (error || !data.user?.id) {
- throw new AuthenticationError();
- }
- return { id: data.user.id };
- }
-
- return getUserFromRequestCookies(request);
+ return resolveOptionalAuthenticatedUser(request, supabase);
}
+
+// Retained for callers that only need a single token string.
+export { extractSessionAccessToken };
diff --git a/src/lib/supabase/database.types.ts b/src/lib/supabase/database.types.ts
index 00ecad79b..55db0bd38 100644
--- a/src/lib/supabase/database.types.ts
+++ b/src/lib/supabase/database.types.ts
@@ -2,2632 +2,2622 @@
across many generated table and RPC shapes, so keep Json broad until the call sites
are narrowed coherently. */
/* eslint-disable @typescript-eslint/no-explicit-any */
-export type Json = any
+export type Json = any;
-export type Vector = number[] | string
+export type Vector = number[] | string;
export type Database = {
// Allows to automatically instantiate createClient with right options
// instead of createClient(URL, KEY)
__InternalSupabase: {
- PostgrestVersion: "14.5"
- }
+ PostgrestVersion: "14.5";
+ };
graphql_public: {
Tables: {
- [_ in never]: never
- }
+ [_ in never]: never;
+ };
Views: {
- [_ in never]: never
- }
+ [_ in never]: never;
+ };
Functions: {
graphql: {
Args: {
- extensions?: Json
- operationName?: string
- query?: string
- variables?: Json
- }
- Returns: Json
- }
- }
+ extensions?: Json;
+ operationName?: string;
+ query?: string;
+ variables?: Json;
+ };
+ Returns: Json;
+ };
+ };
Enums: {
- [_ in never]: never
- }
+ [_ in never]: never;
+ };
CompositeTypes: {
- [_ in never]: never
- }
- }
+ [_ in never]: never;
+ };
+ };
public: {
Tables: {
api_rate_limits: {
Row: {
- bucket: string
- owner_id: string
- request_count: number
- updated_at: string
- window_start: string
- }
+ bucket: string;
+ owner_id: string;
+ request_count: number;
+ updated_at: string;
+ window_start: string;
+ };
Insert: {
- bucket: string
- owner_id: string
- request_count?: number
- updated_at?: string
- window_start?: string
- }
+ bucket: string;
+ owner_id: string;
+ request_count?: number;
+ updated_at?: string;
+ window_start?: string;
+ };
Update: {
- bucket?: string
- owner_id?: string
- request_count?: number
- updated_at?: string
- window_start?: string
- }
- Relationships: []
- }
+ bucket?: string;
+ owner_id?: string;
+ request_count?: number;
+ updated_at?: string;
+ window_start?: string;
+ };
+ Relationships: [];
+ };
audit_logs: {
Row: {
- action: string
- created_at: string
- id: string
- metadata: Json
- owner_id: string | null
- resource_id: string | null
- resource_type: string | null
- }
+ action: string;
+ created_at: string;
+ id: string;
+ metadata: Json;
+ owner_id: string | null;
+ resource_id: string | null;
+ resource_type: string | null;
+ };
Insert: {
- action: string
- created_at?: string
- id?: string
- metadata?: Json
- owner_id?: string | null
- resource_id?: string | null
- resource_type?: string | null
- }
+ action: string;
+ created_at?: string;
+ id?: string;
+ metadata?: Json;
+ owner_id?: string | null;
+ resource_id?: string | null;
+ resource_type?: string | null;
+ };
Update: {
- action?: string
- created_at?: string
- id?: string
- metadata?: Json
- owner_id?: string | null
- resource_id?: string | null
- resource_type?: string | null
- }
- Relationships: []
- }
+ action?: string;
+ created_at?: string;
+ id?: string;
+ metadata?: Json;
+ owner_id?: string | null;
+ resource_id?: string | null;
+ resource_type?: string | null;
+ };
+ Relationships: [];
+ };
clinical_registry_record_sources: {
Row: {
- created_at: string
- document_id: string
- id: string
- note: string | null
- owner_id: string
- record_id: string
- }
+ created_at: string;
+ document_id: string;
+ id: string;
+ note: string | null;
+ owner_id: string;
+ record_id: string;
+ };
Insert: {
- created_at?: string
- document_id: string
- id?: string
- note?: string | null
- owner_id: string
- record_id: string
- }
+ created_at?: string;
+ document_id: string;
+ id?: string;
+ note?: string | null;
+ owner_id: string;
+ record_id: string;
+ };
Update: {
- created_at?: string
- document_id?: string
- id?: string
- note?: string | null
- owner_id?: string
- record_id?: string
- }
- Relationships: []
- }
+ created_at?: string;
+ document_id?: string;
+ id?: string;
+ note?: string | null;
+ owner_id?: string;
+ record_id?: string;
+ };
+ Relationships: [];
+ };
clinical_registry_records: {
Row: {
- best_use: string | null
- catalog_payload: Json
- catalogue_label: string | null
- catchments: string[]
- contacts: Json
- cost: string | null
- created_at: string
- criteria: Json
- eligibility: string | null
- id: string
- kind: string
- last_reviewed_at: string | null
- location: string | null
- navigator_query: string | null
- owner_id: string
- primary_contact: Json | null
- referral: string | null
- referral_info: Json
- review_due_at: string | null
- route: string | null
- slug: string
- source: Json
- source_status: string
- status_chips: Json
- subtitle: string | null
- summary_cards: Json
- tags: string[]
- title: string
- updated_at: string
- validation_status: string
- verification: Json
- }
+ best_use: string | null;
+ catalog_payload: Json;
+ catalogue_label: string | null;
+ catchments: string[];
+ contacts: Json;
+ cost: string | null;
+ created_at: string;
+ criteria: Json;
+ eligibility: string | null;
+ id: string;
+ kind: string;
+ last_reviewed_at: string | null;
+ location: string | null;
+ navigator_query: string | null;
+ owner_id: string;
+ primary_contact: Json | null;
+ referral: string | null;
+ referral_info: Json;
+ review_due_at: string | null;
+ route: string | null;
+ slug: string;
+ source: Json;
+ source_status: string;
+ status_chips: Json;
+ subtitle: string | null;
+ summary_cards: Json;
+ tags: string[];
+ title: string;
+ updated_at: string;
+ validation_status: string;
+ verification: Json;
+ };
Insert: {
- best_use?: string | null
- catalog_payload?: Json
- catalogue_label?: string | null
- catchments?: string[]
- contacts?: Json
- cost?: string | null
- created_at?: string
- criteria?: Json
- eligibility?: string | null
- id?: string
- kind: string
- last_reviewed_at?: string | null
- location?: string | null
- navigator_query?: string | null
- owner_id: string
- primary_contact?: Json | null
- referral?: string | null
- referral_info?: Json
- review_due_at?: string | null
- route?: string | null
- slug: string
- source?: Json
- source_status?: string
- status_chips?: Json
- subtitle?: string | null
- summary_cards?: Json
- tags?: string[]
- title: string
- updated_at?: string
- validation_status?: string
- verification?: Json
- }
+ best_use?: string | null;
+ catalog_payload?: Json;
+ catalogue_label?: string | null;
+ catchments?: string[];
+ contacts?: Json;
+ cost?: string | null;
+ created_at?: string;
+ criteria?: Json;
+ eligibility?: string | null;
+ id?: string;
+ kind: string;
+ last_reviewed_at?: string | null;
+ location?: string | null;
+ navigator_query?: string | null;
+ owner_id: string;
+ primary_contact?: Json | null;
+ referral?: string | null;
+ referral_info?: Json;
+ review_due_at?: string | null;
+ route?: string | null;
+ slug: string;
+ source?: Json;
+ source_status?: string;
+ status_chips?: Json;
+ subtitle?: string | null;
+ summary_cards?: Json;
+ tags?: string[];
+ title: string;
+ updated_at?: string;
+ validation_status?: string;
+ verification?: Json;
+ };
Update: {
- best_use?: string | null
- catalog_payload?: Json
- catalogue_label?: string | null
- catchments?: string[]
- contacts?: Json
- cost?: string | null
- created_at?: string
- criteria?: Json
- eligibility?: string | null
- id?: string
- kind?: string
- last_reviewed_at?: string | null
- location?: string | null
- navigator_query?: string | null
- owner_id?: string
- primary_contact?: Json | null
- referral?: string | null
- referral_info?: Json
- review_due_at?: string | null
- route?: string | null
- slug?: string
- source?: Json
- source_status?: string
- status_chips?: Json
- subtitle?: string | null
- summary_cards?: Json
- tags?: string[]
- title?: string
- updated_at?: string
- validation_status?: string
- verification?: Json
- }
- Relationships: []
- }
+ best_use?: string | null;
+ catalog_payload?: Json;
+ catalogue_label?: string | null;
+ catchments?: string[];
+ contacts?: Json;
+ cost?: string | null;
+ created_at?: string;
+ criteria?: Json;
+ eligibility?: string | null;
+ id?: string;
+ kind?: string;
+ last_reviewed_at?: string | null;
+ location?: string | null;
+ navigator_query?: string | null;
+ owner_id?: string;
+ primary_contact?: Json | null;
+ referral?: string | null;
+ referral_info?: Json;
+ review_due_at?: string | null;
+ route?: string | null;
+ slug?: string;
+ source?: Json;
+ source_status?: string;
+ status_chips?: Json;
+ subtitle?: string | null;
+ summary_cards?: Json;
+ tags?: string[];
+ title?: string;
+ updated_at?: string;
+ validation_status?: string;
+ verification?: Json;
+ };
+ Relationships: [];
+ };
differential_records: {
Row: {
- clinical_hinge: string | null
- created_at: string
- id: string
- kind: string
- last_reviewed_at: string | null
- owner_id: string
- payload: Json
- review_due_at: string | null
- slug: string
- source: Json
- source_status: string
- status: string
- subtitle: string | null
- tags: string[]
- title: string
- updated_at: string
- validation_status: string
- }
+ clinical_hinge: string | null;
+ created_at: string;
+ id: string;
+ kind: string;
+ last_reviewed_at: string | null;
+ owner_id: string;
+ payload: Json;
+ review_due_at: string | null;
+ slug: string;
+ source: Json;
+ source_status: string;
+ status: string;
+ subtitle: string | null;
+ tags: string[];
+ title: string;
+ updated_at: string;
+ validation_status: string;
+ };
Insert: {
- clinical_hinge?: string | null
- created_at?: string
- id?: string
- kind: string
- last_reviewed_at?: string | null
- owner_id: string
- payload?: Json
- review_due_at?: string | null
- slug: string
- source?: Json
- source_status?: string
- status: string
- subtitle?: string | null
- tags?: string[]
- title: string
- updated_at?: string
- validation_status?: string
- }
+ clinical_hinge?: string | null;
+ created_at?: string;
+ id?: string;
+ kind: string;
+ last_reviewed_at?: string | null;
+ owner_id: string;
+ payload?: Json;
+ review_due_at?: string | null;
+ slug: string;
+ source?: Json;
+ source_status?: string;
+ status: string;
+ subtitle?: string | null;
+ tags?: string[];
+ title: string;
+ updated_at?: string;
+ validation_status?: string;
+ };
Update: {
- clinical_hinge?: string | null
- created_at?: string
- id?: string
- kind?: string
- last_reviewed_at?: string | null
- owner_id?: string
- payload?: Json
- review_due_at?: string | null
- slug?: string
- source?: Json
- source_status?: string
- status?: string
- subtitle?: string | null
- tags?: string[]
- title?: string
- updated_at?: string
- validation_status?: string
- }
- Relationships: []
- }
+ clinical_hinge?: string | null;
+ created_at?: string;
+ id?: string;
+ kind?: string;
+ last_reviewed_at?: string | null;
+ owner_id?: string;
+ payload?: Json;
+ review_due_at?: string | null;
+ slug?: string;
+ source?: Json;
+ source_status?: string;
+ status?: string;
+ subtitle?: string | null;
+ tags?: string[];
+ title?: string;
+ updated_at?: string;
+ validation_status?: string;
+ };
+ Relationships: [];
+ };
document_chunks: {
Row: {
- anchor_id: string | null
- chunk_index: number
- content: string
- content_hash: string | null
- created_at: string
- document_id: string
- embedding: Vector
- heading_level: number | null
- id: string
- image_ids: string[]
- index_generation_id: string | null
- metadata: Json
- page_number: number | null
- parent_heading: string | null
- retrieval_synopsis: string | null
- search_tsv: unknown
- section_heading: string | null
- section_path: string[]
- token_estimate: number
- }
+ anchor_id: string | null;
+ chunk_index: number;
+ content: string;
+ content_hash: string | null;
+ created_at: string;
+ document_id: string;
+ embedding: Vector;
+ heading_level: number | null;
+ id: string;
+ image_ids: string[];
+ index_generation_id: string | null;
+ metadata: Json;
+ page_number: number | null;
+ parent_heading: string | null;
+ retrieval_synopsis: string | null;
+ search_tsv: unknown;
+ section_heading: string | null;
+ section_path: string[];
+ token_estimate: number;
+ };
Insert: {
- anchor_id?: string | null
- chunk_index: number
- content: string
- content_hash?: string | null
- created_at?: string
- document_id: string
- embedding: Vector
- heading_level?: number | null
- id?: string
- image_ids?: string[]
- index_generation_id?: string | null
- metadata?: Json
- page_number?: number | null
- parent_heading?: string | null
- retrieval_synopsis?: string | null
- search_tsv?: unknown
- section_heading?: string | null
- section_path?: string[]
- token_estimate?: number
- }
+ anchor_id?: string | null;
+ chunk_index: number;
+ content: string;
+ content_hash?: string | null;
+ created_at?: string;
+ document_id: string;
+ embedding: Vector;
+ heading_level?: number | null;
+ id?: string;
+ image_ids?: string[];
+ index_generation_id?: string | null;
+ metadata?: Json;
+ page_number?: number | null;
+ parent_heading?: string | null;
+ retrieval_synopsis?: string | null;
+ search_tsv?: unknown;
+ section_heading?: string | null;
+ section_path?: string[];
+ token_estimate?: number;
+ };
Update: {
- anchor_id?: string | null
- chunk_index?: number
- content?: string
- content_hash?: string | null
- created_at?: string
- document_id?: string
- embedding?: Vector
- heading_level?: number | null
- id?: string
- image_ids?: string[]
- index_generation_id?: string | null
- metadata?: Json
- page_number?: number | null
- parent_heading?: string | null
- retrieval_synopsis?: string | null
- search_tsv?: unknown
- section_heading?: string | null
- section_path?: string[]
- token_estimate?: number
- }
+ anchor_id?: string | null;
+ chunk_index?: number;
+ content?: string;
+ content_hash?: string | null;
+ created_at?: string;
+ document_id?: string;
+ embedding?: Vector;
+ heading_level?: number | null;
+ id?: string;
+ image_ids?: string[];
+ index_generation_id?: string | null;
+ metadata?: Json;
+ page_number?: number | null;
+ parent_heading?: string | null;
+ retrieval_synopsis?: string | null;
+ search_tsv?: unknown;
+ section_heading?: string | null;
+ section_path?: string[];
+ token_estimate?: number;
+ };
Relationships: [
{
- foreignKeyName: "document_chunks_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "document_chunks_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "document_chunks_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "document_chunks_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
document_embedding_fields: {
Row: {
- content: string
- content_hash: string
- created_at: string
- document_id: string
- embedding: Vector
- field_type: string
- id: string
- metadata: Json
- owner_id: string | null
- search_tsv: unknown
- source_chunk_id: string | null
- }
+ content: string;
+ content_hash: string;
+ created_at: string;
+ document_id: string;
+ embedding: Vector;
+ field_type: string;
+ id: string;
+ metadata: Json;
+ owner_id: string | null;
+ search_tsv: unknown;
+ source_chunk_id: string | null;
+ };
Insert: {
- content: string
- content_hash: string
- created_at?: string
- document_id: string
- embedding: Vector
- field_type: string
- id?: string
- metadata?: Json
- owner_id?: string | null
- search_tsv?: unknown
- source_chunk_id?: string | null
- }
+ content: string;
+ content_hash: string;
+ created_at?: string;
+ document_id: string;
+ embedding: Vector;
+ field_type: string;
+ id?: string;
+ metadata?: Json;
+ owner_id?: string | null;
+ search_tsv?: unknown;
+ source_chunk_id?: string | null;
+ };
Update: {
- content?: string
- content_hash?: string
- created_at?: string
- document_id?: string
- embedding?: Vector
- field_type?: string
- id?: string
- metadata?: Json
- owner_id?: string | null
- search_tsv?: unknown
- source_chunk_id?: string | null
- }
+ content?: string;
+ content_hash?: string;
+ created_at?: string;
+ document_id?: string;
+ embedding?: Vector;
+ field_type?: string;
+ id?: string;
+ metadata?: Json;
+ owner_id?: string | null;
+ search_tsv?: unknown;
+ source_chunk_id?: string | null;
+ };
Relationships: [
{
- foreignKeyName: "document_embedding_fields_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "document_embedding_fields_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "document_embedding_fields_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "document_embedding_fields_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
{
- foreignKeyName: "document_embedding_fields_source_chunk_id_fkey"
- columns: ["source_chunk_id"]
- isOneToOne: false
- referencedRelation: "document_chunks"
- referencedColumns: ["id"]
+ foreignKeyName: "document_embedding_fields_source_chunk_id_fkey";
+ columns: ["source_chunk_id"];
+ isOneToOne: false;
+ referencedRelation: "document_chunks";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
document_images: {
Row: {
- bbox: Json | null
- caption: string
- caption_confidence: number | null
- clinical_priority_score: number | null
- clinical_relevance_score: number
- created_at: string
- crop_completeness: number | null
- document_id: string
- height: number | null
- id: string
- image_hash: string | null
- image_quality_score: number | null
- image_type: string
- labels: string[]
- metadata: Json
- mime_type: string
- ocr_text_density: number | null
- page_number: number | null
- perceptual_hash: string | null
- searchable: boolean
- skip_reason: string | null
- source_kind: string
- storage_path: string
- structured_extraction_confidence: number | null
- visual_duplicate_group: string | null
- width: number | null
- }
+ bbox: Json | null;
+ caption: string;
+ caption_confidence: number | null;
+ clinical_priority_score: number | null;
+ clinical_relevance_score: number;
+ created_at: string;
+ crop_completeness: number | null;
+ document_id: string;
+ height: number | null;
+ id: string;
+ image_hash: string | null;
+ image_quality_score: number | null;
+ image_type: string;
+ labels: string[];
+ metadata: Json;
+ mime_type: string;
+ ocr_text_density: number | null;
+ page_number: number | null;
+ perceptual_hash: string | null;
+ searchable: boolean;
+ skip_reason: string | null;
+ source_kind: string;
+ storage_path: string;
+ structured_extraction_confidence: number | null;
+ visual_duplicate_group: string | null;
+ width: number | null;
+ };
Insert: {
- bbox?: Json | null
- caption?: string
- caption_confidence?: number | null
- clinical_priority_score?: number | null
- clinical_relevance_score?: number
- created_at?: string
- crop_completeness?: number | null
- document_id: string
- height?: number | null
- id?: string
- image_hash?: string | null
- image_quality_score?: number | null
- image_type?: string
- labels?: string[]
- metadata?: Json
- mime_type?: string
- ocr_text_density?: number | null
- page_number?: number | null
- perceptual_hash?: string | null
- searchable?: boolean
- skip_reason?: string | null
- source_kind?: string
- storage_path: string
- structured_extraction_confidence?: number | null
- visual_duplicate_group?: string | null
- width?: number | null
- }
+ bbox?: Json | null;
+ caption?: string;
+ caption_confidence?: number | null;
+ clinical_priority_score?: number | null;
+ clinical_relevance_score?: number;
+ created_at?: string;
+ crop_completeness?: number | null;
+ document_id: string;
+ height?: number | null;
+ id?: string;
+ image_hash?: string | null;
+ image_quality_score?: number | null;
+ image_type?: string;
+ labels?: string[];
+ metadata?: Json;
+ mime_type?: string;
+ ocr_text_density?: number | null;
+ page_number?: number | null;
+ perceptual_hash?: string | null;
+ searchable?: boolean;
+ skip_reason?: string | null;
+ source_kind?: string;
+ storage_path: string;
+ structured_extraction_confidence?: number | null;
+ visual_duplicate_group?: string | null;
+ width?: number | null;
+ };
Update: {
- bbox?: Json | null
- caption?: string
- caption_confidence?: number | null
- clinical_priority_score?: number | null
- clinical_relevance_score?: number
- created_at?: string
- crop_completeness?: number | null
- document_id?: string
- height?: number | null
- id?: string
- image_hash?: string | null
- image_quality_score?: number | null
- image_type?: string
- labels?: string[]
- metadata?: Json
- mime_type?: string
- ocr_text_density?: number | null
- page_number?: number | null
- perceptual_hash?: string | null
- searchable?: boolean
- skip_reason?: string | null
- source_kind?: string
- storage_path?: string
- structured_extraction_confidence?: number | null
- visual_duplicate_group?: string | null
- width?: number | null
- }
+ bbox?: Json | null;
+ caption?: string;
+ caption_confidence?: number | null;
+ clinical_priority_score?: number | null;
+ clinical_relevance_score?: number;
+ created_at?: string;
+ crop_completeness?: number | null;
+ document_id?: string;
+ height?: number | null;
+ id?: string;
+ image_hash?: string | null;
+ image_quality_score?: number | null;
+ image_type?: string;
+ labels?: string[];
+ metadata?: Json;
+ mime_type?: string;
+ ocr_text_density?: number | null;
+ page_number?: number | null;
+ perceptual_hash?: string | null;
+ searchable?: boolean;
+ skip_reason?: string | null;
+ source_kind?: string;
+ storage_path?: string;
+ structured_extraction_confidence?: number | null;
+ visual_duplicate_group?: string | null;
+ width?: number | null;
+ };
Relationships: [
{
- foreignKeyName: "document_images_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "document_images_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "document_images_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "document_images_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
document_index_quality: {
Row: {
- anchor_coverage: number | null
- document_id: string
- extraction_quality: string
- issues: string[]
- metrics: Json
- model_fallback_rate: number | null
- noisy_unit_rate: number | null
- owner_id: string | null
- quality_score: number
- retrievable_visual_hit: boolean | null
- source_span_coverage: number | null
- typed_unit_coverage: number | null
- updated_at: string
- }
+ anchor_coverage: number | null;
+ document_id: string;
+ extraction_quality: string;
+ issues: string[];
+ metrics: Json;
+ model_fallback_rate: number | null;
+ noisy_unit_rate: number | null;
+ owner_id: string | null;
+ quality_score: number;
+ retrievable_visual_hit: boolean | null;
+ source_span_coverage: number | null;
+ typed_unit_coverage: number | null;
+ updated_at: string;
+ };
Insert: {
- anchor_coverage?: number | null
- document_id: string
- extraction_quality?: string
- issues?: string[]
- metrics?: Json
- model_fallback_rate?: number | null
- noisy_unit_rate?: number | null
- owner_id?: string | null
- quality_score?: number
- retrievable_visual_hit?: boolean | null
- source_span_coverage?: number | null
- typed_unit_coverage?: number | null
- updated_at?: string
- }
+ anchor_coverage?: number | null;
+ document_id: string;
+ extraction_quality?: string;
+ issues?: string[];
+ metrics?: Json;
+ model_fallback_rate?: number | null;
+ noisy_unit_rate?: number | null;
+ owner_id?: string | null;
+ quality_score?: number;
+ retrievable_visual_hit?: boolean | null;
+ source_span_coverage?: number | null;
+ typed_unit_coverage?: number | null;
+ updated_at?: string;
+ };
Update: {
- anchor_coverage?: number | null
- document_id?: string
- extraction_quality?: string
- issues?: string[]
- metrics?: Json
- model_fallback_rate?: number | null
- noisy_unit_rate?: number | null
- owner_id?: string | null
- quality_score?: number
- retrievable_visual_hit?: boolean | null
- source_span_coverage?: number | null
- typed_unit_coverage?: number | null
- updated_at?: string
- }
+ anchor_coverage?: number | null;
+ document_id?: string;
+ extraction_quality?: string;
+ issues?: string[];
+ metrics?: Json;
+ model_fallback_rate?: number | null;
+ noisy_unit_rate?: number | null;
+ owner_id?: string | null;
+ quality_score?: number;
+ retrievable_visual_hit?: boolean | null;
+ source_span_coverage?: number | null;
+ typed_unit_coverage?: number | null;
+ updated_at?: string;
+ };
Relationships: [
{
- foreignKeyName: "document_index_quality_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: true
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "document_index_quality_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: true;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "document_index_quality_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: true
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "document_index_quality_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: true;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
document_index_units: {
Row: {
- content: string
- created_at: string
- document_id: string
- embedding: Vector
- extraction_mode: string
- heading_path: string[]
- id: string
- metadata: Json
- normalized_terms: string[]
- owner_id: string | null
- page_end: number | null
- page_start: number | null
- quality_score: number
- search_tsv: unknown
- source_chunk_id: string | null
- source_image_id: string | null
- source_span: Json | null
- title: string
- unit_type: string
- updated_at: string
- }
+ content: string;
+ created_at: string;
+ document_id: string;
+ embedding: Vector;
+ extraction_mode: string;
+ heading_path: string[];
+ id: string;
+ metadata: Json;
+ normalized_terms: string[];
+ owner_id: string | null;
+ page_end: number | null;
+ page_start: number | null;
+ quality_score: number;
+ search_tsv: unknown;
+ source_chunk_id: string | null;
+ source_image_id: string | null;
+ source_span: Json | null;
+ title: string;
+ unit_type: string;
+ updated_at: string;
+ };
Insert: {
- content: string
- created_at?: string
- document_id: string
- embedding: Vector
- extraction_mode?: string
- heading_path?: string[]
- id?: string
- metadata?: Json
- normalized_terms?: string[]
- owner_id?: string | null
- page_end?: number | null
- page_start?: number | null
- quality_score?: number
- search_tsv?: unknown
- source_chunk_id?: string | null
- source_image_id?: string | null
- source_span?: Json | null
- title: string
- unit_type: string
- updated_at?: string
- }
+ content: string;
+ created_at?: string;
+ document_id: string;
+ embedding: Vector;
+ extraction_mode?: string;
+ heading_path?: string[];
+ id?: string;
+ metadata?: Json;
+ normalized_terms?: string[];
+ owner_id?: string | null;
+ page_end?: number | null;
+ page_start?: number | null;
+ quality_score?: number;
+ search_tsv?: unknown;
+ source_chunk_id?: string | null;
+ source_image_id?: string | null;
+ source_span?: Json | null;
+ title: string;
+ unit_type: string;
+ updated_at?: string;
+ };
Update: {
- content?: string
- created_at?: string
- document_id?: string
- embedding?: Vector
- extraction_mode?: string
- heading_path?: string[]
- id?: string
- metadata?: Json
- normalized_terms?: string[]
- owner_id?: string | null
- page_end?: number | null
- page_start?: number | null
- quality_score?: number
- search_tsv?: unknown
- source_chunk_id?: string | null
- source_image_id?: string | null
- source_span?: Json | null
- title?: string
- unit_type?: string
- updated_at?: string
- }
+ content?: string;
+ created_at?: string;
+ document_id?: string;
+ embedding?: Vector;
+ extraction_mode?: string;
+ heading_path?: string[];
+ id?: string;
+ metadata?: Json;
+ normalized_terms?: string[];
+ owner_id?: string | null;
+ page_end?: number | null;
+ page_start?: number | null;
+ quality_score?: number;
+ search_tsv?: unknown;
+ source_chunk_id?: string | null;
+ source_image_id?: string | null;
+ source_span?: Json | null;
+ title?: string;
+ unit_type?: string;
+ updated_at?: string;
+ };
Relationships: [
{
- foreignKeyName: "document_index_units_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "document_index_units_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "document_index_units_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "document_index_units_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
{
- foreignKeyName: "document_index_units_source_chunk_id_fkey"
- columns: ["source_chunk_id"]
- isOneToOne: false
- referencedRelation: "document_chunks"
- referencedColumns: ["id"]
+ foreignKeyName: "document_index_units_source_chunk_id_fkey";
+ columns: ["source_chunk_id"];
+ isOneToOne: false;
+ referencedRelation: "document_chunks";
+ referencedColumns: ["id"];
},
{
- foreignKeyName: "document_index_units_source_image_id_fkey"
- columns: ["source_image_id"]
- isOneToOne: false
- referencedRelation: "document_images"
- referencedColumns: ["id"]
+ foreignKeyName: "document_index_units_source_image_id_fkey";
+ columns: ["source_image_id"];
+ isOneToOne: false;
+ referencedRelation: "document_images";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
document_labels: {
Row: {
- confidence: number
- created_at: string
- document_id: string
- id: string
- label: string
- label_type: string
- metadata: Json
- owner_id: string | null
- source: string
- updated_at: string
- }
+ confidence: number;
+ created_at: string;
+ document_id: string;
+ id: string;
+ label: string;
+ label_type: string;
+ metadata: Json;
+ owner_id: string | null;
+ source: string;
+ updated_at: string;
+ };
Insert: {
- confidence?: number
- created_at?: string
- document_id: string
- id?: string
- label: string
- label_type: string
- metadata?: Json
- owner_id?: string | null
- source?: string
- updated_at?: string
- }
+ confidence?: number;
+ created_at?: string;
+ document_id: string;
+ id?: string;
+ label: string;
+ label_type: string;
+ metadata?: Json;
+ owner_id?: string | null;
+ source?: string;
+ updated_at?: string;
+ };
Update: {
- confidence?: number
- created_at?: string
- document_id?: string
- id?: string
- label?: string
- label_type?: string
- metadata?: Json
- owner_id?: string | null
- source?: string
- updated_at?: string
- }
+ confidence?: number;
+ created_at?: string;
+ document_id?: string;
+ id?: string;
+ label?: string;
+ label_type?: string;
+ metadata?: Json;
+ owner_id?: string | null;
+ source?: string;
+ updated_at?: string;
+ };
Relationships: [
{
- foreignKeyName: "document_labels_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "document_labels_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "document_labels_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "document_labels_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
document_memory_cards: {
Row: {
- card_type: string
- confidence: number
- content: string
- created_at: string
- document_id: string
- embedding: Vector
- id: string
- metadata: Json
- normalized_terms: string[]
- owner_id: string | null
- page_number: number | null
- search_tsv: unknown
- section_id: string | null
- source_chunk_ids: string[]
- source_image_ids: string[]
- title: string
- updated_at: string
- }
+ card_type: string;
+ confidence: number;
+ content: string;
+ created_at: string;
+ document_id: string;
+ embedding: Vector;
+ id: string;
+ metadata: Json;
+ normalized_terms: string[];
+ owner_id: string | null;
+ page_number: number | null;
+ search_tsv: unknown;
+ section_id: string | null;
+ source_chunk_ids: string[];
+ source_image_ids: string[];
+ title: string;
+ updated_at: string;
+ };
Insert: {
- card_type: string
- confidence?: number
- content: string
- created_at?: string
- document_id: string
- embedding: Vector
- id?: string
- metadata?: Json
- normalized_terms?: string[]
- owner_id?: string | null
- page_number?: number | null
- search_tsv?: unknown
- section_id?: string | null
- source_chunk_ids?: string[]
- source_image_ids?: string[]
- title: string
- updated_at?: string
- }
+ card_type: string;
+ confidence?: number;
+ content: string;
+ created_at?: string;
+ document_id: string;
+ embedding: Vector;
+ id?: string;
+ metadata?: Json;
+ normalized_terms?: string[];
+ owner_id?: string | null;
+ page_number?: number | null;
+ search_tsv?: unknown;
+ section_id?: string | null;
+ source_chunk_ids?: string[];
+ source_image_ids?: string[];
+ title: string;
+ updated_at?: string;
+ };
Update: {
- card_type?: string
- confidence?: number
- content?: string
- created_at?: string
- document_id?: string
- embedding?: Vector
- id?: string
- metadata?: Json
- normalized_terms?: string[]
- owner_id?: string | null
- page_number?: number | null
- search_tsv?: unknown
- section_id?: string | null
- source_chunk_ids?: string[]
- source_image_ids?: string[]
- title?: string
- updated_at?: string
- }
+ card_type?: string;
+ confidence?: number;
+ content?: string;
+ created_at?: string;
+ document_id?: string;
+ embedding?: Vector;
+ id?: string;
+ metadata?: Json;
+ normalized_terms?: string[];
+ owner_id?: string | null;
+ page_number?: number | null;
+ search_tsv?: unknown;
+ section_id?: string | null;
+ source_chunk_ids?: string[];
+ source_image_ids?: string[];
+ title?: string;
+ updated_at?: string;
+ };
Relationships: [
{
- foreignKeyName: "document_memory_cards_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "document_memory_cards_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "document_memory_cards_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "document_memory_cards_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
{
- foreignKeyName: "document_memory_cards_section_id_fkey"
- columns: ["section_id"]
- isOneToOne: false
- referencedRelation: "document_sections"
- referencedColumns: ["id"]
+ foreignKeyName: "document_memory_cards_section_id_fkey";
+ columns: ["section_id"];
+ isOneToOne: false;
+ referencedRelation: "document_sections";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
document_pages: {
Row: {
- created_at: string
- document_id: string
- id: string
- metadata: Json
- ocr_used: boolean
- page_number: number
- text: string
- }
+ created_at: string;
+ document_id: string;
+ id: string;
+ metadata: Json;
+ ocr_used: boolean;
+ page_number: number;
+ text: string;
+ };
Insert: {
- created_at?: string
- document_id: string
- id?: string
- metadata?: Json
- ocr_used?: boolean
- page_number: number
- text?: string
- }
+ created_at?: string;
+ document_id: string;
+ id?: string;
+ metadata?: Json;
+ ocr_used?: boolean;
+ page_number: number;
+ text?: string;
+ };
Update: {
- created_at?: string
- document_id?: string
- id?: string
- metadata?: Json
- ocr_used?: boolean
- page_number?: number
- text?: string
- }
+ created_at?: string;
+ document_id?: string;
+ id?: string;
+ metadata?: Json;
+ ocr_used?: boolean;
+ page_number?: number;
+ text?: string;
+ };
Relationships: [
{
- foreignKeyName: "document_pages_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "document_pages_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "document_pages_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "document_pages_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
document_sections: {
Row: {
- chunk_ids: string[]
- created_at: string
- document_id: string
- extraction_quality: string
- heading: string
- heading_path: string[]
- id: string
- metadata: Json
- owner_id: string | null
- page_end: number | null
- page_start: number | null
- section_index: number
- summary: string
- tags: string[]
- updated_at: string
- }
+ chunk_ids: string[];
+ created_at: string;
+ document_id: string;
+ extraction_quality: string;
+ heading: string;
+ heading_path: string[];
+ id: string;
+ metadata: Json;
+ owner_id: string | null;
+ page_end: number | null;
+ page_start: number | null;
+ section_index: number;
+ summary: string;
+ tags: string[];
+ updated_at: string;
+ };
Insert: {
- chunk_ids?: string[]
- created_at?: string
- document_id: string
- extraction_quality?: string
- heading: string
- heading_path?: string[]
- id?: string
- metadata?: Json
- owner_id?: string | null
- page_end?: number | null
- page_start?: number | null
- section_index: number
- summary?: string
- tags?: string[]
- updated_at?: string
- }
+ chunk_ids?: string[];
+ created_at?: string;
+ document_id: string;
+ extraction_quality?: string;
+ heading: string;
+ heading_path?: string[];
+ id?: string;
+ metadata?: Json;
+ owner_id?: string | null;
+ page_end?: number | null;
+ page_start?: number | null;
+ section_index: number;
+ summary?: string;
+ tags?: string[];
+ updated_at?: string;
+ };
Update: {
- chunk_ids?: string[]
- created_at?: string
- document_id?: string
- extraction_quality?: string
- heading?: string
- heading_path?: string[]
- id?: string
- metadata?: Json
- owner_id?: string | null
- page_end?: number | null
- page_start?: number | null
- section_index?: number
- summary?: string
- tags?: string[]
- updated_at?: string
- }
+ chunk_ids?: string[];
+ created_at?: string;
+ document_id?: string;
+ extraction_quality?: string;
+ heading?: string;
+ heading_path?: string[];
+ id?: string;
+ metadata?: Json;
+ owner_id?: string | null;
+ page_end?: number | null;
+ page_start?: number | null;
+ section_index?: number;
+ summary?: string;
+ tags?: string[];
+ updated_at?: string;
+ };
Relationships: [
{
- foreignKeyName: "document_sections_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "document_sections_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "document_sections_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "document_sections_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
document_summaries: {
Row: {
- clinical_specifics: Json
- created_at: string
- document_id: string
- generated_at: string
- id: string
- metadata: Json
- model: string | null
- owner_id: string | null
- source_chunk_ids: string[]
- source_image_ids: string[]
- summary: string
- updated_at: string
- }
+ clinical_specifics: Json;
+ created_at: string;
+ document_id: string;
+ generated_at: string;
+ id: string;
+ metadata: Json;
+ model: string | null;
+ owner_id: string | null;
+ source_chunk_ids: string[];
+ source_image_ids: string[];
+ summary: string;
+ updated_at: string;
+ };
Insert: {
- clinical_specifics?: Json
- created_at?: string
- document_id: string
- generated_at?: string
- id?: string
- metadata?: Json
- model?: string | null
- owner_id?: string | null
- source_chunk_ids?: string[]
- source_image_ids?: string[]
- summary: string
- updated_at?: string
- }
+ clinical_specifics?: Json;
+ created_at?: string;
+ document_id: string;
+ generated_at?: string;
+ id?: string;
+ metadata?: Json;
+ model?: string | null;
+ owner_id?: string | null;
+ source_chunk_ids?: string[];
+ source_image_ids?: string[];
+ summary: string;
+ updated_at?: string;
+ };
Update: {
- clinical_specifics?: Json
- created_at?: string
- document_id?: string
- generated_at?: string
- id?: string
- metadata?: Json
- model?: string | null
- owner_id?: string | null
- source_chunk_ids?: string[]
- source_image_ids?: string[]
- summary?: string
- updated_at?: string
- }
+ clinical_specifics?: Json;
+ created_at?: string;
+ document_id?: string;
+ generated_at?: string;
+ id?: string;
+ metadata?: Json;
+ model?: string | null;
+ owner_id?: string | null;
+ source_chunk_ids?: string[];
+ source_image_ids?: string[];
+ summary?: string;
+ updated_at?: string;
+ };
Relationships: [
{
- foreignKeyName: "document_summaries_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: true
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "document_summaries_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: true;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "document_summaries_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: true
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "document_summaries_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: true;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
document_table_facts: {
Row: {
- action: string | null
- clinical_parameter: string | null
- created_at: string
- document_id: string
- id: string
- metadata: Json
- normalized_terms: string[]
- owner_id: string | null
- page_number: number | null
- row_label: string | null
- search_tsv: unknown
- source_chunk_id: string | null
- source_image_id: string | null
- table_title: string | null
- threshold_value: string | null
- }
+ action: string | null;
+ clinical_parameter: string | null;
+ created_at: string;
+ document_id: string;
+ id: string;
+ metadata: Json;
+ normalized_terms: string[];
+ owner_id: string | null;
+ page_number: number | null;
+ row_label: string | null;
+ search_tsv: unknown;
+ source_chunk_id: string | null;
+ source_image_id: string | null;
+ table_title: string | null;
+ threshold_value: string | null;
+ };
Insert: {
- action?: string | null
- clinical_parameter?: string | null
- created_at?: string
- document_id: string
- id?: string
- metadata?: Json
- normalized_terms?: string[]
- owner_id?: string | null
- page_number?: number | null
- row_label?: string | null
- search_tsv?: unknown
- source_chunk_id?: string | null
- source_image_id?: string | null
- table_title?: string | null
- threshold_value?: string | null
- }
+ action?: string | null;
+ clinical_parameter?: string | null;
+ created_at?: string;
+ document_id: string;
+ id?: string;
+ metadata?: Json;
+ normalized_terms?: string[];
+ owner_id?: string | null;
+ page_number?: number | null;
+ row_label?: string | null;
+ search_tsv?: unknown;
+ source_chunk_id?: string | null;
+ source_image_id?: string | null;
+ table_title?: string | null;
+ threshold_value?: string | null;
+ };
Update: {
- action?: string | null
- clinical_parameter?: string | null
- created_at?: string
- document_id?: string
- id?: string
- metadata?: Json
- normalized_terms?: string[]
- owner_id?: string | null
- page_number?: number | null
- row_label?: string | null
- search_tsv?: unknown
- source_chunk_id?: string | null
- source_image_id?: string | null
- table_title?: string | null
- threshold_value?: string | null
- }
+ action?: string | null;
+ clinical_parameter?: string | null;
+ created_at?: string;
+ document_id?: string;
+ id?: string;
+ metadata?: Json;
+ normalized_terms?: string[];
+ owner_id?: string | null;
+ page_number?: number | null;
+ row_label?: string | null;
+ search_tsv?: unknown;
+ source_chunk_id?: string | null;
+ source_image_id?: string | null;
+ table_title?: string | null;
+ threshold_value?: string | null;
+ };
Relationships: [
{
- foreignKeyName: "document_table_facts_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "document_table_facts_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "document_table_facts_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "document_table_facts_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
{
- foreignKeyName: "document_table_facts_source_chunk_id_fkey"
- columns: ["source_chunk_id"]
- isOneToOne: false
- referencedRelation: "document_chunks"
- referencedColumns: ["id"]
+ foreignKeyName: "document_table_facts_source_chunk_id_fkey";
+ columns: ["source_chunk_id"];
+ isOneToOne: false;
+ referencedRelation: "document_chunks";
+ referencedColumns: ["id"];
},
{
- foreignKeyName: "document_table_facts_source_image_id_fkey"
- columns: ["source_image_id"]
- isOneToOne: false
- referencedRelation: "document_images"
- referencedColumns: ["id"]
+ foreignKeyName: "document_table_facts_source_image_id_fkey";
+ columns: ["source_image_id"];
+ isOneToOne: false;
+ referencedRelation: "document_images";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
documents: {
Row: {
- chunk_count: number
- content_hash: string | null
- created_at: string
- description: string | null
- error_message: string | null
- file_name: string
- file_size: number
- file_type: string
- id: string
- image_count: number
- import_batch_id: string | null
- metadata: Json
- owner_id: string | null
- page_count: number
- search_tsv: unknown
- source_path: string | null
- status: string
- storage_path: string
- title: string
- title_search_tsv: unknown
- updated_at: string
- }
+ chunk_count: number;
+ content_hash: string | null;
+ created_at: string;
+ description: string | null;
+ error_message: string | null;
+ file_name: string;
+ file_size: number;
+ file_type: string;
+ id: string;
+ image_count: number;
+ import_batch_id: string | null;
+ metadata: Json;
+ owner_id: string | null;
+ page_count: number;
+ search_tsv: unknown;
+ source_path: string | null;
+ status: string;
+ storage_path: string;
+ title: string;
+ title_search_tsv: unknown;
+ updated_at: string;
+ };
Insert: {
- chunk_count?: number
- content_hash?: string | null
- created_at?: string
- description?: string | null
- error_message?: string | null
- file_name: string
- file_size?: number
- file_type: string
- id?: string
- image_count?: number
- import_batch_id?: string | null
- metadata?: Json
- owner_id?: string | null
- page_count?: number
- search_tsv?: unknown
- source_path?: string | null
- status?: string
- storage_path: string
- title: string
- title_search_tsv?: unknown
- updated_at?: string
- }
+ chunk_count?: number;
+ content_hash?: string | null;
+ created_at?: string;
+ description?: string | null;
+ error_message?: string | null;
+ file_name: string;
+ file_size?: number;
+ file_type: string;
+ id?: string;
+ image_count?: number;
+ import_batch_id?: string | null;
+ metadata?: Json;
+ owner_id?: string | null;
+ page_count?: number;
+ search_tsv?: unknown;
+ source_path?: string | null;
+ status?: string;
+ storage_path: string;
+ title: string;
+ title_search_tsv?: unknown;
+ updated_at?: string;
+ };
Update: {
- chunk_count?: number
- content_hash?: string | null
- created_at?: string
- description?: string | null
- error_message?: string | null
- file_name?: string
- file_size?: number
- file_type?: string
- id?: string
- image_count?: number
- import_batch_id?: string | null
- metadata?: Json
- owner_id?: string | null
- page_count?: number
- search_tsv?: unknown
- source_path?: string | null
- status?: string
- storage_path?: string
- title?: string
- title_search_tsv?: unknown
- updated_at?: string
- }
+ chunk_count?: number;
+ content_hash?: string | null;
+ created_at?: string;
+ description?: string | null;
+ error_message?: string | null;
+ file_name?: string;
+ file_size?: number;
+ file_type?: string;
+ id?: string;
+ image_count?: number;
+ import_batch_id?: string | null;
+ metadata?: Json;
+ owner_id?: string | null;
+ page_count?: number;
+ search_tsv?: unknown;
+ source_path?: string | null;
+ status?: string;
+ storage_path?: string;
+ title?: string;
+ title_search_tsv?: unknown;
+ updated_at?: string;
+ };
Relationships: [
{
- foreignKeyName: "documents_import_batch_id_fkey"
- columns: ["import_batch_id"]
- isOneToOne: false
- referencedRelation: "import_batches"
- referencedColumns: ["id"]
+ foreignKeyName: "documents_import_batch_id_fkey";
+ columns: ["import_batch_id"];
+ isOneToOne: false;
+ referencedRelation: "import_batches";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
image_caption_cache: {
Row: {
- caption: string
- created_at: string
- id: string
- image_hash: string
- metadata: Json
- mime_type: string | null
- model: string
- owner_id: string
- updated_at: string
- }
+ caption: string;
+ created_at: string;
+ id: string;
+ image_hash: string;
+ metadata: Json;
+ mime_type: string | null;
+ model: string;
+ owner_id: string;
+ updated_at: string;
+ };
Insert: {
- caption: string
- created_at?: string
- id?: string
- image_hash: string
- metadata?: Json
- mime_type?: string | null
- model: string
- owner_id: string
- updated_at?: string
- }
+ caption: string;
+ created_at?: string;
+ id?: string;
+ image_hash: string;
+ metadata?: Json;
+ mime_type?: string | null;
+ model: string;
+ owner_id: string;
+ updated_at?: string;
+ };
Update: {
- caption?: string
- created_at?: string
- id?: string
- image_hash?: string
- metadata?: Json
- mime_type?: string | null
- model?: string
- owner_id?: string
- updated_at?: string
- }
- Relationships: []
- }
+ caption?: string;
+ created_at?: string;
+ id?: string;
+ image_hash?: string;
+ metadata?: Json;
+ mime_type?: string | null;
+ model?: string;
+ owner_id?: string;
+ updated_at?: string;
+ };
+ Relationships: [];
+ };
import_batches: {
Row: {
- completed_at: string | null
- created_at: string
- failed_files: number
- id: string
- include_glob: string
- metadata: Json
- name: string
- owner_id: string | null
- queued_files: number
- skipped_files: number
- source_root: string | null
- status: string
- total_bytes: number
- total_files: number
- updated_at: string
- }
+ completed_at: string | null;
+ created_at: string;
+ failed_files: number;
+ id: string;
+ include_glob: string;
+ metadata: Json;
+ name: string;
+ owner_id: string | null;
+ queued_files: number;
+ skipped_files: number;
+ source_root: string | null;
+ status: string;
+ total_bytes: number;
+ total_files: number;
+ updated_at: string;
+ };
Insert: {
- completed_at?: string | null
- created_at?: string
- failed_files?: number
- id?: string
- include_glob?: string
- metadata?: Json
- name: string
- owner_id?: string | null
- queued_files?: number
- skipped_files?: number
- source_root?: string | null
- status?: string
- total_bytes?: number
- total_files?: number
- updated_at?: string
- }
+ completed_at?: string | null;
+ created_at?: string;
+ failed_files?: number;
+ id?: string;
+ include_glob?: string;
+ metadata?: Json;
+ name: string;
+ owner_id?: string | null;
+ queued_files?: number;
+ skipped_files?: number;
+ source_root?: string | null;
+ status?: string;
+ total_bytes?: number;
+ total_files?: number;
+ updated_at?: string;
+ };
Update: {
- completed_at?: string | null
- created_at?: string
- failed_files?: number
- id?: string
- include_glob?: string
- metadata?: Json
- name?: string
- owner_id?: string | null
- queued_files?: number
- skipped_files?: number
- source_root?: string | null
- status?: string
- total_bytes?: number
- total_files?: number
- updated_at?: string
- }
- Relationships: []
- }
+ completed_at?: string | null;
+ created_at?: string;
+ failed_files?: number;
+ id?: string;
+ include_glob?: string;
+ metadata?: Json;
+ name?: string;
+ owner_id?: string | null;
+ queued_files?: number;
+ skipped_files?: number;
+ source_root?: string | null;
+ status?: string;
+ total_bytes?: number;
+ total_files?: number;
+ updated_at?: string;
+ };
+ Relationships: [];
+ };
ingestion_job_stages: {
Row: {
- artifact_counts: Json
- created_at: string
- document_id: string
- error_class: string | null
- error_message: string | null
- finished_at: string | null
- id: string
- job_id: string
- metadata: Json
- retry_count: number
- stage_name: string
- stage_status: string
- started_at: string
- }
+ artifact_counts: Json;
+ created_at: string;
+ document_id: string;
+ error_class: string | null;
+ error_message: string | null;
+ finished_at: string | null;
+ id: string;
+ job_id: string;
+ metadata: Json;
+ retry_count: number;
+ stage_name: string;
+ stage_status: string;
+ started_at: string;
+ };
Insert: {
- artifact_counts?: Json
- created_at?: string
- document_id: string
- error_class?: string | null
- error_message?: string | null
- finished_at?: string | null
- id?: string
- job_id: string
- metadata?: Json
- retry_count?: number
- stage_name: string
- stage_status?: string
- started_at?: string
- }
+ artifact_counts?: Json;
+ created_at?: string;
+ document_id: string;
+ error_class?: string | null;
+ error_message?: string | null;
+ finished_at?: string | null;
+ id?: string;
+ job_id: string;
+ metadata?: Json;
+ retry_count?: number;
+ stage_name: string;
+ stage_status?: string;
+ started_at?: string;
+ };
Update: {
- artifact_counts?: Json
- created_at?: string
- document_id?: string
- error_class?: string | null
- error_message?: string | null
- finished_at?: string | null
- id?: string
- job_id?: string
- metadata?: Json
- retry_count?: number
- stage_name?: string
- stage_status?: string
- started_at?: string
- }
+ artifact_counts?: Json;
+ created_at?: string;
+ document_id?: string;
+ error_class?: string | null;
+ error_message?: string | null;
+ finished_at?: string | null;
+ id?: string;
+ job_id?: string;
+ metadata?: Json;
+ retry_count?: number;
+ stage_name?: string;
+ stage_status?: string;
+ started_at?: string;
+ };
Relationships: [
{
- foreignKeyName: "ingestion_job_stages_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "ingestion_job_stages_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "ingestion_job_stages_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "ingestion_job_stages_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
ingestion_jobs: {
Row: {
- attempt_count: number
- batch_id: string | null
- completed_at: string | null
- created_at: string
- document_id: string
- error_message: string | null
- id: string
- locked_at: string | null
- locked_by: string | null
- max_attempts: number
- next_run_at: string
- progress: number
- stage: string
- started_at: string | null
- status: string
- updated_at: string
- }
+ attempt_count: number;
+ batch_id: string | null;
+ completed_at: string | null;
+ created_at: string;
+ document_id: string;
+ error_message: string | null;
+ id: string;
+ locked_at: string | null;
+ locked_by: string | null;
+ max_attempts: number;
+ next_run_at: string;
+ progress: number;
+ stage: string;
+ started_at: string | null;
+ status: string;
+ updated_at: string;
+ };
Insert: {
- attempt_count?: number
- batch_id?: string | null
- completed_at?: string | null
- created_at?: string
- document_id: string
- error_message?: string | null
- id?: string
- locked_at?: string | null
- locked_by?: string | null
- max_attempts?: number
- next_run_at?: string
- progress?: number
- stage?: string
- started_at?: string | null
- status?: string
- updated_at?: string
- }
+ attempt_count?: number;
+ batch_id?: string | null;
+ completed_at?: string | null;
+ created_at?: string;
+ document_id: string;
+ error_message?: string | null;
+ id?: string;
+ locked_at?: string | null;
+ locked_by?: string | null;
+ max_attempts?: number;
+ next_run_at?: string;
+ progress?: number;
+ stage?: string;
+ started_at?: string | null;
+ status?: string;
+ updated_at?: string;
+ };
Update: {
- attempt_count?: number
- batch_id?: string | null
- completed_at?: string | null
- created_at?: string
- document_id?: string
- error_message?: string | null
- id?: string
- locked_at?: string | null
- locked_by?: string | null
- max_attempts?: number
- next_run_at?: string
- progress?: number
- stage?: string
- started_at?: string | null
- status?: string
- updated_at?: string
- }
+ attempt_count?: number;
+ batch_id?: string | null;
+ completed_at?: string | null;
+ created_at?: string;
+ document_id?: string;
+ error_message?: string | null;
+ id?: string;
+ locked_at?: string | null;
+ locked_by?: string | null;
+ max_attempts?: number;
+ next_run_at?: string;
+ progress?: number;
+ stage?: string;
+ started_at?: string | null;
+ status?: string;
+ updated_at?: string;
+ };
Relationships: [
{
- foreignKeyName: "ingestion_jobs_batch_id_fkey"
- columns: ["batch_id"]
- isOneToOne: false
- referencedRelation: "import_batches"
- referencedColumns: ["id"]
+ foreignKeyName: "ingestion_jobs_batch_id_fkey";
+ columns: ["batch_id"];
+ isOneToOne: false;
+ referencedRelation: "import_batches";
+ referencedColumns: ["id"];
},
{
- foreignKeyName: "ingestion_jobs_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "ingestion_jobs_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "ingestion_jobs_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "ingestion_jobs_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
medication_records: {
Row: {
- accent: string | null
- category: string | null
- class: string | null
- created_at: string
- id: string
- last_reviewed_at: string | null
- name: string
- owner_id: string
- quick: Json
- review_due_at: string | null
- schedule: string | null
- sections: Json
- slug: string
- source_status: string
- stats: Json
- subclass: string | null
- tag: string | null
- updated_at: string
- validation_status: string
- }
+ accent: string | null;
+ category: string | null;
+ class: string | null;
+ created_at: string;
+ id: string;
+ last_reviewed_at: string | null;
+ name: string;
+ owner_id: string;
+ quick: Json;
+ review_due_at: string | null;
+ schedule: string | null;
+ sections: Json;
+ slug: string;
+ source_status: string;
+ stats: Json;
+ subclass: string | null;
+ tag: string | null;
+ updated_at: string;
+ validation_status: string;
+ };
Insert: {
- accent?: string | null
- category?: string | null
- class?: string | null
- created_at?: string
- id?: string
- last_reviewed_at?: string | null
- name: string
- owner_id: string
- quick?: Json
- review_due_at?: string | null
- schedule?: string | null
- sections?: Json
- slug: string
- source_status?: string
- stats?: Json
- subclass?: string | null
- tag?: string | null
- updated_at?: string
- validation_status?: string
- }
+ accent?: string | null;
+ category?: string | null;
+ class?: string | null;
+ created_at?: string;
+ id?: string;
+ last_reviewed_at?: string | null;
+ name: string;
+ owner_id: string;
+ quick?: Json;
+ review_due_at?: string | null;
+ schedule?: string | null;
+ sections?: Json;
+ slug: string;
+ source_status?: string;
+ stats?: Json;
+ subclass?: string | null;
+ tag?: string | null;
+ updated_at?: string;
+ validation_status?: string;
+ };
Update: {
- accent?: string | null
- category?: string | null
- class?: string | null
- created_at?: string
- id?: string
- last_reviewed_at?: string | null
- name?: string
- owner_id?: string
- quick?: Json
- review_due_at?: string | null
- schedule?: string | null
- sections?: Json
- slug?: string
- source_status?: string
- stats?: Json
- subclass?: string | null
- tag?: string | null
- updated_at?: string
- validation_status?: string
- }
- Relationships: []
- }
+ accent?: string | null;
+ category?: string | null;
+ class?: string | null;
+ created_at?: string;
+ id?: string;
+ last_reviewed_at?: string | null;
+ name?: string;
+ owner_id?: string;
+ quick?: Json;
+ review_due_at?: string | null;
+ schedule?: string | null;
+ sections?: Json;
+ slug?: string;
+ source_status?: string;
+ stats?: Json;
+ subclass?: string | null;
+ tag?: string | null;
+ updated_at?: string;
+ validation_status?: string;
+ };
+ Relationships: [];
+ };
rag_aliases: {
Row: {
- alias: string
- alias_type: string
- canonical: string
- created_at: string
- enabled: boolean
- id: string
- metadata: Json
- owner_id: string | null
- updated_at: string
- weight: number
- }
+ alias: string;
+ alias_type: string;
+ canonical: string;
+ created_at: string;
+ enabled: boolean;
+ id: string;
+ metadata: Json;
+ owner_id: string | null;
+ updated_at: string;
+ weight: number;
+ };
Insert: {
- alias: string
- alias_type: string
- canonical: string
- created_at?: string
- enabled?: boolean
- id?: string
- metadata?: Json
- owner_id?: string | null
- updated_at?: string
- weight?: number
- }
+ alias: string;
+ alias_type: string;
+ canonical: string;
+ created_at?: string;
+ enabled?: boolean;
+ id?: string;
+ metadata?: Json;
+ owner_id?: string | null;
+ updated_at?: string;
+ weight?: number;
+ };
Update: {
- alias?: string
- alias_type?: string
- canonical?: string
- created_at?: string
- enabled?: boolean
- id?: string
- metadata?: Json
- owner_id?: string | null
- updated_at?: string
- weight?: number
- }
- Relationships: []
- }
+ alias?: string;
+ alias_type?: string;
+ canonical?: string;
+ created_at?: string;
+ enabled?: boolean;
+ id?: string;
+ metadata?: Json;
+ owner_id?: string | null;
+ updated_at?: string;
+ weight?: number;
+ };
+ Relationships: [];
+ };
rag_queries: {
Row: {
- answer: string | null
- created_at: string
- id: string
- metadata: Json
- model: string | null
- owner_id: string | null
- query: string
- source_chunk_ids: string[]
- }
+ answer: string | null;
+ created_at: string;
+ id: string;
+ metadata: Json;
+ model: string | null;
+ owner_id: string | null;
+ query: string;
+ source_chunk_ids: string[];
+ };
Insert: {
- answer?: string | null
- created_at?: string
- id?: string
- metadata?: Json
- model?: string | null
- owner_id?: string | null
- query: string
- source_chunk_ids?: string[]
- }
+ answer?: string | null;
+ created_at?: string;
+ id?: string;
+ metadata?: Json;
+ model?: string | null;
+ owner_id?: string | null;
+ query: string;
+ source_chunk_ids?: string[];
+ };
Update: {
- answer?: string | null
- created_at?: string
- id?: string
- metadata?: Json
- model?: string | null
- owner_id?: string | null
- query?: string
- source_chunk_ids?: string[]
- }
- Relationships: []
- }
+ answer?: string | null;
+ created_at?: string;
+ id?: string;
+ metadata?: Json;
+ model?: string | null;
+ owner_id?: string | null;
+ query?: string;
+ source_chunk_ids?: string[];
+ };
+ Relationships: [];
+ };
rag_query_misses: {
Row: {
- candidate_aliases: string[]
- candidate_labels: Json
- cited_chunk_ids: string[]
- clicked_chunk_id: string | null
- clicked_document_id: string | null
- created_at: string
- expected_chunk_id: string | null
- expected_document_id: string | null
- expected_file: string | null
- id: string
- metadata: Json
- miss_reason: string
- normalized_query: string
- owner_id: string | null
- promoted_at: string | null
- promoted_eval_case: boolean
- query: string
- query_class: string | null
- retrieval_strategy: string | null
- review_notes: string | null
- review_status: string
- reviewed_at: string | null
- route: string | null
- top_chunk_ids: string[]
- top_files: string[]
- top_score: number | null
- }
+ candidate_aliases: string[];
+ candidate_labels: Json;
+ cited_chunk_ids: string[];
+ clicked_chunk_id: string | null;
+ clicked_document_id: string | null;
+ created_at: string;
+ expected_chunk_id: string | null;
+ expected_document_id: string | null;
+ expected_file: string | null;
+ id: string;
+ metadata: Json;
+ miss_reason: string;
+ normalized_query: string;
+ owner_id: string | null;
+ promoted_at: string | null;
+ promoted_eval_case: boolean;
+ query: string;
+ query_class: string | null;
+ retrieval_strategy: string | null;
+ review_notes: string | null;
+ review_status: string;
+ reviewed_at: string | null;
+ route: string | null;
+ top_chunk_ids: string[];
+ top_files: string[];
+ top_score: number | null;
+ };
Insert: {
- candidate_aliases?: string[]
- candidate_labels?: Json
- cited_chunk_ids?: string[]
- clicked_chunk_id?: string | null
- clicked_document_id?: string | null
- created_at?: string
- expected_chunk_id?: string | null
- expected_document_id?: string | null
- expected_file?: string | null
- id?: string
- metadata?: Json
- miss_reason?: string
- normalized_query: string
- owner_id?: string | null
- promoted_at?: string | null
- promoted_eval_case?: boolean
- query: string
- query_class?: string | null
- retrieval_strategy?: string | null
- review_notes?: string | null
- review_status?: string
- reviewed_at?: string | null
- route?: string | null
- top_chunk_ids?: string[]
- top_files?: string[]
- top_score?: number | null
- }
+ candidate_aliases?: string[];
+ candidate_labels?: Json;
+ cited_chunk_ids?: string[];
+ clicked_chunk_id?: string | null;
+ clicked_document_id?: string | null;
+ created_at?: string;
+ expected_chunk_id?: string | null;
+ expected_document_id?: string | null;
+ expected_file?: string | null;
+ id?: string;
+ metadata?: Json;
+ miss_reason?: string;
+ normalized_query: string;
+ owner_id?: string | null;
+ promoted_at?: string | null;
+ promoted_eval_case?: boolean;
+ query: string;
+ query_class?: string | null;
+ retrieval_strategy?: string | null;
+ review_notes?: string | null;
+ review_status?: string;
+ reviewed_at?: string | null;
+ route?: string | null;
+ top_chunk_ids?: string[];
+ top_files?: string[];
+ top_score?: number | null;
+ };
Update: {
- candidate_aliases?: string[]
- candidate_labels?: Json
- cited_chunk_ids?: string[]
- clicked_chunk_id?: string | null
- clicked_document_id?: string | null
- created_at?: string
- expected_chunk_id?: string | null
- expected_document_id?: string | null
- expected_file?: string | null
- id?: string
- metadata?: Json
- miss_reason?: string
- normalized_query?: string
- owner_id?: string | null
- promoted_at?: string | null
- promoted_eval_case?: boolean
- query?: string
- query_class?: string | null
- retrieval_strategy?: string | null
- review_notes?: string | null
- review_status?: string
- reviewed_at?: string | null
- route?: string | null
- top_chunk_ids?: string[]
- top_files?: string[]
- top_score?: number | null
- }
+ candidate_aliases?: string[];
+ candidate_labels?: Json;
+ cited_chunk_ids?: string[];
+ clicked_chunk_id?: string | null;
+ clicked_document_id?: string | null;
+ created_at?: string;
+ expected_chunk_id?: string | null;
+ expected_document_id?: string | null;
+ expected_file?: string | null;
+ id?: string;
+ metadata?: Json;
+ miss_reason?: string;
+ normalized_query?: string;
+ owner_id?: string | null;
+ promoted_at?: string | null;
+ promoted_eval_case?: boolean;
+ query?: string;
+ query_class?: string | null;
+ retrieval_strategy?: string | null;
+ review_notes?: string | null;
+ review_status?: string;
+ reviewed_at?: string | null;
+ route?: string | null;
+ top_chunk_ids?: string[];
+ top_files?: string[];
+ top_score?: number | null;
+ };
Relationships: [
{
- foreignKeyName: "rag_query_misses_expected_chunk_id_fkey"
- columns: ["expected_chunk_id"]
- isOneToOne: false
- referencedRelation: "document_chunks"
- referencedColumns: ["id"]
+ foreignKeyName: "rag_query_misses_expected_chunk_id_fkey";
+ columns: ["expected_chunk_id"];
+ isOneToOne: false;
+ referencedRelation: "document_chunks";
+ referencedColumns: ["id"];
},
{
- foreignKeyName: "rag_query_misses_expected_document_id_fkey"
- columns: ["expected_document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "rag_query_misses_expected_document_id_fkey";
+ columns: ["expected_document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "rag_query_misses_expected_document_id_fkey"
- columns: ["expected_document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "rag_query_misses_expected_document_id_fkey";
+ columns: ["expected_document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
rag_response_cache: {
Row: {
- cache_kind: string
- created_at: string
- dependency_version: string
- expires_at: string
- id: string
- indexing_version: string
- normalized_query: string
- owner_id: string | null
- payload: Json
- scope_key: string
- updated_at: string
- }
+ cache_kind: string;
+ created_at: string;
+ dependency_version: string;
+ expires_at: string;
+ id: string;
+ indexing_version: string;
+ normalized_query: string;
+ owner_id: string | null;
+ payload: Json;
+ scope_key: string;
+ updated_at: string;
+ };
Insert: {
- cache_kind: string
- created_at?: string
- dependency_version?: string
- expires_at: string
- id?: string
- indexing_version?: string
- normalized_query: string
- owner_id?: string | null
- payload: Json
- scope_key: string
- updated_at?: string
- }
+ cache_kind: string;
+ created_at?: string;
+ dependency_version?: string;
+ expires_at: string;
+ id?: string;
+ indexing_version?: string;
+ normalized_query: string;
+ owner_id?: string | null;
+ payload: Json;
+ scope_key: string;
+ updated_at?: string;
+ };
Update: {
- cache_kind?: string
- created_at?: string
- dependency_version?: string
- expires_at?: string
- id?: string
- indexing_version?: string
- normalized_query?: string
- owner_id?: string | null
- payload?: Json
- scope_key?: string
- updated_at?: string
- }
- Relationships: []
- }
+ cache_kind?: string;
+ created_at?: string;
+ dependency_version?: string;
+ expires_at?: string;
+ id?: string;
+ indexing_version?: string;
+ normalized_query?: string;
+ owner_id?: string | null;
+ payload?: Json;
+ scope_key?: string;
+ updated_at?: string;
+ };
+ Relationships: [];
+ };
rag_retrieval_logs: {
Row: {
- candidate_count: number
- created_at: string
- embedding_cache_hit: boolean | null
- embedding_field_count: number | null
- embedding_latency_ms: number | null
- id: string
- index_unit_count: number | null
- is_miss: boolean
- mean_hybrid_score: number | null
- memory_card_count: number | null
- metadata: Json
- miss_reason: string | null
- normalized_query: string | null
- owner_id: string | null
- query: string
- query_class: string | null
- rerank_latency_ms: number | null
- retrieval_strategy: string | null
- rpc_latency_ms: number | null
- selected_chunk_ids: string[]
- selected_count: number
- selected_document_ids: string[]
- text_candidate_count: number | null
- top_hybrid_score: number | null
- top_rrf_score: number | null
- top_similarity: number | null
- top_text_rank: number | null
- total_latency_ms: number | null
- vector_candidate_count: number | null
- }
+ candidate_count: number;
+ created_at: string;
+ embedding_cache_hit: boolean | null;
+ embedding_field_count: number | null;
+ embedding_latency_ms: number | null;
+ id: string;
+ index_unit_count: number | null;
+ is_miss: boolean;
+ mean_hybrid_score: number | null;
+ memory_card_count: number | null;
+ metadata: Json;
+ miss_reason: string | null;
+ normalized_query: string | null;
+ owner_id: string | null;
+ query: string;
+ query_class: string | null;
+ rerank_latency_ms: number | null;
+ retrieval_strategy: string | null;
+ rpc_latency_ms: number | null;
+ selected_chunk_ids: string[];
+ selected_count: number;
+ selected_document_ids: string[];
+ text_candidate_count: number | null;
+ top_hybrid_score: number | null;
+ top_rrf_score: number | null;
+ top_similarity: number | null;
+ top_text_rank: number | null;
+ total_latency_ms: number | null;
+ vector_candidate_count: number | null;
+ };
Insert: {
- candidate_count?: number
- created_at?: string
- embedding_cache_hit?: boolean | null
- embedding_field_count?: number | null
- embedding_latency_ms?: number | null
- id?: string
- index_unit_count?: number | null
- is_miss?: boolean
- mean_hybrid_score?: number | null
- memory_card_count?: number | null
- metadata?: Json
- miss_reason?: string | null
- normalized_query?: string | null
- owner_id?: string | null
- query: string
- query_class?: string | null
- rerank_latency_ms?: number | null
- retrieval_strategy?: string | null
- rpc_latency_ms?: number | null
- selected_chunk_ids?: string[]
- selected_count?: number
- selected_document_ids?: string[]
- text_candidate_count?: number | null
- top_hybrid_score?: number | null
- top_rrf_score?: number | null
- top_similarity?: number | null
- top_text_rank?: number | null
- total_latency_ms?: number | null
- vector_candidate_count?: number | null
- }
+ candidate_count?: number;
+ created_at?: string;
+ embedding_cache_hit?: boolean | null;
+ embedding_field_count?: number | null;
+ embedding_latency_ms?: number | null;
+ id?: string;
+ index_unit_count?: number | null;
+ is_miss?: boolean;
+ mean_hybrid_score?: number | null;
+ memory_card_count?: number | null;
+ metadata?: Json;
+ miss_reason?: string | null;
+ normalized_query?: string | null;
+ owner_id?: string | null;
+ query: string;
+ query_class?: string | null;
+ rerank_latency_ms?: number | null;
+ retrieval_strategy?: string | null;
+ rpc_latency_ms?: number | null;
+ selected_chunk_ids?: string[];
+ selected_count?: number;
+ selected_document_ids?: string[];
+ text_candidate_count?: number | null;
+ top_hybrid_score?: number | null;
+ top_rrf_score?: number | null;
+ top_similarity?: number | null;
+ top_text_rank?: number | null;
+ total_latency_ms?: number | null;
+ vector_candidate_count?: number | null;
+ };
Update: {
- candidate_count?: number
- created_at?: string
- embedding_cache_hit?: boolean | null
- embedding_field_count?: number | null
- embedding_latency_ms?: number | null
- id?: string
- index_unit_count?: number | null
- is_miss?: boolean
- mean_hybrid_score?: number | null
- memory_card_count?: number | null
- metadata?: Json
- miss_reason?: string | null
- normalized_query?: string | null
- owner_id?: string | null
- query?: string
- query_class?: string | null
- rerank_latency_ms?: number | null
- retrieval_strategy?: string | null
- rpc_latency_ms?: number | null
- selected_chunk_ids?: string[]
- selected_count?: number
- selected_document_ids?: string[]
- text_candidate_count?: number | null
- top_hybrid_score?: number | null
- top_rrf_score?: number | null
- top_similarity?: number | null
- top_text_rank?: number | null
- total_latency_ms?: number | null
- vector_candidate_count?: number | null
- }
- Relationships: []
- }
+ candidate_count?: number;
+ created_at?: string;
+ embedding_cache_hit?: boolean | null;
+ embedding_field_count?: number | null;
+ embedding_latency_ms?: number | null;
+ id?: string;
+ index_unit_count?: number | null;
+ is_miss?: boolean;
+ mean_hybrid_score?: number | null;
+ memory_card_count?: number | null;
+ metadata?: Json;
+ miss_reason?: string | null;
+ normalized_query?: string | null;
+ owner_id?: string | null;
+ query?: string;
+ query_class?: string | null;
+ rerank_latency_ms?: number | null;
+ retrieval_strategy?: string | null;
+ rpc_latency_ms?: number | null;
+ selected_chunk_ids?: string[];
+ selected_count?: number;
+ selected_document_ids?: string[];
+ text_candidate_count?: number | null;
+ top_hybrid_score?: number | null;
+ top_rrf_score?: number | null;
+ top_similarity?: number | null;
+ top_text_rank?: number | null;
+ total_latency_ms?: number | null;
+ vector_candidate_count?: number | null;
+ };
+ Relationships: [];
+ };
rag_visual_eval_cases: {
Row: {
- active: boolean
- case_name: string
- created_at: string
- document_id: string | null
- expected_image_type: string | null
- expected_terms: string[]
- expected_unit_types: string[]
- id: string
- metadata: Json
- owner_id: string | null
- query: string
- updated_at: string
- }
+ active: boolean;
+ case_name: string;
+ created_at: string;
+ document_id: string | null;
+ expected_image_type: string | null;
+ expected_terms: string[];
+ expected_unit_types: string[];
+ id: string;
+ metadata: Json;
+ owner_id: string | null;
+ query: string;
+ updated_at: string;
+ };
Insert: {
- active?: boolean
- case_name: string
- created_at?: string
- document_id?: string | null
- expected_image_type?: string | null
- expected_terms?: string[]
- expected_unit_types?: string[]
- id?: string
- metadata?: Json
- owner_id?: string | null
- query: string
- updated_at?: string
- }
+ active?: boolean;
+ case_name: string;
+ created_at?: string;
+ document_id?: string | null;
+ expected_image_type?: string | null;
+ expected_terms?: string[];
+ expected_unit_types?: string[];
+ id?: string;
+ metadata?: Json;
+ owner_id?: string | null;
+ query: string;
+ updated_at?: string;
+ };
Update: {
- active?: boolean
- case_name?: string
- created_at?: string
- document_id?: string | null
- expected_image_type?: string | null
- expected_terms?: string[]
- expected_unit_types?: string[]
- id?: string
- metadata?: Json
- owner_id?: string | null
- query?: string
- updated_at?: string
- }
+ active?: boolean;
+ case_name?: string;
+ created_at?: string;
+ document_id?: string | null;
+ expected_image_type?: string | null;
+ expected_terms?: string[];
+ expected_unit_types?: string[];
+ id?: string;
+ metadata?: Json;
+ owner_id?: string | null;
+ query?: string;
+ updated_at?: string;
+ };
Relationships: [
{
- foreignKeyName: "rag_visual_eval_cases_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "rag_visual_eval_cases_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "rag_visual_eval_cases_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "rag_visual_eval_cases_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
rag_visual_eval_runs: {
Row: {
- case_id: string
- created_at: string
- document_id: string | null
- hit_payload: Json
- id: string
- matched_count: number
- passed: boolean
- run_metadata: Json
- top_hit: boolean
- }
+ case_id: string;
+ created_at: string;
+ document_id: string | null;
+ hit_payload: Json;
+ id: string;
+ matched_count: number;
+ passed: boolean;
+ run_metadata: Json;
+ top_hit: boolean;
+ };
Insert: {
- case_id: string
- created_at?: string
- document_id?: string | null
- hit_payload?: Json
- id?: string
- matched_count?: number
- passed: boolean
- run_metadata?: Json
- top_hit: boolean
- }
+ case_id: string;
+ created_at?: string;
+ document_id?: string | null;
+ hit_payload?: Json;
+ id?: string;
+ matched_count?: number;
+ passed: boolean;
+ run_metadata?: Json;
+ top_hit: boolean;
+ };
Update: {
- case_id?: string
- created_at?: string
- document_id?: string | null
- hit_payload?: Json
- id?: string
- matched_count?: number
- passed?: boolean
- run_metadata?: Json
- top_hit?: boolean
- }
+ case_id?: string;
+ created_at?: string;
+ document_id?: string | null;
+ hit_payload?: Json;
+ id?: string;
+ matched_count?: number;
+ passed?: boolean;
+ run_metadata?: Json;
+ top_hit?: boolean;
+ };
Relationships: [
{
- foreignKeyName: "rag_visual_eval_runs_case_id_fkey"
- columns: ["case_id"]
- isOneToOne: false
- referencedRelation: "rag_visual_eval_cases"
- referencedColumns: ["id"]
+ foreignKeyName: "rag_visual_eval_runs_case_id_fkey";
+ columns: ["case_id"];
+ isOneToOne: false;
+ referencedRelation: "rag_visual_eval_cases";
+ referencedColumns: ["id"];
},
{
- foreignKeyName: "rag_visual_eval_runs_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "document_strict_gate_status"
- referencedColumns: ["document_id"]
+ foreignKeyName: "rag_visual_eval_runs_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "document_strict_gate_status";
+ referencedColumns: ["document_id"];
},
{
- foreignKeyName: "rag_visual_eval_runs_document_id_fkey"
- columns: ["document_id"]
- isOneToOne: false
- referencedRelation: "documents"
- referencedColumns: ["id"]
+ foreignKeyName: "rag_visual_eval_runs_document_id_fkey";
+ columns: ["document_id"];
+ isOneToOne: false;
+ referencedRelation: "documents";
+ referencedColumns: ["id"];
},
- ]
- }
+ ];
+ };
storage_cleanup_jobs: {
Row: {
- attempts: number
- completed_at: string | null
- created_at: string
- document_bucket: string
- document_id: string | null
- document_paths: string[]
- document_title: string | null
- id: string
- image_bucket: string
- image_paths: string[]
- last_error: string | null
- metadata: Json
- owner_id: string | null
- status: string
- storage_removed: number
- updated_at: string
- }
+ attempts: number;
+ completed_at: string | null;
+ created_at: string;
+ document_bucket: string;
+ document_id: string | null;
+ document_paths: string[];
+ document_title: string | null;
+ id: string;
+ image_bucket: string;
+ image_paths: string[];
+ last_error: string | null;
+ metadata: Json;
+ owner_id: string | null;
+ status: string;
+ storage_removed: number;
+ updated_at: string;
+ };
Insert: {
- attempts?: number
- completed_at?: string | null
- created_at?: string
- document_bucket?: string
- document_id?: string | null
- document_paths?: string[]
- document_title?: string | null
- id?: string
- image_bucket?: string
- image_paths?: string[]
- last_error?: string | null
- metadata?: Json
- owner_id?: string | null
- status?: string
- storage_removed?: number
- updated_at?: string
- }
+ attempts?: number;
+ completed_at?: string | null;
+ created_at?: string;
+ document_bucket?: string;
+ document_id?: string | null;
+ document_paths?: string[];
+ document_title?: string | null;
+ id?: string;
+ image_bucket?: string;
+ image_paths?: string[];
+ last_error?: string | null;
+ metadata?: Json;
+ owner_id?: string | null;
+ status?: string;
+ storage_removed?: number;
+ updated_at?: string;
+ };
Update: {
- attempts?: number
- completed_at?: string | null
- created_at?: string
- document_bucket?: string
- document_id?: string | null
- document_paths?: string[]
- document_title?: string | null
- id?: string
- image_bucket?: string
- image_paths?: string[]
- last_error?: string | null
- metadata?: Json
- owner_id?: string | null
- status?: string
- storage_removed?: number
- updated_at?: string
- }
- Relationships: []
- }
- }
+ attempts?: number;
+ completed_at?: string | null;
+ created_at?: string;
+ document_bucket?: string;
+ document_id?: string | null;
+ document_paths?: string[];
+ document_title?: string | null;
+ id?: string;
+ image_bucket?: string;
+ image_paths?: string[];
+ last_error?: string | null;
+ metadata?: Json;
+ owner_id?: string | null;
+ status?: string;
+ storage_removed?: number;
+ updated_at?: string;
+ };
+ Relationships: [];
+ };
+ };
Views: {
document_strict_gate_status: {
Row: {
- counts: Json | null
- document_id: string | null
- document_status: string | null
- document_updated_at: string | null
- enrichment_status: string | null
- gate_passed: boolean | null
- generated_labels: number | null
- index_units: number | null
- indexing_v3_agent_status: string | null
- memory_cards: number | null
- missing: string[] | null
- owner_id: string | null
- presence: Json | null
- quality_extraction_quality: string | null
- quality_score: number | null
- sections: number | null
- summary_embedding: boolean | null
- title_embedding: boolean | null
- }
- Relationships: []
- }
- }
+ counts: Json | null;
+ document_id: string | null;
+ document_status: string | null;
+ document_updated_at: string | null;
+ enrichment_status: string | null;
+ gate_passed: boolean | null;
+ generated_labels: number | null;
+ index_units: number | null;
+ indexing_v3_agent_status: string | null;
+ memory_cards: number | null;
+ missing: string[] | null;
+ owner_id: string | null;
+ presence: Json | null;
+ quality_extraction_quality: string | null;
+ quality_score: number | null;
+ sections: number | null;
+ summary_embedding: boolean | null;
+ title_embedding: boolean | null;
+ };
+ Relationships: [];
+ };
+ };
Functions: {
- analyze_rag_tables: { Args: never; Returns: undefined }
+ analyze_rag_tables: { Args: never; Returns: undefined };
consume_api_subject_rate_limit: {
Args: {
- p_subject_key: string
- p_bucket: string
- p_limit: number
- p_window_seconds: number
- }
+ p_subject_key: string;
+ p_bucket: string;
+ p_limit: number;
+ p_window_seconds: number;
+ };
Returns: {
- limited: boolean
- limit_value: number
- remaining: number
- retry_after_seconds: number
- reset_at: string
- }[]
- }
+ limited: boolean;
+ limit_value: number;
+ remaining: number;
+ retry_after_seconds: number;
+ reset_at: string;
+ }[];
+ };
chunk_image_metadata: {
- Args: { chunk_image_ids: string[] }
- Returns: Json
- }
+ Args: { chunk_image_ids: string[] };
+ Returns: Json;
+ };
claim_indexing_v3_agent_jobs: {
Args: {
- p_claim_limit?: number
- p_stale_after_minutes?: number
- p_worker_id: string
- }
+ p_claim_limit?: number;
+ p_stale_after_minutes?: number;
+ p_worker_id: string;
+ };
Returns: {
- attempt_count: number
- batch_id: string
- document_id: string
- documents: Json
- error_message: string
- id: string
- locked_at: string
- locked_by: string
- max_attempts: number
- progress: number
- stage: string
- status: string
- }[]
- }
+ attempt_count: number;
+ batch_id: string;
+ document_id: string;
+ documents: Json;
+ error_message: string;
+ id: string;
+ locked_at: string;
+ locked_by: string;
+ max_attempts: number;
+ progress: number;
+ stage: string;
+ status: string;
+ }[];
+ };
claim_ingestion_jobs: {
Args: {
- p_claim_limit?: number
- p_stale_after_minutes?: number
- p_worker_id: string
- }
+ p_claim_limit?: number;
+ p_stale_after_minutes?: number;
+ p_worker_id: string;
+ };
Returns: {
- attempt_count: number
- batch_id: string
- document_id: string
- documents: Json
- error_message: string
- id: string
- locked_at: string
- locked_by: string
- max_attempts: number
- progress: number
- stage: string
- status: string
- }[]
- }
+ attempt_count: number;
+ batch_id: string;
+ document_id: string;
+ documents: Json;
+ error_message: string;
+ id: string;
+ locked_at: string;
+ locked_by: string;
+ max_attempts: number;
+ progress: number;
+ stage: string;
+ status: string;
+ }[];
+ };
cleanup_abandoned_document_index_generations: {
- Args: { p_document_id?: string | null; p_dry_run?: boolean; p_limit?: number }
- Returns: Json
- }
+ Args: { p_document_id?: string | null; p_dry_run?: boolean; p_limit?: number };
+ Returns: Json;
+ };
commit_document_index_generation: {
Args: {
- p_chunk_count?: number
- p_document_id: string
- p_image_count?: number
- p_index_generation_id: string
- p_metadata?: Json
- p_page_count?: number
- p_pages?: Json
- p_quality?: Json
- p_status?: string
- }
- Returns: Json
- }
+ p_chunk_count?: number;
+ p_document_id: string;
+ p_image_count?: number;
+ p_index_generation_id: string;
+ p_metadata?: Json;
+ p_page_count?: number;
+ p_pages?: Json;
+ p_quality?: Json;
+ p_status?: string;
+ };
+ Returns: Json;
+ };
complete_ingestion_job: {
Args: {
- p_batch_id?: string | null
- p_document_id: string
- p_job_id: string
- p_stage?: string
- }
- Returns: Json
- }
+ p_batch_id?: string | null;
+ p_document_id: string;
+ p_job_id: string;
+ p_stage?: string;
+ };
+ Returns: Json;
+ };
complete_strict_enrichment_job: {
Args: {
- p_agent_version?: string
- p_document_id: string
- p_job_id?: string
- p_stage?: string
- p_visual_indexing_version?: string
- }
+ p_agent_version?: string;
+ p_document_id: string;
+ p_job_id?: string;
+ p_stage?: string;
+ p_visual_indexing_version?: string;
+ };
Returns: {
- completed_job_ids: string[]
- counts: Json
- document_id: string
- gate_passed: boolean
- missing: string[]
- ok: boolean
- presence: Json
- status: string
- }[]
- }
+ completed_job_ids: string[];
+ counts: Json;
+ document_id: string;
+ gate_passed: boolean;
+ missing: string[];
+ ok: boolean;
+ presence: Json;
+ status: string;
+ }[];
+ };
consume_api_rate_limit: {
Args: {
- p_bucket: string
- p_limit: number
- p_owner_id: string
- p_window_seconds: number
- }
+ p_bucket: string;
+ p_limit: number;
+ p_owner_id: string;
+ p_window_seconds: number;
+ };
Returns: {
- limit_value: number
- limited: boolean
- remaining: number
- reset_at: string
- retry_after_seconds: number
- }[]
- }
+ limit_value: number;
+ limited: boolean;
+ remaining: number;
+ reset_at: string;
+ retry_after_seconds: number;
+ }[];
+ };
correct_clinical_query_terms: {
- Args: { input_query: string; min_sim?: number }
- Returns: string
- }
- detect_legacy_ivfflat_indexes: { Args: never; Returns: string[] }
+ Args: { input_query: string; min_sim?: number };
+ Returns: string;
+ };
+ detect_legacy_ivfflat_indexes: { Args: never; Returns: string[] };
document_label_metadata: {
- Args: { p_document_id: string }
- Returns: Json
- }
+ Args: { p_document_id: string };
+ Returns: Json;
+ };
document_summary_text: {
- Args: { p_document_id: string }
- Returns: string
- }
+ Args: { p_document_id: string };
+ Returns: string;
+ };
explain_retrieval_rpc: {
Args: {
- p_analyze?: boolean
- p_document_filters?: string[] | null
- p_match_count?: number
- p_owner_filter?: string | null
- p_query_text: string
- p_rpc: string
- }
- Returns: Json
- }
+ p_analyze?: boolean;
+ p_document_filters?: string[] | null;
+ p_match_count?: number;
+ p_owner_filter?: string | null;
+ p_query_text: string;
+ p_rpc: string;
+ };
+ Returns: Json;
+ };
fail_or_retry_ingestion_job: {
Args: {
- p_batch_id?: string | null
- p_document_id: string
- p_document_status?: string
- p_error_message?: string
- p_job_id: string
- p_next_run_at?: string | null
- p_retry?: boolean
- p_stage?: string
- }
- Returns: Json
- }
+ p_batch_id?: string | null;
+ p_document_id: string;
+ p_document_status?: string;
+ p_error_message?: string;
+ p_job_id: string;
+ p_next_run_at?: string | null;
+ p_retry?: boolean;
+ p_stage?: string;
+ };
+ Returns: Json;
+ };
get_related_document_metadata: {
- Args: { document_ids: string[]; owner_filter?: string | null }
+ Args: { document_ids: string[]; owner_filter?: string | null };
Returns: {
- document_id: string
- labels: Json
- summary: string
- }[]
- }
+ document_id: string;
+ labels: Json;
+ summary: string;
+ }[];
+ };
get_visual_evidence_cards: {
- Args: { p_document_id: string; p_limit?: number }
+ Args: { p_document_id: string; p_limit?: number };
Returns: {
- image_caption: string
- image_storage_path: string
- image_type: string
- page_number: number
- source_image_id: string
- unit_content: string
- unit_id: string
- unit_metadata: Json
- unit_quality_score: number
- unit_title: string
- unit_type: string
- }[]
- }
- invoke_indexing_v3_agent: { Args: { p_limit?: number }; Returns: number }
- invoke_ingestion_worker: { Args: { p_limit?: number }; Returns: number }
+ image_caption: string;
+ image_storage_path: string;
+ image_type: string;
+ page_number: number;
+ source_image_id: string;
+ unit_content: string;
+ unit_id: string;
+ unit_metadata: Json;
+ unit_quality_score: number;
+ unit_title: string;
+ unit_type: string;
+ }[];
+ };
+ invoke_indexing_v3_agent: { Args: { p_limit?: number }; Returns: number };
+ invoke_ingestion_worker: { Args: { p_limit?: number }; Returns: number };
is_committed_artifact_generation: {
- Args: { artifact_metadata: Json; document_metadata: Json }
- Returns: boolean
- }
+ Args: { artifact_metadata: Json; document_metadata: Json };
+ Returns: boolean;
+ };
is_committed_document_generation: {
- Args: { document_metadata: Json; row_generation: string }
- Returns: boolean
- }
+ Args: { document_metadata: Json; row_generation: string };
+ Returns: boolean;
+ };
match_document_chunks: {
Args: {
- document_filter?: string | null
- match_count?: number
- min_similarity?: number
- owner_filter?: string | null
- query_embedding: Vector
- }
+ document_filter?: string | null;
+ match_count?: number;
+ min_similarity?: number;
+ owner_filter?: string | null;
+ query_embedding: Vector;
+ };
Returns: {
- chunk_index: number
- content: string
- document_id: string
- document_labels: Json
- document_summary: string
- file_name: string
- id: string
- image_ids: string[]
- images: Json
- page_number: number
- retrieval_synopsis: string
- section_heading: string
- similarity: number
- source_metadata: Json
- title: string
- }[]
- }
+ chunk_index: number;
+ content: string;
+ document_id: string;
+ document_labels: Json;
+ document_summary: string;
+ file_name: string;
+ id: string;
+ image_ids: string[];
+ images: Json;
+ page_number: number;
+ retrieval_synopsis: string;
+ section_heading: string;
+ similarity: number;
+ source_metadata: Json;
+ title: string;
+ }[];
+ };
match_document_chunks_hybrid: {
Args: {
- document_filters?: string[] | null
- match_count?: number
- min_similarity?: number
- owner_filter?: string | null
- query_embedding: Vector
- query_text: string
- }
+ document_filters?: string[] | null;
+ match_count?: number;
+ min_similarity?: number;
+ owner_filter?: string | null;
+ query_embedding: Vector;
+ query_text: string;
+ };
Returns: {
- chunk_index: number
- content: string
- document_id: string
- file_name: string
- hybrid_score: number
- id: string
- image_ids: string[]
- images: Json
- page_number: number
- retrieval_synopsis: string
- rrf_score: number
- section_heading: string
- similarity: number
- source_metadata: Json
- text_rank: number
- title: string
- }[]
- }
+ chunk_index: number;
+ content: string;
+ document_id: string;
+ file_name: string;
+ hybrid_score: number;
+ id: string;
+ image_ids: string[];
+ images: Json;
+ page_number: number;
+ retrieval_synopsis: string;
+ rrf_score: number;
+ section_heading: string;
+ similarity: number;
+ source_metadata: Json;
+ text_rank: number;
+ title: string;
+ }[];
+ };
match_document_chunks_text: {
Args: {
- document_filters?: string[] | null
- match_count?: number
- owner_filter?: string | null
- query_text: string
- }
+ document_filters?: string[] | null;
+ match_count?: number;
+ owner_filter?: string | null;
+ query_text: string;
+ };
Returns: {
- chunk_index: number
- content: string
- document_id: string
- document_labels: Json
- document_summary: string
- file_name: string
- hybrid_score: number
- id: string
- image_ids: string[]
- images: Json
- page_number: number
- retrieval_synopsis: string
- section_heading: string
- similarity: number
- source_metadata: Json
- text_rank: number
- title: string
- }[]
- }
+ chunk_index: number;
+ content: string;
+ document_id: string;
+ document_labels: Json;
+ document_summary: string;
+ file_name: string;
+ hybrid_score: number;
+ id: string;
+ image_ids: string[];
+ images: Json;
+ page_number: number;
+ retrieval_synopsis: string;
+ section_heading: string;
+ similarity: number;
+ source_metadata: Json;
+ text_rank: number;
+ title: string;
+ }[];
+ };
match_document_embedding_fields_hybrid: {
Args: {
- document_filters?: string[] | null
- match_count?: number
- min_similarity?: number
- owner_filter?: string | null
- query_embedding: Vector
- query_text: string
- }
+ document_filters?: string[] | null;
+ match_count?: number;
+ min_similarity?: number;
+ owner_filter?: string | null;
+ query_embedding: Vector;
+ query_text: string;
+ };
Returns: {
- content: string
- document_id: string
- field_type: string
- hybrid_score: number
- id: string
- similarity: number
- source_chunk_id: string
- text_rank: number
- }[]
- }
+ content: string;
+ document_id: string;
+ field_type: string;
+ hybrid_score: number;
+ id: string;
+ similarity: number;
+ source_chunk_id: string;
+ text_rank: number;
+ }[];
+ };
match_document_embedding_fields_text: {
Args: {
- document_filters?: string[] | null
- match_count?: number
- min_text_rank?: number
- owner_filter?: string | null
- query_text: string
- }
+ document_filters?: string[] | null;
+ match_count?: number;
+ min_text_rank?: number;
+ owner_filter?: string | null;
+ query_text: string;
+ };
Returns: {
- content: string
- document_id: string
- field_type: string
- id: string
- source_chunk_id: string
- text_rank: number
- }[]
- }
+ content: string;
+ document_id: string;
+ field_type: string;
+ id: string;
+ source_chunk_id: string;
+ text_rank: number;
+ }[];
+ };
match_document_index_units_hybrid: {
Args: {
- document_filters?: string[] | null
- match_count?: number
- min_similarity?: number
- owner_filter?: string | null
- query_embedding: Vector
- query_text: string
- }
+ document_filters?: string[] | null;
+ match_count?: number;
+ min_similarity?: number;
+ owner_filter?: string | null;
+ query_embedding: Vector;
+ query_text: string;
+ };
Returns: {
- content: string
- document_id: string
- extraction_mode: string
- heading_path: string[]
- hybrid_score: number
- id: string
- metadata: Json
- normalized_terms: string[]
- page_end: number
- page_start: number
- quality_score: number
- similarity: number
- source_chunk_id: string
- source_image_id: string
- source_span: Json
- text_rank: number
- title: string
- unit_type: string
- }[]
- }
+ content: string;
+ document_id: string;
+ extraction_mode: string;
+ heading_path: string[];
+ hybrid_score: number;
+ id: string;
+ metadata: Json;
+ normalized_terms: string[];
+ page_end: number;
+ page_start: number;
+ quality_score: number;
+ similarity: number;
+ source_chunk_id: string;
+ source_image_id: string;
+ source_span: Json;
+ text_rank: number;
+ title: string;
+ unit_type: string;
+ }[];
+ };
match_document_lookup_chunks_text: {
Args: {
- document_filters: string[] | null
- match_count?: number
- owner_filter?: string | null
- query_text: string
- }
+ document_filters: string[] | null;
+ match_count?: number;
+ owner_filter?: string | null;
+ query_text: string;
+ };
Returns: {
- anchor_id: string
- chunk_index: number
- content: string
- document_id: string
- heading_level: number
- id: string
- image_ids: string[]
- page_number: number
- parent_heading: string
- retrieval_synopsis: string
- section_heading: string
- section_path: string[]
- text_rank: number
- }[]
- }
+ anchor_id: string;
+ chunk_index: number;
+ content: string;
+ document_id: string;
+ heading_level: number;
+ id: string;
+ image_ids: string[];
+ page_number: number;
+ parent_heading: string;
+ retrieval_synopsis: string;
+ section_heading: string;
+ section_path: string[];
+ text_rank: number;
+ }[];
+ };
match_document_memory_cards_hybrid: {
Args: {
- document_filters?: string[] | null
- match_count?: number
- min_similarity?: number
- owner_filter?: string | null
- query_embedding: Vector
- query_text: string
- }
+ document_filters?: string[] | null;
+ match_count?: number;
+ min_similarity?: number;
+ owner_filter?: string | null;
+ query_embedding: Vector;
+ query_text: string;
+ };
Returns: {
- card_type: string
- confidence: number
- content: string
- document_id: string
- hybrid_score: number
- id: string
- metadata: Json
- normalized_terms: string[]
- owner_id: string
- page_number: number
- rrf_score: number
- section_id: string
- similarity: number
- source_chunk_ids: string[]
- source_image_ids: string[]
- text_rank: number
- title: string
- }[]
- }
+ card_type: string;
+ confidence: number;
+ content: string;
+ document_id: string;
+ hybrid_score: number;
+ id: string;
+ metadata: Json;
+ normalized_terms: string[];
+ owner_id: string;
+ page_number: number;
+ rrf_score: number;
+ section_id: string;
+ similarity: number;
+ source_chunk_ids: string[];
+ source_image_ids: string[];
+ text_rank: number;
+ title: string;
+ }[];
+ };
match_document_memory_cards_hybrid_v2: {
Args: {
- document_filters?: string[] | null
- match_count?: number
- min_similarity?: number
- owner_filter?: string | null
- query_embedding: Vector
- query_text: string
- }
+ document_filters?: string[] | null;
+ match_count?: number;
+ min_similarity?: number;
+ owner_filter?: string | null;
+ query_embedding: Vector;
+ query_text: string;
+ };
Returns: {
- card_type: string
- confidence: number
- content: string
- document_id: string
- hybrid_score: number
- id: string
- metadata: Json
- normalized_terms: string[]
- owner_id: string
- page_number: number
- rrf_score: number
- section_id: string
- similarity: number
- source_chunk_ids: string[]
- source_image_ids: string[]
- text_rank: number
- title: string
- }[]
- }
+ card_type: string;
+ confidence: number;
+ content: string;
+ document_id: string;
+ hybrid_score: number;
+ id: string;
+ metadata: Json;
+ normalized_terms: string[];
+ owner_id: string;
+ page_number: number;
+ rrf_score: number;
+ section_id: string;
+ similarity: number;
+ source_chunk_ids: string[];
+ source_image_ids: string[];
+ text_rank: number;
+ title: string;
+ }[];
+ };
match_document_table_facts_text: {
Args: {
- document_filters?: string[] | null
- match_count?: number
- owner_filter?: string | null
- query_text: string
- }
+ document_filters?: string[] | null;
+ match_count?: number;
+ owner_filter?: string | null;
+ query_text: string;
+ };
Returns: {
- action: string
- clinical_parameter: string
- document_id: string
- id: string
- match_reason: string
- page_number: number
- row_label: string
- source_chunk_id: string
- source_image_id: string
- table_title: string
- text_rank: number
- threshold_value: string
- }[]
- }
+ action: string;
+ clinical_parameter: string;
+ document_id: string;
+ id: string;
+ match_reason: string;
+ page_number: number;
+ row_label: string;
+ source_chunk_id: string;
+ source_image_id: string;
+ table_title: string;
+ text_rank: number;
+ threshold_value: string;
+ }[];
+ };
match_documents_for_query: {
Args: {
- match_count?: number
- owner_filter?: string | null
- query_text: string
- }
+ match_count?: number;
+ owner_filter?: string | null;
+ query_text: string;
+ };
Returns: {
- chunk_count: number
- file_name: string
- id: string
- image_count: number
- match_reason: string
- metadata: Json
- owner_id: string
- page_count: number
- status: string
- text_rank: number
- title: string
- }[]
- }
+ chunk_count: number;
+ file_name: string;
+ id: string;
+ image_count: number;
+ match_reason: string;
+ metadata: Json;
+ owner_id: string;
+ page_count: number;
+ status: string;
+ text_rank: number;
+ title: string;
+ }[];
+ };
purge_expired_rag_queries: {
- Args: { p_retention_days?: number }
- Returns: number
- }
+ Args: { p_retention_days?: number };
+ Returns: number;
+ };
refresh_import_batch_status: {
- Args: { p_batch_id: string }
- Returns: Json
- }
+ Args: { p_batch_id: string };
+ Returns: Json;
+ };
repair_enrichment_quality_batch: {
- Args: { p_limit?: number }
- Returns: Json
- }
+ Args: { p_limit?: number };
+ Returns: Json;
+ };
repair_strict_enrichment_gate_batch: {
- Args: { p_limit?: number }
+ Args: { p_limit?: number };
Returns: {
- counts: Json
- document_id: string
- missing: string[]
- presence: Json
- repaired: string[]
- status: string
- }[]
- }
+ counts: Json;
+ document_id: string;
+ missing: string[];
+ presence: Json;
+ repaired: string[];
+ status: string;
+ }[];
+ };
reset_document_index: {
- Args: { p_document_id: string }
- Returns: undefined
- }
- run_all_visual_eval_cases: { Args: { p_limit?: number }; Returns: Json }
+ Args: { p_document_id: string };
+ Returns: undefined;
+ };
+ run_all_visual_eval_cases: { Args: { p_limit?: number }; Returns: Json };
run_visual_eval_case: {
- Args: { p_case_id: string; p_limit?: number }
- Returns: Json
- }
+ Args: { p_case_id: string; p_limit?: number };
+ Returns: Json;
+ };
search_document_chunks: {
Args: {
- match_count?: number
- p_document_id: string
- p_owner_id?: string
- p_query: string
- }
+ match_count?: number;
+ p_document_id: string;
+ p_owner_id?: string;
+ p_query: string;
+ };
Returns: {
- chunk_index: number
- content: string
- id: string
- image_ids: string[]
- page_number: number
- section_heading: string
- text_rank: number
- trigram_score: number
- }[]
- }
- search_schema_health: { Args: never; Returns: Json }
+ chunk_index: number;
+ content: string;
+ id: string;
+ image_ids: string[];
+ page_number: number;
+ section_heading: string;
+ text_rank: number;
+ trigram_score: number;
+ }[];
+ };
+ search_schema_health: { Args: never; Returns: Json };
stamp_document_deep_memory_version: {
- Args: { p_document_id: string; p_version: string }
- Returns: undefined
- }
- }
+ Args: { p_document_id: string; p_version: string };
+ Returns: undefined;
+ };
+ };
Enums: {
- [_ in never]: never
- }
+ [_ in never]: never;
+ };
CompositeTypes: {
- [_ in never]: never
- }
- }
-}
+ [_ in never]: never;
+ };
+ };
+};
-type DatabaseWithoutInternals = Omit
+type DatabaseWithoutInternals = Omit;
-type DefaultSchema = DatabaseWithoutInternals[Extract]
+type DefaultSchema = DatabaseWithoutInternals[Extract];
export type Tables<
DefaultSchemaTableNameOrOptions extends
- | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
- | { schema: keyof DatabaseWithoutInternals },
- TableName extends DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
+ keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) | { schema: keyof DatabaseWithoutInternals },
+ TableName extends (DefaultSchemaTableNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals;
}
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
- : never = never,
+ : never) = never,
> = DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
+ schema: keyof DatabaseWithoutInternals;
}
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
- Row: infer R
+ Row: infer R;
}
? R
: never
- : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
- DefaultSchema["Views"])
- ? (DefaultSchema["Tables"] &
- DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
- Row: infer R
+ : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
+ ? (DefaultSchema["Tables"] & DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
+ Row: infer R;
}
? R
: never
- : never
+ : never;
export type TablesInsert<
- DefaultSchemaTableNameOrOptions extends
- | keyof DefaultSchema["Tables"]
- | { schema: keyof DatabaseWithoutInternals },
- TableName extends DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
+ DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] | { schema: keyof DatabaseWithoutInternals },
+ TableName extends (DefaultSchemaTableNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals;
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
- : never = never,
+ : never) = never,
> = DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
+ schema: keyof DatabaseWithoutInternals;
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
- Insert: infer I
+ Insert: infer I;
}
? I
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
- Insert: infer I
+ Insert: infer I;
}
? I
: never
- : never
+ : never;
export type TablesUpdate<
- DefaultSchemaTableNameOrOptions extends
- | keyof DefaultSchema["Tables"]
- | { schema: keyof DatabaseWithoutInternals },
- TableName extends DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
+ DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] | { schema: keyof DatabaseWithoutInternals },
+ TableName extends (DefaultSchemaTableNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals;
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
- : never = never,
+ : never) = never,
> = DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
+ schema: keyof DatabaseWithoutInternals;
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
- Update: infer U
+ Update: infer U;
}
? U
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
- Update: infer U
+ Update: infer U;
}
? U
: never
- : never
+ : never;
export type Enums<
- DefaultSchemaEnumNameOrOptions extends
- | keyof DefaultSchema["Enums"]
- | { schema: keyof DatabaseWithoutInternals },
- EnumName extends DefaultSchemaEnumNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
+ DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"] | { schema: keyof DatabaseWithoutInternals },
+ EnumName extends (DefaultSchemaEnumNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals;
}
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
- : never = never,
+ : never) = never,
> = DefaultSchemaEnumNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
+ schema: keyof DatabaseWithoutInternals;
}
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
- : never
+ : never;
export type CompositeTypes<
PublicCompositeTypeNameOrOptions extends
- | keyof DefaultSchema["CompositeTypes"]
- | { schema: keyof DatabaseWithoutInternals },
- CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
+ keyof DefaultSchema["CompositeTypes"] | { schema: keyof DatabaseWithoutInternals },
+ CompositeTypeName extends (PublicCompositeTypeNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals;
}
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
- : never = never,
+ : never) = never,
> = PublicCompositeTypeNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
+ schema: keyof DatabaseWithoutInternals;
}
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
- : never
+ : never;
export const Constants = {
graphql_public: {
@@ -2636,4 +2626,4 @@ export const Constants = {
public: {
Enums: {},
},
-} as const
+} as const;
diff --git a/src/lib/use-registry-records.ts b/src/lib/use-registry-records.ts
index 0cf8a09da..79599d5af 100644
--- a/src/lib/use-registry-records.ts
+++ b/src/lib/use-registry-records.ts
@@ -82,8 +82,12 @@ export function useRegistryRecords(
// effect retries with a real header; never expire the session from an
// auth-loading 401. Demo/local API responses can still resolve fast.
if (authStatus === "loading") return;
- if (authStatus === "authenticated") markSessionExpired();
- setState(recordsState("unauthorized", kind));
+ if (authStatus === "authenticated") {
+ markSessionExpired();
+ setState(recordsState("unauthorized", kind));
+ return;
+ }
+ setState(recordsState("error", kind));
return;
}
if (!response.ok) {
@@ -140,8 +144,12 @@ export function useRegistryRecord(kind: RegistryRecordKind, slug: string): Regis
if (!active) return;
if (response.status === 401) {
if (authStatus === "loading") return;
- if (authStatus === "authenticated") markSessionExpired();
- setState({ status: "unauthorized", record: null, linkedDocuments: [], demoMode: false, governance: null });
+ if (authStatus === "authenticated") {
+ markSessionExpired();
+ setState({ status: "unauthorized", record: null, linkedDocuments: [], demoMode: false, governance: null });
+ return;
+ }
+ setState({ status: "error", record: null, linkedDocuments: [], demoMode: false, governance: null });
return;
}
if (response.status === 404) {
diff --git a/supabase/migrations/20260705133000_tighten_search_document_chunks_owner_scope.sql b/supabase/migrations/20260705133000_tighten_search_document_chunks_owner_scope.sql
new file mode 100644
index 000000000..473fdb90c
--- /dev/null
+++ b/supabase/migrations/20260705133000_tighten_search_document_chunks_owner_scope.sql
@@ -0,0 +1,72 @@
+-- Tighten search_document_chunks owner scoping so null p_owner_id only matches public
+-- documents (owner_id IS NULL) and authenticated callers can search both owned and public
+-- documents without matching other owners' private rows.
+
+create or replace function public.search_document_chunks(
+ p_document_id uuid,
+ p_query text,
+ match_count integer default 20,
+ p_owner_id uuid default null
+)
+returns table (
+ id uuid,
+ page_number integer,
+ chunk_index integer,
+ section_heading text,
+ content text,
+ image_ids uuid[],
+ text_rank real,
+ trigram_score real
+)
+language sql
+stable
+set search_path = public, extensions, pg_temp
+as $$
+ with normalized as (
+ select
+ websearch_to_tsquery('english', coalesce(p_query, '')) as query_tsv,
+ lower(trim(coalesce(p_query, ''))) as query_text
+ ),
+ tokens as (
+ select distinct token
+ from normalized,
+ lateral regexp_split_to_table(normalized.query_text, '\s+') as token
+ where length(token) >= 3
+ )
+ select
+ c.id,
+ c.page_number,
+ c.chunk_index,
+ c.section_heading,
+ c.content,
+ c.image_ids,
+ ts_rank_cd(c.search_tsv, normalized.query_tsv)::real as text_rank,
+ similarity(lower(coalesce(c.section_heading, '') || ' ' || c.content), normalized.query_text)::real as trigram_score
+ from public.document_chunks c
+ join public.documents d on d.id = c.document_id
+ cross join normalized
+ where c.document_id = p_document_id
+ and d.status = 'indexed'
+ and (
+ (p_owner_id is null and d.owner_id is null)
+ or (p_owner_id is not null and (d.owner_id is null or d.owner_id = p_owner_id))
+ )
+ and (
+ c.search_tsv @@ normalized.query_tsv
+ or lower(coalesce(c.section_heading, '') || ' ' || c.content) % normalized.query_text
+ or lower(coalesce(c.section_heading, '') || ' ' || c.content) like '%' || normalized.query_text || '%'
+ or exists (
+ select 1
+ from tokens t
+ where lower(coalesce(c.section_heading, '') || ' ' || c.content) like '%' || t.token || '%'
+ or lower(coalesce(c.section_heading, '') || ' ' || c.content) % t.token
+ )
+ )
+ order by
+ ts_rank_cd(c.search_tsv, normalized.query_tsv) desc,
+ similarity(lower(coalesce(c.section_heading, '') || ' ' || c.content), normalized.query_text) desc,
+ c.chunk_index asc
+ limit least(greatest(match_count, 1), 80);
+$$;
+
+grant execute on function public.search_document_chunks(uuid, text, integer, uuid) to service_role;
diff --git a/supabase/migrations/20260705180000_reconcile_search_health_indexes.sql b/supabase/migrations/20260705180000_reconcile_search_health_indexes.sql
new file mode 100644
index 000000000..21e0c281d
--- /dev/null
+++ b/supabase/migrations/20260705180000_reconcile_search_health_indexes.sql
@@ -0,0 +1,202 @@
+-- Reconcile live index drift reported by search_schema_health() on 2026-07-05.
+-- Creates canonical retrieval-support indexes that are missing on live (or only
+-- present under legacy names) and teaches search_schema_health() to accept
+-- verified functional equivalents so replays do not false-fail mid-rollout.
+
+set search_path = public, extensions, pg_catalog;
+
+create index if not exists documents_title_trgm_idx
+ on public.documents using gin (lower(coalesce(title, '') || ' ' || coalesce(file_name, '')) gin_trgm_ops);
+
+create index if not exists document_chunks_content_trgm_idx
+ on public.document_chunks using gin (lower(coalesce(section_heading, '') || ' ' || coalesce(content, '')) gin_trgm_ops);
+
+create index if not exists document_labels_label_trgm_idx
+ on public.document_labels using gin (lower(label) gin_trgm_ops);
+
+create index if not exists document_summaries_summary_trgm_idx
+ on public.document_summaries using gin (lower(summary) gin_trgm_ops);
+
+create index if not exists document_table_facts_owner_document_page_idx
+ on public.document_table_facts(owner_id, document_id, page_number);
+
+create index if not exists document_index_units_owner_chunk_type_idx
+ on public.document_index_units(owner_id, source_chunk_id, unit_type)
+ where source_chunk_id is not null;
+
+create index if not exists document_pages_document_idx
+ on public.document_pages(document_id, page_number);
+
+create index if not exists document_sections_document_idx
+ on public.document_sections(document_id, section_index);
+
+create index if not exists rag_retrieval_logs_owner_created_idx
+ on public.rag_retrieval_logs(owner_id, created_at desc);
+
+create index if not exists rag_retrieval_logs_miss_idx
+ on public.rag_retrieval_logs(is_miss, created_at desc)
+ where is_miss = true;
+
+create or replace function public.search_schema_health()
+returns jsonb
+language plpgsql
+stable
+security definer
+set search_path = public, extensions, pg_catalog, pg_temp
+as $$
+declare
+ missing text[] := array[]::text[];
+ vector_type_oid oid;
+ vector_schema text;
+ index_name text;
+ legacy_ivfflat_indexes text[];
+ zero_vec extensions.vector(1536);
+ probe_text text := 'schema health probe zzznomatch';
+ hybrid_rpcs text[] := array[
+ 'match_document_chunks_hybrid',
+ 'match_document_index_units_hybrid',
+ 'match_document_embedding_fields_hybrid',
+ 'match_document_memory_cards_hybrid'
+ ];
+ rpc_name text;
+ required_indexes constant text[] := array[
+ 'documents_title_trgm_idx',
+ 'document_chunks_content_trgm_idx',
+ 'document_labels_label_trgm_idx',
+ 'document_summaries_summary_trgm_idx',
+ 'document_chunks_embedding_hnsw_idx',
+ 'document_embedding_fields_embedding_hnsw_idx',
+ 'document_memory_cards_embedding_hnsw_idx',
+ 'documents_indexed_owner_title_idx',
+ 'document_table_facts_owner_document_page_idx',
+ 'document_embedding_fields_owner_chunk_idx',
+ 'document_index_units_owner_chunk_type_idx',
+ 'document_table_facts_source_image_idx',
+ 'document_pages_document_idx',
+ 'document_sections_document_idx',
+ 'document_chunks_document_idx',
+ 'document_memory_cards_document_idx',
+ 'document_embedding_fields_document_idx',
+ 'document_table_facts_document_idx',
+ 'document_index_units_document_idx',
+ 'rag_retrieval_logs_owner_created_idx',
+ 'rag_retrieval_logs_miss_idx',
+ 'rag_retrieval_logs_strategy_idx'
+ ];
+ -- Verified live equivalents: same table/column intent, different migration-era name.
+ index_aliases constant jsonb := jsonb_build_object(
+ 'documents_title_trgm_idx', jsonb_build_array('documents_title_search_tsv_idx', 'documents_title_search_idx'),
+ 'document_chunks_content_trgm_idx', jsonb_build_array('document_chunks_search_tsv_idx', 'document_chunks_search_idx'),
+ 'document_table_facts_owner_document_page_idx', jsonb_build_array('document_table_facts_owner_idx'),
+ 'document_pages_document_idx', jsonb_build_array('document_pages_document_id_page_number_key'),
+ 'document_sections_document_idx', jsonb_build_array('document_sections_document_id_idx'),
+ 'rag_retrieval_logs_owner_created_idx', jsonb_build_array('rag_retrieval_logs_owner_id_idx')
+ );
+begin
+ select t.oid, n.nspname
+ into vector_type_oid, vector_schema
+ from pg_type t
+ join pg_namespace n on n.oid = t.typnamespace
+ where t.typname = 'vector'
+ and n.nspname = 'extensions'
+ limit 1;
+
+ if vector_type_oid is null then
+ missing := array_append(missing, 'extensions.vector_type');
+ end if;
+
+ if to_regprocedure('public.match_document_chunks(extensions.vector, integer, double precision, uuid, uuid)') is null then
+ missing := array_append(missing, 'match_document_chunks.extensions_vector_signature');
+ end if;
+ if to_regprocedure('public.match_document_chunks_hybrid(extensions.vector, text, integer, double precision, uuid[], uuid)') is null then
+ missing := array_append(missing, 'match_document_chunks_hybrid.extensions_vector_signature');
+ end if;
+ if to_regprocedure('public.match_document_chunks_text(text, integer, uuid[], uuid)') is null then
+ missing := array_append(missing, 'match_document_chunks_text.signature');
+ end if;
+ if to_regprocedure('public.match_document_lookup_chunks_text(text, uuid[], integer, uuid)') is null then
+ missing := array_append(missing, 'match_document_lookup_chunks_text.signature');
+ end if;
+ if to_regprocedure('public.match_document_memory_cards_hybrid(extensions.vector, text, integer, double precision, uuid[], uuid)') is null then
+ missing := array_append(missing, 'match_document_memory_cards_hybrid.extensions_vector_signature');
+ end if;
+ if to_regprocedure('public.match_document_memory_cards_hybrid_v2(extensions.vector, text, integer, double precision, uuid[], uuid)') is null then
+ missing := array_append(missing, 'match_document_memory_cards_hybrid_v2.extensions_vector_signature');
+ end if;
+ if to_regprocedure('public.match_document_index_units_hybrid(extensions.vector, text, integer, double precision, uuid[], uuid)') is null then
+ missing := array_append(missing, 'match_document_index_units_hybrid.extensions_vector_signature');
+ end if;
+ if to_regprocedure('public.match_document_embedding_fields_hybrid(extensions.vector, text, integer, double precision, uuid[], uuid)') is null then
+ missing := array_append(missing, 'match_document_embedding_fields_hybrid.extensions_vector_signature');
+ end if;
+ if to_regprocedure('public.match_documents_for_query(text, integer, uuid)') is null then
+ missing := array_append(missing, 'match_documents_for_query.signature');
+ end if;
+ if to_regprocedure('public.match_document_table_facts_text(text, integer, uuid[], uuid)') is null then
+ missing := array_append(missing, 'match_document_table_facts_text.signature');
+ end if;
+ if to_regprocedure('public.explain_retrieval_rpc(text, text, integer, uuid, uuid[], boolean)') is null then
+ missing := array_append(missing, 'explain_retrieval_rpc.signature');
+ end if;
+ if to_regclass('public.rag_retrieval_logs') is null then
+ missing := array_append(missing, 'rag_retrieval_logs.table');
+ end if;
+
+ foreach index_name in array required_indexes loop
+ if not exists (
+ select 1
+ from pg_class c
+ join pg_namespace ns on ns.oid = c.relnamespace
+ where ns.nspname = 'public'
+ and c.relname = index_name
+ and c.relkind = 'i'
+ )
+ and not (
+ index_aliases ? index_name
+ and exists (
+ select 1
+ from pg_class c
+ join pg_namespace ns on ns.oid = c.relnamespace
+ where ns.nspname = 'public'
+ and c.relkind = 'i'
+ and c.relname in (
+ select jsonb_array_elements_text(index_aliases -> index_name)
+ )
+ )
+ ) then
+ missing := array_append(missing, index_name);
+ end if;
+ end loop;
+
+ if vector_type_oid is not null then
+ zero_vec := (select ('[' || string_agg('0', ',') || ']') from generate_series(1, 1536))::extensions.vector(1536);
+ foreach rpc_name in array hybrid_rpcs loop
+ begin
+ execute format(
+ 'select 1 from public.%I($1, $2, 1, 0.1, null::uuid[], null::uuid) limit 1',
+ rpc_name
+ ) using zero_vec, probe_text;
+ exception
+ when undefined_function then
+ missing := array_append(missing, rpc_name || '.execution_signature');
+ when others then
+ missing := array_append(missing, rpc_name || '.execution:' || SQLSTATE);
+ end;
+ end loop;
+ end if;
+
+ select public.detect_legacy_ivfflat_indexes() into legacy_ivfflat_indexes;
+
+ return jsonb_build_object(
+ 'ok', cardinality(missing) = 0,
+ 'missing', missing,
+ 'vector_extension_schema', vector_schema,
+ 'legacy_ivfflat_indexes', coalesce(legacy_ivfflat_indexes, array[]::text[]),
+ 'deferred_hnsw_indexes', array[]::text[],
+ 'checked_at', now()
+ );
+end;
+$$;
+
+revoke execute on function public.search_schema_health() from public, anon, authenticated;
+grant execute on function public.search_schema_health() to service_role;
diff --git a/supabase/migrations/20260705220000_promote_locally_reviewed_documents_public.sql b/supabase/migrations/20260705220000_promote_locally_reviewed_documents_public.sql
new file mode 100644
index 000000000..76c9a5560
--- /dev/null
+++ b/supabase/migrations/20260705220000_promote_locally_reviewed_documents_public.sql
@@ -0,0 +1,65 @@
+-- Promote locally reviewed indexed documents to the public corpus (owner_id IS NULL)
+-- so anonymous callers can list, preview, search, and use them in RAG.
+
+begin;
+
+create temporary table promoted_public_documents on commit drop as
+with promoted as (
+ update public.documents d
+ set
+ owner_id = null,
+ metadata = jsonb_set(
+ coalesce(d.metadata, '{}'::jsonb),
+ '{public_corpus}',
+ 'true'::jsonb,
+ true
+ ),
+ updated_at = now()
+ where d.status = 'indexed'
+ and d.owner_id is not null
+ and coalesce(d.metadata->>'clinical_validation_status', 'unverified') in ('locally_reviewed', 'approved')
+ returning d.id, d.owner_id as previous_owner_id
+)
+select id, previous_owner_id from promoted;
+
+update public.document_labels dl
+set owner_id = null, updated_at = now()
+from promoted_public_documents pd
+where dl.document_id = pd.id;
+
+update public.document_summaries ds
+set owner_id = null, updated_at = now()
+from promoted_public_documents pd
+where ds.document_id = pd.id;
+
+update public.document_sections ds
+set owner_id = null, updated_at = now()
+from promoted_public_documents pd
+where ds.document_id = pd.id;
+
+update public.document_memory_cards dmc
+set owner_id = null, updated_at = now()
+from promoted_public_documents pd
+where dmc.document_id = pd.id;
+
+update public.document_table_facts dtf
+set owner_id = null, updated_at = now()
+from promoted_public_documents pd
+where dtf.document_id = pd.id;
+
+update public.document_embedding_fields def
+set owner_id = null, updated_at = now()
+from promoted_public_documents pd
+where def.document_id = pd.id;
+
+update public.document_index_quality diq
+set owner_id = null, updated_at = now()
+from promoted_public_documents pd
+where diq.document_id = pd.id;
+
+update public.document_index_units diu
+set owner_id = null, updated_at = now()
+from promoted_public_documents pd
+where diu.document_id = pd.id;
+
+commit;
diff --git a/supabase/schema.sql b/supabase/schema.sql
index 63657b132..16ce2230c 100644
--- a/supabase/schema.sql
+++ b/supabase/schema.sql
@@ -2396,6 +2396,15 @@ declare
'rag_retrieval_logs_miss_idx',
'rag_retrieval_logs_strategy_idx'
];
+ -- Verified live equivalents: same table/column intent, different migration-era name.
+ index_aliases constant jsonb := jsonb_build_object(
+ 'documents_title_trgm_idx', jsonb_build_array('documents_title_search_tsv_idx', 'documents_title_search_idx'),
+ 'document_chunks_content_trgm_idx', jsonb_build_array('document_chunks_search_tsv_idx', 'document_chunks_search_idx'),
+ 'document_table_facts_owner_document_page_idx', jsonb_build_array('document_table_facts_owner_idx'),
+ 'document_pages_document_idx', jsonb_build_array('document_pages_document_id_page_number_key'),
+ 'document_sections_document_idx', jsonb_build_array('document_sections_document_id_idx'),
+ 'rag_retrieval_logs_owner_created_idx', jsonb_build_array('rag_retrieval_logs_owner_id_idx')
+ );
begin
select t.oid, n.nspname
into vector_type_oid, vector_schema
@@ -2454,6 +2463,19 @@ begin
where ns.nspname = 'public'
and c.relname = index_name
and c.relkind = 'i'
+ )
+ and not (
+ index_aliases ? index_name
+ and exists (
+ select 1
+ from pg_class c
+ join pg_namespace ns on ns.oid = c.relnamespace
+ where ns.nspname = 'public'
+ and c.relkind = 'i'
+ and c.relname in (
+ select jsonb_array_elements_text(index_aliases -> index_name)
+ )
+ )
) then
missing := array_append(missing, index_name);
end if;
diff --git a/tests/api-rate-limit-fallback.test.ts b/tests/api-rate-limit-fallback.test.ts
new file mode 100644
index 000000000..c25bb36c5
--- /dev/null
+++ b/tests/api-rate-limit-fallback.test.ts
@@ -0,0 +1,23 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+afterEach(() => {
+ vi.unstubAllEnvs();
+ vi.resetModules();
+});
+
+describe("allowRateLimitInMemoryFallbackOnUnavailable", () => {
+ it("enables fallback for production deployments", async () => {
+ vi.stubEnv("NODE_ENV", "production");
+ const { allowRateLimitInMemoryFallbackOnUnavailable } = await import("../src/lib/api-rate-limit");
+ expect(allowRateLimitInMemoryFallbackOnUnavailable()).toBe(true);
+ });
+
+ it("enables fallback for local no-auth development", async () => {
+ vi.stubEnv("NODE_ENV", "development");
+ vi.doMock("@/lib/env", () => ({
+ isLocalNoAuthMode: () => true,
+ }));
+ const { allowRateLimitInMemoryFallbackOnUnavailable } = await import("../src/lib/api-rate-limit");
+ expect(allowRateLimitInMemoryFallbackOnUnavailable()).toBe(true);
+ });
+});
diff --git a/tests/api-validation-contract.test.ts b/tests/api-validation-contract.test.ts
index 4bdbf8834..a23c87c0e 100644
--- a/tests/api-validation-contract.test.ts
+++ b/tests/api-validation-contract.test.ts
@@ -147,7 +147,23 @@ function createSupabaseMock(resolve: QueryResolver = () => ok([])) {
calls.push(call);
return new QueryBuilder(call, resolve);
}),
- rpc: vi.fn(async () => ok([])),
+ rpc: vi.fn(async (name: string) => {
+ if (name === "consume_api_subject_rate_limit" || name === "consume_api_rate_limit") {
+ return {
+ data: [
+ {
+ limited: false,
+ limit_value: 100,
+ remaining: 99,
+ retry_after_seconds: 60,
+ reset_at: new Date(Date.now() + 60_000).toISOString(),
+ },
+ ],
+ error: null,
+ };
+ }
+ return ok([]);
+ }),
storage: { from: storageFrom },
storageMocks: { upload, remove, createSignedUrl, storageFrom },
};
@@ -180,6 +196,13 @@ function mockRuntime(client: ReturnType) {
vi.doMock("@/lib/supabase/auth", () => ({
AuthenticationError: class AuthenticationError extends Error {},
requireAuthenticatedUser,
+ getOptionalAuthenticatedUser: vi.fn(async (request: Request) => {
+ const authorization = request.headers.get("authorization") ?? "";
+ if (/^Bearer\s+\S+/i.test(authorization)) return { id: userId };
+ const cookieHeader = request.headers.get("cookie") ?? "";
+ if (cookieHeader.includes("sb-")) return { id: userId };
+ return null;
+ }),
unauthorizedResponse: () =>
new Response(JSON.stringify({ error: "Authentication required." }), {
status: 401,
diff --git a/tests/eval-quality.test.ts b/tests/eval-quality.test.ts
index d4888a935..fa4c7e48c 100644
--- a/tests/eval-quality.test.ts
+++ b/tests/eval-quality.test.ts
@@ -9,12 +9,13 @@ import {
sourceWarningsForRagQualityAnswer,
type RagQualityResult,
} from "../scripts/eval-quality";
-import type { GoldenRetrievalResult } from "../scripts/eval-retrieval";
+import { evaluateGoldenRetrievalCase, type GoldenRetrievalResult } from "../scripts/eval-retrieval";
function retrievalResult(overrides: Partial = {}): GoldenRetrievalResult {
const base: GoldenRetrievalResult = {
id: "retrieval-1",
query: "What ANC threshold should withhold clozapine?",
+ forceEmbedding: false,
expectedQueryClass: "table_threshold",
actualQueryClass: "table_threshold",
expectedDocumentSubstrings: ["clozapine.pdf"],
@@ -166,6 +167,8 @@ describe("eval quality reporting", () => {
structured_threshold_text_match: 1,
});
expect(report.retrieval.summary.second_stage_rerank_rate).toBe(0.5);
+ expect(report.retrieval.summary.force_embedding_case_count).toBe(0);
+ expect(report.retrieval.summary.force_embedding_failure_count).toBe(0);
expect(report.rag.summary.unsupported_correct_rate).toBe(0);
expect(report.rag.summary.numeric_grounding_failure_rate).toBeCloseTo(0.3333, 4);
expect(report.rag.summary.source_governance_danger_failure_rate).toBeCloseTo(0.3333, 4);
@@ -180,6 +183,53 @@ describe("eval quality reporting", () => {
);
});
+ it("fails forced-embedding retrieval cases that return from cache, coverage, or lexical paths", () => {
+ const result = evaluateGoldenRetrievalCase({
+ testCase: {
+ id: "vector-regression",
+ query: "How is panic disorder managed?",
+ expectedQueryClass: "broad_summary",
+ expectedDocumentSubstrings: [],
+ expectedContentTerms: [],
+ topK: 8,
+ expectTableEvidence: false,
+ forceEmbedding: true,
+ },
+ results: [],
+ telemetry: {
+ query_class: "broad_summary",
+ retrieval_strategy: "text_fast_path",
+ embedding_skipped: true,
+ embedding_skip_reason: "strong_document_text_score",
+ text_fast_path_reason: "strong_document_text_score",
+ text_candidate_budget: 32,
+ text_candidate_count: 5,
+ vector_candidate_count: 0,
+ retrieval_layer_counts: { text_candidates: 5 },
+ coverage_gate_decision: "accepted",
+ coverage_gate_reason: "document_title_evidence_gate",
+ second_stage_rerank_used: false,
+ },
+ latencyMs: 10,
+ });
+
+ expect(result.forceEmbedding).toBe(true);
+ expect(result.failures).toEqual(
+ expect.arrayContaining([
+ "forceEmbedding expected embedding to run",
+ "forceEmbedding returned lexical strategy text_fast_path",
+ "forceEmbedding returned coverage gate",
+ "forceEmbedding found no vector-layer candidates",
+ ]),
+ );
+ const report = buildEvalQualityReport({ retrievalResults: [result], ragResults: [] });
+ expect(report.retrieval.summary.force_embedding_case_count).toBe(1);
+ expect(report.retrieval.summary.force_embedding_failure_count).toBe(1);
+ expect(report.blocking_threshold_failures).toEqual(
+ expect.arrayContaining([expect.stringContaining("retrieval force_embedding_failure_count 1 above 0")]),
+ );
+ });
+
it("derives source governance warnings for direct RAG answers without precomputed warnings", () => {
const warnings = sourceWarningsForRagQualityAnswer({
sourceGovernanceWarnings: undefined,
diff --git a/tests/private-access-routes.test.ts b/tests/private-access-routes.test.ts
index 45b5b1e61..d7ae5da91 100644
--- a/tests/private-access-routes.test.ts
+++ b/tests/private-access-routes.test.ts
@@ -276,7 +276,14 @@ function createSupabaseMock(resolve: QueryResolver = defaultQueryResolver) {
function mockRuntime(
client: ReturnType,
ragMock?: Record,
- options: { localNoAuth?: boolean; localOwnerEmail?: string; providerMode?: string; openAiKey?: string } = {},
+ options: {
+ localNoAuth?: boolean;
+ localOwnerEmail?: string;
+ providerMode?: string;
+ openAiKey?: string;
+ publicUploadsEnabled?: boolean;
+ publicWorkspaceOwnerId?: string;
+ } = {},
) {
vi.resetModules();
vi.doUnmock("@/lib/rag");
@@ -298,11 +305,15 @@ function mockRuntime(
OPENAI_API_KEY: options.openAiKey ?? "sk-test",
RAG_PROVIDER_MODE: options.providerMode ?? "auto",
LOCAL_NO_AUTH_OWNER_EMAIL: options.localOwnerEmail,
+ PUBLIC_WORKSPACE_OWNER_ID: options.publicWorkspaceOwnerId,
+ NEXT_PUBLIC_PUBLIC_UPLOADS_ENABLED: options.publicUploadsEnabled ? "true" : undefined,
WORKER_STALE_AFTER_MINUTES: 10,
WORKER_MAX_ATTEMPTS: 3,
},
isDemoMode: () => false,
isLocalNoAuthMode: () => Boolean(options.localNoAuth),
+ publicWorkspaceOwnerId: () => options.publicWorkspaceOwnerId ?? null,
+ publicUploadsEnabled: () => Boolean(options.publicUploadsEnabled),
requireOpenAIEnv: () => undefined,
requireServerEnv: () => undefined,
}));
@@ -322,6 +333,15 @@ function localPortRequest(port: number, path: string, init?: RequestInit) {
return new Request(`http://localhost:${port}${path}`, init);
}
+function matchesOwnerReadScope(call: QueryCall, ownerId?: string | null) {
+ if (ownerId === undefined || ownerId === null) {
+ return call.filters.some((filter) => filter.column === "owner_id" && filter.value === null);
+ }
+ return call.orFilters.some(
+ (filter) => filter.includes(`owner_id.eq.${ownerId}`) && filter.includes("owner_id.is.null"),
+ );
+}
+
function authenticatedRequest(path: string, init?: RequestInit) {
return request(path, {
...init,
@@ -376,14 +396,18 @@ afterEach(() => {
});
describe("private document API access", () => {
- it("still requires a valid token even when local no-auth mode is enabled", async () => {
- const client = createSupabaseMock();
+ it("lists public documents without auth in local no-auth mode", async () => {
+ const documents = [{ id: documentId, owner_id: null, title: "Public guideline" }];
+ const client = createSupabaseMock((call) => (call.table === "documents" ? ok(documents) : ok([])));
mockRuntime(client, undefined, { localNoAuth: true });
const { GET } = await import("../src/app/api/documents/route");
const response = await GET(localPortRequest(4298, "/api/documents"));
+ const body = await payload(response);
- expect(response.status).toBe(401);
+ expect(response.status).toBe(200);
+ expect(body.documents).toEqual(documents);
+ expect(client.calls[0].filters).toContainEqual({ column: "owner_id", value: null });
expect(client.auth.getUser).not.toHaveBeenCalled();
});
@@ -398,144 +422,227 @@ describe("private document API access", () => {
expect(response.status).toBe(200);
expect(client.auth.getUser).toHaveBeenCalledTimes(1);
});
- it("rejects local no-auth private calls from unmanaged localhost ports before Supabase access", async () => {
+ it("rejects local no-auth upload calls from unmanaged localhost ports before Supabase access", async () => {
const client = createSupabaseMock();
mockRuntime(client, undefined, { localNoAuth: true });
- const { GET } = await import("../src/app/api/documents/route");
+ const { POST } = await import("../src/app/api/upload/route");
+ const formData = new FormData();
+ formData.set("file", new File(["hello"], "guideline.txt", { type: "text/plain" }));
- const response = await GET(localPortRequest(3000, "/api/documents"));
+ const response = await POST(
+ localPortRequest(3000, "/api/upload", {
+ method: "POST",
+ body: formData,
+ }),
+ );
- expect(response.status).toBe(401);
+ expect(response.status).toBe(503);
+ expect(await payload(response)).toEqual({ error: "Public uploads are not configured for this workspace." });
expect(client.auth.getUser).not.toHaveBeenCalled();
expect(client.from).not.toHaveBeenCalled();
});
- it("rejects managed-port private calls with stale localhost referers before Supabase access", async () => {
+ it("rejects managed-port upload calls with stale localhost referers before Supabase access", async () => {
const client = createSupabaseMock();
mockRuntime(client, undefined, { localNoAuth: true });
- const { GET } = await import("../src/app/api/documents/route");
+ const { POST } = await import("../src/app/api/upload/route");
+ const formData = new FormData();
+ formData.set("file", new File(["hello"], "guideline.txt", { type: "text/plain" }));
- const response = await GET(
- localPortRequest(4298, "/api/documents", {
+ const response = await POST(
+ localPortRequest(4298, "/api/upload", {
+ method: "POST",
headers: {
referer: "http://localhost:3000/",
},
+ body: formData,
}),
);
- expect(response.status).toBe(401);
+ expect(response.status).toBe(503);
+ expect(await payload(response)).toEqual({ error: "Public uploads are not configured for this workspace." });
expect(client.auth.getUser).not.toHaveBeenCalled();
expect(client.from).not.toHaveBeenCalled();
});
- it("resolves local no-auth owner from documents before listing auth users", async () => {
- const documents = [{ id: documentId, owner_id: userId, title: "Owned document" }];
- const client = createSupabaseMock((call) => {
- if (call.table === "documents" && call.selected === "owner_id") {
- return ok({ owner_id: userId });
- }
- if (call.table === "documents") {
- return ok(documents);
- }
- return ok([]);
- });
- mockRuntime(client, undefined, { localNoAuth: true });
+ it("lists public documents for unauthenticated callers", async () => {
+ const documents = [{ id: documentId, owner_id: null, title: "Public guideline", status: "indexed" }];
+ const client = createSupabaseMock((call) => (call.table === "documents" ? ok(documents) : ok([])));
+ mockRuntime(client);
const { GET } = await import("../src/app/api/documents/route");
- const response = await GET(localPortRequest(4298, "/api/documents"));
+ const response = await GET(request("/api/documents?includeMeta=false"));
const body = await payload(response);
- expect(response.status).toBe(401);
- expect(body).toEqual({ error: "Authentication required." });
+ expect(response.status).toBe(200);
+ expect(body.documents).toEqual(documents);
expect(client.auth.getUser).not.toHaveBeenCalled();
- expect(client.from).not.toHaveBeenCalled();
+ expect(client.calls[0].filters).toContainEqual({ column: "owner_id", value: null });
+ expect(client.calls[0].filters).not.toContainEqual({ column: "owner_id", value: userId });
});
- it("resolves configured local no-auth owner email before document fallback", async () => {
- const documents = [{ id: documentId, owner_id: userId, title: "Configured owner document" }];
- const client = createSupabaseMock((call) => {
- if (call.table === "documents" && call.selected === "owner_id") {
- return ok({ owner_id: otherUserId });
- }
- if (call.table === "documents") {
- return ok(documents);
- }
- return ok([]);
- });
- client.auth.admin.listUsers.mockResolvedValueOnce({
- data: { users: [{ id: userId, email: "clinician@example.test" }], nextPage: 0 },
- error: null,
- });
- mockRuntime(client, undefined, { localNoAuth: true, localOwnerEmail: "clinician@example.test" });
+ it("filters authenticated document listing by owner and public rows", async () => {
+ const documents = [{ id: documentId, owner_id: userId, title: "Owned document" }];
+ const client = createSupabaseMock((call) => (call.table === "documents" ? ok(documents) : ok([])));
+ mockRuntime(client);
const { GET } = await import("../src/app/api/documents/route");
- const response = await GET(localPortRequest(4298, "/api/documents"));
+ const response = await GET(authenticatedRequest("/api/documents"));
const body = await payload(response);
- expect(response.status).toBe(401);
- expect(body).toEqual({ error: "Authentication required." });
- expect(client.auth.admin.listUsers).not.toHaveBeenCalled();
- expect(client.from).not.toHaveBeenCalled();
+ expect(response.status).toBe(200);
+ expect(body.documents).toEqual(documents.map((document) => ({ ...document, labels: [], summary: null })));
+ expect(body.pagination).toMatchObject({ limit: 100, offset: 0, nextOffset: 1, hasMore: false });
+ expect(client.calls[0].orFilters).toContain(`owner_id.eq.${userId},owner_id.is.null`);
+ expect(client.calls[0].selected).toContain("storage_path");
+ expect(client.calls[0].range).toEqual({ from: 0, to: 99 });
});
- it("rejects unauthenticated document listing", async () => {
- const client = createSupabaseMock();
+ it("accepts legacy Supabase auth cookies for private document access", async () => {
+ const documents = [{ id: documentId, owner_id: userId, title: "Owned document" }];
+ const client = createSupabaseMock((call) => (call.table === "documents" ? ok(documents) : ok([])));
mockRuntime(client);
const { GET } = await import("../src/app/api/documents/route");
- const response = await GET(request("/api/documents"));
+ const response = await GET(authenticatedCookieRequest("/api/documents"));
+ const body = await payload(response);
- expect(response.status).toBe(401);
- expect(await payload(response)).toEqual({ error: "Authentication required." });
- expect(client.from).not.toHaveBeenCalled();
+ expect(response.status).toBe(200);
+ expect(client.auth.getUser).toHaveBeenCalledWith(token);
+ expect(body.documents).toEqual(documents.map((document) => ({ ...document, labels: [], summary: null })));
+ expect(client.calls[0].orFilters).toContain(`owner_id.eq.${userId},owner_id.is.null`);
});
- it("filters authenticated document listing by owner", async () => {
+ it("accepts Supabase auth token cookies for private document access", async () => {
const documents = [{ id: documentId, owner_id: userId, title: "Owned document" }];
const client = createSupabaseMock((call) => (call.table === "documents" ? ok(documents) : ok([])));
mockRuntime(client);
const { GET } = await import("../src/app/api/documents/route");
- const response = await GET(authenticatedRequest("/api/documents"));
+ const response = await GET(authenticatedAuthTokenCookieRequest("/api/documents"));
const body = await payload(response);
expect(response.status).toBe(200);
+ expect(client.auth.getUser).toHaveBeenCalledWith(token);
expect(body.documents).toEqual(documents.map((document) => ({ ...document, labels: [], summary: null })));
- expect(body.pagination).toMatchObject({ limit: 100, offset: 0, nextOffset: 1, hasMore: false });
- expect(client.calls[0].filters).toContainEqual({ column: "owner_id", value: userId });
- expect(client.calls[0].selected).toContain("id,owner_id,title");
- expect(client.calls[0].selected).not.toBe("*");
- expect(client.calls[0].range).toEqual({ from: 0, to: 99 });
+ expect(client.calls[0].orFilters).toContain(`owner_id.eq.${userId},owner_id.is.null`);
});
- it("accepts legacy Supabase auth cookies for private document access", async () => {
+ it("allows authenticated users to read public document detail", async () => {
+ const client = createSupabaseMock((call) => {
+ if (call.table === "documents" && matchesOwnerReadScope(call, userId)) {
+ return ok({
+ id: documentId,
+ owner_id: null,
+ title: "Public guideline",
+ file_name: "guideline.pdf",
+ file_type: "application/pdf",
+ page_count: 2,
+ chunk_count: 1,
+ metadata: { index_generation_id: "generation-a" },
+ });
+ }
+ if (call.table === "document_pages") return ok([]);
+ if (call.table === "document_images") return ok([]);
+ if (call.table === "document_chunks") return ok([]);
+ if (call.table === "document_table_facts") return ok([]);
+ return ok([]);
+ });
+ mockRuntime(client);
+ const { GET } = await import("../src/app/api/documents/[id]/route");
+
+ const response = await GET(authenticatedRequest(`/api/documents/${documentId}`), {
+ params: Promise.resolve({ id: documentId }),
+ });
+ const body = await payload(response);
+
+ expect(response.status).toBe(200);
+ expect(body.document).toMatchObject({ id: documentId, title: "Public guideline", owner_id: null });
+ expect(client.calls[0].orFilters).toContain(`owner_id.eq.${userId},owner_id.is.null`);
+ });
+
+ it("allows authenticated users to open public document signed URLs", async () => {
+ const client = createSupabaseMock((call) => {
+ if (call.table === "documents" && matchesOwnerReadScope(call, userId)) {
+ return ok({ storage_path: "public/documents/guideline.pdf", file_type: "application/pdf" });
+ }
+ return ok(null);
+ });
+ mockRuntime(client);
+ const { GET } = await import("../src/app/api/documents/[id]/signed-url/route");
+
+ const response = await GET(authenticatedRequest(`/api/documents/${documentId}/signed-url`), {
+ params: Promise.resolve({ id: documentId }),
+ });
+
+ expect(response.status).toBe(200);
+ expect((await payload(response)).url).toContain("public/documents/guideline.pdf");
+ });
+
+ it("recovers valid cookie auth when a stale bearer header is also present", async () => {
const documents = [{ id: documentId, owner_id: userId, title: "Owned document" }];
const client = createSupabaseMock((call) => (call.table === "documents" ? ok(documents) : ok([])));
mockRuntime(client);
const { GET } = await import("../src/app/api/documents/route");
- const response = await GET(authenticatedCookieRequest("/api/documents"));
+ const response = await GET(
+ request("/api/documents", {
+ headers: {
+ authorization: "Bearer expired-token",
+ cookie: `sb-access-token=${token}`,
+ },
+ }),
+ );
const body = await payload(response);
expect(response.status).toBe(200);
- expect(client.auth.getUser).toHaveBeenCalledWith(token);
expect(body.documents).toEqual(documents.map((document) => ({ ...document, labels: [], summary: null })));
- expect(client.calls[0].filters).toContainEqual({ column: "owner_id", value: userId });
+ expect(client.auth.getUser).toHaveBeenCalledWith(token);
});
- it("accepts Supabase auth token cookies for private document access", async () => {
- const documents = [{ id: documentId, owner_id: userId, title: "Owned document" }];
+ it("omits internal document list fields for anonymous callers", async () => {
+ const documents = [{ id: documentId, owner_id: null, title: "Public guideline", status: "indexed" }];
const client = createSupabaseMock((call) => (call.table === "documents" ? ok(documents) : ok([])));
mockRuntime(client);
const { GET } = await import("../src/app/api/documents/route");
- const response = await GET(authenticatedAuthTokenCookieRequest("/api/documents"));
+ const response = await GET(request("/api/documents?includeMeta=true"));
const body = await payload(response);
expect(response.status).toBe(200);
- expect(client.auth.getUser).toHaveBeenCalledWith(token);
- expect(body.documents).toEqual(documents.map((document) => ({ ...document, labels: [], summary: null })));
- expect(client.calls[0].filters).toContainEqual({ column: "owner_id", value: userId });
+ expect(client.calls[0].selected).not.toContain("storage_path");
+ expect(client.calls[0].selected).not.toContain("content_hash");
+ expect(body.documents).toEqual(documents);
+ });
+
+ it("rate limits anonymous document read bursts", async () => {
+ const client = createSupabaseMock((call) => (call.table === "documents" ? ok([]) : ok([])));
+ mockRuntime(client);
+ client.rpc.mockImplementation(async (name: string, args?: Record) => {
+ if (name === "consume_api_subject_rate_limit" && args?.p_bucket === "document_read") {
+ return {
+ data: [rateLimitRow({ limited: true, remaining: 0, retry_after_seconds: 30 })],
+ error: null,
+ };
+ }
+ if (name === "consume_api_rate_limit") {
+ return { data: [rateLimitRow()], error: null };
+ }
+ return ok([]);
+ });
+ const { GET } = await import("../src/app/api/documents/route");
+
+ const response = await GET(request("/api/documents"));
+
+ expect(response.status).toBe(429);
+ expect(await payload(response)).toMatchObject({
+ error: "Document requests are rate limited. Try again shortly.",
+ retryAfterSeconds: 30,
+ });
+ expect(client.rpc).toHaveBeenCalledWith(
+ "consume_api_subject_rate_limit",
+ expect.objectContaining({ p_bucket: "document_read" }),
+ );
});
it("does not return raw internal database errors", async () => {
@@ -567,7 +674,7 @@ describe("private document API access", () => {
it("allows document signed URLs only for owned documents", async () => {
const client = createSupabaseMock((call) => {
- if (call.table === "documents" && call.filters.some((filter) => filter.value === userId)) {
+ if (call.table === "documents" && matchesOwnerReadScope(call, userId)) {
return ok({ storage_path: `${userId}/documents/${documentId}/source.pdf`, file_type: "application/pdf" });
}
return ok(null);
@@ -602,6 +709,48 @@ describe("private document API access", () => {
expect(client.storageMocks.createSignedUrl).not.toHaveBeenCalled();
});
+ it("allows anonymous signed URLs for public documents", async () => {
+ const client = createSupabaseMock((call) => {
+ if (
+ call.table === "documents" &&
+ call.filters.some((filter) => filter.column === "owner_id" && filter.value === null)
+ ) {
+ return ok({ storage_path: "public/documents/guideline.pdf", file_type: "application/pdf" });
+ }
+ return ok(null);
+ });
+ mockRuntime(client);
+ const { GET } = await import("../src/app/api/documents/[id]/signed-url/route");
+
+ const response = await GET(request(`/api/documents/${documentId}/signed-url`), {
+ params: Promise.resolve({ id: documentId }),
+ });
+ const body = await payload(response);
+
+ expect(response.status).toBe(200);
+ expect(body.url).toContain("public/documents/guideline.pdf");
+ expect(client.auth.getUser).not.toHaveBeenCalled();
+ });
+
+ it("rejects anonymous signed URLs for private documents", async () => {
+ const client = createSupabaseMock((call) => {
+ if (call.table === "documents" && call.filters.some((filter) => filter.column === "owner_id")) {
+ return ok(null);
+ }
+ return ok(null);
+ });
+ mockRuntime(client);
+ const { GET } = await import("../src/app/api/documents/[id]/signed-url/route");
+
+ const response = await GET(request(`/api/documents/${otherDocumentId}/signed-url`), {
+ params: Promise.resolve({ id: otherDocumentId }),
+ });
+
+ expect(response.status).toBe(404);
+ expect(await payload(response)).toEqual({ error: "Document not found." });
+ expect(client.storageMocks.createSignedUrl).not.toHaveBeenCalled();
+ });
+
it("allows image signed URLs only when the parent document is owned", async () => {
const client = createSupabaseMock((call) => {
if (call.table === "document_images") {
@@ -613,7 +762,7 @@ describe("private document API access", () => {
metadata: { index_generation_id: "generation-a" },
});
}
- if (call.table === "documents" && call.filters.some((filter) => filter.value === userId)) {
+ if (call.table === "documents" && matchesOwnerReadScope(call, userId)) {
return ok({ id: documentId, metadata: { index_generation_id: "generation-a" } });
}
return ok(null);
@@ -642,7 +791,7 @@ describe("private document API access", () => {
metadata: { index_generation_id: "generation-a" },
});
}
- if (call.table === "documents" && call.filters.some((filter) => filter.value === userId)) {
+ if (call.table === "documents" && matchesOwnerReadScope(call, userId)) {
return ok({ id: documentId, metadata: {} });
}
return ok(null);
@@ -671,7 +820,7 @@ describe("private document API access", () => {
metadata: { index_generation_id: "generation-new" },
});
}
- if (call.table === "documents" && call.filters.some((filter) => filter.value === userId)) {
+ if (call.table === "documents" && matchesOwnerReadScope(call, userId)) {
return ok({ id: documentId, metadata: { index_generation_id: "generation-old" } });
}
return ok(null);
@@ -711,6 +860,59 @@ describe("private document API access", () => {
expect(client.storageMocks.createSignedUrl).not.toHaveBeenCalled();
});
+ it("rejects anonymous upload with setup guidance when public uploads are not configured", async () => {
+ const client = createSupabaseMock();
+ mockRuntime(client);
+ const { POST } = await import("../src/app/api/upload/route");
+ const formData = new FormData();
+ formData.set("file", new File(["%PDF-1.7"], "guideline.pdf", { type: "application/pdf" }));
+
+ const response = await POST(
+ request("/api/upload", {
+ method: "POST",
+ body: formData,
+ }),
+ );
+
+ expect(response.status).toBe(503);
+ expect(await payload(response)).toEqual({ error: "Public uploads are not configured for this workspace." });
+ expect(client.auth.getUser).not.toHaveBeenCalled();
+ expect(client.storageMocks.upload).not.toHaveBeenCalled();
+ });
+
+ it("uploads anonymous documents to the configured public workspace owner", async () => {
+ const publicOwnerId = "99999999-9999-4999-8999-999999999999";
+ const client = createSupabaseMock((call) => {
+ if (call.table === "documents" && call.operation === "select" && call.maybeSingle) return ok(null);
+ if (call.table === "documents" && call.operation === "insert") {
+ const inserted = call.insertPayload as { id: string; owner_id: string; storage_path: string };
+ return ok({ id: inserted.id, owner_id: inserted.owner_id, storage_path: inserted.storage_path });
+ }
+ if (call.table === "ingestion_jobs" && call.operation === "insert")
+ return ok({ id: "job-1", document_id: documentId });
+ return ok([]);
+ });
+ mockRuntime(client, undefined, { publicUploadsEnabled: true, publicWorkspaceOwnerId: publicOwnerId });
+ const { POST } = await import("../src/app/api/upload/route");
+ const formData = new FormData();
+ formData.set("file", new File(["%PDF-1.7"], "guideline.pdf", { type: "application/pdf" }));
+
+ const response = await POST(
+ request("/api/upload", {
+ method: "POST",
+ body: formData,
+ }),
+ );
+
+ expect(response.status).toBe(201);
+ expect(client.auth.getUser).not.toHaveBeenCalled();
+ expect(
+ client.calls.find((call) => call.table === "documents" && call.operation === "insert")?.insertPayload,
+ ).toMatchObject({
+ owner_id: publicOwnerId,
+ });
+ });
+
it("stores uploaded documents with owner_id and a user-scoped storage path", async () => {
const client = createSupabaseMock((call) => {
if (call.table === "documents" && call.operation === "insert") {
@@ -2095,9 +2297,13 @@ describe("private document API access", () => {
}
return ok([]);
});
- client.rpc.mockImplementation(async (name: string) =>
- name === "search_document_chunks" ? fail("missing rpc") : ok([]),
- );
+ client.rpc.mockImplementation(async (name: string) => {
+ if (name === "search_document_chunks") return fail("missing rpc");
+ if (name === "consume_api_rate_limit" || name === "consume_api_subject_rate_limit") {
+ return { data: [rateLimitRow()], error: null };
+ }
+ return ok([]);
+ });
mockRuntime(client);
const { GET } = await import("../src/app/api/documents/[id]/search/route");
@@ -2788,6 +2994,41 @@ describe("private document API access", () => {
);
});
+ it("degrades invalid bearer tokens to anonymous search scope", async () => {
+ const searchChunksWithTelemetry = vi.fn(async () => ({
+ results: [],
+ telemetry: {
+ search_cache_hit: false,
+ text_fast_path_latency_ms: 0,
+ embedding_skipped: true,
+ embedding_latency_ms: 0,
+ embedding_cache_hit: false,
+ supabase_rpc_latency_ms: 0,
+ rerank_latency_ms: 0,
+ retrieval_strategy: "text_fast_path",
+ },
+ }));
+ const client = createSupabaseMock();
+ mockRuntime(client, { searchChunksWithTelemetry });
+ const searchRoute = await import("../src/app/api/search/route");
+
+ const response = await searchRoute.POST(
+ request("/api/search", {
+ method: "POST",
+ headers: {
+ authorization: "Bearer expired-token",
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({ query: "monitoring" }),
+ }),
+ );
+
+ expect(response.status).toBe(200);
+ expect(searchChunksWithTelemetry).toHaveBeenCalledWith(
+ expect.objectContaining({ ownerId: undefined, allowGlobalSearch: true }),
+ );
+ });
+
it("rate limits anonymous answer bursts before generation", async () => {
const answerQuestionWithScope = vi.fn(async () => ({
answer: "Public evidence.",
@@ -2957,7 +3198,9 @@ describe("private document API access", () => {
}));
const client = createSupabaseMock();
client.rpc.mockImplementation(async (name: string) =>
- name === "consume_api_rate_limit" ? fail("limiter table unavailable") : ok([]),
+ name === "consume_api_rate_limit" || name === "consume_api_subject_rate_limit"
+ ? fail("limiter table unavailable")
+ : ok([]),
);
mockRuntime(client, { searchChunksWithTelemetry });
const { POST } = await import("../src/app/api/search/route");
diff --git a/tests/public-access-deep.test.ts b/tests/public-access-deep.test.ts
new file mode 100644
index 000000000..9d0cf39d1
--- /dev/null
+++ b/tests/public-access-deep.test.ts
@@ -0,0 +1,224 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+const publicDocumentId = "11111111-1111-4111-8111-111111111111";
+
+beforeEach(() => {
+ vi.resetModules();
+});
+
+afterEach(() => {
+ vi.restoreAllMocks();
+ vi.resetModules();
+ vi.unstubAllEnvs();
+});
+
+describe("public access deep checks", () => {
+ it("rejects unauthenticated access to operator-only routes", async () => {
+ vi.doMock("@/lib/env", () => ({ isDemoMode: () => false, env: {} }));
+ const auth = {
+ AuthenticationError: class AuthenticationError extends Error {},
+ requireAuthenticatedUser: vi.fn(async () => {
+ throw new auth.AuthenticationError();
+ }),
+ unauthorizedResponse: () => Response.json({ error: "Authentication required." }, { status: 401 }),
+ };
+ vi.doMock("@/lib/supabase/admin", () => ({
+ createAdminClient: vi.fn(() => ({ auth: { getUser: vi.fn() } })),
+ }));
+ vi.doMock("@/lib/supabase/auth", () => auth);
+
+ const cases = [
+ {
+ routePath: "../src/app/api/jobs/route",
+ handler: "GET" as const,
+ request: new Request("http://localhost/api/jobs"),
+ },
+ {
+ routePath: "../src/app/api/ingestion/jobs/route",
+ handler: "GET" as const,
+ request: new Request("http://localhost/api/ingestion/jobs"),
+ },
+ {
+ routePath: "../src/app/api/ingestion/batches/route",
+ handler: "GET" as const,
+ request: new Request("http://localhost/api/ingestion/batches"),
+ },
+ {
+ routePath: "../src/app/api/documents/bulk/route",
+ handler: "POST" as const,
+ request: new Request("http://localhost/api/documents/bulk", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ action: "delete", documentIds: [publicDocumentId] }),
+ }),
+ },
+ {
+ routePath: "../src/app/api/documents/[id]/summarize/route",
+ handler: "POST" as const,
+ request: new Request(`http://localhost/api/documents/${publicDocumentId}/summarize`, { method: "POST" }),
+ params: { id: publicDocumentId },
+ },
+ {
+ routePath: "../src/app/api/eval-cases/route",
+ handler: "POST" as const,
+ request: new Request("http://localhost/api/eval-cases", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ query: "What monitoring is needed?",
+ rating: "good",
+ answer: "Monitor FBC.",
+ queryMode: "auto",
+ queryClass: "table_threshold",
+ }),
+ }),
+ },
+ ] as const;
+
+ for (const testCase of cases) {
+ vi.resetModules();
+ vi.doMock("@/lib/env", () => ({ isDemoMode: () => false, env: {} }));
+ vi.doMock("@/lib/supabase/admin", () => ({
+ createAdminClient: vi.fn(() => ({ auth: { getUser: vi.fn() } })),
+ }));
+ vi.doMock("@/lib/supabase/auth", () => auth);
+ const mod = await import(testCase.routePath);
+ const handler = mod[testCase.handler];
+ const response = await handler(
+ testCase.request,
+ "params" in testCase ? { params: Promise.resolve(testCase.params) } : undefined,
+ );
+ expect(response.status, testCase.routePath).toBe(401);
+ }
+ });
+
+ it("does not expose secret values from the health endpoint", async () => {
+ vi.doMock("@/lib/env", () => ({
+ env: {
+ NEXT_PUBLIC_SUPABASE_URL: "https://sjrfecxgysukkwxsowpy.supabase.co",
+ SUPABASE_SERVICE_ROLE_KEY: "super-secret-service-role-key",
+ OPENAI_API_KEY: "sk-super-secret-openai-key",
+ },
+ isDemoMode: () => false,
+ }));
+ const { GET } = await import("../src/app/api/health/route");
+ const response = await GET(new Request("https://clinical.example/api/health?deep=1"));
+ const body = await response.json();
+ const serialized = JSON.stringify(body);
+
+ expect(serialized).not.toContain("super-secret-service-role-key");
+ expect(serialized).not.toContain("sk-super-secret-openai-key");
+ expect(body.checks.supabaseConfig).toBe("ok");
+ expect(body.checks.openaiConfig).toBe("ok");
+ });
+});
+
+describe("production anonymous retrieval scope", () => {
+ beforeEach(() => {
+ vi.resetModules();
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ vi.resetModules();
+ vi.unstubAllEnvs();
+ });
+
+ it("fails closed in production when anonymous global retrieval omits owner scope", async () => {
+ vi.doUnmock("@/lib/rag");
+ vi.stubEnv("NODE_ENV", "production");
+ vi.doMock("@/lib/env", () => ({
+ env: {
+ OPENAI_API_KEY: "sk-test",
+ RAG_PROVIDER_MODE: "offline",
+ RAG_SEARCH_CACHE_TTL_MS: 0,
+ RAG_ANSWER_CACHE_TTL_MS: 0,
+ },
+ isDemoMode: () => false,
+ isLocalNoAuthMode: () => false,
+ requestedOpenAIAnswerModels: () => ({ fast: "gpt-test", strong: "gpt-test" }),
+ }));
+ vi.doMock("@/lib/supabase/admin", () => ({
+ createAdminClient: vi.fn(() => ({
+ from: vi.fn(() => ({
+ select: vi.fn(() => ({
+ eq: vi.fn(() => ({
+ is: vi.fn(() => ({
+ order: vi.fn(() => ({
+ limit: vi.fn(async () => ({ data: [], error: null })),
+ })),
+ })),
+ })),
+ })),
+ })),
+ rpc: vi.fn(async () => ({ data: [], error: null })),
+ })),
+ }));
+
+ const { searchChunksWithTelemetry } = await import("../src/lib/rag");
+
+ await expect(
+ searchChunksWithTelemetry({
+ query: "clozapine monitoring",
+ allowGlobalSearch: true,
+ }),
+ ).rejects.toThrow(/ownerId|tenant/i);
+ });
+});
+
+describe("test-runtime anonymous retrieval scope", () => {
+ beforeEach(() => {
+ vi.resetModules();
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ vi.resetModules();
+ vi.unstubAllEnvs();
+ });
+
+ it("allows anonymous global retrieval in test runtime where owner scope stays permissive", async () => {
+ vi.doUnmock("@/lib/rag");
+ vi.doMock("@/lib/env", () => ({
+ env: {
+ OPENAI_API_KEY: undefined,
+ RAG_PROVIDER_MODE: "offline",
+ RAG_SEARCH_CACHE_TTL_MS: 0,
+ RAG_ANSWER_CACHE_TTL_MS: 0,
+ },
+ isDemoMode: () => false,
+ isLocalNoAuthMode: () => false,
+ requestedOpenAIAnswerModels: () => ({ fast: "gpt-test", strong: "gpt-test" }),
+ }));
+ vi.doMock("@/lib/supabase/admin", () => ({
+ createAdminClient: vi.fn(() => ({
+ from: vi.fn(() => ({
+ select: vi.fn(() => ({
+ eq: vi.fn(() => ({
+ is: vi.fn(() => ({
+ order: vi.fn(() => ({
+ limit: vi.fn(async () => ({ data: [], error: null })),
+ })),
+ in: vi.fn(() => ({
+ order: vi.fn(() => ({
+ limit: vi.fn(async () => ({ data: [], error: null })),
+ })),
+ })),
+ })),
+ })),
+ })),
+ })),
+ rpc: vi.fn(async () => ({ data: [], error: null })),
+ })),
+ }));
+
+ const { searchChunksWithTelemetry } = await import("../src/lib/rag");
+ const result = await searchChunksWithTelemetry({
+ query: "clozapine monitoring",
+ allowGlobalSearch: true,
+ });
+
+ expect(result.results).toEqual([]);
+ expect(result.telemetry).toBeDefined();
+ });
+});
diff --git a/tests/registry-records-route.test.ts b/tests/registry-records-route.test.ts
index 388bd52e5..5571c270c 100644
--- a/tests/registry-records-route.test.ts
+++ b/tests/registry-records-route.test.ts
@@ -198,11 +198,8 @@ describe("registry records API", () => {
expect(payload.records.some((record) => record.slug === "13yarn")).toBe(true);
expect(payload.matches?.[0]?.record.slug).toBe("13yarn");
expect(client.from).not.toHaveBeenCalled();
- expect(client.rpc).toHaveBeenCalledWith(
- "consume_api_subject_rate_limit",
- expect.objectContaining({ p_bucket: "registry" }),
- );
- expect(client.rpc).not.toHaveBeenCalledWith("consume_api_rate_limit", expect.anything());
+ expect(client.rpc).not.toHaveBeenCalled();
+ expect(client.auth.getUser).not.toHaveBeenCalled();
});
it("rejects an invalid kind with a validation error", async () => {
@@ -311,11 +308,8 @@ describe("registry records API", () => {
expect(payload.record.slug).toBe("13yarn");
expect(payload.linkedDocuments).toEqual([]);
expect(client.from).not.toHaveBeenCalled();
- expect(client.rpc).toHaveBeenCalledWith(
- "consume_api_subject_rate_limit",
- expect.objectContaining({ p_bucket: "registry" }),
- );
- expect(client.rpc).not.toHaveBeenCalledWith("consume_api_rate_limit", expect.anything());
+ expect(client.rpc).not.toHaveBeenCalled();
+ expect(client.auth.getUser).not.toHaveBeenCalled();
});
it("returns 404 for an unknown slug", async () => {
diff --git a/tests/retrieval-query-variants.test.ts b/tests/retrieval-query-variants.test.ts
index ddb62411e..a2f06474e 100644
--- a/tests/retrieval-query-variants.test.ts
+++ b/tests/retrieval-query-variants.test.ts
@@ -127,6 +127,18 @@ describe("retrieval query variants", () => {
expect(textCandidateBudgetForQueryClass("unsupported_or_general", 12)).toBe(24);
});
+ it("keeps forced-embedding retrieval out of the ordinary search cache key", () => {
+ const baseArgs = {
+ query: "How is panic disorder managed?",
+ topK: 8,
+ minSimilarity: 0.12,
+ };
+
+ expect(retrievalPlanCacheQuery(baseArgs, "broad_summary")).not.toBe(
+ retrievalPlanCacheQuery({ ...baseArgs, forceEmbedding: true }, "broad_summary"),
+ );
+ });
+
it("allows direct document title hits to skip embedding retrieval", () => {
expect(
decideTextFastPath(
diff --git a/tests/setup-status-route.test.ts b/tests/setup-status-route.test.ts
index 170aac7f5..f952b2ef8 100644
--- a/tests/setup-status-route.test.ts
+++ b/tests/setup-status-route.test.ts
@@ -7,10 +7,13 @@ afterEach(() => {
});
describe("/api/setup-status", () => {
- it("requires auth for non-local production requests before returning setup posture", async () => {
+ it("returns setup posture for anonymous production requests without exposing secret values", async () => {
vi.stubEnv("NODE_ENV", "production");
- const getUser = vi.fn();
- const createAdminClient = vi.fn(() => ({ auth: { getUser } }));
+ const from = vi.fn(async () => ({ error: null, data: [], count: 0 }));
+ const createAdminClient = vi.fn(() => ({
+ from,
+ rpc: vi.fn(),
+ }));
vi.doMock("@/lib/env", () => ({
env: {
NEXT_PUBLIC_SUPABASE_URL: "https://sjrfecxgysukkwxsowpy.supabase.co",
@@ -24,16 +27,29 @@ describe("/api/setup-status", () => {
isLocalNoAuthMode: () => false,
}));
vi.doMock("@/lib/supabase/admin", () => ({ createAdminClient }));
+ vi.doMock("@/lib/supabase/health", () => ({
+ probeSupabaseHealth: vi.fn(async () => ({ ok: true })),
+ isSupabaseUnavailableError: () => false,
+ formatSupabaseUnavailableError: (error: unknown) => String(error),
+ }));
+ vi.doMock("@/lib/supabase/project", () => ({
+ checkSupabaseProjectConfig: () => ({ status: "ready", detail: "Clinical KB Database target is configured." }),
+ formatSupabaseProjectCheck: () => "Clinical KB Database target is configured.",
+ }));
const { GET } = await import("../src/app/api/setup-status/route");
const response = await GET(new Request("https://clinical.example/api/setup-status"));
const body = await response.json();
- expect(response.status).toBe(401);
- expect(body).toEqual({ error: "Authentication required." });
- expect(JSON.stringify(body)).not.toContain("OPENAI");
- expect(JSON.stringify(body)).not.toContain("Supabase");
- expect(createAdminClient).toHaveBeenCalledTimes(1);
- expect(getUser).not.toHaveBeenCalled();
+ expect(response.status).toBe(200);
+ expect(body).toMatchObject({
+ demoMode: false,
+ checks: expect.arrayContaining([
+ expect.objectContaining({ id: "env" }),
+ expect.objectContaining({ id: "openai" }),
+ ]),
+ });
+ expect(JSON.stringify(body)).not.toContain("service-role-key");
+ expect(JSON.stringify(body)).not.toContain("openai-key");
});
});
diff --git a/tests/supabase-schema.test.ts b/tests/supabase-schema.test.ts
index 68cd98773..6dd16d6c9 100644
--- a/tests/supabase-schema.test.ts
+++ b/tests/supabase-schema.test.ts
@@ -80,6 +80,10 @@ const registryCatalogPayloadMigration = readFileSync(
new URL("../supabase/migrations/20260705030000_registry_catalog_payload.sql", import.meta.url),
"utf8",
).replace(/\s+/g, " ");
+const searchHealthIndexesMigration = readFileSync(
+ new URL("../supabase/migrations/20260705180000_reconcile_search_health_indexes.sql", import.meta.url),
+ "utf8",
+).replace(/\s+/g, " ");
const ragQueriesRetentionMigration = readFileSync(
new URL("../supabase/migrations/20260629060603_rag_queries_retention.sql", import.meta.url),
"utf8",
@@ -739,6 +743,17 @@ describe("Supabase schema Data API grants", () => {
"add column if not exists catalog_payload jsonb not null default '{}'::jsonb",
);
});
+
+ it("reconciles search_schema_health index drift with canonical creates and live aliases", () => {
+ expect(searchHealthIndexesMigration).toContain("create index if not exists documents_title_trgm_idx");
+ expect(searchHealthIndexesMigration).toContain("create index if not exists document_labels_label_trgm_idx");
+ expect(searchHealthIndexesMigration).toContain("create index if not exists rag_retrieval_logs_miss_idx");
+ expect(searchHealthIndexesMigration).toContain("index_aliases constant jsonb := jsonb_build_object(");
+ expect(searchHealthIndexesMigration).toContain("'documents_title_search_tsv_idx'");
+ expect(searchHealthIndexesMigration).toContain("'document_pages_document_id_page_number_key'");
+ expect(schema).toContain("index_aliases constant jsonb := jsonb_build_object(");
+ expect(schema).toContain("jsonb_array_elements_text(index_aliases -> index_name)");
+ });
});
describe("RC9 — lexical text path must not fabricate a cosine similarity", () => {
diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts
index 3d2bae636..2eb32ebbf 100644
--- a/tests/ui-smoke.spec.ts
+++ b/tests/ui-smoke.spec.ts
@@ -143,6 +143,7 @@ async function mockLocalProjectIdentity(page: Page) {
}
async function mockPrivateUnauthenticatedApi(page: Page) {
+ await mockLocalProjectIdentity(page);
await page.route("**/api/setup-status**", async (route) => {
await route.fulfill({
json: { demoMode: false, checks: readySetupChecks },
@@ -706,6 +707,36 @@ test.describe("Clinical KB UI smoke coverage", () => {
});
}
+ test("anonymous user can see enabled live search without a forced sign-in gate", async ({ page }) => {
+ await page.setViewportSize({ width: 1280, height: 900 });
+ await mockPrivateUnauthenticatedApi(page);
+ await page.route(/\/api\/search(?:\?.*)?$/, async (route) => {
+ await route.fulfill({ json: { results: [], telemetry: { retrieval_strategy: "text_fast_path" } } });
+ });
+ await gotoApp(page, "/");
+ await waitForDemoDashboardReady(page);
+
+ await expect(page.getByText("Create your Clinical Guide account")).toHaveCount(0);
+ await expect(page.getByText("Search request was not authorized by the server.")).toHaveCount(0);
+ await expect(page.getByTestId("global-search-input")).toBeEnabled();
+ });
+
+ test("anonymous mobile user can search without a forced sign-in gate", async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 820 });
+ await mockPrivateUnauthenticatedApi(page);
+ await page.route(/\/api\/search(?:\?.*)?$/, async (route) => {
+ await route.fulfill({ json: { results: [], telemetry: { retrieval_strategy: "text_fast_path" } } });
+ });
+ await gotoApp(page, "/");
+ await waitForDemoDashboardReady(page);
+
+ await expect(page.getByText("Create your Clinical Guide account")).toHaveCount(0);
+ await expect(page.getByText("Service unavailable")).toHaveCount(0);
+ await expect(page.getByText("API unavailable")).toHaveCount(0);
+ await expect(page.getByText("Search request was not authorized by the server.")).toHaveCount(0);
+ await expect(page.getByTestId("global-search-input")).toBeEnabled();
+ });
+
test("desktop sidebar mode sync and accessibility affordances stay coherent", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await mockDemoApi(page);
diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts
index ef9823b7d..3037cfaa4 100644
--- a/tests/ui-tools.spec.ts
+++ b/tests/ui-tools.spec.ts
@@ -524,12 +524,13 @@ test.describe("Clinical KB applications launcher", () => {
test("mode home deep links preserve focus=1 on initial load", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
- for (const path of ["/services?focus=1", "/forms?focus=1"]) {
- await gotoLauncher(page, path);
- const sharedSearch = page.getByTestId("global-search-input");
- await expect(sharedSearch).toBeVisible();
- await expect(sharedSearch).toBeFocused();
- }
+ await gotoLauncher(page, "/services?focus=1");
+ await expect(page.getByTestId("services-home").getByTestId("global-search-input")).toBeVisible();
+ await expect(page.getByTestId("services-home").getByTestId("global-search-input")).toBeFocused();
+
+ await gotoLauncher(page, "/forms?focus=1");
+ await expect(page.getByTestId("forms-home").getByTestId("global-search-input")).toBeVisible();
+ await expect(page.getByTestId("forms-home").getByTestId("global-search-input")).toBeFocused();
});
test("services mode shows source-backed records in search results", async ({ page }) => {
@@ -573,11 +574,12 @@ test.describe("Clinical KB applications launcher", () => {
test("form detail pages keep the shared forms search wired to form results", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await gotoLauncher(page, "/forms/transport-crisis-form");
+ await expect(page.getByTestId("form-detail-page")).toBeVisible();
// Structural coverage — runs on every browser, WebKit included: the form
// detail page renders inside the shared shell with the Forms-mode composer
// present and no stale results.
- await expect(page.getByRole("button", { name: "Mode Forms" })).toBeVisible();
+ await expect(page.getByRole("button", { name: "Mode Forms" })).toBeVisible({ timeout: 20_000 });
await expect(page.getByRole("heading", { level: 1, name: "Transport order" })).toBeVisible();
await expect(page.getByTestId("form-search-results")).toHaveCount(0);
const formsSearchInput = page.locator('input[placeholder="Search forms..."]:visible').first();