From 253da4c2e48871f7e402c62a63d7e3b6a6d75936 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 09:49:45 +0000 Subject: [PATCH] fix(nav,therapy): make the density profile a required choice, and state review status on the two listings that quote a record Two root causes from the PR #2686 session, both small. 1. A rail could inherit a density profile it never chose. InPageSectionRail took `density` as an optional prop with a default, so medication's rail sat on Therapy's `extended` profile without naming it. That coupling was invisible to `registryModeNavDensityProfiles`, which lists only the modes that render the top bar, so checking that registry said the change was Therapy-only when it was not. Retuning `extended` unfolded medication's counted labels early, and only ui-smoke's prescribing journey caught it. `density` is now required at both the rail and the header layer. A default cannot be safe here: the value is calibrated against one label family's measured widths, so inheriting another's is always a guess. Making it required costs each call site one line and makes the consumer set answerable by `git grep "density:"`. The compiler immediately found two more silent inheritors in tests, which is the fix demonstrating itself. 2. Two listings quoted a record's clinical prose without its review state. Related therapies and linked pathway steps both print another record's name beside a sentence of its `bestUsedFor` or `clinicalSummary`, with nothing saying whether that record has been reviewed. The 2026-09-02 audit recorded this under L03c as a consistency gap, which it was while a catalogue-wide notice also stated the caveat. PR #2686 removed that notice at the owner's request, leaving the per-record badge as the whole disclosure, so these two surfaces became the only places a clinician can read therapy guidance with no review state at all. Both now render the same StatusBadge every other therapy surface renders, in a compact size added for dense rows. The default size is unchanged, so no existing surface moves. An unlinked pathway step names no record and so states nothing. Both fixes are pinned: the disclosure contract test now covers the two listings, and a new contract case asserts the profile is named rather than defaulted. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HUKKfk7hcKZ2UuqF3pwxMY --- .../medication-nav-header.tsx | 5 +++- .../in-page-nav/in-page-nav-header.tsx | 8 ++++-- .../in-page-nav/in-page-section-rail.tsx | 16 ++++++++++-- .../therapy-compass/pathway-step-stack.tsx | 11 +++++++- .../record/related-therapies.tsx | 9 +++++++ src/components/therapy-compass/ui.tsx | 6 ++--- tests/in-page-nav-header.dom.test.tsx | 4 +-- tests/mode-nav-contract.test.ts | 26 +++++++++++++++++++ tests/therapy-review-regressions.test.ts | 8 ++++++ 9 files changed, 82 insertions(+), 11 deletions(-) diff --git a/src/components/clinical-dashboard/medication-nav-header.tsx b/src/components/clinical-dashboard/medication-nav-header.tsx index 6fa1ab6b4..40fe2fae4 100644 --- a/src/components/clinical-dashboard/medication-nav-header.tsx +++ b/src/components/clinical-dashboard/medication-nav-header.tsx @@ -177,7 +177,10 @@ export function MedicationNavHeader({ if (isMedicationTabId(id)) onSelectTab(id); }} // Every slot carries a section count, so the four-slot band clips. - rail={{ label: "Medication sections", countedLabels: true }} + // `extended-counted`, named rather than inherited. Its four slots each + // carry an icon, a label and a count badge, so they need the wider bands + // this profile was calibrated for — not Therapy's wordmark bands. + rail={{ label: "Medication sections", density: "extended-counted", countedLabels: true }} /> ); } diff --git a/src/components/in-page-nav/in-page-nav-header.tsx b/src/components/in-page-nav/in-page-nav-header.tsx index cf03617e8..d234556aa 100644 --- a/src/components/in-page-nav/in-page-nav-header.tsx +++ b/src/components/in-page-nav/in-page-nav-header.tsx @@ -139,8 +139,12 @@ export type InPageNavHeaderProps = */ rail?: { label: string; - /** Calibrated label family — moves only the band widths. */ - density?: ModeNavDensityProfile; + /** + * Calibrated label family, moving only the band widths. Required: see + * the note on `InPageSectionRail`'s own `density` for why inheriting a + * default here shipped a regression. + */ + density: ModeNavDensityProfile; /** `true` when every slot carries a count badge beside its label. */ countedLabels?: boolean; }; diff --git a/src/components/in-page-nav/in-page-section-rail.tsx b/src/components/in-page-nav/in-page-section-rail.tsx index d94bfcf59..1fef2e80f 100644 --- a/src/components/in-page-nav/in-page-section-rail.tsx +++ b/src/components/in-page-nav/in-page-section-rail.tsx @@ -35,7 +35,7 @@ export function InPageSectionRail({ sectionSheetOpen, label, testIdPrefix, - density = "extended-counted", + density, countedLabels = false, }: { sections: readonly PageSection[]; @@ -50,8 +50,20 @@ export function InPageSectionRail({ * Which calibrated label family this rail's labels belong to. It only moves * the container width at which each band becomes active — never the order, * and never which item folds first. + * + * Required, with no default, and that is the point. It used to default to + * `extended`, so medication's rail inherited Therapy's profile without ever + * naming it — invisible to `registryModeNavDensityProfiles`, which lists only + * the modes that render the top bar. Retuning `extended` for Therapy in + * PR #2686 therefore silently unfolded medication's counted labels early, and + * `ui-smoke`'s prescribing journey was the only thing that caught it. + * + * A default cannot be safe here: the value is a calibration against one + * label family's measured widths, so inheriting someone else's is always a + * guess. Making it required costs each call site one line and makes the + * consumer set answerable by `git grep "density:"`. */ - density?: ModeNavDensityProfile; + density: ModeNavDensityProfile; /** * `true` when every slot carries a count badge beside its label, which needs * roughly a third more width per slot. diff --git a/src/components/therapy-compass/pathway-step-stack.tsx b/src/components/therapy-compass/pathway-step-stack.tsx index e0d75464e..15e801107 100644 --- a/src/components/therapy-compass/pathway-step-stack.tsx +++ b/src/components/therapy-compass/pathway-step-stack.tsx @@ -9,6 +9,7 @@ import { cn } from "@/components/ui-primitives"; import type { Pathway, PathwayStep, Therapy } from "./data/types"; import { pathwayLinkedStepCount } from "./pathway-review-label"; +import { StatusBadge } from "./ui"; type PathwayStepStackProps = { steps: PathwayStep[]; @@ -61,6 +62,11 @@ function StepCard({ {roleLabel} + {/* Only when the step resolves to a record. `description` falls + back to that record's `bestUsedFor`, so a linked step quotes + its clinical prose and owes its review state; an unlinked step + names no record and has none to state. */} + {therapy ? : null}

{description}

@@ -106,7 +112,10 @@ function StepCard({