Les suggestions de posologie
When a prescriber picks a drug in the ehr-lab prescription drawer, the app
queries MedicalDB by Posos (the BIM) for that drug's recommended dosage
regimens and shows them as semi-structured sentences — one line per
regimen, whose structured elements stay visually distinct (dose · frequency ·
duration · route) with context chips (nature, patient criteria, associated
conditions). Selecting one attaches it to the prescription line and feeds the
FHIR MedicationRequest.dosageInstruction (see
FHIR R4 mapping).
Where the data comes from
- Endpoint: the
bim-v2GraphQL API, same host and IAP service-account auth as the drug autocomplete (POSOS_API_URL/POSOS_AUDIENCE). Without credentials the picker runs on a small labelled demo referential (two drugs), like the autocomplete. - Concept resolution: a specialty (
urn:posos:cis) reaches the posology-bearing clinical drug throughbranded_drugs; a clinical-drug code (urn:posos:medicabase) matches itsposos/medicabasecodings directly. - Only
INDICATION_SPECIFICrows become suggestions.COMPLEX_DOSAGErows (free RCP text) are shown read-only under the list;ALERT,DOSAGE_UNITandPATHWAYrows are out of scope here (they feed dosage checks, not prescriptions). Duplicate continuous criteria returned by the BIM joins are deduplicated — including the same constraint expressed in several units (an age range in years, months, weeks and days collapses to one chip, shown in the most speaking unit: years above 2 years, else months, else weeks).
Ranking (per patient)
Sorting happens server-side, in pure domain code
(apps/ehr-lab/src/domain/posology.ts), for the patient being prescribed:
- Groups by indication (condition + modifiers, e.g. « cystite (aigu) »).
The displayed/persisted coding prefers SNOMED CT (CIM-10 then MedDRA as
fallbacks), but the indication carries all the codings the BIM knows and
the match against the patient's coded problems and history items tries
each of them — strict equality for SNOMED, hierarchical prefix for CIM-10
(A01 ⊃ A010), label equality as a last resort. Matching groups come first
and open by default. Chart problems are themselves coded SNOMED CT first
(the Posos referential order in
searchIndications). - Inside a group, regimens sort by applicability to the patient:
- the app evaluates what it actually knows — age (birth date → BIM age groups and AGE criteria), sex, the chart's latest body measurements (weight, height, derived BMI — see the vitals section of the FHIR mapping), and the estimated glomerular filtration rate (see below). Other continuous criteria remain « unknown » and are displayed, never eliminatory;
- regimens with at least one positively matching criterion and no contradiction rank first; when such a match exists, non-matching regimens are dimmed — the "make the rest barely visible" rule;
- a regimen gated on an associated condition or adjustment (renal failure, haemodialysis…) that is NOT documented in the chart never ranks as a patient match — it stays visible, neutral. Documenting the condition (e.g. chronic kidney disease among the problems) lets it compete again;
- regimens contradicting known facts (an adult on a child-only regimen) are folded behind a « N more regimens » toggle.
- Then by nature: usual/standard first, then initial, attack (loading), maintenance, titration; maximal/minimal limits last.
- Ties break on breadth: without a patient match, the widest criteria first; with one, the most specific match first. Last resort: regimens listed for fewer indications outrank the multi-indication catch-alls, then sub-nature order (« Schéma posologique 1 » before « 2 »).
Why a group is highlighted — and when it isn't
A highlighted indication group carries a small icon whose tooltip says why: the
chart problem that matched, and/or the patient criteria the first regimen
satisfies. That explanation is only worth showing when it distinguishes this
patient, so a satisfied criterion counts only if it is discriminating
(isDiscriminating):
- an age group counts only when it excludes adults — infant, child, adolescent, senior. « Adulte » is satisfied by the majority of charts and « Adulte / Enfant » is barely narrower; almost every BIM regimen carries one, so announcing it would put the icon on every indication of a drug, and a signal present everywhere is not a signal;
- sex and continuous criteria (weight, height, BMI, GFR, creatinine clearance) always count: the BIM only attaches them where they change the regimen.
That alone isn't enough, because a highlight is comparative: the icon answers
"why is this group ahead of the others?", and a reason every group shares answers
nothing. Real case: amoxicillin carries « Poids ≥ 40 kg » on all 15 of its
indications — it is the adult/child switch in disguise — so each one would have
worn the same sparkle. markDistinctive therefore keeps, per group, only the
discriminating matches that the other matching groups do not all carry.
Measured on the live BIM, for a 73-year-old with a documented community pneumonia: 15 icons before, 1 after — on the indication matching his chart problem. For a 6-year-old with a documented strep throat and recurrent otitis: 11 before, 2 after — his two documented problems. In practice the age group is usually uniform across a drug's indications, so what survives is mostly the chart problem match, which is exactly the strongest thing the app has to say.
Every criterion is still listed as a chip, matched or not. These rules govern the explanation of the highlight, not what is displayed.
The semi-structured sentence
The sentence is assembled from the BIM's own French timing words
(period_word_first/period_word_second: « en », « par », « tous les »…) with
minimal grammar rules (drop a redundant frequency before « tous les », add
« prise(s) »/« fois » counters, singular/plural of « jour(s) »-style units).
Dose, frequency, administration duration, intake moments, treatment duration
and route are emitted as typed parts so the UI can emphasise the dose and
keep separators — and the joined text is what gets persisted as
dosageInstruction.text.
Renal function: computed by the device, never here
Renal criteria are the most frequent conditions on real BIM regimens, and the
app deliberately contains no renal formula. Estimating renal function is a
clinical calculation: reimplementing Cockcroft, MDRD or CKD-EPI in ehr-lab would
place it outside the CE marking of the Posos device, which already exposes it.
The app therefore sends the raw chart (birth date, sex, dated observations) to
AORT getCalc and displays each value with the formula the device applied.
The BIM distinguishes two quantities, and so does the app — one device call each, both from the same creatinine:
| BIM criterion | Unit on the criteria | Device call | Status |
|---|---|---|---|
DEBIT_GLOREMULAIRE |
mL/min/1.73 m², indexed — 301 criteria, all in this unit | formula: "EKFC" |
Decidable |
CLAIRANCE_CREATININE |
mL/min (232) or mL/s (116), not indexed | formula: "Cockroft" |
Decidable — mL/s is the same quantity ÷ 60, a unit conversion the BIM itself writes both ways (15–30 mL/min, 0.25–0.5 mL/s) |
Confusing the two would be a clinical error: for a patient whose body surface area is far from 1.73 m² the values diverge. On a 73-year-old, 82 kg, creatinine 180 µmol/L the device returns 30.75 indexed and 37.54 absolute — a stratification cut at 30 falls on opposite sides of the two.
Two things the app does not trust:
- The formula spelling is exact and case-sensitive. Cockcroft is spelled
Cockroft(onecin the middle);Cockcroft,COCKROFTandcockroftall fail withmissing formula. - The returned unit label. With
Cockroft, AORT labels the answermL/min/1.73m2although Cockcroft-Gault is by definition not indexed — the arithmetic settles it (37.54 is exactly(140 − age) × weight × 1.23 / creatinine, with no body-surface indexing, which would have given 32.97). The app takes the unit from the formula it asked for, never from the label, and keys criterion matching on that. Same family asBODY_SURFACE_AREA, returned askg/m2. Both are worth reporting upstream.
Seven of the 355 clearance criteria carry the indexed unit — the type says clearance, the unit says GFR. That is a database inconsistency, not an equivalence to guess: those stay undecided.
Each criterion is shown against its own quantity, with the date of the
biology used: we display the age of the value and let the prescriber judge,
rather than discarding it on our own authority (the BIM does carry
validity_duration fields on criteria, but none are populated today — when they
are, that becomes the source of truth).
When the device lacks parameters it answers foundParameters: false and that
quantity is simply absent — Cockcroft needs a weight, so a chart without one
yields a GFR but no clearance, and only the matching criterion is decided. With
no creatinine at all, or no credentials, no call is made: renal criteria stay
« unknown », exactly as before this feature. Nothing is extrapolated to fill a
gap, and a calculator outage never blocks prescribing.
Refining ranges
Many BIM regimens carry ranges (750–1000 mg, 2 to 3 intakes, every 4 to
6 h, for 10 to 21 days). Selecting such a regimen opens a small « refine for
this patient » block — one numeric input per range, pre-filled with the
range minimum — and the sentence is re-rendered live with the same grammar
(the pure domain functions run client-side too). The recorded dosage uses the
refined values: a precise doseQuantity instead of a doseRange, a single
frequency/period, a precise boundsDuration. A cleared input falls back
to the original range for that element.
What selection records
The picked regimen travels back with the prescription line and is re-sanitized
server-side, then mapped to FHIR: dose (Quantity or Range, UCUM codes straight
from the BIM, including mg/kg), timing.repeat, route, additional
instructions, the indication as reasonCode, and the BIM nature/source as
urn:ehr-lab:* extensions. A line prescribed without picking anything carries
no dosage, exactly as before. The audit trace of the prescription includes the
retained dosage sentence.