Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/process-hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ All approved render-surface modules are extracted. `ClinicalDashboard.tsx` went

- **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.
- **Follow-up (2026-07-06):** the codified `match_document_embedding_fields_text` kept the legacy `(owner_filter is null or d.owner_id = owner_filter)` predicate instead of `retrieval_owner_matches`, so it ignores the public-owner sentinel (anonymous sentinel would match zero rows; a real owner id excludes public docs). Migration `20260706130000_fix_embedding_fields_text_owner_sentinel` recreates it with the shared predicate — **prepared but NOT applied to live**; latent until the `_text` RPC is wired into app code, so apply with the next approved push.
- **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.
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
"@types/react-dom": "^19.2.3",
"@vitest/coverage-v8": "^4.1.9",
"eslint": "^9.39.4",
"eslint-config-next": "16.2.9",
"eslint-config-next": "16.2.10",
"playwright": "^1.61.1",
"prettier": "^3.9.4",
"tailwindcss": "^4.3.1",
Expand Down
9 changes: 8 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { getPlaywrightBaseUrl } from "./scripts/playwright-base-url";

const baseURL = getPlaywrightBaseUrl();

// Sandboxed CI/cloud containers often ship a preinstalled Chromium and block
// browser downloads; point this at that binary instead of the managed one.
const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;

export default defineConfig({
testDir: "./tests",
testMatch: /.*ui-(smoke|stress|accessibility|tools|tools-task-directory|overlap)\.spec\.ts/,
Expand All @@ -22,7 +26,10 @@ export default defineConfig({
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
use: {
...devices["Desktop Chrome"],
...(chromiumExecutablePath ? { launchOptions: { executablePath: chromiumExecutablePath } } : {}),
},
},
{
name: "firefox",
Expand Down
6 changes: 5 additions & 1 deletion scripts/dev-free-port.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ function removePortArgs(args) {
function canListenOnHost(port, host) {
return new Promise((resolve) => {
const server = net.createServer();
server.once("error", () => resolve(false));
server.once("error", (error) => {
// An unsupported address family (e.g. no IPv6 in the container) cannot
// hold the port, so it must not disqualify it as busy.
resolve(error.code === "EAFNOSUPPORT" || error.code === "EADDRNOTAVAIL");
});
server.once("listening", () => {
server.close(() => resolve(true));
});
Expand Down
6 changes: 5 additions & 1 deletion scripts/run-playwright.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ function sleep(ms) {
function canListenOnHost(port, host) {
return new Promise((resolve) => {
const server = net.createServer();
server.once("error", () => resolve(false));
server.once("error", (error) => {
// An unsupported address family (e.g. no IPv6 in the container) cannot
// hold the port, so it must not disqualify it as busy.
resolve(error.code === "EAFNOSUPPORT" || error.code === "EADDRNOTAVAIL");
});
server.once("listening", () => server.close(() => resolve(true)));
server.listen(port, host);
});
Expand Down
5 changes: 5 additions & 0 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2453,7 +2453,7 @@
urlDocumentSearchBootstrappedRef.current = true;
void executeSearch(searchText, mode, scopeFilters);
// URL search intentionally runs once when the selected mode can execute.
}, [canRunSearch, answerThreadBootstrapped]);

Check warning on line 2456 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions / verify

React Hook useEffect has missing dependencies: 'executeSearch' and 'scopeFilters'. Either include them or remove the dependency array

useEffect(() => {
const updateHash = () => {
Expand Down Expand Up @@ -2729,6 +2729,11 @@
return;
}
if (!canRunSearch) {
// requestId was already bumped above, so a superseded in-flight request's
// finally block can no longer reset loading — reset it here or the answer
// skeleton can stay on screen indefinitely.
setLoading(false);
setAnswerProgress(null);
setError(errorCopy.searchSetupNotReady);
return;
}
Expand Down
4 changes: 4 additions & 0 deletions src/lib/source-governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ const frontendVisibleWarningCodes = new Set<SourceGovernanceWarning["code"]>([
"outdated_source",
"poor_extraction",
"weak_evidence",
// Since the public-corpus promotion (all indexed documents are anonymously
// searchable regardless of clinical_validation_status), "not locally
// validated" is a clinically material caveat, not routine review metadata.
"unverified_source",
]);

function isLocalMetadataText(value: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
-- match_document_embedding_fields_text (codified from live drift in
-- 20260705230000) kept the legacy `(owner_filter is null or ...)` predicate,
-- so it ignores the public-owner sentinel every other retrieval function
-- honours via retrieval_owner_matches (20260705210000). Latent today (the app
-- only calls the _hybrid variant) but wrong the moment this RPC is wired in:
-- the sentinel would match zero rows and a real owner id would exclude public
-- documents. Recreate it with the shared owner predicate.
set search_path = public, extensions, pg_temp;

-- Replay guard: drop first in case the live/preview OUT signature drifted,
-- which makes `create or replace` fail during migration replay.
drop function if exists public.match_document_embedding_fields_text(text, integer, double precision, uuid[], uuid);

create or replace function public.match_document_embedding_fields_text(
query_text text, match_count integer default 16, min_text_rank double precision default 0.0,
document_filters uuid[] default null, owner_filter uuid default null
)
returns table (
id uuid, document_id uuid, source_chunk_id uuid, field_type text, content text, text_rank double precision
)
language sql stable set search_path = public, extensions, pg_temp
as $$
with q as (select websearch_to_tsquery('english', coalesce(query_text, '')) as tsq),
ranked as (
select f.id, f.document_id, f.source_chunk_id, f.field_type, f.content,
ts_rank_cd(f.search_tsv, q.tsq)::double precision as text_rank
from public.document_embedding_fields f
join public.documents d on d.id = f.document_id
cross join q
where f.source_chunk_id is not null
and (document_filters is null or f.document_id = any(document_filters))
and public.retrieval_owner_matches(owner_filter, d.owner_id)
and d.status = 'indexed' and f.search_tsv @@ q.tsq
)
select * from ranked where text_rank >= min_text_rank
order by text_rank desc, id limit match_count;
$$;

revoke execute on function public.match_document_embedding_fields_text(text, integer, double precision, uuid[], uuid) from public, anon, authenticated;
grant execute on function public.match_document_embedding_fields_text(text, integer, double precision, uuid[], uuid) to service_role;
2 changes: 1 addition & 1 deletion supabase/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3198,7 +3198,7 @@ as $$
cross join q
where f.source_chunk_id is not null
and (document_filters is null or f.document_id = any(document_filters))
and (owner_filter is null or d.owner_id = owner_filter)
and public.retrieval_owner_matches(owner_filter, d.owner_id)
and d.status = 'indexed' and f.search_tsv @@ q.tsq
)
select * from ranked where text_rank >= min_text_rank
Expand Down
12 changes: 8 additions & 4 deletions tests/source-governance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ describe("source governance warnings", () => {
expect(hasDangerSourceGovernanceWarning(warnings)).toBe(false);
});

it("keeps routine review metadata notes out of frontend-visible governance warnings", () => {
it("keeps review-due notes hidden while surfacing the unvalidated-source caveat", () => {
const warnings = sourceGovernanceWarnings({
results: [
result({
Expand Down Expand Up @@ -183,7 +183,10 @@ describe("source governance warnings", () => {
expect(warnings.map((warning) => warning.code)).toEqual(
expect.arrayContaining(["review_due_source", "unverified_source"]),
);
expect(frontendSourceGovernanceWarnings(warnings)).toEqual([]);
// With the whole indexed corpus promoted public regardless of validation
// status, "not locally validated" must stay visible; review-due remains a
// routine metadata note.
expect(frontendSourceGovernanceWarnings(warnings).map((warning) => warning.code)).toEqual(["unverified_source"]);
});

it("surfaces only clinically material warnings to the frontend", () => {
Expand All @@ -203,9 +206,10 @@ describe("source governance warnings", () => {
});
const visibleCodes = frontendSourceGovernanceWarnings(warnings).map((warning) => warning.code);

expect(visibleCodes).toEqual(expect.arrayContaining(["weak_evidence", "outdated_source", "poor_extraction"]));
expect(visibleCodes).toEqual(
expect.arrayContaining(["weak_evidence", "outdated_source", "poor_extraction", "unverified_source"]),
);
expect(visibleCodes).not.toContain("review_due_source");
expect(visibleCodes).not.toContain("unverified_source");
expect(visibleCodes).not.toContain("non_local_source");
});

Expand Down
Loading