The score is the worse of the systolic and diastolic sub-scores, and
until now nothing said which one it was or how far over its ceiling the
reading sat. The grader now returns that basis together with the score,
as one value, and it is carried as one value all the way to the pillar
so an explanation can never belong to a different grading run than the
number it explains.
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
Close four findings from the health-score audit:
- Hypotension cliff: the below-floor branch reused the over-target table,
so 1 mmHg under the clinical floor dropped from 100 to ~81 — a 19-point
discontinuity. A dedicated under-floor curve now starts at 100 AT the
floor and descends smoothly (floor-10->70, -20->45, -30->20), so the
score is continuous on both sides of every boundary on both axes.
- Rollup vs live divergence: the graded BP score weighed a high-variance
day 1:1 on the rollup path (one per-day-mean pair) but N:1 on the live
path (N per-event pairs), drifting the pillar by up to ~20 points
depending on DAY-bucket warmth. gradeBpScoreFromSeries now takes an
optional per-pair count; the rollup path passes perDayPairCount so both
paths produce the same score for the same data.
- Resting proxy degenerate days: a day with fewer than three PULSE
samples no longer contributes a resting estimate, so a lone workout
reading can't masquerade as resting HR.
- Pulse-status proxy day boundary: thread the user's timezone into the
resting-proxy day-key so it buckets identically to the targets route.
Fairness anchors unchanged (134/87 -> 57, 165/105 -> 22).
The BP pillar value equalled the binary all-time in-target rate: the
share of paired readings where both systolic and diastolic sat at or
below the age-band ceiling. A borderline stage-1 profile (diastolic a
few mmHg over the ceiling on most readings) collapsed the pillar to the
teens even though the reading is "a bit high", not catastrophic — and
improvement never surfaced because the rate is all-time and binary.
Replace the binary rate AS THE SCORE with a smooth clinical-proximity
grade. A recency-weighted representative reading (45-day half-life) is
mapped per axis onto a 0-100 curve anchored on the same age-band ceiling
the rest of the app uses — optimal ≈ 100, at the ceiling ≈ 85, then
piecewise-linear down through the over-target bands; hypotension is
penalised symmetrically below the clinical floors. The pillar takes the
worse of the two axes, so an elevated diastolic counts without
binary-zeroing the result. A 134/87 profile against a 129/79 ceiling now
reads in the mid-50s.
The in-target rate stays available as a secondary "X% of readings in
range" stat. The healthScore.components.bp shape is unchanged — only the
value moves.