From 18a5f42dad73695f95301574a3bb08c1341d025f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 16:28:42 +0000 Subject: [PATCH 1/2] docs(medications): clinician reading worklist for the lexicon review (#318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `#318` asks a clinician to read the generated lexicon sheet top-down and fill in its sign-off block. This adds a reading aid that makes that pass short. It changes no lexicon, no catalogue data, and does not touch the sign-off block — the sheet stays UNREVIEWED, which only the clinician can change. The sheet was regenerated against this base and came back byte-identical, so it is current: 28 catalogue terms, 1 flagged. Two of the three defects `#318` cites are already closed: the ARB/Carbapenem substring match is fixed and now guarded, and lithium is reachable across 9 rows. Only the divergent Warfarin pair remains, and it is worse than the row states — the two records carry three interaction rows each with zero in common, so which record is opened changes which warnings appear. Three findings the sheet did not surface, none needing live access: - `tcas` selects slug `dothiepin`; the catalogue slug is `dosulepin`, so the slug matches zero records and Dosulepin — flagged FATAL in overdose — fires none of the 20 severe TCA rows. Mechanical defect, no clinical opinion needed to confirm. - `nsaids` selects on `subclassIncludes: ["NSAID"]`, so Celecoxib and Parecoxib (subclass `COX-2 Inhibitor`, tag `NSAID`) sit outside all 38 severe rows, while Meloxicam is included only because its subclass is spelled `NSAID (COX-2 preferential)`. - `maois` selects on `subclassIncludes: ["MAOI"]`, so Moclobemide (subclass `RIMA`) sits outside all 17 severe rows. Its tyramine risk is genuinely lower; its serotonin-syndrome risk is not. The review instrument itself has two blind spots, verified by executing the guard's own logic rather than reasoning about it: `missedClassMembers()` skips surface stems shorter than four characters, which leaves the check unable to fire for `tcas` and `arbs` (`ppis` is rescued by its long surface), and the check reads only class and subclass, never `tag`. So the sheet's printed "Checks that ran and found nothing" line is overstated for two terms — which is how the Dosulepin miss stayed invisible. Gate: npm run verify:pr-local selected the low-risk docs route and completed 11 checks with "failed: (none)". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PZL59KVSBWURMKvfc9mbJr --- docs/medication-lexicon-review-worklist.md | 188 ++++++++++++++++++ .../46750cbf-f4f4-4a00-ab93-bdac644afbde.json | 10 + 2 files changed, 198 insertions(+) create mode 100644 docs/medication-lexicon-review-worklist.md create mode 100644 docs/outstanding-issues-inbox/46750cbf-f4f4-4a00-ab93-bdac644afbde.json diff --git a/docs/medication-lexicon-review-worklist.md b/docs/medication-lexicon-review-worklist.md new file mode 100644 index 0000000000..14fe335424 --- /dev/null +++ b/docs/medication-lexicon-review-worklist.md @@ -0,0 +1,188 @@ +# Medication lexicon — clinician reading worklist (`#318`) + +**This is a reading aid, not a review.** It does not fill in the sign-off block in +`docs/medication-interaction-lexicon-review.md`, and it makes no clinical determination. Its only +job is to make the top-down pass that `#318` asks for as short as it can honestly be. + +Generated against `origin/main` at `8069188`. The review sheet was regenerated at that commit and +came back byte-identical to the committed copy, so the sheet is current: 28 catalogue terms, +1 flagged. + +## Read this first + +**Two of the row's three known defects are already closed**, and one new mechanical defect was found +that the sheet's own guard could not see. + +| Defect as stated in `#318` | State now | +| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| ARB matching Carbapenem across 16 CRITICAL/HIGH rows | **Closed.** `arbs` resolves to Candesartan only, and a standing substring check now guards it. | +| Two divergent Warfarin records | **Open.** Still the single flagged item. §2.6 — highest consequence on this page. | +| Lithium unreachable from eight HIGH rows | **Closed.** `lithium` resolves to Lithium carbonate (IR/SR) across 9 rows / 9 severe, and lithium is absent from the unreachable-drugs table. | + +**New — a dead slug drops a TCA from 20 severe rows.** `§2.1`. This one is a broken reference, not a +clinical judgment call, and it is the reason `§4` matters: the sheet's "Checks that ran and found +nothing" line is **overstated for two terms**, `tcas` among them. + +## 1. The top ten terms by severe usage + +`Severe` = CRITICAL or HIGH interaction rows the term fires on. These ten carry **236 of the 390 +severe firings across all 28 terms (61%)**, so a pass that stops after ten has covered most of the +red-and-amber surface. + +| # | Term | Rows / Severe | Resolves to | Look at | +| --- | ----------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| 1 | `nsaids` | 39 / 38 | 6 — Aspirin, Diclofenac, Ibuprofen, Ketorolac, Meloxicam, Naproxen | **§2.2** | +| 2 | `opioids` | 36 / 35 | 14 — Buprenorphine (SL/depot), Buprenorphine + naloxone, Buprenorphine patch, Codeine, Fentanyl, Hydromorphone (IR/IV), **Loperamide**, Methadone, Morphine (IR/IV), Morphine SR/MR, Oxycodone IR, Oxycodone SR/MR, Tapentadol SR, Tramadol IR | §2.4 | +| 3 | `benzodiazepines` | 32 / 32 | 8 — Alprazolam, Clonazepam, Diazepam, Lorazepam, Midazolam, Nitrazepam, Oxazepam, Temazepam | clean | +| 4 | `beta-blockers` | 23 / 22 | 7 — Atenolol, Bisoprolol, Carvedilol, Labetalol, Metoprolol, Propranolol, Sotalol | clean | +| 5 | `acei` | 20 / 20 | 1 — Perindopril | §2.5 | +| 6 | `tcas` | 22 / 20 | 5 — Amitriptyline, Clomipramine, Doxepin, Imipramine, Nortriptyline | **§2.1** | +| 7 | `ssris` | 21 / 19 | 6 — Citalopram, Escitalopram, Fluoxetine, Fluvoxamine, Paroxetine, Sertraline | clean | +| 8 | `diuretics` | 18 / 17 | 6 — Amiloride, Eplerenone, Frusemide, Hydrochlorothiazide, Indapamide, Spironolactone | clean | +| 9 | `maois` | 19 / 17 | 2 — Phenelzine, Tranylcypromine | §2.3 | +| 10 | `arbs` | 16 / 16 | 1 — Candesartan | §2.5 | + +"clean" means I enumerated every catalogue record whose class or subclass names that class and found +the term already covers all of them, with the deliberate exclusions accounted for. + +## 2. The findings — six items, in priority order + +The prose a clinician sees is always verbatim catalogue text. What is in question is only which +drugs a phrase was taken to mean. + +### 2.1 `tcas` carries a dead slug — Dosulepin is silently excluded from 20 severe rows + +**This is a defect, not a judgment call, and it needs no clinical opinion to confirm.** + +`src/lib/medication-interaction-lexicon.ts` selects TCAs by an explicit slug list: + +``` +select: { slugs: ["amitriptyline", "nortriptyline", "imipramine", "clomipramine", "doxepin", "dothiepin"] } +``` + +Six slugs, but the catalogue record's slug is **`dosulepin`**, not `dothiepin`. A search of +`data/medications-snapshot.json` for `"dothiepin"` returns **zero** records — the slug matches +nothing. The drug is in the catalogue (`class: Antidepressant`, `subclass: TCA`, and its own +overview reads "Tricyclic Antidepressant (also known as Dothiepin)"), so the lexicon was written +against the older Australian/UK name while the catalogue uses the current INN. + +Consequence: Dosulepin fires **none** of the 20 severe `tcas` rows. Its own catalogue record flags +`Toxicity in OD: FATAL` and `Anticholinergic: HIGH`. + +**Your call:** confirm Dosulepin should be a TCA for interaction purposes (I expect yes), and the +fix is a one-word slug correction plus a regenerate. I have not made it — `#318` says do not change +the lexicon. + +### 2.2 `nsaids` excludes Celecoxib and Parecoxib — missed-alert direction, 38 severe rows + +`nsaids` selects on `subclassIncludes: ["NSAID"]`. The catalogue holds two more analgesics whose +subclass reads `COX-2 Inhibitor` and `COX-2 Inhibitor (Injectable)` — Celecoxib and Parecoxib. +Neither string contains "NSAID", so neither is selected. Both records carry `tag: "NSAID"`, which +the selector does not read. + +Note the inconsistency this creates: Meloxicam **is** included, only because its subclass happens to +be spelled `NSAID (COX-2 preferential)`. So a COX-2-preferential NSAID is in and two selective COX-2 +inhibitors are out, on spelling rather than pharmacology. + +**Your call:** celecoxib's GI-bleeding risk is genuinely lower, so exclusion from bleeding-risk rows +may be right. Its renal, lithium-level, and ACE-I/ARB "triple whammy" interactions are not lower. +Should `nsaids` cover the coxibs for the non-bleeding rows? This may be a row-by-row answer rather +than a blanket include. + +### 2.3 `maois` excludes Moclobemide — missed-alert direction, 17 severe rows + +Moclobemide **is** in the catalogue: `class: Antidepressant`, `subclass: RIMA`. Phenelzine and +Tranylcypromine are `Irreversible MAOI`. The selector is `subclassIncludes: ["MAOI"]`, and the +string `RIMA` does not contain `MAOI`, so Moclobemide sits outside all 17 severe rows. + +**Your call:** moclobemide is a reversible, selective MAO-A inhibitor. Its tyramine/food risk is +genuinely much lower than the irreversible agents, so excluding it from dietary rows is defensible. +Its serotonin-syndrome risk with SSRIs, TCAs, tramadol or other serotonergics is not lower. Should +`maois` cover Moclobemide for the serotonergic rows specifically? + +Moclobemide is reachable by name from 4 places in the interaction index, so it is not invisible to +the feature — only to this class term. + +### 2.4 `opioids` includes Loperamide — false-alert direction, 35 severe rows + +Loperamide is `class: Antidiarrhoeal`, `subclass: Peripheral Opioid Agonist`, so it matches on the +substring `Opioid`. It is a true opioid agonist but peripherally restricted, and does not +meaningfully cross the blood-brain barrier at therapeutic doses. + +**Your call:** for the sedation and respiratory-depression rows — opioids paired with +benzodiazepines, alcohol or other CNS depressants — should Loperamide fire? If not, it is producing +false CRITICAL/HIGH alerts on a common OTC antidiarrhoeal. Loperamide does carry real QTc and +cardiac risk in overdose, so this too may be row-dependent. + +### 2.5 `acei` and `arbs` resolve to one drug each — confirm this is intended catalogue scope + +`acei` → Perindopril only (20 severe). `arbs` → Candesartan only (16 severe). `loop-diuretics` → +Frusemide only. + +**No lexicon defect here** — I verified by exact record name that ramipril, lisinopril, irbesartan, +telmisartan and valsartan are not in `data/medications-snapshot.json` at all. This is catalogue +coverage, exactly as the sheet says. + +**Your call:** is one ACE inhibitor and one ARB the right scope for your prescribing? A patient on +ramipril or irbesartan gets **no** alert from these 36 severe rows, and per the sheet's "What this +tool can never warn about" section, silence looks identical on screen to a checked-and-clear result. + +### 2.6 `anticoagulants` — the flagged Warfarin split, plus an internal inconsistency + +Rank 12 by severe usage (18 rows / 15 severe), but it carries the sheet's only flagged defect. + +**The flagged defect.** The catalogue holds two records both named **Warfarin** +(`warfarin-vka`, `warfarin-anticoagulant`), with identical class `Anticoagulant` and subclass +`Vitamin K Antagonist`. They carry 3 interaction rows each and **zero in common**. Which record a +clinician opens changes which warnings appear, with no visible cue that a second Warfarin exists. +This is a catalogue reconciliation, not a lexicon fix. + +**The inconsistency.** `anticoagulants` also resolves to Clopidogrel, Dipyridamole and Ticagrelor — +antiplatelets, not anticoagulants — because the catalogue files all three under class +`Anticoagulant`. Meanwhile Aspirin, which carries the _same_ class `Anticoagulant` (subclass +`Antiplatelet / NSAID`), sits on the sheet's deliberate-exclusion list. + +**Your call:** three antiplatelets are in and one is out, on identical class metadata. Is that the +intended clinical line, or should `anticoagulants` and `antiplatelets` be cleanly separated? + +## 3. What I did not do + +- Did not modify `src/lib/medication-interaction-lexicon.ts` or any catalogue data. +- Did not fill in, or pre-fill, the sign-off block. That is the clinician action `#318` asks for. +- Did not treat `check:medication-lexicon-report` as review. Regeneration was a no-op, which proves + only that the sheet describes the current lexicon — not that any mapping is correct. `#318`'s stop + condition is unchanged. + +## 4. The review instrument has two blind spots — worth fixing before the next reading pass + +The sheet prints "Checks that ran and found nothing: **Missed class members** — no catalogue drug +whose own class or subclass names a term's phrase was left out of that term." For three terms that +sentence is **not true**, because the check could not run. + +`missedClassMembers()` in `scripts/build-medication-lexicon-report.ts` (line 442) skips any surface +whose singular stem is shorter than four characters: + +``` +const stem = surface.toLowerCase().replace(/s$/, ""); +if (stem.length < 4) return false; +``` + +That guard exists to stop short tokens generating false positives, but it silently disables the +whole check for a three-letter class acronym unless some _other_ surface on the same term is long +enough to carry it. I executed the guard's own logic against each term's real surfaces and +haystacks rather than reasoning about it: + +| Term | Usable stems after the `< 4` filter | Verdict | Severe rows | Real miss? | +| ------ | -------------------------------------------------------------- | --------------- | ----------- | ---------------------------- | +| `tcas` | `tricyclic`, `tricyclic antidepressant`, `anticholinergic tca` | **cannot fire** | 20 | **Yes — Dosulepin (§2.1)** | +| `arbs` | _none_ | **cannot fire** | 16 | No — catalogue holds one ARB | +| `ppis` | `proton pump inhibitor` | runs correctly | 8 | n/a | + +`ppis` is rescued by its long surface; `tcas` is not, because none of its three usable stems match +the haystack `antidepressant tca`, and `arbs` has no usable stem at all. + +Second blind spot: the check reads only `class` and `subclass`, never `tag`. Celecoxib and Parecoxib +carry `tag: "NSAID"` (§2.2) and are invisible to it for that reason. + +Neither fix is made here. Both are cheap, and doing them before the clinical pass would mean the +"ran clean" lines can be trusted at face value. diff --git a/docs/outstanding-issues-inbox/46750cbf-f4f4-4a00-ab93-bdac644afbde.json b/docs/outstanding-issues-inbox/46750cbf-f4f4-4a00-ab93-bdac644afbde.json new file mode 100644 index 0000000000..97602ca081 --- /dev/null +++ b/docs/outstanding-issues-inbox/46750cbf-f4f4-4a00-ab93-bdac644afbde.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "id": "46750cbf-f4f4-4a00-ab93-bdac644afbde", + "createdOn": "2026-08-15", + "action": "update", + "payload": { + "id": "#318", + "detail": "docs/medication-interaction-lexicon-review.md is generated by npm run medications:lexicon-report and expands every lexicon term to the catalogue drugs it resolves to, with how many CRITICAL/HIGH rows depend on it, sorted by severe usage. It is marked UNREVIEWED and its sign-off table is unfilled, so every red and amber drug-drug interaction alert is currently an unvalidated mapping over source-backed text. The wording shown to a clinician is always verbatim catalogue prose; what is unreviewed is which drugs a phrase like 'NSAIDs' or 'CNS depressants' was taken to mean. Next: a clinician reads the term table top-down (it is sorted so the top ten terms carry most of the severe usage) and fills in the sign-off block. Stop: do not treat check:medication-lexicon-report passing as review - that check only proves the sheet describes the current lexicon, not that the mappings are correct. WORKLIST PREPARED 2026-08-15 (still UNREVIEWED; the clinician sign-off is untouched and remains the only thing that closes this row). docs/medication-lexicon-review-worklist.md gives the top ten terms by severe usage (236 of 390 severe firings, 61 percent) with resolved drug sets and six prioritised questions. The sheet was regenerated against main at 8069188 and came back byte-identical, so it is current. TWO OF THE THREE DEFECTS THIS ROW CITES ARE ALREADY CLOSED: the ARB/Carbapenem substring match is fixed and now guarded, and lithium is reachable (9 rows / 9 severe, absent from the unreachable table). Only the divergent Warfarin pair remains, and it is worse than stated - warfarin-vka and warfarin-anticoagulant carry 3 interaction rows each with ZERO in common, so which record is opened changes which warnings appear. THREE NEW FINDINGS, none of which needed live access. (1) MECHANICAL DEFECT, no clinical opinion required: the tcas selector lists slug 'dothiepin' but the catalogue slug is 'dosulepin', so the slug matches zero records and Dosulepin - flagged FATAL in overdose - fires none of the 20 severe TCA rows. (2) nsaids selects on subclassIncludes NSAID, so Celecoxib and Parecoxib (subclass 'COX-2 Inhibitor', tag 'NSAID') sit outside all 38 severe rows while Meloxicam is included only because its subclass is spelled 'NSAID (COX-2 preferential)'. (3) maois selects on subclassIncludes MAOI, so Moclobemide (subclass 'RIMA') sits outside all 17 severe rows; its tyramine risk is genuinely lower but its serotonin-syndrome risk is not. Also noted: opioids includes Loperamide (peripheral agonist) across 35 severe rows in the false-alert direction, and anticoagulants includes three antiplatelets while deliberately excluding Aspirin on identical class metadata. THE REVIEW INSTRUMENT ITSELF HAS TWO BLIND SPOTS, verified by executing the guard's own logic rather than reasoning about it: missedClassMembers() in scripts/build-medication-lexicon-report.ts skips any surface stem shorter than four characters, which makes the check unable to fire for tcas and arbs (ppis is rescued by its long surface), and the check reads only class and subclass, never tag. So the sheet's printed 'Checks that ran and found nothing' line is overstated for two terms - which is how the Dosulepin miss stayed invisible. Fixing those two blind spots before the clinical pass would make the 'ran clean' lines trustworthy. No lexicon, catalogue or sign-off content was changed." + } +} From 242487bb847ddcde10dd72c4f78cabda771fd856 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 16:31:00 +0000 Subject: [PATCH 2/2] docs(ledger): record the PR #1991 handoff review Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PZL59KVSBWURMKvfc9mbJr --- ...106f9029aa52fbe2ba972c2610971d057b366f68f12bc87baba.record.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/branch-review-records/1750578822f17106f9029aa52fbe2ba972c2610971d057b366f68f12bc87baba.record.md diff --git a/docs/branch-review-records/1750578822f17106f9029aa52fbe2ba972c2610971d057b366f68f12bc87baba.record.md b/docs/branch-review-records/1750578822f17106f9029aa52fbe2ba972c2610971d057b366f68f12bc87baba.record.md new file mode 100644 index 0000000000..6025d3ff54 --- /dev/null +++ b/docs/branch-review-records/1750578822f17106f9029aa52fbe2ba972c2610971d057b366f68f12bc87baba.record.md @@ -0,0 +1 @@ +| 2026-08-15 | PR #1991 | 18a5f42dad73695f95301574a3bb08c1341d025f | docs/medication-lexicon-review-worklist.md + one #318 inbox request (46750cbf); no lexicon, catalogue or sign-off change | Authored handoff. Worklist prepared for the #318 clinician pass; row stays OPEN and UNREVIEWED. Verified two of the row's three cited defects are already closed (ARB/Carbapenem guarded; lithium reachable) and only the Warfarin split remains, with zero interaction rows in common between the two records. Three new findings not surfaced by the sheet: tcas selects dead slug dothiepin so Dosulepin (FATAL in OD) fires none of 20 severe rows; nsaids subclass match excludes Celecoxib/Parecoxib from 38 severe rows; maois subclass match excludes Moclobemide (RIMA) from 17 severe rows. Also found two blind spots in the review instrument itself - missedClassMembers() skips sub-4-char stems so the check cannot fire for tcas or arbs, and it never reads tag - so the sheet's 'checks ran clean' line is overstated for two terms. One near-miss corrected before publication: Omeprazole was not a catalogue record, it was a substring hit inside Esomeprazole. | verify:pr-local low-risk docs route, 11 checks completed, failed: (none); check:ledger-write-discipline passed 806918834b43..HEAD; check:outstanding-issues passed 341 rows; medications:lexicon-report regenerated byte-identical. verify:ui NOT run - Playwright chromium-1194 vs pinned 1234 (#255/#312) fails closed in this container; no browser coverage claimed. |