Skip to main content
Synced from the repo — do not edit here

Canonical source: docs/claude/mobile-D3-lab-verdict-handoff.md. This page is generated by docs/scripts/sync-handbook.mjs. Edit the source file in the repo; changes appear here on the next build.

D3 — Mobile: read the persisted lab verdict (hybrid agent handoff)

Owner: hybrid agent (wt-hybrid, Flutter). Part of: labs one-source epic #377 / #392. Why now: George's iPhone-sim screenshot showed Albumin values 33 and 58 g/dL labeled "In range" — impossible for serum albumin. Root cause is on mobile: it computes the in/out-of-range badge from the vendor's range (Junction shipped albumin 0–100 g/dL → everything "in range"), and never reads the verdict the backend now computes.

What the backend now does (DONE, deploy-held — you can rely on this contract)

Every lab doc written to users/{uid}/labs (and the clinic mirror) now carries our own verdict, computed once from the canonical registry, sex-aware:

fieldmeaning
verdict"in_range" | "out_of_range"authoritative, use this for the badge
verdictSourcee.g. "core:adult", "generated-default:male" — provenance
refRangeLow / refRangeHighour reference bounds, in the doc's canonical unit
vendorInterpretationthe vendor's own label (Junction's "normal") — audit only, do NOT trust
minRangeValue / maxRangeValuevendor-supplied range — may be garbage; fallback only

Written by import_lab_values (PDF/manual) and the Junction persist path (functions_junction._persist_junction_results_to_labs). Existing/historical docs are backfilled by basis-functions/tools/backfill_lab_verdicts.py (stamps verdict + canonical analyteKey). So after deploy + backfill, every doc has verdict.

Analyte keys are also canonicalized to ONE key per lab (alias map — see below), so labAlbuminSerum / raw LOINC forms are rewritten to labAlbumin.

Your task (mobile)

  1. Parse the new fields on the Lab model — basiscore/lib/src/models/summary/subtypes/lab.dart currently has minRangeValue / maxRangeValue / isBelowMinRange but no verdict. Add: verdict (String?), refRangeLow (double?), refRangeHigh (double?). Keep them optional.
  2. Change the in/out-of-range badge to PREFER the persisted verdict. Wherever the "In range" / out-of-range status is computed for a Lab (grep the trends-detail + day-memory render paths; the badge is driven off minRangeValue/maxRangeValue/isBelowMinRange today), use this precedence:
    1. clinic-custom benchmark, if you support it on mobile (clinician override)
    2. verdict field (out_of_range → red/high-or-low, in_range → green) ← NEW, authoritative
    3. fall back to the current local min/max compute ONLY when verdict is null (pre-backfill docs)
  3. Show OUR range, not the vendor's. For the displayed reference range, prefer refRangeLow/refRangeHigh over minRangeValue/maxRangeValue. Never render vendorInterpretation.
  4. Align canonical_lab_key.dart with the backend alias map. Mobile already collapses keys (good — that's why the 11 albumin points show as one card). Make it match the backend exactly: basis-functions/functions/generated/lab_analyte_aliases.json is the source of truth (45 aliases). Two must-keep rules from that file:
    • Serum/plasma/whole-blood variants collapse to the base (labAlbuminSerumlabAlbumin, labCalcium_SerumlabCalcium, labMercury_WholeBloodlabMercury, …).
    • Cellular / RBC / urine / stool variants stay SEPARATE (RBC magnesium ≠ serum magnesium; urine albumin ≠ serum albumin). Do NOT collapse those. Ideally generate a Dart alias map from that JSON so it can't drift; at minimum reconcile the keyword map against it.

How to verify (George's real account, uid GoOU3d2QcyRZMD83IhbQeIclpwh2)

After the backend deploy + backfill, on the sim: open Albumin. The two junk values (33, 58 g/dL) must show out-of-range (not "In range"), the four real values (4.75.0) in range, all in ONE card, reference shown as 3.5 – 5.0 g/dL.

Do NOT

  • Do not recompute verdicts on mobile when verdict is present — that reintroduces divergence (the whole point is compute-once). Local compute is a fallback for un-backfilled docs only.
  • Do not touch basisflow-web or basis-functions — those halves (D1 backend, D2 staff web) are done.