Comprendre et références · Comprendre

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-v2 GraphQL 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 through branded_drugs; a clinical-drug code (urn:posos:medicabase) matches its posos/medicabase codings directly.
  • Only INDICATION_SPECIFIC rows become suggestions. COMPLEX_DOSAGE rows (free RCP text) are shown read-only under the list; ALERT, DOSAGE_UNIT and PATHWAY rows 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:

  1. 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).
  2. 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.
  3. Then by nature: usual/standard first, then initial, attack (loading), maintenance, titration; maximal/minimal limits last.
  4. 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 (one c in the middle); Cockcroft, COCKROFT and cockroft all fail with missing formula.
  • The returned unit label. With Cockroft, AORT labels the answer mL/min/1.73m2 although 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 as BODY_SURFACE_AREA, returned as kg/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.