# 0014 — Allergy checking is name matching, and says so

**Status:** Accepted · 2026-08-02

## Context

Handing a patient a drug they are recorded as allergic to is one of the few
things a clinic system can help prevent that actually kills people. The failure
is rarely ignorance — the allergy is usually *on the record*. It is that nobody
scrolled up.

The full answer to this is clinical decision support: drug classes,
cross-sensitivity, interactions, dose ceilings, renal adjustment. That needs a
licensed drug knowledge base — First Databank, Multum, BNF — with per-seat
licensing that costs more than this entire product and a contractual maintenance
obligation to keep it current.

The tempting middle path is to approximate one: hard-code a table of drug
classes, infer cross-sensitivity, ship something that *looks* like decision
support. That is the option we reject, and rejecting it is the whole point of
this record. An approximation would be authoritative-looking and silently wrong
in exactly the cases that matter — a prescriber who believes the software checks
interactions stops checking them, and the software's silence becomes a positive
assertion of safety it has no basis for.

## Decision

Match by **name and by ingredient**, against the allergies recorded on the
patient's own record. Nothing else.

Three graded outcomes, because one undifferentiated banner is how alert fatigue
starts:

| Match | Basis | Requires a written reason |
| --- | --- | --- |
| `exact` | the allergen names the product or its generic | yes |
| `ingredient` | the allergen is in the product's ingredient list | yes |
| `possible` | normalised names overlap as substrings (allergen ≥ 6 chars) | no |

Names are normalised before comparison: lower-cased, dosage tokens removed
(`Amoxicillin 500mg`), punctuation stripped, and salt and ester suffixes dropped
(`hydrochloride`, `trihydrate`, `phosphate`…). A salt changes the formulation,
not what an immune system reacts to.

**The ingredient list is what makes this work at all.** "Penicillin" recorded,
amoxicillin prescribed: two different words, and only the ingredient column
connects them. Every seeded formulary entry carries one, the field is first on
the formulary form, and its help text explains why.

**Warn, never block.** Prescribing over a recorded allergy is a real and common
clinical judgement — a childhood rash recorded as "penicillin allergy" against a
serious infection with no good alternative. A hard block does not prevent the
prescription; it moves it onto paper, outside the record, outside the audit
trail, and outside the next clinician's view.

**What is refused is doing it silently.** An `exact` or `ingredient` warning
requires a written reason before the prescription can be issued, and that reason
is stored on the prescription row — not only in the audit trail — so the next
clinician reads it where they are already looking. `possible` matches do not
demand one, because requiring justification for every substring coincidence is
precisely how the requirement gets treated as noise, and then the ingredient
match that mattered gets clicked past with it.

**Retracted allergies never fire.** Only entries with status `active` are
checked. A warning that fires on withdrawn data is worse than no warning: it
teaches prescribers that the warnings are wrong.

**The limitation is stated in the code, in the UI and here.** The class docblock
says what it is not. The prescribing screen carries the line "Checked against
recorded allergies by name and ingredient only. This is not an interaction
check." Nobody in the building should believe more of it than it does.

## Consequences

**Good.** The case that kills people in small clinics is caught, with no
licensing cost and no external dependency — which matters, because each
installation is a standalone cPanel account with no guarantee of outbound network
access.

**Good.** The override reason becomes clinical documentation. "Rash aged 6,
tolerated amoxicillin twice since" on the record is worth more than a blocked
prescription and a phone call.

**Bad, and known.** A cephalosporin is not flagged against a penicillin allergy.
Sulfonamide cross-reactivity is not modelled. No interaction, duplicate-therapy,
dose or renal checking exists. A clinic that needs these needs a knowledge base,
and the honest answer is to say so rather than to imply coverage.

**Bad.** Checking quality depends on formulary data quality. An entry with an
empty ingredient list degrades to name matching only. This is why the seeder
populates ingredients for every starter entry and why the field is prominent
rather than buried.

**Bad.** Free-typed drug names — permitted, because no formulary contains every
product and refusing to prescribe off-list would make the system unusable — are
checked by name alone. This is a deliberate trade against a system people would
route around.

## Future

The seam is `AllergyChecker`. A licensed knowledge base is introduced by binding
a different implementation behind the same call, gated as a licensed feature —
the prescribing service, the UI and the override rule do not change. This is why
the checker takes ids and returns DTOs rather than reaching into models.
