From 61da1b267df76d97adb1d57d0f43d856bec83451 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:39:14 +0800 Subject: [PATCH 1/9] feat(documents): add high-yield clinical summary --- src/components/DocumentViewer.tsx | 1 - .../document-clinical-summary.tsx | 495 ++++++++++++++++++ .../document-overview-landing.tsx | 137 +---- tests/document-clinical-summary.dom.test.tsx | 70 +++ tests/document-clinical-summary.test.ts | 88 ++++ tests/ui-smoke.spec.ts | 22 + 6 files changed, 683 insertions(+), 130 deletions(-) create mode 100644 src/components/document-viewer/document-clinical-summary.tsx create mode 100644 tests/document-clinical-summary.dom.test.tsx create mode 100644 tests/document-clinical-summary.test.ts diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index b16087306..6ba75b09e 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -1304,7 +1304,6 @@ export function DocumentViewer({
= { + safety: { + icon: ShieldAlert, + iconClassName: "bg-[color:var(--danger-soft)] text-[color:var(--danger)]", + labelClassName: "bg-[color:var(--danger-soft)] text-[color:var(--danger)]", + }, + monitoring: { + icon: Clock3, + iconClassName: "bg-[color:var(--info-soft)] text-[color:var(--info)]", + labelClassName: "bg-[color:var(--info-soft)] text-[color:var(--info)]", + }, + action: { + icon: ClipboardCheck, + iconClassName: "bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]", + labelClassName: "bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]", + }, +}; + +function firstRenderableItem(items: DocumentSummaryProfileItem[], usedText: Set) { + return items.find((item) => { + if (item.support === "not_found") return false; + const text = cleanClinicalSummaryText(item.text); + return Boolean(text) && !usedText.has(text.toLowerCase()); + }); +} + +function profilePriorityCandidates(profile: ClinicalDocumentSummaryProfile): PriorityCandidate[] { + return [ + { + id: "escalation", + title: "Escalation and safety", + label: "Critical safety", + tone: "safety", + items: profile.escalation_risk_warnings ?? [], + }, + { + id: "monitoring", + title: "Timing and monitoring", + label: "Monitoring", + tone: "monitoring", + items: [...(profile.thresholds_timing ?? []), ...(profile.medication_dose_monitoring ?? [])], + }, + { + id: "action", + title: "Key clinical action", + label: "Clinical action", + tone: "action", + items: [ + ...(profile.key_clinical_actions ?? []), + ...(profile.required_forms_documentation ?? []), + ...(profile.applies_to ?? []), + ], + }, + ]; +} + +function prioritiesFromProfile(profile: ClinicalDocumentSummaryProfile): DocumentClinicalPriority[] { + const usedText = new Set(); + + return profilePriorityCandidates(profile).flatMap((candidate) => { + const item = firstRenderableItem(candidate.items, usedText); + if (!item) return []; + const text = cleanClinicalSummaryText(item.text); + usedText.add(text.toLowerCase()); + const page = item.pages.find((value) => Number.isInteger(value) && value > 0) ?? null; + return [ + { + id: candidate.id, + title: candidate.title, + label: candidate.label, + text, + page, + support: item.support, + tone: candidate.tone, + }, + ]; + }); +} + +function prioritiesFromFormattedSummary(document: ClinicalDocument): DocumentClinicalPriority[] { + const formatted = formatDocumentSummary(document.summary?.summary); + return formatted.sections + .flatMap((section) => + section.items.map((item) => ({ + heading: section.heading, + text: cleanClinicalSummaryText(item), + })), + ) + .filter((item) => item.text) + .slice(0, 3) + .map((item, index) => ({ + id: `summary-${index + 1}`, + title: item.heading ?? "Key clinical point", + label: index === 0 ? "Key point" : "Clinical detail", + text: item.text, + page: null, + support: null, + tone: "action", + })); +} + +export function buildDocumentClinicalSummaryModel(document: ClinicalDocument): DocumentClinicalSummaryModel { + const profile = document.summary?.clinical_specifics?.profile; + const formatted = formatDocumentSummary(document.summary?.summary); + const priorities = profile ? prioritiesFromProfile(profile) : prioritiesFromFormattedSummary(document); + const profileOverview = cleanClinicalSummaryText(profile?.overview ?? ""); + const usefulProfileOverview = + profileOverview && !/source-backed review/i.test(profileOverview) ? profileOverview : ""; + const summary = cleanClinicalSummaryText( + usefulProfileOverview || + formatted.lead || + formatted.sections[0]?.items.join(" ") || + priorities + .slice(0, 2) + .map((priority) => priority.text) + .join(" "), + ); + return { + summary, + priorities, + }; +} + +function supportLabel(support: DocumentSummarySupportLevel | null) { + if (support === "direct") return "Direct support"; + if (support === "partial") return "Partial support"; + return "Indexed summary"; +} + +function SupportStatus({ support }: { support: DocumentSummarySupportLevel | null }) { + const Icon = support === "direct" ? BadgeCheck : support === "partial" ? CircleDashed : FileText; + return ( + + + ); +} + +function PriorityPageLink({ + page, + href, + onNavigate, +}: { + page: number; + href: string; + onNavigate: (page: number) => void; +}) { + return ( + { + if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + event.preventDefault(); + onNavigate(page); + }} + className="inline-flex min-h-tap items-center gap-1 px-1 text-xs font-bold text-[color:var(--clinical-accent)] transition hover:text-[color:var(--clinical-accent-hover)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] sm:min-h-9" + > + View p.{page} + + ); +} + +function PriorityContent({ + priority, + index, + pageHref, + onPageChange, + compact = false, +}: { + priority: DocumentClinicalPriority; + index: number; + pageHref: (page: number) => string; + onPageChange: (page: number) => void; + compact?: boolean; +}) { + const tone = priorityTone[priority.tone]; + const Icon = tone.icon; + + if (compact) { + return ( +
+ + +
+
+

{priority.title}

+ + {priority.label} + +
+

+ +

+
+ + {priority.page ? ( + + ) : null} +
+
+
+ ); + } + + return ( +
+ + {String(index + 1).padStart(2, "0")} + + + +
+
+

{priority.title}

+ + {priority.label} + +
+

+ +

+
+
+ + {priority.page ? ( + + ) : null} +
+
+ ); +} + +export function DocumentClinicalSummary({ + document, + pageHref, + onPageChange, +}: { + document: ClinicalDocument; + pageHref: (page: number) => string; + onPageChange: (page: number) => void; +}) { + const model = buildDocumentClinicalSummaryModel(document); + const [summaryExpanded, setSummaryExpanded] = useState(false); + const [prioritiesExpanded, setPrioritiesExpanded] = useState(true); + const [mobilePrioritiesOpen, setMobilePrioritiesOpen] = useState(false); + const mobilePrioritiesButtonRef = useRef(null); + const prioritiesId = useId(); + const canExpandSummary = model.summary.length > 140; + + function navigateFromSheet(page: number) { + setMobilePrioritiesOpen(false); + onPageChange(page); + } + + return ( + <> +
+
+
+
+ + +

+ High-yield clinical summary +

+
+ + +
+ {model.summary ? ( +

+ +

+ ) : ( +

+ A structured clinical summary has not been indexed for this document yet. +

+ )} + {canExpandSummary ? ( + + ) : null} +
+ + {model.priorities.length ? ( + <> + + +
+ {model.priorities.map((priority, index) => ( + + ))} +
+ + ) : null} +
+ + setMobilePrioritiesOpen(false)} + returnFocusRef={mobilePrioritiesButtonRef} + title="Clinical priorities" + description="Expanded, source-linked clinical detail" + headerLeading={ + + + } + contentClassName="sm:max-w-xl" + bodyClassName="p-0 sm:p-0" + portal + testId="clinical-priorities-sheet" + > +
+ {model.priorities.map((priority, index) => ( + + ))} +
+
+ + ); +} diff --git a/src/components/document-viewer/document-overview-landing.tsx b/src/components/document-viewer/document-overview-landing.tsx index d1e4e1444..4a8cc6689 100644 --- a/src/components/document-viewer/document-overview-landing.tsx +++ b/src/components/document-viewer/document-overview-landing.tsx @@ -1,8 +1,9 @@ -// Overview landing for the document viewer: the header card, quick actions, -// overview/key-sections/useful-pages tiles, and page-jump chips. Extracted from -// DocumentViewer.tsx (maturity X3) as a pure move. -import { Download, FileText, Loader2, Sparkles, Tag, Target } from "lucide-react"; +// Overview landing for the document viewer: the compact header, quick actions, +// and high-yield clinical summary. Extracted from DocumentViewer.tsx (maturity +// X3) as a pure move. +import { Download, Loader2, Sparkles, Target } from "lucide-react"; import { documentDisplayTitle, documentOrganizationProfile } from "@/components/DocumentOrganizationBadges"; +import { DocumentClinicalSummary } from "@/components/document-viewer/document-clinical-summary"; import { formatDocumentLabelDisplay } from "@/lib/document-tags"; import { DocumentActionAnchor, @@ -12,10 +13,7 @@ import { documentFileKind, documentTileTone, } from "@/components/clinical-dashboard/document-ui"; -import { cn, panel, floatingControl, primaryControl, sourceCard, textMuted } from "@/components/ui-primitives"; -import { cleanClinicalSummaryText } from "@/lib/source-text-sanitizer"; -import { formatDocumentSummary } from "@/lib/document-summary-formatting"; -import { formatClinicalDate } from "@/lib/source-metadata"; +import { cn, panel, floatingControl, primaryControl } from "@/components/ui-primitives"; import type { ClinicalDocument } from "@/lib/types"; import type { PageRow } from "./types"; @@ -42,67 +40,8 @@ function documentTypeEyebrow(document: ClinicalDocument) { return typeLabel ? formatDocumentLabelDisplay(typeLabel, "document_type") : "Clinical document"; } -function documentOverviewText(document: ClinicalDocument) { - const profile = document.summary?.clinical_specifics?.profile; - // The stored raw summary opens with PDF-header boilerplate on many live - // documents, so route it through the smart formatter and show its lead - // sentences instead of the raw string. - const formattedSummary = profile?.overview ? null : formatDocumentSummary(document.summary?.summary); - const overview = profile?.overview - ? cleanClinicalSummaryText(profile.overview) - : (formattedSummary?.lead ?? formattedSummary?.sections[0]?.items.join(" ") ?? ""); - if (overview && !/source-backed review/i.test(overview)) return overview; - return "A clear overview of this document, useful pages, and source PDF access."; -} - -function documentKeySections(document: ClinicalDocument) { - const labels = (document.labels ?? []).map((label) => label.label).filter(Boolean); - return Array.from(new Set(labels)).slice(0, 3); -} - -function DocumentPagePreview({ - href, - pageNumber, - onNavigate, -}: { - href: string; - pageNumber: number | null; - onNavigate: (page: number) => void; -}) { - // A real "jump to page" chip rather than a fake wireframe thumbnail that looks - // like a skeleton that never resolves. - return ( - { - if ( - pageNumber === null || - event.button !== 0 || - event.metaKey || - event.ctrlKey || - event.shiftKey || - event.altKey - ) - return; - event.preventDefault(); - onNavigate(pageNumber); - }} - className="inline-flex min-h-tap items-center gap-1.5 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-3 text-sm font-semibold text-[color:var(--text)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--clinical-accent)]/40 hover:bg-[color:var(--clinical-accent-soft)] hover:text-[color:var(--clinical-accent)]" - > - - ); -} -function usefulDocumentPages(initialPage: number, pages: PageRow[]) { - return Array.from(new Set([initialPage, ...pages.map((page) => page.page_number)])) - .filter((page) => Number.isFinite(page)) - .slice(0, 3); -} - export function DocumentOverviewLanding({ document, - initialPage, signedUrl, pages, pageHref, @@ -114,7 +53,6 @@ export function DocumentOverviewLanding({ canSummarizeDocument, }: { document: ClinicalDocument; - initialPage: number; signedUrl: string | null; pages: PageRow[]; pageHref: (page: number) => string; @@ -125,10 +63,7 @@ export function DocumentOverviewLanding({ downloading: boolean; canSummarizeDocument: boolean; }) { - const keySections = documentKeySections(document); - const usefulPages = usefulDocumentPages(initialPage, pages); const documentType = compactDocumentType(document); - const overviewText = documentOverviewText(document); return (
@@ -148,15 +83,8 @@ export function DocumentOverviewLanding({ - {overviewText ? ( -

{overviewText}

- ) : null} {/* Search relevance badges are rendered in document search results; the viewer has no ranking context. */}
@@ -203,56 +131,7 @@ export function DocumentOverviewLanding({ - -
-
- - -
-

Overview

-

{documentOverviewText(document)}

-
-
-
- -
-
- - -
-

Key sections

-
- {(keySections.length ? keySections : ["Overview", "Useful pages", "Source PDF"]).map((section) => ( - - {section} - - ))} -
-
-
-
- -
-
- - -
-

Useful pages

-

Most relevant pages for this document.

-
- {(usefulPages.length ? usefulPages : [initialPage]).map((page) => ( - - ))} -
-
-
-
+ ); } diff --git a/tests/document-clinical-summary.dom.test.tsx b/tests/document-clinical-summary.dom.test.tsx new file mode 100644 index 000000000..7273263f7 --- /dev/null +++ b/tests/document-clinical-summary.dom.test.tsx @@ -0,0 +1,70 @@ +import { fireEvent, render, screen, within } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { DocumentClinicalSummary } from "@/components/document-viewer/document-clinical-summary"; +import type { ClinicalDocument, ClinicalDocumentSummaryProfile, DocumentSummaryProfileItem } from "@/lib/types"; + +function item(text: string, page: number): DocumentSummaryProfileItem { + return { + text, + pages: [page], + support: "direct", + evidence_type: "text", + source_chunk_ids: [`chunk-${page}`], + source_image_ids: [], + }; +} + +const profile: ClinicalDocumentSummaryProfile = { + overview: + "Safe lithium initiation and maintenance requires baseline renal, thyroid and calcium testing, correctly timed trough levels after initiation and dose changes, and immediate withholding with urgent assessment whenever toxicity is suspected.", + applies_to: [], + key_clinical_actions: [item("Check renal, thyroid and calcium status before treatment.", 4)], + medication_dose_monitoring: [], + thresholds_timing: [item("Take trough levels 12 hours after the last dose.", 6)], + escalation_risk_warnings: [item("Withhold lithium and urgently assess suspected toxicity.", 9)], + required_forms_documentation: [], + not_covered: [], + important_tables_images: [], + best_questions: [], + source_quality_notes: [], +}; + +const document = { + id: "document-1", + title: "Lithium monitoring protocol", + file_name: "lithium-monitoring.pdf", + created_at: "2026-07-24T00:00:00.000Z", + summary: { + id: "summary-1", + document_id: "document-1", + summary: profile.overview, + clinical_specifics: { profile }, + }, +} as ClinicalDocument; + +describe("DocumentClinicalSummary", () => { + it("expands the mobile summary without introducing a visible control row", () => { + render( `?page=${page}`} onPageChange={vi.fn()} />); + + const toggle = screen.getByTestId("toggle-document-summary"); + expect(toggle).toHaveAttribute("aria-expanded", "false"); + fireEvent.click(toggle); + expect(toggle).toHaveAttribute("aria-expanded", "true"); + expect(toggle).toHaveTextContent("Show less"); + }); + + it("opens the mobile priorities sheet and navigates from its source page link", () => { + const onPageChange = vi.fn(); + render( + `?page=${page}`} onPageChange={onPageChange} />, + ); + + fireEvent.click(screen.getByTestId("open-clinical-priorities")); + const sheet = screen.getByTestId("clinical-priorities-sheet"); + expect(sheet).toBeVisible(); + fireEvent.click(within(sheet).getByRole("link", { name: "View p.9" })); + + expect(onPageChange).toHaveBeenCalledWith(9); + expect(screen.queryByTestId("clinical-priorities-sheet")).not.toBeInTheDocument(); + }); +}); diff --git a/tests/document-clinical-summary.test.ts b/tests/document-clinical-summary.test.ts new file mode 100644 index 000000000..8e993f313 --- /dev/null +++ b/tests/document-clinical-summary.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "vitest"; +import { buildDocumentClinicalSummaryModel } from "@/components/document-viewer/document-clinical-summary"; +import type { ClinicalDocument, ClinicalDocumentSummaryProfile, DocumentSummaryProfileItem } from "@/lib/types"; + +function item( + text: string, + pages: number[], + support: DocumentSummaryProfileItem["support"] = "direct", +): DocumentSummaryProfileItem { + return { + text, + pages, + support, + evidence_type: "text", + source_chunk_ids: ["chunk-1"], + source_image_ids: [], + }; +} + +function profile(overrides: Partial = {}): ClinicalDocumentSummaryProfile { + return { + overview: "Safe lithium care requires baseline tests, timed monitoring, and urgent action for toxicity.", + applies_to: [], + key_clinical_actions: [item("Check renal, thyroid and calcium status before treatment.", [4])], + medication_dose_monitoring: [], + thresholds_timing: [item("Take trough levels 12 hours after the last dose.", [6])], + escalation_risk_warnings: [item("Withhold lithium and urgently assess suspected toxicity.", [9])], + required_forms_documentation: [], + not_covered: [], + important_tables_images: [], + best_questions: [], + source_quality_notes: [], + ...overrides, + }; +} + +function documentWithProfile(summaryProfile: ClinicalDocumentSummaryProfile): ClinicalDocument { + return { + id: "document-1", + title: "Lithium monitoring protocol", + file_name: "lithium-monitoring.pdf", + created_at: "2026-07-24T00:00:00.000Z", + summary: { + id: "summary-1", + document_id: "document-1", + summary: "Stored summary fallback.", + clinical_specifics: { profile: summaryProfile }, + }, + } as ClinicalDocument; +} + +describe("buildDocumentClinicalSummaryModel", () => { + it("orders source-backed priorities by safety, monitoring, then clinical action", () => { + const model = buildDocumentClinicalSummaryModel(documentWithProfile(profile())); + + expect(model.summary).toContain("baseline tests"); + expect(model.priorities.map((priority) => priority.title)).toEqual([ + "Escalation and safety", + "Timing and monitoring", + "Key clinical action", + ]); + expect(model.priorities.map((priority) => priority.page)).toEqual([9, 6, 4]); + }); + + it("skips unsupported and duplicate profile items", () => { + const repeated = "Take trough levels 12 hours after the last dose."; + const model = buildDocumentClinicalSummaryModel( + documentWithProfile( + profile({ + escalation_risk_warnings: [item("Unsupported warning.", [3], "not_found")], + thresholds_timing: [item(repeated, [6])], + key_clinical_actions: [item(repeated, [6])], + }), + ), + ); + + expect(model.priorities).toHaveLength(1); + expect(model.priorities[0]).toMatchObject({ title: "Timing and monitoring", page: 6 }); + }); + + it("does not surface generic source-review copy as the high-yield summary", () => { + const model = buildDocumentClinicalSummaryModel( + documentWithProfile(profile({ overview: "Source-backed review of the indexed document." })), + ); + + expect(model.summary).toBe("Stored summary fallback."); + }); +}); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 20fdd89d6..0566ed5df 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -3353,6 +3353,15 @@ test.describe("Clinical KB UI smoke coverage", () => { await expect(page.getByRole("heading", { level: 1, name: "Synthetic lithium monitoring protocol" })).toBeVisible({ timeout: 30_000, }); + const clinicalSummary = page.getByTestId("document-clinical-summary"); + await expect(clinicalSummary).toBeVisible(); + await expect(clinicalSummary.getByRole("heading", { name: "High-yield clinical summary" })).toBeVisible(); + const clinicalPriorities = clinicalSummary.getByRole("button", { name: /Clinical priorities/ }); + await expect(clinicalPriorities).toHaveAttribute("aria-expanded", "true"); + await clinicalPriorities.click(); + await expect(clinicalPriorities).toHaveAttribute("aria-expanded", "false"); + await expect(page.getByRole("heading", { name: "Key sections", exact: true })).toHaveCount(0); + await expect(page.getByRole("heading", { name: "Useful pages", exact: true })).toHaveCount(0); const summaryCard = page.getByTestId("high-yield-summary"); await expect(summaryCard).toBeVisible(); await expect(summaryCard).toHaveJSProperty("open", false); @@ -3388,6 +3397,19 @@ test.describe("Clinical KB UI smoke coverage", () => { await expect(page.getByRole("heading", { level: 1, name: "Synthetic lithium monitoring protocol" })).toBeVisible({ timeout: 30_000, }); + const clinicalSummary = page.getByTestId("document-clinical-summary"); + const summaryToggle = clinicalSummary.getByTestId("toggle-document-summary"); + await expect(clinicalSummary).toBeVisible(); + await expect(summaryToggle).toBeVisible(); + await expect(summaryToggle).toHaveAttribute("aria-expanded", "false"); + await summaryToggle.click(); + await expect(summaryToggle).toHaveAttribute("aria-expanded", "true"); + await expect(summaryToggle).toContainText("Show less"); + await clinicalSummary.getByTestId("open-clinical-priorities").click(); + const prioritiesSheet = page.getByRole("dialog", { name: "Clinical priorities" }); + await expect(prioritiesSheet).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(prioritiesSheet).toHaveCount(0); const indexedText = page.locator("#source-text"); const summary = page.getByTestId("high-yield-summary"); const images = page.locator("#source-images"); From 6bbce2b97477cb4497624abe2a37c74864e872c8 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:53:29 +0800 Subject: [PATCH 2/9] fix(documents): use named summary type token --- src/components/document-viewer/document-clinical-summary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/document-viewer/document-clinical-summary.tsx b/src/components/document-viewer/document-clinical-summary.tsx index eba970a47..5e9844de0 100644 --- a/src/components/document-viewer/document-clinical-summary.tsx +++ b/src/components/document-viewer/document-clinical-summary.tsx @@ -356,7 +356,7 @@ export function DocumentClinicalSummary({ {model.summary ? (

From 88a986dd240acaa68fcc340e7629caadc4074275 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:01:29 +0800 Subject: [PATCH 3/9] docs: record PR 1169 thread verification --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 4808a2600..324a42286 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -747,3 +747,4 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-24 | remediate-audit-system-issues (PR #1160) | bdf530fc8c6faaa4491c510396b47872fc39bf25 | Run PR sweep: CI fix + threads + drift | second re-merge after main moved to 2e68888f3 during first push; clean ort merge (ledger + layout.tsx); taskkill /T retained; sitemap prettier retained | merge only; no provider-backed checks run | | 2026-07-24 | codex/query-ribbon-search-headings (PR #1166) | c94e89f392f578c4b2c749195dd485b74959074c | Run PR re-sync sweep | Before: CONFLICTING. After: merged origin/main clean. CI re-running. | merge origin/main and/or conflict re-check only; no provider-backed checks run | | 2026-07-24 | cursor/comprehensive-repo-review-ledger-d9a1 (PR #1150) | 345c02cdbaefb13aeb951a14674aedfe4648a50e | Run PR re-sync sweep | Before: CONFLICTING. After: merged origin/main clean. | merge origin/main and/or conflict re-check only; no provider-backed checks run | +| 2026-07-25 | codex/document-clinical-summary-20260725 (PR #1169) | 6bbce2b97477cb4497624abe2a37c74864e872c8 | Open-PR maintenance: review-thread verification | Before: 2 unresolved Codex threads; branch current with main and required CI running. After: both persisted-profile/placeholder-summary fixes confirmed on the exact head and ready for reply-then-resolve; no further code change required. | `node scripts/run-vitest.mjs run tests/document-clinical-summary.test.ts tests/document-clinical-summary.dom.test.tsx --reporter=dot` pass (5/5); `git diff --check` pass; no provider-backed checks run. | From ef4ffca5687a76fd05a28c5a84b1132e0e1193f2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 17:01:39 +0000 Subject: [PATCH 4/9] fix(documents): guard profile pages and filter placeholder summaries - Default missing pages arrays to [] before page lookup - Skip enrichment placeholder text in clinical summary display Co-authored-by: BigSimmo --- .../document-clinical-summary.tsx | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/components/document-viewer/document-clinical-summary.tsx b/src/components/document-viewer/document-clinical-summary.tsx index 5e9844de0..257249767 100644 --- a/src/components/document-viewer/document-clinical-summary.tsx +++ b/src/components/document-viewer/document-clinical-summary.tsx @@ -70,6 +70,21 @@ const priorityTone: Record) { return items.find((item) => { if (item.support === "not_found") return false; @@ -116,7 +131,7 @@ function prioritiesFromProfile(profile: ClinicalDocumentSummaryProfile): Documen if (!item) return []; const text = cleanClinicalSummaryText(item.text); usedText.add(text.toLowerCase()); - const page = item.pages.find((value) => Number.isInteger(value) && value > 0) ?? null; + const page = profileItemPages(item).find((value) => Number.isInteger(value) && value > 0) ?? null; return [ { id: candidate.id, @@ -157,13 +172,11 @@ export function buildDocumentClinicalSummaryModel(document: ClinicalDocument): D const profile = document.summary?.clinical_specifics?.profile; const formatted = formatDocumentSummary(document.summary?.summary); const priorities = profile ? prioritiesFromProfile(profile) : prioritiesFromFormattedSummary(document); - const profileOverview = cleanClinicalSummaryText(profile?.overview ?? ""); - const usefulProfileOverview = - profileOverview && !/source-backed review/i.test(profileOverview) ? profileOverview : ""; + const profileOverview = usefulSummaryText(profile?.overview ?? ""); const summary = cleanClinicalSummaryText( - usefulProfileOverview || - formatted.lead || - formatted.sections[0]?.items.join(" ") || + profileOverview || + usefulSummaryText(formatted.lead ?? "") || + usefulSummaryText(formatted.sections[0]?.items.join(" ") ?? "") || priorities .slice(0, 2) .map((priority) => priority.text) From 42abd25379c924500f7c2ca2e28d1337d40cad0c Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:06:43 +0800 Subject: [PATCH 5/9] fix(documents): harden clinical summary fallbacks --- .../document-clinical-summary.tsx | 12 +++++++----- tests/document-clinical-summary.test.ts | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/components/document-viewer/document-clinical-summary.tsx b/src/components/document-viewer/document-clinical-summary.tsx index 257249767..defa7aaf4 100644 --- a/src/components/document-viewer/document-clinical-summary.tsx +++ b/src/components/document-viewer/document-clinical-summary.tsx @@ -70,7 +70,7 @@ const priorityTone: Record priority.text) - .join(" "), + usefulSummaryText( + priorities + .slice(0, 2) + .map((priority) => priority.text) + .join(" "), + ), ); return { summary, diff --git a/tests/document-clinical-summary.test.ts b/tests/document-clinical-summary.test.ts index 8e993f313..542e0252f 100644 --- a/tests/document-clinical-summary.test.ts +++ b/tests/document-clinical-summary.test.ts @@ -78,6 +78,22 @@ describe("buildDocumentClinicalSummaryModel", () => { expect(model.priorities[0]).toMatchObject({ title: "Timing and monitoring", page: 6 }); }); + it("handles persisted profile items without a pages array", () => { + const legacyItem = { + ...item("Take a trough level after the last dose.", []), + pages: undefined, + } as unknown as DocumentSummaryProfileItem; + const model = buildDocumentClinicalSummaryModel( + documentWithProfile( + profile({ + thresholds_timing: [legacyItem], + }), + ), + ); + + expect(model.priorities.find((priority) => priority.title === "Timing and monitoring")?.page).toBeNull(); + }); + it("does not surface generic source-review copy as the high-yield summary", () => { const model = buildDocumentClinicalSummaryModel( documentWithProfile(profile({ overview: "Source-backed review of the indexed document." })), From 605a47b551a03774fab41416bf980dfbc9610221 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:36:10 +0800 Subject: [PATCH 6/9] fix(documents): tolerate malformed summary profiles --- .../document-clinical-summary.tsx | 18 +++++++++++++----- tests/document-clinical-summary.test.ts | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/components/document-viewer/document-clinical-summary.tsx b/src/components/document-viewer/document-clinical-summary.tsx index defa7aaf4..4e5b388f1 100644 --- a/src/components/document-viewer/document-clinical-summary.tsx +++ b/src/components/document-viewer/document-clinical-summary.tsx @@ -93,6 +93,14 @@ function firstRenderableItem(items: DocumentSummaryProfileItem[], usedText: Set< }); } +function profileItems(value: unknown): DocumentSummaryProfileItem[] { + if (!Array.isArray(value)) return []; + return value.filter( + (item): item is DocumentSummaryProfileItem => + typeof item === "object" && item !== null && typeof (item as { text?: unknown }).text === "string", + ); +} + function profilePriorityCandidates(profile: ClinicalDocumentSummaryProfile): PriorityCandidate[] { return [ { @@ -100,14 +108,14 @@ function profilePriorityCandidates(profile: ClinicalDocumentSummaryProfile): Pri title: "Escalation and safety", label: "Critical safety", tone: "safety", - items: profile.escalation_risk_warnings ?? [], + items: profileItems(profile.escalation_risk_warnings), }, { id: "monitoring", title: "Timing and monitoring", label: "Monitoring", tone: "monitoring", - items: [...(profile.thresholds_timing ?? []), ...(profile.medication_dose_monitoring ?? [])], + items: [...profileItems(profile.thresholds_timing), ...profileItems(profile.medication_dose_monitoring)], }, { id: "action", @@ -115,9 +123,9 @@ function profilePriorityCandidates(profile: ClinicalDocumentSummaryProfile): Pri label: "Clinical action", tone: "action", items: [ - ...(profile.key_clinical_actions ?? []), - ...(profile.required_forms_documentation ?? []), - ...(profile.applies_to ?? []), + ...profileItems(profile.key_clinical_actions), + ...profileItems(profile.required_forms_documentation), + ...profileItems(profile.applies_to), ], }, ]; diff --git a/tests/document-clinical-summary.test.ts b/tests/document-clinical-summary.test.ts index 542e0252f..62de72661 100644 --- a/tests/document-clinical-summary.test.ts +++ b/tests/document-clinical-summary.test.ts @@ -94,6 +94,21 @@ describe("buildDocumentClinicalSummaryModel", () => { expect(model.priorities.find((priority) => priority.title === "Timing and monitoring")?.page).toBeNull(); }); + it("ignores malformed persisted profile groups and items", () => { + const malformed = profile({ + thresholds_timing: { text: "not an array" }, + medication_dose_monitoring: [null, "invalid", { support: "direct" }], + escalation_risk_warnings: [null, item("Escalate valid safety concerns.", [7])], + } as unknown as Partial); + + const model = buildDocumentClinicalSummaryModel(documentWithProfile(malformed)); + + expect(model.priorities.map((priority) => priority.text)).toEqual([ + "Escalate valid safety concerns.", + "Check renal, thyroid and calcium status before treatment.", + ]); + }); + it("does not surface generic source-review copy as the high-yield summary", () => { const model = buildDocumentClinicalSummaryModel( documentWithProfile(profile({ overview: "Source-backed review of the indexed document." })), From 75c40c82f4cace438261e129e59a2579a59b4c5a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:36:28 +0800 Subject: [PATCH 7/9] docs: record PR 1169 profile guard --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 1f59fc5c3..3a2b5d3ef 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -760,3 +760,4 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-25 | codex/document-clinical-summary-20260725 (PR #1169) | 6bbce2b97477cb4497624abe2a37c74864e872c8 | Open-PR maintenance: review-thread verification | Before: 2 unresolved Codex threads; branch current with main and required CI running. After: both persisted-profile/placeholder-summary fixes confirmed on the exact head and ready for reply-then-resolve; no further code change required. | `node scripts/run-vitest.mjs run tests/document-clinical-summary.test.ts tests/document-clinical-summary.dom.test.tsx --reporter=dot` pass (5/5); `git diff --check` pass; no provider-backed checks run. | | 2026-07-25 | cursor/pr-babysit-bugbot-agents-6c52 (PR #1167) | ee44812aae9dad1973d8302eba5bfca5000dffb6 | Open-PR maintenance: review-thread fixes | Before: 8 unresolved Codex/CodeRabbit threads; branch current with main. After: target-head pinning, fresh-main verification, exact `cursor[bot]` identity checks, explicit mutation/provider authorization, direct reply-then-resolve semantics, and no-op ledger bookkeeping are documented. | Prettier check on both agent files pass; `git diff --check` pass; GitHub author probe confirmed `cursor[bot]` account type `Bot`; no provider-backed checks run. | | 2026-07-25 | codex/fix-merge-conflicts-and-ci-on-open-prs (PR #1170) | e979fc892f11a17b1a8f2ef1ab058ef40d629182 | Open-PR maintenance: CI fix + threads + drift | Before: Static PR checks failed because route-group moves made nine valid legacy `src/app/*` documentation references appear missing; 0 unresolved threads; branch already contained current main. After: docs-link resolution checks known App Router route groups and the focused failure is fixed. | `node scripts/check-docs-links.mjs` pass (1162 references); Prettier check pass; `git diff --check` pass; no provider-backed checks run. | +| 2026-07-25 | codex/document-clinical-summary-20260725 (PR #1169) | 605a47b551a03774fab41416bf980dfbc9610221 | Open-PR maintenance: malformed persisted profile guard | Before: one actionable thread showed non-array or malformed persisted summary groups could throw during render. After: every priority group is normalized through an array/item guard and malformed values are ignored while valid items still render. | Focused Vitest 7/7 pass; Prettier and diff checks pass; no provider-backed checks run. | From b6a13ebce00d7fed71cdd09f6c85824a4a538ee5 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:38:49 +0800 Subject: [PATCH 8/9] fix(documents): keep empty summaries unverified --- .../document-viewer/document-clinical-summary.tsx | 11 +++++++---- tests/document-clinical-summary.dom.test.tsx | 11 +++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/components/document-viewer/document-clinical-summary.tsx b/src/components/document-viewer/document-clinical-summary.tsx index 4e5b388f1..dbf944343 100644 --- a/src/components/document-viewer/document-clinical-summary.tsx +++ b/src/components/document-viewer/document-clinical-summary.tsx @@ -348,6 +348,7 @@ export function DocumentClinicalSummary({ const mobilePrioritiesButtonRef = useRef(null); const prioritiesId = useId(); const canExpandSummary = model.summary.length > 140; + const hasRenderableSummary = Boolean(model.summary || model.priorities.length); function navigateFromSheet(page: number) { setMobilePrioritiesOpen(false); @@ -371,10 +372,12 @@ export function DocumentClinicalSummary({ High-yield clinical summary - - + {hasRenderableSummary ? ( + + + ) : null} {model.summary ? (

{ expect(onPageChange).toHaveBeenCalledWith(9); expect(screen.queryByTestId("clinical-priorities-sheet")).not.toBeInTheDocument(); }); + + it("does not label an unindexed empty state as source-backed", () => { + const emptyDocument = { ...document, summary: undefined } as ClinicalDocument; + + render( + `?page=${page}`} onPageChange={vi.fn()} />, + ); + + expect(screen.getByText("A structured clinical summary has not been indexed for this document yet.")).toBeVisible(); + expect(screen.queryByText("Source-backed")).not.toBeInTheDocument(); + }); }); From 09043be976af266f9abd1eb745dec952802bde6f Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:56:27 +0800 Subject: [PATCH 9/9] fix(documents): verify summary evidence metadata --- .../document-clinical-summary.tsx | 15 ++++++++++--- tests/document-clinical-summary.test.ts | 21 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/components/document-viewer/document-clinical-summary.tsx b/src/components/document-viewer/document-clinical-summary.tsx index dbf944343..125f8fcd6 100644 --- a/src/components/document-viewer/document-clinical-summary.tsx +++ b/src/components/document-viewer/document-clinical-summary.tsx @@ -42,6 +42,7 @@ export type DocumentClinicalPriority = { export type DocumentClinicalSummaryModel = { summary: string; priorities: DocumentClinicalPriority[]; + sourceBacked: boolean; }; type PriorityCandidate = { @@ -85,6 +86,14 @@ function profileItemPages(item: DocumentSummaryProfileItem): number[] { return Array.isArray(item.pages) ? item.pages : []; } +function verifiedSupport(item: DocumentSummaryProfileItem): DocumentSummarySupportLevel | null { + if (item.support !== "direct" && item.support !== "partial") return null; + const hasLinkedSource = [item.source_chunk_ids, item.source_image_ids].some( + (ids) => Array.isArray(ids) && ids.some((id) => typeof id === "string" && id.trim()), + ); + return hasLinkedSource ? item.support : null; +} + function firstRenderableItem(items: DocumentSummaryProfileItem[], usedText: Set) { return items.find((item) => { if (item.support === "not_found") return false; @@ -147,7 +156,7 @@ function prioritiesFromProfile(profile: ClinicalDocumentSummaryProfile): Documen label: candidate.label, text, page, - support: item.support, + support: verifiedSupport(item), tone: candidate.tone, }, ]; @@ -195,6 +204,7 @@ export function buildDocumentClinicalSummaryModel(document: ClinicalDocument): D return { summary, priorities, + sourceBacked: priorities.some((priority) => priority.support === "direct" || priority.support === "partial"), }; } @@ -348,7 +358,6 @@ export function DocumentClinicalSummary({ const mobilePrioritiesButtonRef = useRef(null); const prioritiesId = useId(); const canExpandSummary = model.summary.length > 140; - const hasRenderableSummary = Boolean(model.summary || model.priorities.length); function navigateFromSheet(page: number) { setMobilePrioritiesOpen(false); @@ -372,7 +381,7 @@ export function DocumentClinicalSummary({ High-yield clinical summary - {hasRenderableSummary ? ( + {model.sourceBacked ? (