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

Canonical source: docs/claude/mobile-393-part-b-answers.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.

#393 Part B — answers from the one-source owner

Replies to the 6 open questions in mobile-393-part-b-learnings.md. Part A verified — excellent work, and catching the residual Basis App write is exactly the confirmation we wanted. Your Part B scoping is correct: ServiceDayMemory.insights (mergeStats overlay) is the right single seam, the latest=most-recent-localDate rule is right (the #394 bug), and the name/unit-mismatch traps are real.

Spec-location note taken: this file + the handoff live in the master repo (/Users/G/basis/docs/claude/). Copy both into the wt/hybrid branch if you want them to travel with it.

Core principle to hold onto

The digest is the already-resolved value — #381 copies the healthSummaries value verbatim (no unit conversion). So: (a) the canonical unit is fixed per field (the digest carries no per-field unit), and (b) mobile must not re-resolve or re-aggregate — read the scalar for the day and only format it.


Q1 — Canonical must-match set

The contract = what basisflow-web MetricsTab + the client portal (#391) display, since those are the surfaces users compare mobile against. That's these 10–14 fields: steps · rhr · hrvSdnn · hrvRmssd · sleepMinutes · exerciseMinutes · zone1Minutes…zone5Minutes · activeCalories · spo2 · weight. Prioritize RHR, steps, HRV (both), zone minutes, sleep — highest visibility + where divergence was worst. Body-comp (bodyFatPct/waistCircumference/ffmi/gripStrength*/deadHang) + macros are second-tier (low divergence, route when convenient). You do NOT need the full digest field set for "matches web/portal."

Q2 — activeCalories / mood / energyLevel / stress

  • activeCalories IS shown on web/portal → it should be digest-sourced, but via Path B (hero/energy) since it has no InsightType tile. Don't invent a tile. In scope for consistency.
  • mood / energyLevel / stress are subjective, single-source, user-entered — NOT multi-source wearables, so there's no divergence to fix. Leave them local/as-is. Out of one-source scope. Do not create tiles or digest-source them.

Q3 — glucose

Confirmed. The digest carries a single per-day glucose scalar (daily average). TIR / variability / min / max are CGM-derived analytics that are NOT in the digest → keep those on local recompute. Only the daily average is digest-sourced. (George has no CGM so there's no live value to eyeball, but the contract is one scalar.)

Q4 — Phasing

Ship Path A (tiles) first, then Path B (hero), then Path C (trend headline). Reasoning:

  • Path A is the highest-traffic surface and where users directly compare to web/portal — ~90% of the visible consistency win.
  • Path B hero cards show recovery/cardio SCORES = providerScores (Oura-computed, single-source) — they already match across surfaces, so bringing them over is lower urgency and can't diverge in the interim.
  • Path C is lowest priority. Phasing is safe here precisely because the un-migrated surfaces (hero scores) don't diverge. Don't block A on B/C.

Q5 — Units (VERIFIED against real data — canonical, fixed per field)

Digest field(s)UnitEncoder note
weightkg ✓ (hs unit=kg, 84.8)
waterMlmL ✓ (hs unit=mL)encodeWater expects its volume unit → convert from mL
sleepMinutes exerciseMinutes mindfulnessMinutes fastingMinutes zone1..5Minutesminutes ✓ (hs unit=minute)encodeDuration expects seconds×60
activeCalories dietaryCalories calorieskcal
vo2Maxml/kg/min ✓ (hs unit=ml/kg/min)
rhr maxHrbpm · hrvSdnn hrvRmssdms · spo2
glucosemg/dL (verify on a CGM user before shipping)
gripStrength*UNVERIFIED — hs unit="unit" (unspecified), value ~47.6 (likely kg)display raw, do NOT unit-convert until confirmed against HealthKit source
deadHangUNVERIFIED — no data on George; assume seconds (a hold duration)confirm on a user who has it

Rule of thumb: the digest value is already in the canonical stored unit (the hs unit field). If you want to be bulletproof later, we can have #381 stamp a _units map into the digest — for now, use this table.

Q6 — Fallback / source visibility

Defer for v1 — show the resolved value only, no source chip. BUT note what _sources/_shadow is: it's the data behind the cycle-to-drill affordance from the resolution decision (the user inspects/picks Oura vs Apple). So when we do surface it, it's not a debug chip — it's the "which device" picker + the path to a user-curated preference (which then feeds back into resolution). So: v1 = value only; source provenance is a designed follow-on, same mechanism as the curated-preference override. Don't build it now.


One addition to your plan

Your mergeStats overlay (digest wins per (InsightType, day), local fills gaps, best-effort/SWR) is exactly right. Add one guard: only overlay a digest field when it's a number present for that day — never overwrite a real local value with a missing/zero digest field (a backfill-gap day). Same "absent ≠ 0" rule we used server-side (#480 nutrition). That keeps offline/gap days showing the local value instead of blanking.

Ping me on the field contract anytime — canonical spec is docs/claude/one-true-source-plan.md.


Follow-up 2026-07-16 (zones + tier-2 units) — pass to the hybrid agent

  1. HR zones stay APP-OWNED. Zones are single-source (per-workout) — not the multi-source conflict #381 fixes. A server recompute produced a ~30% delta vs the app value (measured on real prod data) → surfaces would disagree during rollout. So: Part A gate must EXEMPT zone1Minuteszone5Minutes (app keeps writing them; block all other metrics). Mobile keeps local zone display. Server zone computation is shelved (kept dormant in functions_digest_writer.py).
  2. Calories — keep BOTH: activeCalories (burned, hero) AND dietaryCalories (consumed, own nutrition tile — don't drop). Dietary data also blocked by #487 (empty meal nutrients).
  3. bodyFatPct — include now, unit % (typical 10–25%).
  4. gripStrength — unit lbs, and TWO metrics: gripStrengthLeft + gripStrengthRight (separate).
  5. deadHangseconds, include.
  6. Verification caveat: sim has no local Apple Health data → Part B only tested injected/Terra values. Do a REAL-DEVICE check (local Apple Health populated) before the mobile release.