Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9dad71b
Fix content access gaps for anonymous and public document users
BigSimmo Jul 5, 2026
3f38a33
Reconcile search_schema_health index drift on live Supabase
BigSimmo Jul 5, 2026
413245a
fix(db): reconcile live drift and harden security without RAG regression
BigSimmo Jul 5, 2026
976c287
chore(db): codify live search_document_chunks owner-scope migration
BigSimmo Jul 5, 2026
8275263
chore(db): mirror search_document_chunks in schema and fix edge JSONB…
BigSimmo Jul 5, 2026
f7b0edb
fix(edge): use JobStatusRpcResult for JSONB status RPC parsing
BigSimmo Jul 5, 2026
6f1d525
style: fix Prettier formatting for CI verify gate
BigSimmo Jul 5, 2026
09b3567
fix(rag): add allowGlobalSearch to document lookup chunk args type
BigSimmo Jul 5, 2026
65674db
fix(db): reconcile live drift and harden security without RAG regression
BigSimmo Jul 5, 2026
3179742
fix(edge): use JobStatusRpcResult for JSONB status RPC parsing
BigSimmo Jul 5, 2026
aee99a7
style: fix Prettier formatting for CI verify gate
BigSimmo Jul 5, 2026
6c69cb1
fix(test): remove duplicate closing brace in supabase-schema spec
BigSimmo Jul 5, 2026
34370ee
fix: resolve edge JSONB typing and owner-scope schema test import
BigSimmo Jul 5, 2026
8ab94ea
chore(db): mirror search_document_chunks owner scope in schema.sql
BigSimmo Jul 5, 2026
2e31212
merge: sync main into fix-all-db-issues branch
cursoragent Jul 5, 2026
c0b5f74
merge: integrate remote branch updates
cursoragent Jul 5, 2026
f4809e1
fix(edge): guard null parseJobStatusRpcResult in updateAgentJobStatus
cursoragent Jul 5, 2026
c66df0e
Potential fix for pull request finding
BigSimmo Jul 6, 2026
e04da81
Potential fix for pull request finding
BigSimmo Jul 6, 2026
3fc7de8
merge: resolve conflict in supabase-schema tests — keep both owner-sc…
Copilot Jul 6, 2026
7f4e709
fix(db): rename reconcile migration to avoid 20260705220000 version c…
BigSimmo Jul 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ SUPABASE_PROJECT_REF=sjrfecxgysukkwxsowpy
SUPABASE_PROJECT_NAME=Clinical KB Database
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-or-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Direct Postgres connection string for scripts/migrations that need SQL access.
# Server-only; never expose in client code or NEXT_PUBLIC_ vars.
SUPABASE_DB_URL=postgresql://postgres:password@db.sjrfecxgysukkwxsowpy.supabase.co:5432/postgres
# Edge Function only (indexing-v3-agent cron auth). Not read by Next.js env.ts.
# Set in Supabase Edge Function secrets / deployment env, not in the browser bundle.
INDEXING_V3_AGENT_SECRET=your-long-random-cron-shared-secret
# Secret required for /api/health?deep=1 Supabase probe (x-health-deep-token header).
HEALTH_DEEP_PROBE_SECRET=your-long-random-health-deep-probe-secret

# Local-only no-auth mode (development only).
# Enable one or both of these to load real data without per-request browser auth:
Expand Down
9 changes: 9 additions & 0 deletions docs/process-hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ All approved render-surface modules are extracted. `ClinicalDashboard.tsx` went
- Auth server is capped at 10 absolute DB connections (Supabase advisor); switch to percentage-based allocation in the dashboard before scaling instance size (not settable via SQL/MCP).
- `storage_cleanup_jobs` live indexes drifted from `supabase/schema.sql`: live carries legacy auto-names (`storage_cleanup_jobs_document_id_idx`, `storage_cleanup_jobs_owner_id_idx`, a non-partial `storage_cleanup_jobs_status_created_idx`) that the hardening defs superseded. Migration `20260703030000_reconcile_storage_cleanup_jobs_indexes` is **prepared but NOT applied** — it drops the legacy names and (re)creates the intended named/partial indexes to match schema.sql. Functional-not-broken (the document_id FK is covered), so apply to live only with explicit approval. `20260703000000`/`010000` are also absent from live `schema_migrations` and will self-heal on the next `supabase db push`.

## Live database drift reconciliation (2026-07-05)

- **RESOLVED:** `indexing_v3_agent_jobs` table + `claim_indexing_v3_agent_jobs` + `update_indexing_v3_agent_job_status` were recorded as applied but absent on live. Migration `20260705230000_reconcile_live_database_drift` idempotently re-applied them; live verified post-push.
- **RESOLVED:** `match_document_embedding_fields_text` codified with service_role-only execute.
- **RESOLVED:** `rag_visual_eval_*` tables codified with service_role-only RLS.
- **RESOLVED:** Live-only `20260705133000_tighten_search_document_chunks_owner_scope` mirrored in `schema.sql`.
- **Edge function follow-up:** deploy `indexing-v3-agent` after merge so JSONB status RPC parsing is live.
- **Operator-only:** publishable key rotation (`docs/archive/operator-decisions-2026-07-04.md`).

## PR merge gate: tiered CI + required checks (2026-07-02)

- CI is now two parallel PR jobs instead of one serial 6-7 minute job: `verify` (runtime alignment, edge typecheck, CI-safe production readiness, lint, typecheck, unit tests with coverage gate, build — ~3 min) and `ui-smoke` (Chromium Playwright smoke against its own dev server — ~4.5 min). Wall-clock PR feedback drops to the slower of the two, and a flaky smoke rerun no longer repeats lint/typecheck/tests/build.
Expand Down
12 changes: 10 additions & 2 deletions docs/supabase-migration-reconciliation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Supabase Migration Reconciliation

Last reviewed: 2026-07-04
Last reviewed: 2026-07-05

Target project: Clinical KB Database (`sjrfecxgysukkwxsowpy`)

Expand All @@ -27,7 +27,15 @@ These previously local-only versions were verified in the live project history b

## Current Status (July 2026)

The repo now includes additional July 2026 migrations beyond the June checkpoint above, including:
Migration `20260705230000_reconcile_live_database_drift.sql` codifies live-only drift discovered 2026-07-05:

- `indexing_v3_agent_jobs` table and claim/update RPCs (recorded as applied in history but absent on live at inspection time)
- `match_document_embedding_fields_text` RPC with service-role-only execute grants (was present on live with anon/auth execute)
- `rag_visual_eval_cases` / `rag_visual_eval_runs` tables with service-role-only RLS (were present on live without RLS)

`supabase/schema.sql` has been reconciled to match. Apply the migration through the normal linked workflow when ready; do not use raw dashboard SQL for retrieval RPCs.

The repo also includes additional July 2026 migrations beyond the June checkpoint above, including:

- Retrieval RPC codification and hybrid execution smoke (`20260701140631`, related July 1 fixes)
- Legacy vector index drops and `search_schema_health()` reconciliation (`20260702014803`, `20260702021604`)
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@
"pdfjs-dist": "^6.1.200",
"pdfkit": "^0.19.1",
"postcss": "^8.5.15",
"postgres": "3.4.9",
"react": "19.2.7",
"react-dom": "19.2.7",
"zod": "^4.4.3"
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-document-label-governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ async function main() {
console.log(`Low confidence generated labels: ${report.analytics.lowConfidence}`);
console.log(`Quality warnings: ${report.analytics.qualityIssues.length}`);
console.log(`Blocking quality issues: ${report.analytics.blockingQualityIssues.length}`);
console.log(`Missing gold-label rows: ${report.analytics.missingGoldLabels.length}`);
console.log(`Missing gold-label rows (advisory): ${report.analytics.missingGoldLabels.length}`);
console.log(
`Relevance checks: ${report.relevanceChecks.filter((check) => check.passed).length}/${report.relevanceChecks.length} passed`,
);
Expand Down
26 changes: 19 additions & 7 deletions src/app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
import { timingSafeEqual } from "node:crypto";
import { NextResponse } from "next/server";
import { env, isDemoMode } from "@/lib/env";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

// Liveness/readiness probe for load balancers and uptime monitors. It reports
// whether the server is configured to operate for real (Supabase + OpenAI present)
// and, with ?deep=1, whether Supabase is actually reachable. The payload exposes only
// boolean configuration presence and operational status — never secret values.
function allowDeepHealthProbe(request: Request): boolean {
const secret = env.HEALTH_DEEP_PROBE_SECRET;
if (!secret) return false;
const token = request.headers.get("x-health-deep-token");
if (!token) return false;
if (token.length !== secret.length) return false;
const expected = Buffer.from(secret, "utf8");
const received = Buffer.from(token, "utf8");
return timingSafeEqual(expected, received);
Comment thread
Copilot marked this conversation as resolved.
}

export async function GET(request: Request) {
const deep = new URL(request.url).searchParams.get("deep") === "1";
const supabaseConfigured = Boolean(env.NEXT_PUBLIC_SUPABASE_URL && env.SUPABASE_SERVICE_ROLE_KEY);

const checks: Record<string, "ok" | "missing" | "error" | "skipped"> = {
const checks: Record<string, "ok" | "missing" | "error" | "skipped" | "unauthorized"> = {
supabaseConfig: supabaseConfigured ? "ok" : "missing",
openaiConfig: env.OPENAI_API_KEY ? "ok" : "missing",
};

if (deep) {
if (supabaseConfigured && !isDemoMode()) {
if (!allowDeepHealthProbe(request)) {
checks.supabase = "unauthorized";
} else if (supabaseConfigured && !isDemoMode()) {
try {
const [{ createAdminClient }, { probeSupabaseHealth }] = await Promise.all([
import("@/lib/supabase/admin"),
Expand All @@ -34,7 +44,9 @@ export async function GET(request: Request) {
}
}

const ready = !Object.values(checks).includes("missing") && !Object.values(checks).includes("error");
const ready = !Object.values(checks).some(
(value) => value === "missing" || value === "error" || value === "unauthorized",
);

return NextResponse.json(
{
Expand Down
5 changes: 1 addition & 4 deletions src/lib/document-label-governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,6 @@ export function buildDocumentLabelGovernanceReport(documents: LabelGovernanceDoc
analytics,
qaSample,
relevanceChecks,
passed:
analytics.blockingQualityIssues.length === 0 &&
analytics.missingGoldLabels.length === 0 &&
relevanceChecks.every((check) => check.passed),
passed: analytics.blockingQualityIssues.length === 0 && relevanceChecks.every((check) => check.passed),
};
}
2 changes: 2 additions & 0 deletions src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const envSchema = z.object({
SUPABASE_PROJECT_REF: z.string().optional(),
SUPABASE_PROJECT_NAME: z.string().optional(),
SUPABASE_SERVICE_ROLE_KEY: z.string().optional(),
SUPABASE_DB_URL: z.string().url().optional(),
HEALTH_DEEP_PROBE_SECRET: z.string().min(16).optional(),
NEXT_PUBLIC_LOCAL_NO_AUTH: z.enum(["true", "false"]).optional().default("false"),
LOCAL_NO_AUTH: z.enum(["true", "false"]).optional().default("false"),
LOCAL_NO_AUTH_OWNER_EMAIL: z.string().optional(),
Expand Down
5 changes: 3 additions & 2 deletions supabase/functions/indexing-v3-agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1882,7 +1882,7 @@ async function updateAgentJobStatus(
error: string | null = null,
nextRunAt: string | null = null,
): Promise<void> {
const rows = await sql<Array<{ ok: boolean }>>`
const rows = await sql<JobStatusRpcResult[]>`
select *
from public.update_indexing_v3_agent_job_status(
${job.document_id}::uuid,
Expand All @@ -1891,7 +1891,8 @@ async function updateAgentJobStatus(
${nextRunAt}::timestamptz
)
`;
if (!rows[0]?.ok) {
const result = parseJobStatusRpcResult(rows[0], "update_indexing_v3_agent_job_status");
if (!result?.ok) {
throw new Error(`Failed to update indexing_v3_agent_jobs status to ${status} for document ${job.document_id}`);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-- 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.
-- Codify live migration tighten_search_document_chunks_owner_scope (20260705133000).
-- Fail closed: null p_owner_id may only search public documents (owner_id is null).
-- 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,
Expand Down Expand Up @@ -69,4 +69,5 @@ as $$
limit least(greatest(match_count, 1), 80);
$$;

revoke execute on function public.search_document_chunks(uuid, text, integer, uuid) from public, anon, authenticated;
grant execute on function public.search_document_chunks(uuid, text, integer, uuid) to service_role;
Loading
Loading