Skip to content

fix: BG response shows mmol/hr, coach derives longest run from activity data#193

Merged
psjostrom merged 5 commits into
mainfrom
worktree-bg-fuel-bugs-only
May 13, 2026
Merged

fix: BG response shows mmol/hr, coach derives longest run from activity data#193
psjostrom merged 5 commits into
mainfrom
worktree-bg-fuel-bugs-only

Conversation

@psjostrom
Copy link
Copy Markdown
Owner

@psjostrom psjostrom commented May 13, 2026

Summary

Two surgical bug fixes plus salvaged ideas/TODO from the abandoned PR #192.

Bug 1 — BGResponsePanel showed -0.0 mmol/L /min for stable runs. Per-window slope averaging cancels positive/negative windows; rounding to 1 decimal kills the sign and rounds to zero. Switched the headline to medianRate × 60 with an mmol/hr label and per-hour color thresholds. Per-bucket displays unchanged.

Bug 2 — Coach AI prompt hardcoded "Longest distance: 10km". Actual longest is 14km (W11 Long, Apr 22). Now derived from completed events; sentence omitted when no data.

Bug 3 — adapt-plan recommends 90/75 g/h up from 62. Deferred. Likely downstream of Bug 1 feeding the regression bad signal. Watch one cycle before tightening MAX_FUEL_MULTIPLIER or EXTRAPOLATION_FACTOR.

Salvage from #192

PR #192 is abandoned. Conceptually-good ideas preserved as future-work entries without bringing the implementation:

  • IDEAS.md (Next): Timezone-Correct Planned Events; BG-Context Status Banner
  • IDEAS.md (Parked): Server-Owned runBGContext via Scout Batch; Personal Hypo Floor; Pump-During-Runs Setting
  • TODO.md (Tech Debt): activity_streams.glucose is a derived cache (cross-references the Parked entry)

Test plan

  • npx tsc --noEmit passes
  • npx vitest run — 1429/1429 pass
  • npx eslint on touched files clean
  • Mobile preview: BG Response section in Intel shows real per-hour rates (not -0.0)
  • Mobile preview: Coach answer to "what's my longest run?" cites 14km (or whichever is actually longest), not 10km

🤖 Generated with Claude Code

psjostrom and others added 2 commits May 13, 2026 10:58
…tivity data

Bug 1: BGResponsePanel headline showed "-0.0 mmol/L /min" for stable
runs. Per-window slope averaging cancels positive/negative windows,
rounding to -0.0. Switch headline to medianRate × 60 with "mmol/hr"
label and per-hour color thresholds. Per-bucket displays unchanged.

Bug 2: Coach AI prompt hardcoded "Longest distance: 10km" — actual
longest is 14km (W11 Long, Apr 22). Derive from completed events;
omit the sentence when no data.

Bug 3 ("adapt-plan recommends 90/75 g/h up from 62") deferred — likely
downstream of Bug 1 feeding the model bad signal. Watch one cycle
before tightening MAX_FUEL_MULTIPLIER or EXTRAPOLATION_FACTOR.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR #192 ballooned a 3-bug fix into a 97-file rewrite that's been
abandoned. Salvaged the conceptually-good ideas as future-work
entries without bringing the implementation:

IDEAS.md (Next):
- Timezone-Correct Planned Events — processPlannedEvents parses
  start_date_local with parseISO; wrong UTC instant for non-UTC users
  on Vercel. Self-contained ~30-line PR.
- BG-Context Status Banner — surface upstream BG fetch failures
  (Scout outage, missing creds) instead of silently rendering empty
  predictions.

IDEAS.md (Parked):
- Server-Owned runBGContext via Scout Batch — drop the persisted
  column, compute on every read by batching one Scout request.
- Personal Hypo Floor — per-runner BG threshold below which hypos
  cluster, combined with Riddell 2017 consensus.
- Pump-During-Runs Setting — replace hardcoded "All runs are
  pump-off" assertion in coach + run-analysis prompts.

TODO.md (Tech Debt):
- activity_streams.glucose is a derived cache (same anti-pattern as
  the deferred run_bg_context derivation; cross-references the
  IDEAS.md Parked entry).

The IDEAS.md component-name updates from #192 (BGResponsePanel →
DuringPatternCards/AfterPatternCards/TomorrowCard) are intentionally
not salvaged — those components don't exist on main.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
springa Ready Ready Preview, Comment May 13, 2026 10:13am

…ket displays

The headline switch in 174e6ab fixed the "-0.0 mmol/L /min" lie but left
every other display in BGResponsePanel.tsx on per-minute units with the
pre-existing rateColor() thresholds (-0.5, -1.5). Those translate to -30
and -90 mmol/hr — physiologically unreachable — so the per-activity
breakdown, StartingBGSection, EntrySlopeSection, and TimeDecaySection
rendered every dot green regardless of the actual drop.

Single boundary fix: convert per-min to per-hour at the display layer
via a new `perHour(perMin)` helper. rateColor() thresholds drop to -1 /
-3 mmol/hr, matching the headline's logic (which is now collapsed into
the same function). Five call sites converted; SuggestionCard text
label updated to "mmol/hr".

No data change — model still stores rates per-minute. The lie was at
the display boundary, the fix is at the display boundary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…Chart

The previous commit (9868957) fixed BGResponsePanel but left two siblings
on per-min units with broken thresholds:

- BGCompact (Overview tab top — the card user actually sees first) had
  copy-pasted rateColor/rateLabel with the same -0.5/-1.5 per-min
  thresholds. Headline showed "-0.0 Stable" with green dots universally.
- BGScatterChart axis labeled "mmol/L /min", tooltip said "mmol/L/5m"
  (already inconsistent with itself), crash reference line at
  CRASH_DROP_RATE = -1.5 per-min = -90 mmol/hr — off-screen forever.

Extracted perHour / rateColor / rateLabel into lib/bgRateDisplay.ts so
the next sibling component can't drift again. All three consumers
(BGResponsePanel, BGCompact, BGScatterChart) import from there. BGCompact
headline switches to medianRate × 60 (matches BGResponsePanel logic).
CRASH_DROP_RATE retuned from -1.5 (per-min) to -3 (mmol/hr); only
consumer is BGScatterChart.

BGCompact tests updated to use realistic per-min fixtures (e.g.
medianRate -0.04 → displays "-2.4 mmol/hr"). Assertions still check
user-visible text ("Stable", "Moderate", "Fast drop") — no
implementation-detail testing.

Bug 3 (adapt-plan recommending 90/75 g/h) is the remaining piece, still
deferred per option D — likely downstream of these display lies feeding
the model's regression.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@psjostrom psjostrom enabled auto-merge (squash) May 13, 2026 10:11
@psjostrom psjostrom merged commit 369b597 into main May 13, 2026
5 checks passed
@psjostrom psjostrom deleted the worktree-bg-fuel-bugs-only branch May 13, 2026 10:15
psjostrom added a commit that referenced this pull request May 14, 2026
* fix(bg-model): cap fuel-rate recommendations to clinical titration step; add spread guard

PR #193 fixed the BG drop display unit but left the model recommending
absurd fuel rates (e.g. 90 g/h Easy from a 62 g/h baseline). Two
research-grounded changes:

1. **Spread guard:** regression requires tested fuel rates to differ
   by ≥ FUEL_STEP_GH (10 g/h). Below that, the slope fits noise across
   a narrow window and solving for "ideal drop" extrapolates absurd
   targets. Real example from Per's data: 4 g/h spread (60-64), drop
   nearly identical across groups, regression returned raw target
   143 g/h capped to 90. Falls through to extrapolation, which moves
   one step at a time.

2. **Step cap:** `MAX_FUEL_MULTIPLIER = 1.5` (50% jump per cycle)
   replaced with `current + FUEL_STEP_GH` (10 g/h max increment per
   cycle). The 50% multiplier had no clinical basis and violated
   incremental CHO titration guidance — sudden CHO increases cause GI
   distress (Costa et al. 2023 systematic review). The 10 g/h step
   matches the standard sports-nutrition gut-training increment
   (Jeukendrup & Killer 2010) and the "Rule of 15" magnitude from the
   Riddell et al. 2017 T1D exercise consensus.

`MAX_FUEL_ABSOLUTE = 90` kept — it's the gut absorption ceiling for an
untrained gut (single-source SGLT1 limit ~60 g/h, 60-90 with
glucose-fructose blends and gut adaptation; >90 only with elite-level
adaptation).

End-to-end verified against 67 real activities:
- Easy:     90 → 65 g/h (regression skipped, extrapolation result)
- Long:     56 → 56 g/h (regression still triggers, real signal across
                         12 g/h spread)
- Interval: 60 → 60 g/h (default, drop doesn't meet MIN_DROP_TO_SUGGEST)

IDEAS.md: added "Phase-Aware Fuel Modeling" — investigation while
implementing this fix surfaced that the model's bigger problem is
averaging drop rate across both pre-fuel (minutes 0-25, no fuel
landed yet) and post-fuel (minutes 25+, fuel kicking in) phases. Easy
runs are dominated by pre-fuel kinetics that more fuel cannot fix.
Worth a separate focused PR.

TODO.md: queued post-run hyper investigation — needs runBGContext data
which the debug script disabled to skip Scout.

Tests: 1431 → 1431 pass (existing regression test updated for the new
cap value 66 → 55; two new tests cover the spread guard and step cap).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(bg-model): trim rotting comments, add boundary + cap-vs-penalty tests

Address review feedback on PR #195:

- Drop `MAX_FUEL_MULTIPLIER` reference from `capFuel` comment — the constant
  no longer exists in the codebase, so the reference would rot on merge.
- Drop the trailing "Falls through to the extrapolation path" sentence from
  the spread-guard comment — it narrates the other branch and would go stale
  if extrapolation semantics change.
- Add boundary test: spread of exactly `FUEL_STEP_GH` (10 g/h) qualifies for
  regression. Guards against a future `>=` → `>` flip.
- Add cap-vs-penalty ordering test: with raw target above the step cap,
  spike penalty subtracts from raw before the cap clamps (penalty-then-cap),
  not the other way around.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant