Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d87f7b1
feat(medications): patient details dock pill and drug-interaction ale…
claude Aug 12, 2026
9bccbf4
docs(ledger): record review for patient-interactions-drug-alerts
claude Aug 12, 2026
76a22e4
feat(medications): surface interactions on the record page and ice ev…
claude Aug 12, 2026
7b658ce
Changes before error encountered
Copilot Aug 12, 2026
cd910af
fix medication profile effect import
BigSimmo Aug 12, 2026
7b061f2
fix interaction lexicon completeness contract
BigSimmo Aug 12, 2026
8ba615d
fix medication interaction fail-safe evaluation
BigSimmo Aug 12, 2026
b9f5868
fix interaction index false negatives
BigSimmo Aug 12, 2026
1ae7122
test medication interaction safety regressions
BigSimmo Aug 12, 2026
ca3cb41
fix medication profile cap before persistence
BigSimmo Aug 12, 2026
7c4a69b
test medication profile cap persistence
BigSimmo Aug 12, 2026
f270832
strengthen mechanism-only interaction coverage
BigSimmo Aug 12, 2026
87cc433
fix medication interaction short-name matching
BigSimmo Aug 12, 2026
2ef8ae5
surface incomplete medication review state
BigSimmo Aug 12, 2026
848e538
fix(medications): apply patient dock hide overshoot
BigSimmo Aug 12, 2026
ff76974
test patient details dock hide overshoot
BigSimmo Aug 12, 2026
352473e
fix patient details dashboard dock overshoot
BigSimmo Aug 12, 2026
0a19c47
refactor(medications): compact the interaction callout onto Disclosure
claude Aug 12, 2026
70c556a
fix(medications): coherent no-data copy, restore safety docs, unbreak…
claude Aug 12, 2026
ff41705
fix(medications): regenerate stale interaction index and gate it
claude Aug 12, 2026
101acc0
Merge current main into PR 1874
BigSimmo Aug 13, 2026
550b986
Adapt responsive stress coverage to patient dock
BigSimmo Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9,151 changes: 9,151 additions & 0 deletions data/medication-interaction-index.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/codebase-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ One shared composer (`master-search-header.tsx`) serves every mode. Placement:
- **Intentionally composer-free routes**: `/differentials/presentations/*` and `/differentials/compare` (comparison workflow owns its chrome), `/documents/[id]` viewer (has its own in-document ask composer), `/documents/source/*` (document flow owns mobile chrome). Do not re-flag these in search-consistency audits.
- **Shared in-page navigation**: `src/components/in-page-nav/` is the default template for section navigation on any mode page (`docs/search-chrome-behaviour.md`). `in-page-nav-header.tsx` (`InPageNavHeader`) owns the header row, both sheets and the `PhoneHeaderCollapsePortal` wrapper; `page-section-index.ts` (`PageSection`, `toDocumentSections`, `sectionTargetIds`) is the declaration shape; `use-resolved-page-sections.ts` narrows a declaration to the anchors actually rendered at this breakpoint; `use-in-page-section-nav.ts` composes that with `useDocumentSectionSpy` and `jumpToDocumentSection`; `use-page-section-weights.ts` measures segment weights; `use-in-page-chrome-metrics.ts` publishes `--inpage-anchor-offset`; `in-page-nav-classes.ts` holds the shared anchor (`inPageAnchor`) and actions-sheet row classes; `in-page-section-rail.tsx` (`InPageSectionRail`) is the optional visible second rail, opted into with `rail={{ label }}` by panel-swap routes with few sections (medications only) in place of the weighted track. Anchor measurement itself is `src/components/sticky-chrome-metrics.ts` (`useStickyChromeMetrics`), shared with the document viewer's `use-document-chrome-metrics.ts`. Mounted by `differentials/differential-detail-page.tsx`, `services/service-detail-page.tsx`, `forms/form-detail-page.tsx`, `dsm/dsm-differential-considerations-page.tsx`, and — through a colocated `"use client"` nav-header sibling that owns and exports the route's section table — `specifiers/specifier-nav-header.tsx`, `formulation/formulation-nav-header.tsx`, `dsm/dsm-diagnosis-nav-header.tsx`, `factsheets/factsheet-nav-header.tsx` and `clinical-dashboard/medication-nav-header.tsx`. The sibling is mandatory for the four Server Component pages (neither `onSelectSection` nor a `LucideIcon` crosses the RSC boundary) and the convention for the rest. Two adopters swap panels instead of scrolling — `differential-detail-page.tsx` and the medication record page — so they pass explicit weights, carry no `inPageAnchor`, and use neither `useResolvedPageSections` nor the scroll spy. Every declared section is pinned against rendered DOM by `tests/in-page-nav-route-sections.dom.test.tsx` (anchors for the scrolling routes, swapped-in panels for the tab routes).
- **Shared secondary navigation**: `src/components/page-secondary-navigation.tsx` (`PageSecondaryNavigation`, mode destinations only). Mode destinations come from `src/lib/mode-secondary-navigation.ts` (`modeSecondaryNavigationRegistry`, no "Home" item). `GlobalSearchShell` renders it in normal flow at the top of `#main-content` for its owned namespaced modes; it self-suppresses on clean mode homes, on Therapy Compass, and on every information page — `hasLocalInformationPageNavigation` is now just `isInformationPage`, because each of those routes owns its own in-page navigation. The older shared `SecondaryNavigation` component was deleted here (`/issues #271`): its `section` kind and "On this page" pill rail went when the last six information routes moved onto `InPageNavHeader`, and the surviving `route`/`action` kinds had no production constructor left — `RegistryModeNav` renders `ModeNav`, not `SecondaryNavigation`, so the only remaining caller was its own test file, which went with it.
- **Patient context (decision support)**: the app's patient-specific surface, spanning two engines over one session-scoped profile. Store: `src/lib/patient-profile-storage.ts` (sessionStorage external store, anonymous physiology + catalogue medication slugs, cleared on tab close) behind `clinical-dashboard/patient-profile-context.tsx` (`PatientProfileProvider`, mounted for the whole shell). Engines: `src/lib/medication-patient-alerts.ts` (physiology — age/renal/hepatic/QTc/pregnancy/allergy, with an `unassessed` fail-safe) and `src/lib/medication-interactions.ts` (drug–drug, against the entered medication list). The interaction engine reads the generated `data/medication-interaction-index.json`, built by `scripts/build-medication-interaction-index.ts` (`npm run medications:interactions`, staleness-gated by `check:medication-interactions`) from the curated `src/lib/medication-interaction-lexicon.ts` — the catalogue's `Key Interactions` rows are prose, and that lexicon is what resolves their class/mechanism/non-drug terms to catalogue targets. `composeMedicationVerdict` folds both engines into the one tone a result row wears; **green is unreachable whenever either engine left something unassessed or unresolved**, degrading to a neutral "needs manual review" instead. UI: `clinical-dashboard/patient-profile-panel.tsx` (the form, including the medication picker), `medication-considerations.tsx` (both detail-page blocks and the verdict badge/edge helpers), and `patient-details-dock-action.tsx` (the phone dock pill — see the addon-slot section of `docs/search-chrome-behaviour.md`). Regulatory status is **open**: `docs/samd-classification-medication-considerations.md`.
- **Local filter fields** (sidebar "Search chats", document drawer "Find a document"/"Find a source PDF") are scoped filters, not global search; they share the `fieldControlWithIcon`/`fieldIcon` primitives.
- **Wiring conventions** for buttons and route navigation (and the gates that enforce them — the dead-button ESLint rule and the orphan-route reachability test) live in `docs/wiring-conventions.md`.

Expand Down
6 changes: 3 additions & 3 deletions docs/design-system/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,7 @@ tests to run.

Registered public components: 54
Components with a valid design-sync preview: 54
Components with product imports: 31
Components with product imports: 32

This generated snapshot is a local source-derived inventory. It does not assert remote design-project publication.

Expand All @@ -973,7 +973,7 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `CitationList` | source | yes | yes | no | yes | no | 0 |
| `ConfirmDialog` | layout | yes | yes | no | yes | no | 0 |
| `DateDisplay` | source | yes | yes | inherited-global-root | yes | no | 3 |
| `Disclosure` | layout | yes | yes | no | yes | no | 0 |
| `Disclosure` | layout | yes | yes | inherited-global-root | yes | no | 1 |
| `DisclosureGroup` | layout | yes | yes | inherited-global-root | yes | no | 1 |
| `DoseLine` | answer | yes | yes | no | yes | no | 0 |
| `DownloadLink` | controls | yes | yes | no | yes | no | 0 |
Expand Down Expand Up @@ -1001,7 +1001,7 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `SearchField` | controls | yes | yes | no | yes | no | 0 |
| `SegmentedControl` | controls | yes | yes | inherited-global-root | yes | no | 8 |
| `Select` | controls | yes | yes | inherited-global-root | yes | no | 2 |
| `Sheet` | layout | yes | yes | inherited-global-root | yes | no | 26 |
| `Sheet` | layout | yes | yes | inherited-global-root | yes | no | 27 |
| `Skeleton` | feedback | yes | yes | inherited-global-root | yes | no | 6 |
| `SourceDesignationBadge` | source | yes | yes | inherited-global-root | yes | no | 1 |
| `SourceProvenance` | source | yes | yes | inherited-global-root | yes | no | 1 |
Expand Down
13 changes: 8 additions & 5 deletions docs/design-system/adoption-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -524,8 +524,8 @@
"source": "src/components/ui/disclosure.tsx",
"sourceExported": true,
"entryExported": true,
"directImportFiles": [],
"productImportFiles": [],
"directImportFiles": ["src/components/clinical-dashboard/medication-considerations.tsx"],
"productImportFiles": ["src/components/clinical-dashboard/medication-considerations.tsx"],
"designSync": {
"listedInSourceMap": true,
"listedInDtsProps": true,
Expand All @@ -534,6 +534,7 @@
},
"testFiles": [
"tests/design-sync-visual-exports.test.ts",
"tests/medication-interaction-surfaces.dom.test.tsx",
"tests/ui-forms-section-nav.spec.ts",
"tests/ui-v2-components.dom.test.tsx"
],
Expand All @@ -545,8 +546,8 @@
},
"built": true,
"locallyRegistered": true,
"v2ShellMounted": false,
"v2MountMode": "none",
"v2ShellMounted": true,
"v2MountMode": "inherited-global-root",
"proofDeclared": true,
"baselineCommitted": false
},
Expand Down Expand Up @@ -1499,6 +1500,7 @@
"src/components/clinical-dashboard/master-search-header.tsx",
"src/components/clinical-dashboard/medication-record-page.tsx",
"src/components/clinical-dashboard/mode-action-popup.tsx",
"src/components/clinical-dashboard/patient-details-dock-action.tsx",
"src/components/clinical-dashboard/result-filter-control.tsx",
"src/components/clinical-dashboard/settings-dialog.tsx",
"src/components/differentials/diagnosis-map-panel.tsx",
Expand Down Expand Up @@ -1528,6 +1530,7 @@
"src/components/clinical-dashboard/master-search-header.tsx",
"src/components/clinical-dashboard/medication-record-page.tsx",
"src/components/clinical-dashboard/mode-action-popup.tsx",
"src/components/clinical-dashboard/patient-details-dock-action.tsx",
"src/components/clinical-dashboard/result-filter-control.tsx",
"src/components/clinical-dashboard/settings-dialog.tsx",
"src/components/differentials/diagnosis-map-panel.tsx",
Expand Down Expand Up @@ -3393,7 +3396,7 @@
"summary": {
"registeredComponentCount": 54,
"previewCount": 54,
"productImportedComponentCount": 31,
"productImportedComponentCount": 32,
"rootCount": 59,
"productionRouteCount": 51,
"nextUiEntryCount": 103
Expand Down
39 changes: 39 additions & 0 deletions docs/samd-classification-medication-considerations.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,45 @@ This is the app's first **patient-specific decision-support surface**: output is
tailored to individual patient parameters rather than presenting the same
reference content to everyone.

### Scope widened: drug–drug interaction alerting

A later change extended the same profile with the patient's **current medication
list** and added interaction alerting against it:

- `PatientProfile.medications` (catalogue slugs, same session-scoped store).
- A generated, reviewable index (`data/medication-interaction-index.json`, built
by `scripts/build-medication-interaction-index.ts` from a curated lexicon)
resolving the catalogue's prose `Key Interactions` rows to catalogue targets.
- `src/lib/medication-interactions.ts`, which matches those rows against the
entered list and returns severity-toned findings.
- Red/amber/green/grey verdict edges on prescribing result rows, and a matched
-interactions block on the medication detail page.

This is a **material widening** of the question this note is open on. Interaction
checking is a canonical clinical-decision-support function, and unlike the
physiology considerations it produces an alert about a specific _combination_ the
clinician has entered. The reviewer's questions below should be answered for this
surface too, not only for the original considerations feature.

Mitigations specific to the interaction surface:

- Green never means "safe". It means "no interaction found among the medications
you entered", and is unreachable whenever any interaction row on that
medication could not be machine-resolved — those degrade to a neutral
"N rows need manual review" state instead (`composeMedicationVerdict`).
- Every alert renders the **verbatim** catalogue row text; the tool never
paraphrases a clinical statement.
- The medication picker accepts catalogue drugs only, so a clinician cannot type
a drug the tool has no data for and read the resulting silence as an all-clear.
- Colour is never the only channel: each verdict also carries an icon and a text
label.

Residual risk the reviewer should weigh: the lexicon is hand-curated, so a missed
term is a false negative. It is fail-safe by construction (unresolved → grey, not
green) but the resolution rate is not 100% — currently 400 of 523 rows resolve,
and `tests/medication-interaction-lexicon-coverage.test.ts` ratchets that figure
so it cannot silently regress.

## Why this needs a classification decision

Software that provides patient-specific treatment/prescribing recommendations
Expand Down
2 changes: 1 addition & 1 deletion docs/scripts-index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Scripts index

Curated map of `scripts/` (232 files) and the `package.json` script surface (240 entries),
Curated map of `scripts/` (233 files) and the `package.json` script surface (242 entries),
grouped by purpose. This is orientation, not an exhaustive per-file listing — the authoritative
command list is `package.json`, and `npm run docs:check-scripts` verifies every `npm run <x>`
referenced in docs resolves to a real script. `npm run docs:update` refreshes the exact counts above.
Expand Down
40 changes: 40 additions & 0 deletions docs/search-chrome-behaviour.md
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,46 @@ Coverage: `tests/header-scroll-hide-contract.test.ts` (wiring), `tests/use-hide-

Run `npm run verify:phone-chrome` for phone-chrome work. For executable changes its classifier checks installed/lock parity first, runs focused static contracts and only the browser/PWA owners and route journeys implicated by the changed files, then escalates to `npm run verify:ui` automatically for shared chrome foundations. Documentation-only scopes run only documentation guards. Use `-- --dry-run` to inspect the plan, `-- --files <comma-separated paths>` for an explicit scope, and `-- --full=always|never` only for a deliberate override.

## Phone dock addon slot (page-owned action above the pill)

A page may dock **one** action row above the phone search pill. It is not a
floating element: it portals into a slot rendered _inside_ the dock's `<form>`
(`master-search-header.tsx`), so it inherits the dock's `position: fixed`,
z-index, safe-area padding and scroll-hide transform. There is no bottom-offset
arithmetic and no second scroll listener anywhere in an addon.

Two claimants exist, and they are mutually exclusive by surface:

| Addon kind | Slot id | Claimed by |
| ----------------------- | ----------------------------------------- | ----------------------------------------------------------- |
| `differentials-compare` | `differentials-mobile-compare-addon-slot` | Differentials submitted search / `/differentials/diagnoses` |
| `patient-details` | `patient-details-addon-slot` | Prescribing submitted search (dashboard-owned) |

Rules:

- **One addon at a time.** `data-footer-addon` is a single attribute value, and
the backdrop scrim height, the hide-transform overshoot and the content reserve
all key off it. Register a new kind in `PhoneDockAddonKind`
(`src/lib/mode-home-composer.ts`) and prove exclusivity in
`tests/phone-dock-addon-contract.test.ts`.
- **Four numbers move together** for every kind: the `--phone-dock-<kind>-clearance`
and `-compact-clearance` tokens, the matching reserve constant and resolver
branches in `mobile-composer-reserve.ts`, the two backdrop scrim heights, and the
`[data-scroll-hidden="true"]` transform overshoot that stops a subpixel strip
peeping at the viewport edge.
- **Only claim the addon where the pill actually mounts.** The reserve inflates on
the claim, not on the render, so claiming a route whose component never mounts
opens a blank band at the bottom. `/medications` is a standalone mode home with
the composer in the hero and no dock at all; `/medications/[slug]` already opens
the patient sheet from its own nav header, so neither claims the addon.
- **Gate the portal at 639px**, matching `.phone-footer-layer`'s `sm:fixed`. The two
Compare bars gate at 1023px, which between 640–1023px portals into a slot on a
form that is not fixed. Do not copy that.

Coverage: `tests/phone-dock-addon-contract.test.ts` (registry, exclusivity, CSS/TS
value parity), `tests/patient-details-dock-action.dom.test.tsx` (portal target,
breakpoint, sheet wiring).

## Change checklist

Before changing search bar behaviour:
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@
"differentials:seed": "node scripts/run-tsx.mjs scripts/seed-differential-records.ts",
"medications:import": "node scripts/run-tsx.mjs scripts/import-medications-export.ts",
"medications:seed": "node scripts/run-tsx.mjs scripts/seed-medication-records.ts",
"medications:interactions": "node scripts/run-tsx.mjs scripts/build-medication-interaction-index.ts",
"check:medication-interactions": "node scripts/run-tsx.mjs scripts/build-medication-interaction-index.ts --check",
"reindex": "node scripts/run-tsx.mjs scripts/reindex.ts",
"reindex:health": "node scripts/run-tsx.mjs scripts/reindex-health.ts",
"ingestion:autopilot": "node scripts/run-tsx.mjs scripts/ingestion-autopilot.ts",
Expand Down
Loading
Loading