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 residualBasis Appwrite is exactly the confirmation we wanted. Your Part B scoping is correct:ServiceDayMemory.insights(mergeStatsoverlay) is the right single seam, the latest=most-recent-localDaterule 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 thewt/hybridbranch 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
InsightTypetile. 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) | Unit | Encoder note |
|---|---|---|
weight | kg ✓ (hs unit=kg, 84.8) | |
waterMl | mL ✓ (hs unit=mL) | encodeWater expects its volume unit → convert from mL |
sleepMinutes exerciseMinutes mindfulnessMinutes fastingMinutes zone1..5Minutes | minutes ✓ (hs unit=minute) | encodeDuration expects seconds → ×60 |
activeCalories dietaryCalories calories | kcal | |
vo2Max | ml/kg/min ✓ (hs unit=ml/kg/min) | |
rhr maxHr | bpm · hrvSdnn hrvRmssd | ms · spo2 |
glucose | mg/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 |
deadHang | UNVERIFIED — 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
- 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
zone1Minutes–zone5Minutes(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). - Calories — keep BOTH:
activeCalories(burned, hero) ANDdietaryCalories(consumed, own nutrition tile — don't drop). Dietary data also blocked by #487 (empty meal nutrients). - bodyFatPct — include now, unit
%(typical 10–25%). - gripStrength — unit
lbs, and TWO metrics:gripStrengthLeft+gripStrengthRight(separate). - deadHang —
seconds, include. - 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.