diff --git a/docs/process-hardening.md b/docs/process-hardening.md index a4a678c7d..4feb9b7da 100644 --- a/docs/process-hardening.md +++ b/docs/process-hardening.md @@ -34,13 +34,14 @@ This document turns the current process review into phased, durable repo practic - **2026-07-03:** extracted `answer-content.tsx` — `SourceImage`, `ScopeAndGovernanceNotice`, the answer/source formatters, `SourcePreviewContent`, `NaturalLanguageAnswer`, `UserQuestionBubble`, `KeyClinicalItems` (block 510–1249 against the post-drift monolith), moved verbatim (block diff empty; testid/aria checksum byte-identical). Two shared helpers went to clean homes instead of a monolith↔module cycle: `useMobilePreviewSheet` (+ its media-query snapshot helpers) → new `clinical-dashboard/use-mobile-preview-sheet.ts`, and `comparableAnswerText` → `clinical-dashboard/display-text.ts`. Retargeted the `NaturalLanguageAnswer` AST pin to scan `answer-content.tsx`, and widened `rendered-text-formatting.test.ts` negative scans to the monolith+module corpus (both strengthened, not weakened). Reusable finding: the answer/evidence families' helpers already live in `@/lib/*` and extracted sibling modules, so extractions re-import rather than needing wide monolith exports; only `comparableAnswerText`/`useMobilePreviewSheet` were monolith-internal. - **2026-07-03:** extracted `evidence-panels.tsx` — the clinical-detail/notes helper family + `AnswerSupportSummaryCard`, `ClinicalNotesChecklistPanel`, `SafetyFindingsPanel`, `EvidenceGapPanel`, `EvidenceCounts`, `AnswerSourceStatus`, `EvidenceSummaryCard`, `AnswerInsightBar`, `EvidenceVerificationStrip`, `AnswerFeedbackPanel`, `RenderModelSourceList`, `VerificationWorkspace`, `AnswerViewModeControl`, `EvidenceMapTable`, `AnswerSafetyNotice`, `QuoteCards` (contiguous block 484–2311, moved verbatim). Monolith 7909 → 6084 lines. **This module needs a monolith↔module cycle** — `ClinicalNotesChecklistPanel` renders `` (staying in the monolith as B3), so evidence-panels imports `ClinicalOutputPanel` + `clinicalQueryModeOptions` + `type AnswerFeedbackType` back from `@/components/ClinicalDashboard`. This is the SAME pattern already used by `global-mockup-search-shell.tsx` (imports `SettingsDialog` back); the repo has no `import/no-cycle` rule and the refs are render/runtime-time so init is safe (the `ui-smoke` build is the definitive cycle check). The monolith imports 26 symbols back (incl. the exported detail-helper family for output-panel). Robust dependency finding: the danger/reverse regex missed `const X: Type =` and `const X = {…}` forms (caught `clinicalQueryModeOptions` + `simpleClinicalTableProps` only via typecheck) — use a name-only regex `^(export )?(async )?(function|const|type) NAME` next time. +- **2026-07-03:** extracted `output-panel.tsx` — `ClinicalOutputPanel` (block 487–688, moved verbatim). Monolith 6084 → 5882 lines. Clean move: empty danger set (no monolith-internal deps) and empty reverse set (the monolith never rendered it — its only consumer is `evidence-panels`'s `ClinicalNotesChecklistPanel`). This **replaced** the evidence-panels↔monolith cycle with an evidence-panels↔output-panel one: `evidence-panels` now imports `ClinicalOutputPanel` from `clinical-dashboard/output-panel` (still imports `clinicalQueryModeOptions` + `type AnswerFeedbackType` from the monolith); `output-panel` imports the detail-helper family + `AnswerViewModeControl`/`EvidenceMapTable` from `evidence-panels`. Retargeted the AST pin (`ClinicalOutputPanel` now resolves via the `scannedFiles` array's `output-panel.tsx` entry) and added `output-panel.tsx` to the `rendered-text-formatting.test.ts` corpus. Stripped 8 now-orphaned monolith imports. + #### Remaining decomposition — hand-off (do on a stable `main`, one module per commit) -The approved move map (`docs/redesign/04-deferred.md` §2) has 3 modules left. Recommended order: +The approved move map (`docs/redesign/04-deferred.md` §2) has 2 modules left. Recommended order: -1. `output-panel.tsx` — `ClinicalOutputPanel` (**AST-pinned** — retarget `dashboardPath` in `tests/clinical-dashboard-merge-artifacts.test.ts`; the test now scans a `scannedFiles` array — add this file to it). It already imports the detail helpers from `evidence-panels`; when it moves out of the monolith, update `evidence-panels`'s back-import of `ClinicalOutputPanel` to point at `output-panel.tsx` instead of `@/components/ClinicalDashboard` (and move `clinicalQueryModeOptions`/`AnswerFeedbackType` to a shared home or keep the back-import). -2. `visual-evidence.tsx` — `VisualEvidenceStrip`, `InlineTableCard`, `MobileEvidenceSheetContent`, `MobileEvidenceTabPanel`, `UnifiedEvidenceDrawerContent` (+ `supportDotClass`/`supportLabel`/`EvidenceClaimsList`/`EvidenceGapsPanel` helpers, currently just after `ClinicalOutputPanel`). -3. `document-results.tsx` — `WhyThisMatchedPanel`, `RelatedDocumentsPanel`, `StagedAnswerResultSurface`. +1. `visual-evidence.tsx` — `VisualEvidenceStrip`, `InlineTableCard`, `MobileEvidenceSheetContent`, `MobileEvidenceTabPanel`, `UnifiedEvidenceDrawerContent` (+ `supportDotClass`/`supportLabel`/`EvidenceClaimsList`/`EvidenceGapsPanel`/`compactClinicalTableCaption`/`visualEvidenceHeader` helpers, currently just after `WhyThisMatchedPanel`). +2. `document-results.tsx` — `WhyThisMatchedPanel`, `RelatedDocumentsPanel`, `StagedAnswerResultSurface`. For each: trace which module-scope helpers/icons/types it uses; move solely-consumed ones with it, import shared ones; strip newly-orphaned monolith imports (lint flags them); run the per-module gate above; commit immediately. Keep the main `ClinicalDashboard` export in `ClinicalDashboard.tsx` (the barrel/bridge stays). Admin surfaces (`DocumentDrawer`, `SettingsDialog`, `ToolsHub`, `MobileSectionFab`) are out of the approved map — a later pass. diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index e2f0295a4..5498c9e44 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -126,7 +126,6 @@ import { } from "@/components/clinical-dashboard/display-text"; import { NaturalLanguageAnswer, - plainAnswerText, ScopeAndGovernanceNotice, SourceImage, UserQuestionBubble, @@ -135,18 +134,12 @@ import { AnswerFeedbackPanel, AnswerSafetyNotice, AnswerSupportSummaryCard, - AnswerViewModeControl, answerHasCentralTable, answerSupportPriority, ClinicalNotesChecklistPanel, - clinicalDetailContentCount, - clinicalDetailMeta, - clinicalDetailSummaryItems, clinicalNotesCount, clinicalNotesDisplayCountForAnswer, compactEvidenceSummary, - displayItemsForClinicalDetailSection, - EvidenceMapTable, type EvidenceTabName, simpleClinicalTableProps, evidenceMapRowsFromRenderModel, @@ -156,7 +149,6 @@ import { primaryVisualTable, QuoteCards, SafetyFindingsPanel, - sortClinicalDetailSections, VerificationWorkspace, } from "@/components/clinical-dashboard/evidence-panels"; import { useMobilePreviewSheet } from "@/components/clinical-dashboard/use-mobile-preview-sheet"; @@ -484,209 +476,6 @@ function normalizeNavigationHash(hash: string) { return navigationHashes.includes(hash as (typeof navigationHashes)[number]) ? hash : "#search"; } -export function ClinicalOutputPanel({ - answer, - collapsed = false, - showLead = true, - viewMode = "standard", - onViewModeChange, - evidenceMapRows, -}: { - answer: RagAnswer; - collapsed?: boolean; - showLead?: boolean; - viewMode?: AnswerViewMode; - onViewModeChange?: (mode: AnswerViewMode) => void; - evidenceMapRows?: AnswerEvidenceMapRow[]; -}) { - const sections = - viewMode === "high_yield" ? buildHighYieldClinicalOutputSections(answer) : buildClinicalOutputSections(answer); - const rows = evidenceMapRows ?? buildAnswerEvidenceMap(answer); - if (sections.length === 0 && (viewMode !== "evidence_map" || rows.length === 0)) return null; - const leadSection = sections.find((section) => section.id === "bottom-line") ?? sections[0]; - const primaryAnswer = plainAnswerText(answer.answer); - const detailSections = sections - .filter((section) => section.id !== "verify-source") - .filter((section) => (showLead ? section.id !== leadSection?.id : section.id !== "bottom-line")) - .map((section) => ({ - ...section, - items: displayItemsForClinicalDetailSection(section, primaryAnswer, showLead), - })) - .filter((section) => section.items.length > 0 || Boolean(section.tables?.length)); - const orderedDetailSections = sortClinicalDetailSections(detailSections); - const summaryItems = clinicalDetailSummaryItems(orderedDetailSections); - const title = - viewMode === "evidence_map" - ? "Evidence map" - : viewMode === "high_yield" - ? "High-yield clinical details" - : showLead - ? "Clinical answer" - : "Structured clinical details"; - const description = - viewMode === "evidence_map" - ? "Mapped answer sections to linked source support and source status." - : viewMode === "high_yield" - ? "Actions, thresholds, cautions, escalation triggers, monitoring, and dose details." - : showLead - ? "Dense source-backed structure for review." - : "Adaptive source-backed support below the concise answer."; - - const content = ( -
-
- : undefined} - hideDescriptionOnMobile - compactMobile - /> -
- {summaryItems.length ? ( -
- {summaryItems.map((item) => ( - - {item.label} - {item.value} - - ))} -
- ) : null} - {showLead && leadSection ? ( -
-
- - - -
-

- {leadSection.title} -

-

- -

-
-
-
- ) : null} - {viewMode === "evidence_map" ? ( -
- -
- ) : orderedDetailSections.length ? ( -
- {orderedDetailSections.map((section) => { - const isWide = section.id === "thresholds" || Boolean(section.tables?.length); - const itemCount = clinicalDetailContentCount(section); - const meta = clinicalDetailMeta(section); - const Icon = meta.icon; - return ( -
-
-
- -
-

- {meta.eyebrow} -

-

- {section.title} -

-
-
- {itemCount} -
- {section.tables?.length ? ( -
- {section.tables.map((table) => ( -
- -
- ))} -
- ) : null} - {section.items.length ? ( -
    - {section.items.map((item, index) => ( -
  • -
  • - ))} -
- ) : null} -
- ); - })} -
- ) : null} -
- ); - - if (collapsed) { - return ( - - {content} - - ); - } - - return content; -} - function WhyThisMatchedPanel({ sources }: { sources: SearchResult[] }) { const visibleSources = sources.slice(0, 3); if (visibleSources.length === 0) return null; diff --git a/src/components/clinical-dashboard/evidence-panels.tsx b/src/components/clinical-dashboard/evidence-panels.tsx index 88e1138aa..3f82128ac 100644 --- a/src/components/clinical-dashboard/evidence-panels.tsx +++ b/src/components/clinical-dashboard/evidence-panels.tsx @@ -28,7 +28,8 @@ import { } from "lucide-react"; import { AccessibleTable } from "@/components/AccessibleTable"; -import { ClinicalOutputPanel, clinicalQueryModeOptions, type AnswerFeedbackType } from "@/components/ClinicalDashboard"; +import { clinicalQueryModeOptions, type AnswerFeedbackType } from "@/components/ClinicalDashboard"; +import { ClinicalOutputPanel } from "@/components/clinical-dashboard/output-panel"; import { keyClinicalItemsFromSections, keyClinicalItemsFromTable, diff --git a/src/components/clinical-dashboard/output-panel.tsx b/src/components/clinical-dashboard/output-panel.tsx new file mode 100644 index 000000000..705ce66a6 --- /dev/null +++ b/src/components/clinical-dashboard/output-panel.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { CheckCircle2, ListChecks } from "lucide-react"; + +import { AccessibleTable } from "@/components/AccessibleTable"; +import { SafeBoldText } from "@/components/SafeBoldText"; +import { plainAnswerText } from "@/components/clinical-dashboard/answer-content"; +import { SectionHeading, UtilityDrawer } from "@/components/clinical-dashboard/dashboard-shell"; +import { + AnswerViewModeControl, + clinicalDetailContentCount, + clinicalDetailMeta, + clinicalDetailSummaryItems, + displayItemsForClinicalDetailSection, + EvidenceMapTable, + simpleClinicalTableProps, + sortClinicalDetailSections, +} from "@/components/clinical-dashboard/evidence-panels"; +import { cn, iconTilePremium, metadataPill, panelSubtle, subtleStatusPill } from "@/components/ui-primitives"; +import type { RagAnswer } from "@/lib/types"; +import { + type AnswerEvidenceMapRow, + type AnswerViewMode, + buildAnswerEvidenceMap, + buildClinicalOutputSections, + buildHighYieldClinicalOutputSections, +} from "@/lib/ward-output"; + +export function ClinicalOutputPanel({ + answer, + collapsed = false, + showLead = true, + viewMode = "standard", + onViewModeChange, + evidenceMapRows, +}: { + answer: RagAnswer; + collapsed?: boolean; + showLead?: boolean; + viewMode?: AnswerViewMode; + onViewModeChange?: (mode: AnswerViewMode) => void; + evidenceMapRows?: AnswerEvidenceMapRow[]; +}) { + const sections = + viewMode === "high_yield" ? buildHighYieldClinicalOutputSections(answer) : buildClinicalOutputSections(answer); + const rows = evidenceMapRows ?? buildAnswerEvidenceMap(answer); + if (sections.length === 0 && (viewMode !== "evidence_map" || rows.length === 0)) return null; + const leadSection = sections.find((section) => section.id === "bottom-line") ?? sections[0]; + const primaryAnswer = plainAnswerText(answer.answer); + const detailSections = sections + .filter((section) => section.id !== "verify-source") + .filter((section) => (showLead ? section.id !== leadSection?.id : section.id !== "bottom-line")) + .map((section) => ({ + ...section, + items: displayItemsForClinicalDetailSection(section, primaryAnswer, showLead), + })) + .filter((section) => section.items.length > 0 || Boolean(section.tables?.length)); + const orderedDetailSections = sortClinicalDetailSections(detailSections); + const summaryItems = clinicalDetailSummaryItems(orderedDetailSections); + const title = + viewMode === "evidence_map" + ? "Evidence map" + : viewMode === "high_yield" + ? "High-yield clinical details" + : showLead + ? "Clinical answer" + : "Structured clinical details"; + const description = + viewMode === "evidence_map" + ? "Mapped answer sections to linked source support and source status." + : viewMode === "high_yield" + ? "Actions, thresholds, cautions, escalation triggers, monitoring, and dose details." + : showLead + ? "Dense source-backed structure for review." + : "Adaptive source-backed support below the concise answer."; + + const content = ( +
+
+ : undefined} + hideDescriptionOnMobile + compactMobile + /> +
+ {summaryItems.length ? ( +
+ {summaryItems.map((item) => ( + + {item.label} + {item.value} + + ))} +
+ ) : null} + {showLead && leadSection ? ( +
+
+ + + +
+

+ {leadSection.title} +

+

+ +

+
+
+
+ ) : null} + {viewMode === "evidence_map" ? ( +
+ +
+ ) : orderedDetailSections.length ? ( +
+ {orderedDetailSections.map((section) => { + const isWide = section.id === "thresholds" || Boolean(section.tables?.length); + const itemCount = clinicalDetailContentCount(section); + const meta = clinicalDetailMeta(section); + const Icon = meta.icon; + return ( +
+
+
+ +
+

+ {meta.eyebrow} +

+

+ {section.title} +

+
+
+ {itemCount} +
+ {section.tables?.length ? ( +
+ {section.tables.map((table) => ( +
+ +
+ ))} +
+ ) : null} + {section.items.length ? ( +
    + {section.items.map((item, index) => ( +
  • +
  • + ))} +
+ ) : null} +
+ ); + })} +
+ ) : null} +
+ ); + + if (collapsed) { + return ( + + {content} + + ); + } + + return content; +} diff --git a/tests/clinical-dashboard-merge-artifacts.test.ts b/tests/clinical-dashboard-merge-artifacts.test.ts index a9e0ed411..ed0f1a24c 100644 --- a/tests/clinical-dashboard-merge-artifacts.test.ts +++ b/tests/clinical-dashboard-merge-artifacts.test.ts @@ -10,6 +10,7 @@ import { describe, expect, it } from "vitest"; const scannedFiles = [ "src/components/ClinicalDashboard.tsx", "src/components/clinical-dashboard/answer-content.tsx", + "src/components/clinical-dashboard/output-panel.tsx", ].map((relativePath) => { const path = resolve(process.cwd(), relativePath); const source = readFileSync(path, "utf8"); diff --git a/tests/rendered-text-formatting.test.ts b/tests/rendered-text-formatting.test.ts index 7b1779c85..075f8503d 100644 --- a/tests/rendered-text-formatting.test.ts +++ b/tests/rendered-text-formatting.test.ts @@ -20,7 +20,8 @@ describe("document-derived text must route through a formatter", () => { // the code as it moves out — assertions check the combined dashboard surfaces. const answerContent = componentSource("clinical-dashboard/answer-content.tsx"); const evidenceContent = componentSource("clinical-dashboard/evidence-panels.tsx"); - const dashboardSurfaces = `${dashboard}\n${answerContent}\n${evidenceContent}`; + const outputPanel = componentSource("clinical-dashboard/output-panel.tsx"); + const dashboardSurfaces = `${dashboard}\n${answerContent}\n${evidenceContent}\n${outputPanel}`; it("renders exact quotes through the verbatim cleaner, never raw", () => { // Allow `${quote.quote}` inside template literals (React keys, clipboard text);