Healthcare data · interoperability QA

fhir-quality-gate

A FHIR bundle can be structurally valid and still wrong in ways that cost money. The drifted bundle here is valid JSON, valid FHIR R4, and passes a structural validator. It also carries three type-2 diabetes diagnoses written E119 instead of E11.9 — enough to drop 3 patients out of a 46-patient quality-measure denominator without raising anything at all. fhir-gate catches 12 of 12 planted defect types and 0 on the clean bundle.

Public · synthetic demo
The problem

Neither run raised an error. Both produced a number.

Both bundles carry identical base data — same 300 patients, same observations, same seed. Every difference below is a planted defect, not sampling noise. The measure: adults with active type-2 diabetes (E11.9) who have a glucose result on file.

① What a quality measure computes on each
bundlepatientsdenominatornumeratormeasure
clean300462656.5%
drifted300432455.8%

Three patients left the denominator because of a missing decimal point, and two glucose results vanished — one referencing a patient not in the bundle, one labelled with the SNOMED system URI instead of LOINC. Neither run errored. Both produce a number that looks like a measure result.

12/12defect types caught
0findings on the clean bundle
6.5%cohort shift from one missing period
The money shot

Three that show up in real integration work

② F9 — invisible, not wrong

A glucose result carrying LOINC 2339-0 under http://snomed.info/sct is not rejected by anything — it simply never matches a query filtering on system = 'http://loinc.org'. Nothing errors. The result is just not there. This is the failure mode that takes weeks to find, because every individual record looks fine when you open it.

③ F5 — the one that reaches a patient

The unit says inches, the magnitude is still centimetres. Downstream BMI is computed from a height of 170 inches. No validator objects, because in is a perfectly good UCUM unit.

④ F12 — cheapest to fix, most expensive to miss

E119 is four characters that a payer's adjudication engine rejects outright. Here it removed three patients from a 46-patient denominator — a 6.5% shift in the cohort, from one missing period.

⑤ The 12 checks
#severitywhat it catches
F2criticalduplicate resource id in one bundle — counted twice, or overwritten
F4criticalPatient with no identifier — a new patient on every import
F5criticalunit does not match the LOINC code — a height of 170 becomes 170 inches
F7criticalsubject reference points outside the bundle — orphaned from every roll-up
F8criticalvalue outside the plausible band — systolic 4200 mm[Hg] moves a cohort mean
F9criticalLOINC code declared under the SNOMED system URI — invisible, not wrong
F12criticalICD-10-CM without its decimal point — the claim is rejected
F1highgender outside the value set (M not male) — coerced to unknown
F3highbirthDate in the future — age-banded measures exclude the patient
F6highcode and display disagree — humans read one thing, systems compute another
F10highdateTime with no timezone offset — a 23:00 result lands on the wrong day
F11highstatus FINAL where the value set is lowercase — treated as missing
What this is not

Not a structural validator.

It assumes the bundle already parses and already conforms to R4 — use the HL7 validator for that. These checks sit one layer up, where the resource is valid and the meaning is still wrong.

Terminology is pinned in fhirgate/checks.py rather than fetched, so the repo runs offline and the result is reproducible. In production those tables come from a terminology server; the check logic does not change.

Reproduce it
./scripts/run_evidence.sh          # everything, into evidence/
python3 -m fhirgate.cli bundles/drifted.json --today 2026-08-04

No dependencies beyond the standard library. Bundles are generated deterministically from seed 20260804.

Honest limitations
· Synthetic bundles (300 patients, seed 20260804) — a demonstrator of the method, not a benchmark.
· Terminology tables are pinned, not fetched from a terminology server.
· Structural R4 conformance is assumed, not tested — that is the HL7 validator's job.
· No PHI: every resource is generated, none is derived from a real record.
This is a synthetic sample demonstrating the method. Inspect the checks, the fixtures, and the reproducible evidence ↗