fix(medications): reach lithium, retire the dead z-drugs term, make lexicon review flags evidence-bearing - #1923
Conversation
…iew flags evidence-bearing Follow-up on the three items the lexicon review sheet raised. Two were false alarms, one was real, and chasing them surfaced a worse defect underneath. **`z-drugs` was dead.** Its phrasing appears nowhere in the corpus — no row says "Z-drugs", "zolpidem-type hypnotics" or even "hypnotics" — so it could never fire, while implying a coverage the tool does not have. Removed. Zolpidem and zopiclone are named only via "CNS depressants" and "sedatives", both deliberately unenumerable `mechanism` terms, so those rows stay unresolved and the medication holds at grey. Enumerating them would turn a fail-safe grey into a confident red across an ill-defined class, which is a clinical decision rather than a lexicon edit. **`antipsychotics` and `acei`/`arbs` were both correct.** All 26 antipsychotics are genuine; the catalogue holds exactly one ACE inhibitor and one ARB. The flags that raised them asked questions instead of answering them, which spent reviewer attention on non-issues and buried the one real finding. So: - The single-drug flag is replaced by `missedClassMembers`, which looks for catalogue drugs whose own class or subclass names a term's phrase but were left out of it — the missed-alert direction. It finds none, and the sheet now says so under "checks that ran and found nothing" rather than leaving a reviewer unable to tell "checked, clean" from "never checked". - The breadth flag is scoped to substring selectors. A count is a poor signal for a hand-written `classes` list; every catalogue term above 14 drugs today is one. **Lithium was unreachable, and that is the real finding.** The new coverage section lists the medications no interaction row names, which exposed lithium sitting in it. The record is "Lithium carbonate (IR/SR)" and drug-name matching does not strip a parenthesised suffix, so nothing matched the bare "Lithium" that all eight referring rows write: NSAIDs (diclofenac, meloxicam, naproxen), diuretics (frusemide, hydrochlorothiazide, indapamide), psyllium and iodine. Lithium plus an NSAID and lithium plus a thiazide are textbook toxicity interactions and the tool was silent on both. Fixed with an explicit name alias rather than by loosening `stripDosageForm`: the generic first-word fallback that would catch this also matches "Sodium" in a row about sodium content, "Vitamin" against Vitamin K in the warfarin rows, and "Potassium" against hyperkalaemia prose. An alias is precise and, unlike a matcher change, appears in the review sheet for a clinician to confirm. Resolution 355 -> 358 rows, 420 drug-matched, 149 -> 146 medications holding at grey. 127 of 328 medications remain unreachable; that residue is genuine corpus coverage and is now stated as the feature's honest boundary. Pinned by three new tests: lithium reachable from all six drug rows and warned end-to-end through the evaluator, and no lexicon term that can never fire.
…ctions-drug-alerts-3tztvw
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in:26 minutes Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (18)
Comment |
This pull request has been ignored for the connected project Preview Branches by Supabase. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:4b5f9b0da8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…ing as an all-clear Three follow-ups, smallest fix each. **The parenthesised-name gap was live, not latent.** I previously reported it as latent on the strength of a check that asked whether the affected drugs were reachable *anywhere* rather than reachable *from the rows that name them*. They were not. Nine rows across four more drugs were silently missing a counterparty for exactly the reason lithium was: naloxone naming Buprenorphine, codeine and midazolam naming Morphine, the carbapenems naming Sodium valproate, and four rows naming Olanzapine. Fixed at the root rather than with four more aliases: surfaces are now derived from the parenthetical-free form as well as the raw name, and splitting debris carrying an unbalanced bracket is dropped. The lithium alias stays, because rows write the bare "Lithium" and the record is "Lithium carbonate (IR/SR)". Pinned by the general invariant instead of by drug name: **if a row writes a catalogue drug's name, that row must resolve to that drug.** Deliberately a statement about content that exists — it says nothing about drugs the corpus never mentions, which is coverage rather than a defect. Resolution 358 -> 360 rows, 423 drug-matched. **Silence on an uncheckable drug composed to green.** 127 of 328 catalogue medications are named by no interaction row, so entering one produces no alert — and the guard against reading that as reassurance only covered the drug being VIEWED (`dataAvailable: false`), never the drugs in the PATIENT's list. A patient on such a drug produced zero interactions, zero unresolved rows, and a confident green. `evaluateMedicationInteractions` now reports `unreachableCounterparties`, `composeMedicationVerdict` treats it as a fourth route to incomplete so green becomes unreachable, and the considerations block names the uncovered drugs and states that the absence of a warning is not evidence of safety for them. The set is derived from the index at module load rather than baked into the artefact, so the two cannot drift. Two things kept narrow on purpose: a drug that did produce a finding is excluded from the notice, since telling a clinician a drug "was not cross-checked" directly above an alert about that same drug is worse than saying nothing; and the compact callout still renders nothing at zero interactions, so the notice lives in the always-rendered considerations section rather than inventing a new visual state for a control whose entire job is to summarise findings. **Captured three durable items** as outstanding-issue inbox requests: the unfilled lexicon sign-off (P1), the divergent duplicate Warfarin records needing a clinical owner (P2), and the 127-medication corpus coverage gap (P2). Each is merge-safe and applies on the next `npm run issues:reconcile`.
…-alerts-3tztvw' into claude/patient-interactions-drug-alerts-3tztvw
…able `triamcinolone` and `riboflavin` each carry one interaction row whose entire text is "NONE." — the catalogue stating explicitly that nothing interacts. `SEVERITY_PATTERN` requires a dash, so both parsed as severity `unknown` and counted unresolved, which held each drug at "needs manual review" over a row that says exactly one thing. Fail-safe direction, but the tool was claiming it could not read something it read perfectly well. This is the one change in this branch that moves a verdict TOWARDS green, so it is deliberately narrow: only tokens asserting ABSENCE (`NONE`, `SAFE`) may resolve a row that names no counterparty and matches no term. A bare "CRITICAL." states a severity without stating what interacts — genuinely unreadable, and it stays unresolved. Both directions are pinned, the second by an invariant rather than by drug name, so the carve-out cannot widen unnoticed. Resolution 360 -> 362 rows; medications that can ever show green 184 -> 186 of 328; unparsed severity tokens now zero.
Summary
Removed the dead
z-drugslexicon term. Its phrasing appears nowhere in the corpus — no interaction row says "Z-drugs", "zolpidem-type hypnotics" or even "hypnotics" — so it could never fire while implying a coverage the tool does not have. Zolpidem and zopiclone are named only through "CNS depressants" (10 rows) and "sedatives" (2 rows), both deliberately unenumerablemechanismterms, so those rows stay unresolved and the medication holds at grey. Enumerating them would turn a fail-safe grey into a confident red across an ill-defined class, which is a clinical decision rather than a lexicon edit, so it is recorded in the review sheet instead.Verified
antipsychoticsandacei/arbs; both were correct, and the flags that raised them were the defect. All 26 antipsychotics the term resolves to are genuine, and the catalogue holds exactly one ACE inhibitor (perindopril) and one ARB (candesartan), so neither is a narrow selector. Those two heuristics asked questions instead of answering them, which spent clinical-reviewer attention on non-issues and buried the one real finding beneath them. The single-drug flag is replaced by amissedClassMemberscheck that looks for catalogue drugs whose own class or subclass names a term's phrase but were left out of it — the missed-alert direction, which is the dangerous one. It finds none, and the sheet now states that under a new "checks that ran and found nothing" heading so a reviewer can tell "checked, clean" from "never checked". The breadth flag is scoped to substring selectors, because a raw count is a poor signal for a hand-writtenclasseslist and every catalogue term above 14 drugs today is one.Fixed lithium being unreachable — the substantive finding. A new coverage section in the review sheet lists the medications that no interaction row names, and lithium was sitting in it. The catalogue record is "Lithium carbonate (IR/SR)"; drug-name matching derives its surfaces from the record name and
stripDosageFormdoes not strip a parenthesised suffix, so nothing ever matched the bare "Lithium" that all eight referring rows write: NSAIDs (diclofenac, meloxicam, naproxen), diuretics (frusemide, hydrochlorothiazide, indapamide), psyllium and iodine. Lithium plus an NSAID and lithium plus a thiazide are textbook lithium-toxicity interactions and the tool was silent on both. Fixed with an explicit name alias in the lexicon rather than by loosening the matcher: the generic first-word fallback that would also catch this matches "Sodium" in a row about sodium content, "Vitamin" against Vitamin K in the warfarin rows, and "Potassium" against hyperkalaemia prose. An alias is precise and, unlike a matcher change, appears in the review sheet for a clinician to confirm.Added a coverage section stating the feature's honest boundary. 127 of the catalogue's 328 medications are named by no interaction row at all, so entering one produces silence — which on screen is indistinguishable from "checked, nothing found". The section groups them by class. Most of it is genuine corpus coverage (antibiotics, antidiabetics, aperients, vitamins) that can only be widened by writing interaction rows, not by editing the lexicon; lithium was the exception, content that existed and could not be reached.
Net effect on resolution: 355 → 358 rows fully read, 417 → 420 drug-matched, and 149 → 146 medications holding at grey. No alert wording changed; every note still renders verbatim catalogue text.
Verification
npm run verify:pr-local— 18/18 stages on the merged tree,failed: (none),not reached: (none), 586 test files / 6395 tests passed with no collection errors. Run again after merging the 67 intervening commits frommain, not only before.npm run check:production-readiness— ran. Its 2 FAILs are missingNEXT_PUBLIC_SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEYandOPENAI_API_KEYin this container, which is the expected provider-capability gap for an offline environment and is not caused by this diff.UI verification not run: this change touches the lexicon, the generated index, the report generator and documentation. No component, route, style, or browser behaviour is modified, so there is no changed UI path for
verify:uito exercise.Live retrieval evals not run:
pr-policyclassifies these paths asragRanking: false, and nothing undersrc/lib/rag/**, clinical-search, retrieval-selection, ranking-config or answer-ranking is touched. The medication interaction index is a separate offline artefact from the RAG retrieval path.Risk and rollout
z-drugsremoval cannot suppress an alert, because the term matched zero rows before removal; resolved-row count is unchanged by it. The fail-safe invariant is untouched: green remains unreachable whenever any interaction row on a medication could not be machine-resolved, and unresolved rows still degrade to a neutral "needs manual review" state.npm run medications:interactions, andnpm run check:medication-interactionsfails if the committed artefact and the lexicon disagree, so a revert cannot leave a stale index behind.Clinical Governance Preflight
Clinical KB Database(sjrfecxgysukkwxsowpy) — no Supabase configuration, migration, or environment value is touched by this change.data/medications-snapshot.json.docs/samd-classification-medication-considerations.mdis updated in this change. That note remains OPEN and unassigned; this change adds the lithium finding and the coverage boundary to it rather than narrowing the open question.Notes
warfarin-vka,warfarin-anticoagulant) that share no interaction rows at all — three each, zero in common — so which record a clinician opens decides which warnings they see. That is a catalogue data defect rather than a lexicon fault, and reconciling the records is a clinical content decision, so it is reported and pinned by a test rather than silently patched. It still needs an owner.🤖 Generated with Claude Code
https://claude.ai/code/session_01X12okkARLhFQ2zgcPoFp54
Generated by Claude Code