Skip to content

Fire the resting-HR drift alert that was only ever declared - #121

Open
ak710 wants to merge 1 commit into
saksham2001:mainfrom
ak710:feat/resting-hr-drift
Open

Fire the resting-HR drift alert that was only ever declared#121
ak710 wants to merge 1 commit into
saksham2001:mainfrom
ak710:feat/resting-hr-drift

Conversation

@ak710

@ak710 ak710 commented Aug 3, 2026

Copy link
Copy Markdown

Closes #112

Merge order: 2 of 9. Branches off main, independent of #120.

What

restingHRDrift has been a CoachAnomalyKind since the proactive-alert path landed, with a comment explaining why nothing raised it: the detector reads a 12-hour packet, and drift needs a multi-day baseline. But RestingHRBaselineService already learns and persists that baseline — it just never reached the packet.

Design notes

  • Both sides are measured the same way — the interpolated 10th percentile, sharing RestingHRBaselineService's own helper (made internal, with the percentile constant named). Comparing a night's mean against a 30-day percentile would produce a difference that is mostly an artefact of the two formulas.
  • The night is bounded by the sleep session, not a fixed clock window, so a late night or a shift schedule is measured over the hours actually slept.
  • Fires at +5 bpm, upward only. A resting HR below baseline is usually good news and not worth an unprompted alert.
  • Gates: established baseline, ≥10 overnight samples, night ≤2 days old. The sample floor is 10 because a YCBT ring floors its all-day interval at 30 minutes — a full night is only ~14 samples there, against ~84 on a 5-minute Colmi.
  • Ordered last of the three detectors. A short night usually raises resting HR too, so when both trip the sleep alert names the cause while this would only restate its consequence.

Docs

docs/project/anomaly-alerts.md is new and documents all three detectors — the two that were already shipping were undocumented.

Testing

PulseLoopTests/RestingHRDriftTests.swift — 11 tests covering the threshold knots, both precedence cases, and the builder's percentile/sample-floor behaviour. Full suite green (882).

restingHRDrift has been a CoachAnomalyKind since the proactive-alert path
landed, with a comment explaining why nothing raised it: the detector reads a
12-hour context packet, and drift is only meaningful against a multi-day
baseline. But the baseline already exists — RestingHRBaselineService learns and
persists it — so the packet just needed to carry it in.

The night and the baseline are both measured as the interpolated 10th percentile
of heart rate, sharing RestingHRBaselineService's own percentile helper.
Comparing a night's mean against a 30-day percentile would have produced a
difference that was mostly an artefact of the two formulas. The night is bounded
by the sleep session rather than a fixed clock window, so a late night or a
shift schedule is measured over the hours actually slept.

Fires at +5 bpm and only upward — a resting HR below baseline is usually good
news and not worth an unprompted alert. Gated on an established baseline, at
least 10 overnight samples (a YCBT ring floors its interval at 30 minutes, so a
full night is only ~14 there against ~84 on a 5-minute Colmi), and a night no
more than two days old.

Ordered last of the three detectors: a short night usually raises resting HR
too, so when both trip the sleep alert names the cause while drift would only
restate its consequence.

Every threshold is documented in docs/project/anomaly-alerts.md, which also
covers the two detectors that were already shipping but undocumented.

Co-Authored-By: Claude Opus 5 <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.

[Feature]: restingHRDrift is declared but nothing ever raises it

1 participant