Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# Supabase project values for the live "Clinical KB Database" project.
# Supabase project values for the live "Clinical KB Database" project.
# Keep service role keys server-only; never prefix them with NEXT_PUBLIC_ or
# import them into client components.
NEXT_PUBLIC_SUPABASE_URL=https://sjrfecxgysukkwxsowpy.supabase.co
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 `20260705220000_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
2 changes: 1 addition & 1 deletion docs/source-review-priority-2026-07-02.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ ceiling 0.2) to 0.5398 in the afternoon run. The jump is not corpus decay: the r
review-flagged sources are no longer buried and the metric now reports the true corpus state surfacing in
golden-case top results. The bounded debt acceptance in
`docs/release-source-metadata-debt-2026-06-30.json` was re-accepted at a 0.6 ceiling (expiry unchanged,
2026-07-31) on the condition that the documents below are clinically reviewed first.
2026-08-31) on the condition that the documents below are clinically reviewed first.

Corpus context (live DB, 2026-07-02): 2,065 indexed documents — 1,397 current/locally_reviewed, 481
`review_due`, 132 `unknown` status, 130 `unverified` validation, 0 outdated, 0 poor-extraction.
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 `20260705220000_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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"visual:backfill": "tsx scripts/backfill-visual-intelligence.ts",
"backfill:text-normalization": "tsx scripts/backfill-text-normalization.ts",
"check:supabase-project": "tsx scripts/check-supabase-project.ts",
"check:retrieval-owner": "tsx scripts/check-retrieval-owner-migration.ts",
"check:indexing": "tsx scripts/check-indexing.ts",
"check:type-scale": "node scripts/check-type-scale.mjs",
"recover:ingestion": "tsx scripts/recover-ingestion-queue.ts",
Expand All @@ -68,6 +69,7 @@
"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",
"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",
Expand Down Expand Up @@ -108,7 +110,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
135 changes: 135 additions & 0 deletions scripts/apply-governance-dashboard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import fs from "node:fs";

const path = "src/components/ClinicalDashboard.tsx";
let s = fs.readFileSync(path, "utf8");

if (!s.includes("SourceReviewQueuePanel")) {
s = s.replace(
'import { DocumentSearchResultsPanel, type SearchFacets } from "@/components/clinical-dashboard/document-search-results";',
'import { DocumentSearchResultsPanel, type SearchFacets } from "@/components/clinical-dashboard/document-search-results";\nimport { SourceReviewQueuePanel } from "@/components/clinical-dashboard/source-review-queue-panel";',
);
}

if (!s.includes("search-request-token")) {
s = s.replace(
`import {
frontendSourceGovernanceWarnings,
groupSourceGovernanceWarnings,
type SourceGovernanceWarning,
} from "@/lib/source-governance";`,
`import {
frontendSourceGovernanceWarnings,
groupSourceGovernanceWarnings,
serializeSourceGovernanceWarning,
type SourceGovernanceWarning,
} from "@/lib/source-governance";
import {
invalidateSearchRequests,
isLatestSearchRequest,
type SearchRequestToken,
} from "@/lib/search-request-token";`,
);
}

s = s.replace(
"const searchRequestSeqRef = useRef(0);",
`const searchRequestSeqRef = useRef<SearchRequestToken>(0);

function invalidateInFlightSearchRequests() {
searchRequestSeqRef.current = invalidateSearchRequests(searchRequestSeqRef.current);
}`,
);

s = s.replace(
"const requestId = ++searchRequestSeqRef.current;",
`const requestId = invalidateSearchRequests(searchRequestSeqRef.current);
searchRequestSeqRef.current = requestId;`,
);

s = s.replace(
/if \(requestId === searchRequestSeqRef\.current\) setAnswerProgress\(message\);/g,
"if (isLatestSearchRequest(requestId, searchRequestSeqRef.current)) setAnswerProgress(message);",
);

s = s.replace(
/if \(requestId === searchRequestSeqRef\.current\) \{/g,
"if (isLatestSearchRequest(requestId, searchRequestSeqRef.current)) {",
);

s = s.replace(
"function crossModeSearch(mode: AppModeId, crossQuery: string) {\n modeChangeFromUiRef.current = true;",
"function crossModeSearch(mode: AppModeId, crossQuery: string) {\n invalidateInFlightSearchRequests();\n modeChangeFromUiRef.current = true;",
);

s = s.replace(
"function selectSearchMode(mode: AppModeId) {\n modeChangeFromUiRef.current = true;",
"function selectSearchMode(mode: AppModeId) {\n invalidateInFlightSearchRequests();\n modeChangeFromUiRef.current = true;",
);

s = s.replace(
"function startNewChat() {\n modeChangeFromUiRef.current = true;",
"function startNewChat() {\n invalidateInFlightSearchRequests();\n modeChangeFromUiRef.current = true;",
);

s = s.replace(
"sourceGovernanceWarnings: sourceGovernanceWarnings.map((warning) => warning.message),",
"sourceGovernanceWarnings: sourceGovernanceWarnings.map(serializeSourceGovernanceWarning),",
);

s = s.replace(
`<DocumentSearchResultsPanel
matches={documentMatches}
recordMatches={recordSearchMatches}`,
`<DocumentSearchResultsPanel
matches={documentMatches}
sources={sources}
recordMatches={recordSearchMatches}`,
);

if (!s.includes("<SourceReviewQueuePanel sources={sources} />")) {
s = s.replace(
` />
</div>
</div>
</UtilityDrawer>`,
` />
<SourceReviewQueuePanel sources={sources} />
</div>
</div>
</UtilityDrawer>`,
);
}

const shortcutIdx = s.indexOf("async function runDocumentSearchShortcut");
if (shortcutIdx !== -1) {
const canRunIdx = s.indexOf(" if (!canRunSearch) {", shortcutIdx);
const fnEnd = s.indexOf("\n function handleTagSearch", canRunIdx);
const block = s.slice(canRunIdx, fnEnd);
if (!block.includes("invalidateInFlightSearchRequests()")) {
const newBlock = block
.replace(
" setQuery(trimmedSearchText);",
" invalidateInFlightSearchRequests();\n setQuery(trimmedSearchText);",
)
.replace(
" if (updateUrl) updateDocumentSearchUrl(trimmedSearchText, targetMode);\n\n try {",
" if (updateUrl) updateDocumentSearchUrl(trimmedSearchText, targetMode);\n\n const requestId = invalidateSearchRequests(searchRequestSeqRef.current);\n searchRequestSeqRef.current = requestId;\n\n try {",
)
.replace(
" applySearchResult(payload);",
" if (isLatestSearchRequest(requestId, searchRequestSeqRef.current)) {\n applySearchResult(payload);\n }",
)
.replace(
' setError(requestError instanceof Error ? requestError.message : "Document search failed");',
' if (isLatestSearchRequest(requestId, searchRequestSeqRef.current)) {\n setError(requestError instanceof Error ? requestError.message : "Document search failed");\n }',
)
.replace(
" setLoading(false);\n setAnswerProgress(null);",
" if (isLatestSearchRequest(requestId, searchRequestSeqRef.current)) {\n setLoading(false);\n setAnswerProgress(null);\n }",
);
s = s.slice(0, canRunIdx) + newBlock + s.slice(fnEnd);
}
}

fs.writeFileSync(path, s);
console.log("ClinicalDashboard governance edits applied");
137 changes: 137 additions & 0 deletions scripts/capture-support-chip-screenshots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { chromium } from "playwright-core";

import { getPlaywrightBaseUrl } from "./playwright-base-url";
import { demoAnswer, demoDocuments } from "../src/lib/demo-data";

const outputDir = process.env.SCREENSHOT_DIR ?? join(process.cwd(), "artifacts", "screenshots");
mkdirSync(outputDir, { recursive: true });

const question = "What clozapine monitoring items are shown in the table image?";

const readySetupChecks = [
{ id: "env", label: ".env.local configured", status: "ready", detail: "Test environment ready." },
{ id: "project", label: "Clinical KB Database target", status: "ready", detail: "Test Supabase project ready." },
{ id: "schema", label: "supabase/schema.sql applied", status: "ready", detail: "Test schema ready." },
{ id: "search", label: "Search RPC and vector indexes", status: "ready", detail: "Test search schema ready." },
{ id: "openai", label: "OpenAI API key available", status: "ready", detail: "Test OpenAI ready." },
{ id: "worker", label: "npm run worker running", status: "unknown", detail: "Worker not required for UI smoke." },
];

function answerStreamBody(payload: unknown) {
return [
`event: progress\ndata: ${JSON.stringify({ stage: "retrieving", message: "Searching indexed documents." })}`,
`event: final\ndata: ${JSON.stringify(payload)}`,
"",
].join("\n\n");
}

async function mockDemoApi(page: import("playwright-core").Page, baseUrl: string) {
await page.route(/\/api\/local-project-id$/, async (route) => {
await route.fulfill({
json: {
appName: "Clinical KB",
projectId: "test-project",
identityPath: "/api/local-project-id",
localServer: {
currentUrl: baseUrl,
currentPort: Number(new URL(baseUrl).port || 4298),
projectPortStart: 4298,
projectPortEnd: 53210,
safeLocalOrigin: true,
requestOrigin: null,
requestReferer: null,
unsafeLocalCaller: null,
},
},
});
});
await page.route("**/api/setup-status**", async (route) => {
await route.fulfill({ json: { demoMode: true, checks: readySetupChecks } });
});
await page.route(/\/api\/documents(?:\?.*)?$/, async (route) => {
await route.fulfill({
json: {
documents: demoDocuments,
demoMode: true,
pagination: {
limit: 150,
offset: 0,
total: demoDocuments.length,
nextOffset: demoDocuments.length,
hasMore: false,
},
},
});
});
await page.route(/\/api\/answer(?:\/stream)?(?:\?.*)?$/, async (route) => {
const body = route.request().postDataJSON() as {
query?: string;
documentId?: string;
documentIds?: string[];
};
const payload = demoAnswer(body?.query ?? question, body?.documentId, body?.documentIds);
const pathname = new URL(route.request().url()).pathname;
if (pathname.endsWith("/stream")) {
await route.fulfill({
body: answerStreamBody(payload),
contentType: "text/event-stream; charset=utf-8",
headers: { "Cache-Control": "no-cache, no-transform" },
});
return;
}
await route.fulfill({ json: payload });
});
}

async function main() {
const baseUrl = getPlaywrightBaseUrl();
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 390, height: 820 } });
await mockDemoApi(page, baseUrl);
await page.goto(`${baseUrl}/`, { waitUntil: "domcontentloaded" });
await page.waitForLoadState("networkidle").catch(() => undefined);

const questionInput = page
.locator('[aria-label^="Search indexed guidelines by question or keyword"]:visible')
.first();
await questionInput.fill(question);
await page.locator('[aria-label="Generate source-backed answer"]:visible').first().click();

await page.getByTestId("plain-answer-response").waitFor({ state: "visible", timeout: 30_000 });
await page.getByTestId("answer-follow-up-suggestions").waitFor({ state: "visible", timeout: 30_000 });
await page.getByTestId("answer-support-action-row").waitFor({ state: "attached", timeout: 30_000 });

const mainContent = page.locator("#main-content");

await mainContent.evaluate((element) => {
element.scrollTop = element.scrollHeight;
});
await page.waitForTimeout(500);
const expandedPath = join(outputDir, "support-chips-expanded-at-bottom.png");
await page.screenshot({ path: expandedPath, fullPage: false });

await mainContent.evaluate((element) => {
element.scrollTop = Math.max(0, element.scrollHeight - element.clientHeight - 180);
});
await page
.waitForFunction(() => {
const row = document.querySelector('[data-testid="answer-support-action-row"]');
return row?.getAttribute("data-collapsed") === "true";
}, { timeout: 10_000 })
.catch(() => undefined);
await page.waitForTimeout(500);
const collapsedPath = join(outputDir, "support-chips-collapsed-above-composer.png");
await page.screenshot({ path: collapsedPath, fullPage: false });

const collapsed = await page.getByTestId("answer-support-action-row").getAttribute("data-collapsed");
console.log(JSON.stringify({ outputDir, collapsed, expandedPath, collapsedPath, baseUrl }, null, 2));

await browser.close();
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
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
Loading