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

Canonical source: docs/claude/mobile-one-source-confirmations.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.

Mobile one-source-of-truth — confirmations + work needed (hybrid agent)

From: main integration session · For: hybrid agent (basishybrid, wt-hybrid) Context: the backend one-source work for wearables and labs has landed (deploy-held). This is the single checklist of what mobile needs to do / confirm so we can close everything that isn't purely blocked on an app-store build. For each item: [CONFIRM] = tell us it's already done + show the artifact, or [BUILD] = please implement.

For each, "code-closeable" = committed + verified in the sim on George's real account (uid GoOU3d2QcyRZMD83IhbQeIclpwh2) per the acceptance bar. The app-store release is the separate deploy-held step — don't block the code milestone on it.


A. WEARABLES — mobile stops writing the digest (#393)

Backend state (live): users/{uid}/dailyDigest/{localDate} is now server-written and authoritative (#381 deployed) — vendor-collapse → eligibility-gate → strength-default resolution. The app must stop competing with it.

[CONFIRM] 1 — the app no longer WRITES the digest.

  • Which file/function used to write dailyDigest (a sync/summary service)? Show it's now removed or behind a kill-flag.
  • Checkable: grep -rn "dailyDigest" basishybrid/lib basiscore/lib → no .set(/.update(/.doc(...).set writing to dailyDigest.

[CONFIRM] 2 — display READS the server digest.

  • The Today/Trends metrics + HR-zone cards read the dailyDigest fields written by the server, not a local recompute from SQLite.

[CONFIRM] 3 — branch + verification.

  • Branch (wt/hybrid?), committed y/n, and a sim screenshot on George's account showing the metrics match what basisflow-web shows (same headline value per metric).

Note: zones stay app-owned (decided) — do NOT switch zones to a server value; just make sure the app isn't also writing the digest. Exempt zones from the "read server value" gate.

Closeable when: 1–3 confirmed. Deploy-held part: the app build.


B. LABS — three separate items

Backend contract every lab doc now carries (via import_lab_values + Junction, deploy-held):

fielduse
verdict"in_range" | "out_of_range"authoritative, drive the badge from this
verdictSource, refRangeLow, refRangeHighprovenance + our reference bounds (canonical unit)
vendorInterpretationvendor's own label (Junction "normal") — audit only, do NOT display/trust
analyteKeycanonicalized to ONE key per lab (alias map below)

B1. [BUILD] Read the persisted verdict (D3)

Full spec: docs/claude/mobile-D3-lab-verdict-handoff.md. In short:

  • 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?).
  • Badge precedence: clinic-custom → persisted verdict → local min/max recompute (local only as fallback for un-backfilled docs). Prefer refRangeLow/High over vendor min/max for the shown range. Never render vendorInterpretation.
  • Verify (needs D1 deployed first): a doc with verdict: out_of_range renders red/flagged, not "in range." Until D1 deploys you can code it + unit-test against a hand-set doc.

B2. [CONFIRM] Scan/manual entry routes through the backend gate (#385)

  • Mobile manual + scan lab-save must call the backend import_lab_values callable (so it gets canonicalization + unit-normalization + verdict), not a direct Firestore labs write.
  • Checkable: the save path (service_lab_analysis.dart / lab entry flow) invokes the callable.
  • If it's still a direct write → this is [BUILD]: route it through the callable.

B3. [CONFIRM] Key canonicalization aligned with the backend alias map

  • basishybrid/lib/services/canonical_lab_key.dart must match basis-functions/functions/generated/lab_analyte_aliases.json (45 aliases).
  • Rules that MUST hold: serum/plasma/whole-blood variants collapse to the base (labAlbuminSerumlabAlbumin, labCalcium_SerumlabCalcium, labMercury_WholeBloodlabMercury); cellular / RBC / urine / stool variants STAY separate (RBC Mg ≠ serum Mg; urine albumin ≠ serum).
  • Ideally generate the Dart map from that JSON so it can't drift; at minimum reconcile it.
  • Confirm which analytes mobile currently collapses vs the 45-entry map (diff).

C. BENCHMARKS — mobile divergence (investigation → decision)

Web now generates its lab benchmark table from the registry (lib/lab-benchmarks-generated.tslab_registry.json). Mobile still hand-maintains its own Dart thresholds → mobile ≠ web. Before we can one-source it, we need to know the shape.

[CONFIRM] 1 — where mobile's benchmark thresholds live.

  • Point us at the file(s) mobile uses for lab in/out-of-range thresholds AND for wearable metric thresholds (steps/sleep/RHR/HRV/zones). Hardcoded Dart map(s)? A generated file?

[CONFIRM] 2 — feasibility of generation.

  • Could mobile consume a generated Dart benchmarks file emitted from lab_registry.json (the way web reads lab-benchmarks-generated.ts), replacing the hand-maintained tables? Any blocker (build-runner, package boundary, offline needs)?

Closeable when: we have the file list + a yes/no on the generation path. Then we decide who emits the Dart file. (No mobile code change required for this step — just the answers.)


What we need back (summary)

  • A (#393): confirm digest-write removed + display reads server digest + sim proof. [mostly CONFIRM]
  • B1 (D3): build verdict read (spec in the D3 doc). [BUILD]
  • B2 (#385): confirm scan/manual uses the backend callable, or route it there. [CONFIRM/BUILD]
  • B3 (keys): confirm canonical_lab_key.dart matches the 45-entry alias map. [CONFIRM]
  • C (benchmarks): name the mobile benchmark file(s) + say if a generated Dart file is viable. [CONFIRM]

Reply per item with the artifact (code snippet / grep result / sim screenshot / file path) so we can close the code milestones and isolate what's only waiting on the app build.


C — RESOLVED: backend now emits the Dart benchmark files (hybrid: wire them in)

The "who emits it" question is answered — the backend generators now write two generated Dart files into basiscore from the same registries web uses. Mobile stops hand-maintaining benchmark thresholds and consumes these:

  1. basiscore/lib/src/models/summary/lab_benchmarks_generated.dartkGeneratedLabBenchmarks (Map<String, GeneratedLabBenchmark>, in/out-of-range from lab_registry.json, gender variants).
  2. basiscore/lib/src/models/summary/metric_benchmarks_generated.dartkGeneratedMetricBenchmarks (Map<String, MetricBenchmark>, full scheme a/b/c/d + optimal tiers + age/gender variants + a period field, from metric_benchmarks.json). Includes the corrected age×gender VO2max, new weekly Zone 2 / Zone 5, and FFMI.

Both are regenerated + CI-drift-gated by the backend (generate_lab_benchmarks.py / generate_metric_benchmarks.py). DO NOT hand-edit them — values change in the JSON.

Work needed (mobile)

  1. Wire kGeneratedLabBenchmarks + kGeneratedMetricBenchmarks into the existing TrendType → BenchmarkType → BenchmarkHelpers.findBenchmarkForType path, replacing the hand-maintained Dart thresholds (clinic_benchmarks.dart / trend_type.dart). Clinic-custom benchmarks still override; these are the defaults.
  2. period == 'weekly' (zone minutes): before comparing, sum the trailing 7 days of the metric (zone2Minutes/zone5Minutes) — the benchmark declares the window; the evaluator aggregates. Daily metrics compare the day's value as today.
  3. FFMI is a DERIVED metric — compute fatFreeMass / heightM² where fatFreeMass = weight × (1 − bodyFat%) (height-normalized to 1.8m: ffmi + 6.1×(1.8 − heightM)). Body-fat% = labTotalBodyFat. Surface it as a metric so the benchmark applies. (Most accurate source = DEXA lean mass when #500 lands.)
  4. Key reconciliation: confirm the benchmark keys (zone2Minutes, zone5Minutes, ffmi, vo2, etc.) match the TrendType keys mobile looks up; if they differ, add the mapping (don't rename the generated keys — those are the shared contract).

Verify

On George's account in the sim: a metric with an age/gender band (e.g. VO2max) shows the correct in/optimal thresholds for his age+sex; a weekly metric (Zone 2) evaluates against the 7-day sum, not a single day.