Skip to content
1 change: 1 addition & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,3 +681,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-21 | claude/patient-profile-input-bounds-123366 (PR #1045: FV-03 fail-safe input bounds) | 75303e8b8 | Clinical-governance verification of FV-03 (patient-profile numeric fields → medication-safety alert engine). 4-agent adversarial workflow (consumer map + suppression audit + physiological bounds + synthesis). | ADJUST→implemented. Consumer map: evaluatePatientAlerts is the ONLY numeric consumer, no dose arithmetic, sanitize() is the sole guaranteed chokepoint. Suppression audit found naive null-routing UNSAFE via the bare-renal both-null hole (medication-patient-alerts.ts:286) — nulling one out-of-range renal input while the other is present-normal → false all-clear; fixed with &&→|| (0 bare-renal contraindication rows in corpus → no-op on current data). Bounds VALIDATED (age 0-130, egfr 0-250, crcl 0-400, qtc 240-800, scr µmol/L 15-3000 unit-aware): never reject a legitimate clinical extreme. Reject-to-null (never clamp). | typecheck, lint, format:check, full unit+jsdom 349 files/3120 passed/0 failed (incl. 38 new/updated FV-03 tests), design-system-contract (baselines unchanged), type-scale, icon-scale, check:production-readiness READY. verify:ui in CI. No provider calls. |
| 2026-07-21 | claude/clinical-kb-pwa-review-asi3wb (Option A wave verdict — no code change; #1040 merged as cde6c5c) | canary run 29827012719 (#61, main cde6c5c) vs banked #60 (29800029819) | OPTION A WAVE ADOPTED — FIRST FULLY-GREEN 44-CASE CANARY IN PROGRAM HISTORY (Blocking failures: None). (1) Option A payoff EXCEEDED: citation_failure_rate 0.0227→0; the neuroleptic-side-effect-escalation case flipped from wrong-doc→failed-generation→1-citation-fallback to **strong route, successful gpt-5.6-sol generation, passed in 15.4s with no fallback marker** — the rescued S3 retrieval fixed generation itself, not just the citation count; expected_source_hit 0.6364→0.6591. (2) Golden held exactly as the blast-radius analysis promised: 36/36 PASS, content_recall 1.0, mrr@10 0.8921 BYTE-IDENTICAL to the pre-wave baseline (zero ordering movement — no golden case fires the predicate), irrelevant@10 0.1083→0.0917 (slightly better). (3) Parity payoff PARTIAL: monitoring targeting 1/5→2/5 (olanzapine-lai flipped — previously called a retrieval-depth residual; quetiapine-dose also flipped on the dose side); lithium-range (232ch) + metabolic (73ch, byte-identical answer to #60) did NOT flip despite offline-proven fixes — their live chunk sets evidently contain no admissible schedule sentence even under the widened gate → reclassified as retrieval-depth/live-content residuals joining adhd; below the ≥3/5 target but strictly improved, no regression anywhere. Dose 2/5 vs 2/4: same passing count, applicable set grew (new quality-metformin-renal-dosing miss = eval-set churn, not regression). (4) No-worse EXCEEDED: relevance 0.5333→0.6 (the two-step watch-item slide FULLY REVERSED to the #58 level), targeting_rate 0.6667→0.6957, fail_closed 0.9 held, readability/artifact_leaks 1.0, route ceilings 0, grounded 1.0, unsupported_correct 1.0, numeric 0, p95 22.8s, red_result 3/3. Adoption per the measured-gain rule: primary goal achieved, three case flips, relevance recovered, zero regressions. Residual queue: monitoring retrieval-depth trio (lithium-range/metabolic/adhd), E-3d H2 discards, weekly ANSWER_CASE_LIMIT 8→44 raise now unblocked (gate would be green), comparison-class coverage. Wave spend +~$2-4 → Phase E + Option A total ~$12-20 of ≤$20. | Evidence: run #61 job log read (Threshold Status: None; Answer Metrics; neuroleptic diagnostics row; targeting metric_rates + 6-miss list; golden 36 PASS lines + summary). Revert drill NOT triggered. |
| 2026-07-21 | claude/clinical-kb-pwa-review-asi3wb (PR: I9 weekly coverage raise) | see PR head | ADDENDUM 5 post-green item I9 (plan-authorized "after reds fixed"): weekly scheduled canary ANSWER_CASE_LIMIT default 8→44 — the Sunday 18:00 UTC cron now guards the FULL answer-quality case set instead of the first 8 (both #57 blocking reds historically lived OUTSIDE the first 8, leaving the weekly gate blind to them). Unblocked by run #61 proving the citation gate green on the full 44. Cost: est +$1-2/week (user-authorized in the plan). Contract test pin updated in lockstep (eval-canary-workflow.test.ts). Dispatch shapes unchanged (input override still wins); operational-risk diff, plain-revert rollback. | check:github-actions PASS; check:ci-scope PASS; check:gate-manifest PASS (20/20); eval-canary-workflow contract 4/4; prettier clean; no provider calls |
| 2026-07-21 | claude/database-governance-audit-10b6ed (PR #1051: source-governance audit — safe subset) | cee396730 | Governance-metadata observability + UI display + provenance flow test; no ranking/retrieval/generation surface touched. | IMPLEMENTED + handed off (not a review of prior work). Resolved audit #1 (logger.warn on unrecognized enum values; return value unchanged), #2 (review_due_source added to frontendVisibleWarningCodes → answer-level badge; warning-severity, no refusal impact), #9 (source_metadata retained on safety-finding citations + governance pill in SafetyFindingsListContent), #13 (new tests/provenance-flow.test.ts: DB-normalize→governance→client payload sources+safety citations→render policy). Deferred #4/5/6/8/10 (RAG-protected ranking/selection/LLM-context/cache — need live eval-canary+approval), #11/#5 flag debt (D5/D4), #3 (is_public schema/RLS), #7 (conflict-detection scope), #12 (canary automation). Rebased onto origin/main (was 18 behind; conflict-free — none of the 18 commits touched the 8 files). PR-policy CI green (confirmed no ragRankingPatterns match). | verify:pr-local exit 0 (351 files/3129 tests, production build, client-bundle secret scan, offline RAG fixtures 36/36); typecheck + lint + prettier green. verify:ui NOT run locally: pre-existing globals.css Tailwind/Turbopack dev-compile error (git-clean, unrelated; prod build passed) — CI Production UI job covers it. check:production-readiness deferred (offline env/config validator; PR changes no env/secret/config inputs; secretless worktree). No provider calls. |
28 changes: 27 additions & 1 deletion src/components/clinical-dashboard/evidence-panels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ import {
type SafetyFinding,
type SafetyFindingKind,
} from "@/lib/clinical-safety";
import { normalizeSourceMetadata, sourceStatusLabel } from "@/lib/source-metadata";
import { normalizeSourceMetadata, sourceStatusLabel, validationStatusLabel } from "@/lib/source-metadata";
import { normalizeExtractedGlyphs, sourceTextForVerbatimQuote } from "@/lib/source-text-sanitizer";
import type {
AnswerSection,
Expand Down Expand Up @@ -868,6 +868,23 @@ function SafetyFindingRowIcon({ kind }: { kind: SafetyFindingKind }) {
return <CircleAlert aria-hidden="true" className="h-5 w-5" />;
}

// Issue 9: governance provenance retained on safety-finding citations lets the safety
// panel badge sources that are outdated, due for review, or not locally validated —
// the same currency/validation signals shown for ordinary source citations. Current
// and unknown-status sources add no badge (they carry no actionable caveat here).
function safetyFindingGovernanceLabels(citation: SafetyFinding["citation"]): string[] {
const metadata = citation.source_metadata ? normalizeSourceMetadata(citation.source_metadata) : null;
if (!metadata) return [];
const labels: string[] = [];
if (metadata.document_status === "outdated" || metadata.document_status === "review_due") {
labels.push(sourceStatusLabel(metadata));
}
if (metadata.clinical_validation_status === "unverified") {
labels.push(validationStatusLabel(metadata));
}
return labels;
}

export function SafetyFindingsListContent({ findings }: { findings: SafetyFinding[] }) {
if (findings.length === 0) return null;

Expand Down Expand Up @@ -908,6 +925,15 @@ export function SafetyFindingsListContent({ findings }: { findings: SafetyFindin
<span className="truncate">{formatCompactCitationLabel(finding.citation)}</span>
<ExternalLink aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
</Link>
{safetyFindingGovernanceLabels(finding.citation).map((label) => (
<span
key={label}
data-testid="safety-finding-governance"
className={cn(subtleStatusPill, "min-h-6 px-2 text-2xs", toneWarning)}
>
{label}
</span>
))}
</div>
<p className="mt-1.5 text-sm leading-5 text-[color:var(--text-heading)]">{finding.text}</p>
</div>
Expand Down
5 changes: 5 additions & 0 deletions src/lib/answer-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ function clientSafetyWarning(warning: SafetyWarning): SafetyWarning {
chunk_index: citation.chunk_index,
...(citation.similarity === undefined ? {} : { similarity: citation.similarity }),
...(citation.provenance === undefined ? {} : { provenance: citation.provenance }),
// Issue 9: keep governance provenance on safety-finding citations. Regular
// source citations already retain it (answer-client-payload `source_metadata:
// "client"`); dropping it here left the safety panel unable to badge outdated /
// review-due / unverified provenance for its citations.
...(citation.source_metadata === undefined ? {} : { source_metadata: citation.source_metadata }),
},
};
}
Expand Down
5 changes: 5 additions & 0 deletions src/lib/source-governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ const frontendVisibleWarningCodes = new Set<SourceGovernanceWarning["code"]>([
// validated" is a clinically material caveat, not routine review metadata.
"unverified_source",
"registry_record_source",
// A source past its review date is a material currency caveat: guidance may have
// moved on. Surfacing the badge keeps the governance notice consistent with the
// review-due language already emitted in the render-policy copy text
// (answer-render-policy.buildWarnings), which the badge list previously suppressed.
"review_due_source",
]);

function isLocalMetadataText(value: string) {
Expand Down
26 changes: 21 additions & 5 deletions src/lib/source-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { logger } from "@/lib/logger";
import type { ClinicalSourceMetadata } from "@/lib/types";

const knownStatuses = new Set(["current", "review_due", "outdated", "unknown"]);
Expand All @@ -8,8 +9,18 @@ function stringOrNull(value: unknown) {
return typeof value === "string" && value.trim() ? value.trim() : null;
}

function enumOrDefault<T extends string>(value: unknown, allowed: Set<string>, fallback: T): T {
return typeof value === "string" && allowed.has(value) ? (value as T) : fallback;
function enumOrDefault<T extends string>(value: unknown, allowed: Set<string>, fallback: T, field: string): T {
if (typeof value === "string" && allowed.has(value)) return value as T;
// A present-but-unrecognized string is a real data-entry defect (typo, renamed
// enum, malformed ingest) that would otherwise collapse into the fallback and be
// indistinguishable from a genuinely-absent value. Trace it so it is fixable.
// Absent / null / empty values are the legitimate default and stay silent — they
// are the common case and would drown the signal. The returned value is unchanged,
// so this is observability only: no ranking/retrieval behaviour changes.
if (typeof value === "string" && value.trim()) {
logger.warn(`source-metadata: unrecognized ${field}`, { field, value });
}
return fallback;
}

export function normalizeSourceMetadata(input: unknown): ClinicalSourceMetadata {
Expand All @@ -31,9 +42,14 @@ export function normalizeSourceMetadata(input: unknown): ClinicalSourceMetadata
uploaded_at: stringOrNull(value.uploaded_at),
indexed_at: stringOrNull(value.indexed_at),
uploaded_by: stringOrNull(value.uploaded_by),
document_status: enumOrDefault(value.document_status, knownStatuses, "unknown"),
clinical_validation_status: enumOrDefault(value.clinical_validation_status, knownValidation, "unverified"),
extraction_quality: enumOrDefault(value.extraction_quality, knownExtraction, "unknown"),
document_status: enumOrDefault(value.document_status, knownStatuses, "unknown", "document_status"),
clinical_validation_status: enumOrDefault(
value.clinical_validation_status,
knownValidation,
"unverified",
"clinical_validation_status",
),
extraction_quality: enumOrDefault(value.extraction_quality, knownExtraction, "unknown", "extraction_quality"),
};
}

Expand Down
5 changes: 4 additions & 1 deletion tests/answer-client-payload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ describe("toClientAnswerPayload", () => {
expect(payload.sources![0].content).toBe(source.retrieval_synopsis);
expect(payload.sources![0].content.length).toBeLessThan(source.content.length);
expect(payload.safetyWarnings).toHaveLength(1);
expect(payload.safetyWarnings![0].citation).not.toHaveProperty("source_metadata");
// Issue 9: governance provenance is retained on safety-finding citations so the
// safety panel can badge outdated / review-due / unverified sources, consistent
// with regular source citations (which already keep source_metadata).
expect(payload.safetyWarnings![0].citation).toHaveProperty("source_metadata", { document_status: "current" });
expect(extractSafetyFindings(payload)).toHaveLength(1);
});

Expand Down
152 changes: 152 additions & 0 deletions tests/provenance-flow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { describe, expect, it } from "vitest";

import { buildAnswerRenderModel } from "../src/lib/answer-render-policy";
import { buildGovernedAnswerClientResponse } from "../src/lib/answer-response";
import { frontendSourceGovernanceWarnings, sourceGovernanceWarnings } from "../src/lib/source-governance";
import { normalizeSourceMetadata } from "../src/lib/source-metadata";
import type { RagAnswer, SearchResult } from "../src/lib/types";

// Issue 13: a single end-to-end guard that governance provenance survives every hop
// of the pipeline — DB-boundary normalization → governance warnings → governed client
// payload (both source citations AND safety-finding citations) → render policy. Unit
// tests cover each stage in isolation; this proves the joints between them, and is the
// test that would have caught the safety-citation metadata drop (Issue 9).

function buildSource(overrides: Partial<SearchResult> = {}): SearchResult {
return {
id: "chunk-1",
document_id: "doc-1",
title: "Clozapine monitoring guideline",
file_name: "clozapine.pdf",
page_number: 1,
chunk_index: 0,
section_heading: null,
content: "Full blood count weekly for 18 weeks.",
image_ids: [],
similarity: 0.42,
source_metadata: {
source_title: "Clozapine monitoring guideline",
publisher: "WA Health",
jurisdiction: "Australia/WA",
version: null,
publication_date: null,
review_date: null,
uploaded_at: null,
indexed_at: null,
uploaded_by: null,
document_status: "current",
clinical_validation_status: "approved",
extraction_quality: "good",
},
indexing_quality: {
document_id: "doc-1",
quality_score: 0.9,
extraction_quality: "good",
metrics: {},
issues: [],
},
table_facts: [],
images: [],
...overrides,
};
}

describe("source governance provenance flows end-to-end", () => {
it("carries warning-level governance metadata from DB normalization through the client payload to the render policy", () => {
const source = buildSource({
// Proven safety-deriving content (mirrors answer-client-payload fixture): the
// useful clinical sentence is a contraindication, so a safety finding is derived.
content: `${"Routine context. ".repeat(60)}Contraindicated in severe disease.`,
source_metadata: {
source_title: "Clozapine monitoring guideline",
publisher: "WA Health",
jurisdiction: "Australia/WA",
version: null,
publication_date: null,
review_date: "2020-01-01",
uploaded_at: null,
indexed_at: null,
uploaded_by: null,
document_status: "review_due",
clinical_validation_status: "unverified",
extraction_quality: "good",
},
});

// Hop 1 — DB boundary: raw metadata normalizes to the governance fields.
const normalized = normalizeSourceMetadata(source.source_metadata);
expect(normalized.document_status).toBe("review_due");
expect(normalized.clinical_validation_status).toBe("unverified");

// Hop 2 — governance: currency + validation caveats are both frontend-visible.
const warnings = sourceGovernanceWarnings({ results: [source] });
const visibleCodes = frontendSourceGovernanceWarnings(warnings).map((warning) => warning.code);
expect(visibleCodes).toEqual(expect.arrayContaining(["review_due_source", "unverified_source"]));

const answer = {
answer: "Review the monitoring guidance before prescribing.",
grounded: true,
confidence: "medium",
citations: [],
sources: [source],
} as RagAnswer;

const response = buildGovernedAnswerClientResponse(answer);

// Warning-level governance does not refuse — the answer (and its provenance) flows through.
expect(response.refused).toBe(false);

// Hop 3a — client payload source citations retain provenance.
expect(response.payload.sources?.[0]?.source_metadata).toMatchObject({
document_status: "review_due",
clinical_validation_status: "unverified",
});

// Hop 3b — safety-finding citations retain provenance (Issue 9 regression guard).
expect(response.payload.safetyWarnings?.length ?? 0).toBeGreaterThan(0);
expect(response.payload.safetyWarnings?.[0]?.citation.source_metadata).toMatchObject({
document_status: "review_due",
clinical_validation_status: "unverified",
});

// Hop 4 — render policy surfaces the governance message in the render model.
const renderModel = buildAnswerRenderModel({ ...answer, sourceGovernanceWarnings: warnings } as RagAnswer);
expect(renderModel.warnings).toEqual(
expect.arrayContaining(["One or more supporting sources are due for review."]),
);
});

it("fails conservatively (refuses) when a supporting source is outdated, instead of passing stale guidance through", () => {
const outdatedSource = buildSource({
document_id: "doc-outdated",
source_metadata: {
source_title: "Superseded protocol",
publisher: "WA Health",
jurisdiction: "Australia/WA",
version: null,
publication_date: null,
review_date: null,
uploaded_at: null,
indexed_at: null,
uploaded_by: null,
document_status: "outdated",
clinical_validation_status: "approved",
extraction_quality: "good",
},
});

const response = buildGovernedAnswerClientResponse({
answer: "Outdated guidance.",
grounded: true,
confidence: "medium",
citations: [],
sources: [outdatedSource],
} as RagAnswer);

// An outdated source is a danger-class governance signal: the governed contract
// refuses and strips sources rather than serving stale clinical guidance.
expect(response.refused).toBe(true);
expect(response.payload.sources).toHaveLength(0);
expect(response.payload.safetyWarnings).toHaveLength(0);
});
});
Loading
Loading