diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index a694ec191..1095084cf 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -757,6 +757,8 @@ This file is append-only. Never rewrite or delete an existing review record; app
| 2026-07-24 | cursor/frontend-ui-review-docs-e8d9 (PR #1146) | 2b17f5fcbe06307e7ff50ad0373789daa8f07a01 | 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-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. |
| 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. |
| 2026-07-25 | cursor/search-performance-review-4ee9 (PR #1134) | 692834a86e612cc8b311dc6895e007f182f5c5b8 | Open-PR maintenance: superseded docs-link thread and clean main sync | Before: branch was behind current main with one outdated docs-link thread; its product tree already matched main. After: merged current main cleanly and verified the route-group-aware docs-link fix now covers legacy route references. RAG impact: no retrieval behaviour change — history sync and docs tooling verification only. | `node scripts/check-docs-links.mjs` pass (1154 references); clean merge-tree; no live RAG canary or provider-backed check run. |
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)]",
+ },
+};
+
+const PLACEHOLDER_SUMMARY_RE = /source-backed review/i;
+
+function isPlaceholderClinicalSummary(text: string): boolean {
+ return PLACEHOLDER_SUMMARY_RE.test(text);
+}
+
+function usefulSummaryText(text: string): string {
+ const cleaned = cleanClinicalSummaryText(text);
+ return cleaned && !isPlaceholderClinicalSummary(cleaned) ? cleaned : "";
+}
+
+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;
+ const text = cleanClinicalSummaryText(item.text);
+ return Boolean(text) && !usedText.has(text.toLowerCase());
+ });
+}
+
+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 [
+ {
+ id: "escalation",
+ title: "Escalation and safety",
+ label: "Critical safety",
+ tone: "safety",
+ items: profileItems(profile.escalation_risk_warnings),
+ },
+ {
+ id: "monitoring",
+ title: "Timing and monitoring",
+ label: "Monitoring",
+ tone: "monitoring",
+ items: [...profileItems(profile.thresholds_timing), ...profileItems(profile.medication_dose_monitoring)],
+ },
+ {
+ id: "action",
+ title: "Key clinical action",
+ label: "Clinical action",
+ tone: "action",
+ items: [
+ ...profileItems(profile.key_clinical_actions),
+ ...profileItems(profile.required_forms_documentation),
+ ...profileItems(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 = profileItemPages(item).find((value) => Number.isInteger(value) && value > 0) ?? null;
+ return [
+ {
+ id: candidate.id,
+ title: candidate.title,
+ label: candidate.label,
+ text,
+ page,
+ support: verifiedSupport(item),
+ 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 = usefulSummaryText(profile?.overview ?? "");
+ const summary = cleanClinicalSummaryText(
+ profileOverview ||
+ usefulSummaryText(formatted.lead ?? "") ||
+ usefulSummaryText(formatted.sections[0]?.items.join(" ") ?? "") ||
+ usefulSummaryText(
+ priorities
+ .slice(0, 2)
+ .map((priority) => priority.text)
+ .join(" "),
+ ),
+ );
+ return {
+ summary,
+ priorities,
+ sourceBacked: priorities.some((priority) => priority.support === "direct" || priority.support === "partial"),
+ };
+}
+
+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 (
+
+
+ {supportLabel(support)}
+
+ );
+}
+
+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.sourceBacked ? (
+
+
+ Source-backed
+
+ ) : null}
+
+ {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)]"
- >
-
- Jump to p.{pageNumber ?? "n/a"}
-
- );
-}
-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..b7b54064b
--- /dev/null
+++ b/tests/document-clinical-summary.dom.test.tsx
@@ -0,0 +1,81 @@
+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();
+ });
+
+ 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();
+ });
+});
diff --git a/tests/document-clinical-summary.test.ts b/tests/document-clinical-summary.test.ts
new file mode 100644
index 000000000..fe4693d28
--- /dev/null
+++ b/tests/document-clinical-summary.test.ts
@@ -0,0 +1,140 @@
+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]);
+ expect(model.sourceBacked).toBe(true);
+ });
+
+ 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("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("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("keeps text-only persisted profile items unverified", () => {
+ const textOnlyItem = {
+ text: "Review monitoring after dose changes.",
+ pages: [5],
+ } as DocumentSummaryProfileItem;
+ const model = buildDocumentClinicalSummaryModel(
+ documentWithProfile(
+ profile({
+ escalation_risk_warnings: [],
+ thresholds_timing: [textOnlyItem],
+ key_clinical_actions: [],
+ }),
+ ),
+ );
+
+ expect(model.priorities).toHaveLength(1);
+ expect(model.priorities[0]?.support).toBeNull();
+ expect(model.sourceBacked).toBe(false);
+ });
+
+ 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");