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
2 changes: 2 additions & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD

| Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks |
| ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-07-24 | PR #1125 / `codex/answer-relevance-fail-closed` | `8d9fb2408f13e305138749655214baa0020fcfd4` | Follow-up: clear comparison/`documentBreakdown` in untrusted clinical notes | APPROVE for the scoped P2. `trustGatedAnswerForClinicalNotes` now clears `documentBreakdown`, `comparisonMatrix`, and `comparisonEvaluationState` when relevance is not source-backed, so Clinical Notes → ClinicalOutputPanel cannot rebuild comparison-detail tables from raw `best_quote` values. Prior visual/section/quote gates remain. Residual risk is still deliberate low-trust rendering for legacy payloads without `isSourceBacked: true`. | Focused jsdom/policy regressions: `tests/visual-evidence-tabs.dom.test.tsx` 5/5 after hardening the comparison case (caption + matrix values absent). Thread disposition posted and resolved. No live RAG/OpenAI/Supabase mutation. |
| 2026-07-24 | `codex/answer-relevance-fail-closed` | `ac0d4305478a0bc8fef03894b78ec5911912c08a` | Missing answer-relevance metadata across render policy and live dashboard grounding | APPROVE after resolving two review P2s. A shared `isAnswerSourceBacked` predicate now requires explicit `isSourceBacked: true`; missing or explicitly negative relevance cannot retain high render trust, a grounded dashboard state, visual/table evidence, or a clinical-notes table bypass. Explicitly source-backed answers preserve supported behavior. Retrieval, ranking, generation, source selection and stored data are unchanged. Highest residual risk is deliberate compatibility tightening for older answer payloads without relevance metadata; they render low-trust and expose review sources rather than richer evidence blocks. | Initial red policy proof failed with `high`; two later red proofs exposed retained visual evidence and the clinical-notes raw-table affordance, then passed after both render-model gates. Focused render/provenance/clinical-safety tests passed 37/37; the focused DOM/policy pair passed 31/31. Offline RAG passed 21 suites/308 tests and 36/36 fixtures; production-readiness was READY against `Clinical KB Database` read-only; `verify:pr-local` passed runtime, formatting, lint, typecheck, all 366 test files (3,254 passed/1 skipped), production build (1,677 pages), client-bundle secret scan and RAG fixture validation; the earlier local `verify:ui` passed 267/267. After the final UI fix, `verify:cheap` again passed all 20 non-test gates, lint, typecheck and 3,254 tests, with only tracked issue #067 timing out under machine load; its isolated retry also exceeded the same 30-second limit and was not repeated. Fresh exact-head hosted checks are required. No live RAG, OpenAI request, Supabase mutation, Railway action, production data operation or deployment ran. |
| 2026-07-24 | `codex/safety-plan-no-patient-data-contract` | `b94987c94537f3114a3429848fa908bdecd1d80a` | Safety Plan Generator identifier, local-state, copy, print, privacy-notice and PIA contract | APPROVE. No P0-P2 finding. The patient name/initials field is removed; the builder now asks for identifier-free minimum content, retains working state only in the mounted React component, and makes clipboard/print/PDF export an explicit handling boundary. The PIA and product privacy copy distinguish this local-only tool from provider-backed questions. Highest residual risk is outside Clinical KB: users must handle exported copies under an approved clinical-record process, which the UI now states at the export controls. | Privacy/component DOM 3/3 plus updated privacy-copy 2/2; focused Chromium copy/print/no-fetch-or-XHR 1/1; `verify:cheap` passed all 21 gates, 366 files and 3,245 tests with 1 skip; production-readiness READY using the existing canonical environment without a provider call; production build and client-bundle secret scan passed; offline RAG fixture/manifest 36 cases/21 suites passed. `verify:pr-local` passed runtime, formatting, lint and typecheck, then stopped on the unrelated load-sensitive `reconciliation-preflight` 30-second timeout; that test passed 5/5 isolated and the preceding full suite passed, so the unchanged five-minute gate was not retried. No Supabase, OpenAI, Railway, live RAG, production data or deployment action ran. |
| 2026-07-24 | PR #1114 / `codex/universal-ledger-main-followup-20260724` | `eedbe594a89eaa0aad8a517ee8808b089cb5ff57` + reviewed working diff | Streamlined-sidebar handoff in the merged universal ledger | APPROVE. PRs #1112, #1116, and #1117 landed the universal-ledger baseline, two preserved UI tasks, and credential/hook hardening; this follow-up preserves that current-main schema and adds only the still-missing sidebar delivery as collision-free `#066`. The queue remains gap-free and every recommended ID has one open row. Sidebar implementation stays recoverable from remote branch `origin/codex/sidebar-test-fix-20260723` at full commit `cd54e68fbf7b07b5dffe3220e36af2caa528da54` until its local/browser/build/hosted merge proof is complete. | Focused sidebar/favourites tests previously passed 18/18 on the recorded implementation commit; this ledger follow-up passed Prettier, remote-ref recovery, 36-row contiguous-order/46-open-ID/next-ID invariants, and `git diff --check`. The broader reconciled precursor passed `verify:cheap` (365 files, 3,242 passed/1 skipped); fresh exact-head hosted CI is required on PR #1114. No OpenAI, Supabase, Railway, deployment, live-app, credential, or production-data action ran. |
| 2026-07-24 | `codex/universal-ledger-main-final` | `527988c2ccabc98b4d0673d33360c971df65fa0e` + reviewed working diff | Current-main universal-ledger reconciliation after PR #1106 superseded PR #1109 | READY. Kept PR #1106's current-main ledger and IDs, carried forward only non-duplicate recommended work from the superseded branch, and fixed the confirmed SessionStart empty-open defect. Every active recommendation now has a durable open ID; exposed-GitHub-token containment is the first A1 item; the Safety Plan privacy contract, absent-relevance fail-closed rule, stranded-upload recovery, threshold-conflict design, catalogue-toolbar convergence, and Current Clinical Work brief are retained without duplicating #1106's legal/config/release/staging/seed packages. Resolved #014/#034 claims stay archived, and PR #1110's scheduled-diagnostics priority remains intact. Highest residual risk is manual queue/open-table drift. | Empty-open fixture and real-ledger Bash execution passed; 33 contiguous recommendations reference tracked open IDs; 43 open items; no duplicate queued IDs; `docs:check-links`, `docs:check-index`, `docs:check-scripts`, `check:skills`, Prettier, and `git diff --check` passed. No OpenAI, Supabase, Railway, production, deployment, live-app, or credential action. |
Expand Down
4 changes: 2 additions & 2 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ import {
maxStoredAnswerTurns,
savePersistedAnswerThread,
} from "@/lib/answer-thread-storage";
import { buildAnswerRenderModel } from "@/lib/answer-render-policy";
import { buildAnswerRenderModel, isAnswerSourceBacked } from "@/lib/answer-render-policy";
import {
frontendSourceGovernanceWarnings,
groupSourceGovernanceWarnings,
Expand Down Expand Up @@ -2907,7 +2907,7 @@ export function ClinicalDashboard({
const answerGrounded =
answer?.grounded === true &&
answer.confidence !== "unsupported" &&
currentRelevance?.isSourceBacked !== false &&
isAnswerSourceBacked(answer) &&
answerRenderModel?.trust !== "unsupported";
const sourceLookup = useMemo(() => new Map(sources.map((source) => [source.id, source])), [sources]);
const answerPreformatted = isPreformattedGroundedAnswer(answer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ function StagedAnswerResultSurfaceImpl({
>
<ClinicalNotesChecklistPanel
answer={answer}
visualEvidence={renderModel.visualEvidence}
viewMode={answerViewMode}
evidenceMapRows={answerEvidenceMapRows}
sourceLinks={renderModel.primarySources}
Expand Down
60 changes: 48 additions & 12 deletions src/components/clinical-dashboard/evidence-panels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import {
toneSuccess,
toneWarning,
} from "@/components/ui-primitives";
import { type AnswerRenderModel, type SourceLink } from "@/lib/answer-render-policy";
import { isAnswerSourceBacked, type AnswerRenderModel, type SourceLink } from "@/lib/answer-render-policy";
import { documentCitationHref, formatCitationLabel, formatCompactCitationLabel } from "@/lib/citations";
import {
extractSafetyFindings,
Expand Down Expand Up @@ -499,12 +499,6 @@ function clinicalNoteHasDistinctDetail(row: ClinicalNotesRow) {
return Boolean(detail) && detail !== title;
}

function clinicalNotesTableEvidenceCount(answer: RagAnswer) {
return (answer.visualEvidence ?? answer.smartPanel?.visualEvidence ?? []).filter(
(item) => item.accessibleTableMarkdown || item.tableRows?.length,
).length;
}

function clinicalNotesRowsForTab(
sections: ClinicalDetailSection[],
tab: ClinicalNotesTabId,
Expand Down Expand Up @@ -574,6 +568,36 @@ function clinicalNotesAvailableTabs(sections: ClinicalDetailSection[]) {
.filter((tab) => tab.count > 0);
}

/**
* Align clinical-notes inputs with the fail-closed render model: when an answer
* is not explicitly source-backed, strip structured clinical payloads so the
* notes sheet cannot reconstruct actionable monitoring/escalation/comparison
* content from untrusted sections, quotes, or documentBreakdown (visual
* evidence is passed separately).
*/
function trustGatedAnswerForClinicalNotes(
answer: RagAnswer,
visualEvidence: VisualEvidenceCard[] = answer.visualEvidence ?? [],
): RagAnswer {
if (isAnswerSourceBacked(answer)) {
return {
...answer,
visualEvidence,
smartPanel: answer.smartPanel ? { ...answer.smartPanel, visualEvidence } : answer.smartPanel,
};
}
return {
...answer,
Comment thread
BigSimmo marked this conversation as resolved.
answerSections: [],
quoteCards: [],
documentBreakdown: [],
Comment thread
BigSimmo marked this conversation as resolved.
comparisonMatrix: undefined,
comparisonEvaluationState: undefined,
visualEvidence,
smartPanel: answer.smartPanel ? { ...answer.smartPanel, visualEvidence, quotes: [] } : answer.smartPanel,
};
}

/**
* Builds the non-empty clinical detail sections used by the clinical notes view.
*
Expand All @@ -598,13 +622,16 @@ function clinicalNotesDetailSectionsForAnswer(answer: RagAnswer, viewMode: Answe
}

export function clinicalNotesDisplayCountForAnswer(answer: RagAnswer, viewMode: AnswerViewMode, fallback: number) {
const tabs = clinicalNotesAvailableTabs(clinicalNotesDetailSectionsForAnswer(answer, viewMode));
const tabs = clinicalNotesAvailableTabs(
clinicalNotesDetailSectionsForAnswer(trustGatedAnswerForClinicalNotes(answer), viewMode),
);
const largestTabCount = tabs.reduce((largest, tab) => Math.max(largest, tab.count), 0);
return Math.max(1, largestTabCount || fallback);
}

export function ClinicalNotesChecklistPanel({
answer,
visualEvidence,
viewMode,
evidenceMapRows,
sourceLinks = [],
Expand All @@ -614,6 +641,7 @@ export function ClinicalNotesChecklistPanel({
onOpenTables,
}: {
answer: RagAnswer;
visualEvidence: VisualEvidenceCard[];
viewMode: AnswerViewMode;
evidenceMapRows: AnswerEvidenceMapRow[];
sourceLinks?: SourceLink[];
Expand All @@ -622,7 +650,8 @@ export function ClinicalNotesChecklistPanel({
onCopy: () => void;
onOpenTables?: () => void;
}) {
const detailSections = clinicalNotesDetailSectionsForAnswer(answer, viewMode);
const renderableAnswer = trustGatedAnswerForClinicalNotes(answer, visualEvidence);
const detailSections = clinicalNotesDetailSectionsForAnswer(renderableAnswer, viewMode);
const tabs = clinicalNotesAvailableTabs(detailSections);
const defaultTab = tabs.find((tab) => tab.id === "actions")?.id ?? tabs[0]?.id ?? "actions";
const [requestedTab, setRequestedTab] = useState<ClinicalNotesTabId>(defaultTab);
Expand All @@ -632,14 +661,21 @@ export function ClinicalNotesChecklistPanel({
const notesPanelId = `${tabBaseId}-panel`;
const activeTab = tabs.some((tab) => tab.id === requestedTab) ? requestedTab : defaultTab;
const rows = clinicalNotesRowsForTab(detailSections, activeTab, sourceLinks, bestSource);
const tableEvidenceCount = clinicalNotesTableEvidenceCount(answer);
const tableEvidenceCount = visualEvidence.filter(
(item) => item.accessibleTableMarkdown || item.tableRows?.length,
).length;
const [added, setAdded] = useState(false);
const warningRows = clinicalNotesRowsForTab(detailSections, "safety", sourceLinks, bestSource);
const warningCount = warningRows.filter((row) => row.tone === "warn").length || warningRows.length;

if (!tabs.length || rows.length === 0) {
return (
<ClinicalOutputPanel answer={answer} showLead={false} viewMode={viewMode} evidenceMapRows={evidenceMapRows} />
<ClinicalOutputPanel
answer={renderableAnswer}
showLead={false}
viewMode={viewMode}
evidenceMapRows={evidenceMapRows}
/>
);
}

Expand Down Expand Up @@ -1027,7 +1063,7 @@ export function evidenceTabCount({
}

export function clinicalNotesCount(answer: RagAnswer) {
return buildHighYieldClinicalOutputSections(answer).filter((section) =>
return buildHighYieldClinicalOutputSections(trustGatedAnswerForClinicalNotes(answer)).filter((section) =>
["action", "escalation", "thresholds", "cautions", "monitoring", "medication", "source-gap"].includes(section.id),
).length;
}
Expand Down
10 changes: 7 additions & 3 deletions src/lib/answer-render-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,13 @@ function answerRelevance(answer: RagAnswer): EvidenceRelevance | undefined {
return answer.relevance ?? answer.smartPanel?.relevance;
}

export function isAnswerSourceBacked(answer: RagAnswer): boolean {
return answerRelevance(answer)?.isSourceBacked === true;
Comment thread
BigSimmo marked this conversation as resolved.
}

function deriveTrust(answer: RagAnswer): AnswerRenderTrust {
const relevance = answerRelevance(answer);
const retrievalBlocked = answer.retrievalDiagnostics?.gateStatus === "blocked";
const sourceBacked = relevance?.isSourceBacked !== false;
const sourceBacked = isAnswerSourceBacked(answer);
const hasFaithfulnessWarning = Boolean(answer.faithfulnessWarning || answer.unverifiedNumericTokens?.length);
const evidenceGap = answer.responseMode === "evidence_gap";

Expand Down Expand Up @@ -742,7 +745,8 @@ export function buildAnswerRenderModel(
const rawQuotes = answer.quoteCards ?? answer.smartPanel?.quotes ?? [];
const rawVisualEvidence = answer.visualEvidence ?? answer.smartPanel?.visualEvidence ?? [];
const rawRelatedDocuments = answer.relatedDocuments ?? answer.smartPanel?.relatedDocuments ?? [];
const visualLimit = hasDirectVisualNeed(answer) || trust === "high" ? caps.visual : 0;
const visualLimit =
isAnswerSourceBacked(answer) && (hasDirectVisualNeed(answer) || trust === "high") ? caps.visual : 0;
Comment thread
BigSimmo marked this conversation as resolved.
const quoteCards = dedupeQuotes(rawQuotes, primarySources, caps.quotes);
const visualEvidence = dedupeVisualEvidence(rawVisualEvidence, primarySources, visualLimit);
const tables = buildCanonicalTables(visualEvidence);
Expand Down
28 changes: 28 additions & 0 deletions tests/answer-render-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ function answer(overrides: Partial<RagAnswer> = {}): RagAnswer {
answer: "For red-range blood results, withhold clozapine and contact the monitoring service.",
grounded: true,
confidence: "high",
relevance: {
verdict: "direct",
label: "Direct source support",
matchedTerms: ["clozapine", "monitoring"],
missingTerms: [],
directSourceCount: 1,
weakSourceCount: 0,
score: 0.95,
supportReason: "A direct source supports the answer.",
isSourceBacked: true,
},
citations: [citation()],
sources: [baseSource],
answerSections: [
Expand Down Expand Up @@ -278,6 +289,23 @@ describe("answer render policy", () => {
expect(model.warnings).not.toContain("A supporting source is due for review.");
});

it("fails closed when answer relevance metadata is absent", () => {
const model = buildAnswerRenderModel(
answer({
relevance: undefined,
smartPanel: undefined,
}),
);

expect(model.trust).toBe("low");
expect(model.allowedBlocks).not.toContain("quoteCards");
expect(model.allowedBlocks).not.toContain("visualEvidence");
expect(model.allowedBlocks).not.toContain("relatedDocuments");
expect(model.visualEvidence).toEqual([]);
expect(model.tables).toEqual([]);
expect(model.copyText).not.toContain("Displayed table evidence");
});

it("does not render high trust for high-risk claims supported only by unverified evidence", () => {
const model = buildAnswerRenderModel(
answer({
Expand Down
Loading
Loading