Wrong-Patient Safety
The look-alike-patient problem, and how the system refuses to guess.
What you're looking at
Automating an EMR means a computer clicks a patient's row and writes to their chart. The catastrophic failure is picking the wrong patient. This page shows that failure mode — and proves the system stops before it happens.
Why it's hard
On legacy, Citrix, and RDP EMRs the software reads the screen
with OCR — pixels, not the database. Some patient IDs differ by a
single look-alike character: a letter O vs the digit
0, a lowercase l vs the digit 1. OCR
reads those two different patients as the same
text — so the computer literally cannot tell them apart from the
screen.
The defense
When an identifier is that ambiguous, openadapt-flow halts instead of guessing. Below, every dangerous look-alike → it stops; a genuinely clean patient → it proceeds normally; a different patient → it catches the mismatch. It stops exactly when it should, and acts when it's safe.
How to read each card
- Left — the patient the workflow was recorded on.
- Right — the row present at replay. For the dangerous cards this is a different patient whose row looks the same; for the clean control it's the same patient re-rendered.
- The magnified strip — the patient ID, the only real difference between the two.
- “What the computer sees (OCR)” — the text read from the pixels. When it's identical on both sides, that's the danger.
- The verdict — what the system actually did.
Every pair below is a real screenshot, read with the repo's own
OCR and judged by the exact check that ships in production
(verify_target_identity). No AI model, no network, fully
reproducible: python -m benchmark.safety_gallery.generate.
A patient ID where a letter O stands in for a zero
Two different patients who happen to share a name and date of birth. Their patient IDs look identical on screen — MG4408 and MG44O8 — because one uses the digit 0 where the other uses the letter O. Reading the screen, the computer sees the same text for both.
These are two different patients. Their IDs differ by a single character — 0 vs O (the digit 0 vs the letter O) — which the computer cannot tell apart when it reads the screen.
MG4408replayMG44O8mg4408sorensen,philip1975-03-12mactive2026-05-04open
mg4408sorensen,philip1975-03-12mactive2026-05-04open
A patient ID where a lowercase L stands in for a one
Same name, same date of birth, two different people. One ID is MG4118, the other MG41l8 — a digit 1 versus a lowercase L. On screen the two IDs are indistinguishable.
These are two different patients. Their IDs differ by a single character — 1 vs l (the digit 1 vs a lowercase L) — which the computer cannot tell apart when it reads the screen.
MG4118replayMG41l8mg4118okafor,daniel1968-11-04mactive2026-05-06open
mg4118okafor,daniel1968-11-04mactive2026-05-06open
An all-numbers ID with letter O's swapped for zeros
Some patient IDs are all numbers, with no letters to anchor them — and they collapse too. 100512 (with digit zeros) and 1OO512 (with letter O's) read as the exact same text off the screen.
These are two different patients. Their IDs differ by 2 look-alike characters — 0 vs O (the digit 0 vs the letter O) — which the computer cannot tell apart when it reads the screen.
100512replay1OO512100512delgado,maria1982-07-22factive2026-05-08open
100512delgado,maria1982-07-22factive2026-05-08open
A dashed ID that an earlier version of the check let through
Patient IDs with a dash in them used to slip past an earlier version of this check. MG-4408 versus MG-44O8 — again a digit 0 versus a letter O. The system now ignores dashes and other separators before comparing, so this no longer gets through.
These are two different patients. Their IDs differ by a single character — 0 vs O (the digit 0 vs the letter O) — which the computer cannot tell apart when it reads the screen.
MG-4408replayMG-44O8mg-4408bianchi,robert1959-02-18mactive2026-05-10open
mg-4408bianchi,robert1959-02-18mactive2026-05-10open
Two different patients who share a name and date of birth
Here the computer CAN read the two IDs apart — MG5439 versus MG7263 — but the two patients share a name and date of birth, so matching on name and birth date alone would treat them as the same person. Comparing the patient ID catches that they are different people.
These are two different patients. Their IDs are genuinely different and the computer can read them apart — MG5439 vs MG7263. Matching on name and date of birth alone would treat them as the same person; comparing the ID catches the difference.
mg5439halloran,susan1975-03-12factive2026-05-12open
mg7263halloran,susan1975-03-12factive2026-05-12open
The correct patient, shown again (an ID with no look-alike characters)
The genuinely correct patient, shown again at replay. Their ID (RC79284) contains none of the look-alike characters, so there is nothing ambiguous. The system should proceed here — and does. This proves it is not simply refusing everything.
rc79284montgomery,james1990-09-30mactive2026-05-14open
rc79284montgomery,james1990-09-30mactive2026-05-14open
A completely different patient
A completely different patient is sitting where the recorded one was (Castellano, Angela was recorded; Nakamura, Thomas appears at replay). The system should catch that these are different people — and does. This proves it is not simply approving everything.
rc44823castellano,angela1977-05-14factive2026-05-16open
rc77235nakamura,thomas1963-08-08mactive2026-05-16open
What this does NOT protect against
This page would be dishonest without the failures it does not fix.
Pulled straight from docs/LIMITS.md and the fault-model study:
- Identity covers only ARMED steps — and real bundles arm a minority of clicks.
The gate runs only where a step carries recorded identity context. The most recent live OpenEMR check armed 4 of 12 click steps; the rest (login buttons, icon-only pencils, too-generic bands) compile with NO identity check at all. A wrong-entity click on an unarmed step is still silent. Coverage is now an auditable per-step metric, but disclosure does not close the gap.
- Phantom success on transactional writes — postconditions read the SCREEN, not the system of record.
A 2026-07-12 fault-model study (benchmark/fault_model) drove 90 replays through a real persistence boundary and found the vision postconditions silently mishandle 5 of 7 transactional fault classes: a duplicate submission or double-click writes a SECOND record behind a clean success; an optimistic-UI update the backend later rejects reports success over an empty database; a partial save drops a field; a stale/concurrent edit overwrites another user's change. None is render drift, so self-healing cannot catch them, and the screen showed success. Closing this needs verification against the record (an API/DB read) plus an at-most-once guard — neither is expressible in a vision-only replay.
- The pure-pixel over-halt cost — refusing a collapsible MRN also refuses the CORRECT patient.
Because the OCR tier ABSTAINS on ANY collapsible identifier, it also halts the right patient whenever the true row's own MRN carries an O/0 or l/1 — a measured 43.6-49.3% false-abort on the frozen adversarial corpora (the safe, cheap direction). On browser (DOM) and native desktop (UIA/AX) the structured-text tier verifies these with no availability cost, because 0 and O are distinct characters in the tree; the abstain cost bites only on pure-pixel substrates (Citrix/RDP/VDI, broken a11y).
- The irreducible floor: a font that renders O and 0 (or l and 1) pixel-identical.
Where two glyphs rasterize to the same pixels, NO vision method — OCR, pixel-compare, or VLM — can separate them; the distinction does not exist in the raster. None was found among 14 common UI fonts (benchmark/pixel_identity), but it is a real, disclosed limit rather than a solved problem.
The check turns a wrong-patient approval into a stop; it does not make the rest of the automation all-seeing. The limits above are open problems, disclosed on purpose.