Repository navigation
Healthspan v2 (2/3): six-month windows, %HRR zones, SRI, persisted per-driver years - #13
Merged
ayiskakov merged 11 commits intoSep 23, 2026
Merged
Conversation
HRZones.heartRateReserveZones builds the Karvonen 50/60/70/80/90 % HRR zone set, the model the Healthspan zone targets are stated in and the one StrainScorer already uses.
SleepRegularity.session reads the wake segments of a computed staging array; an imported minute dictionary keeps its span with no wake stretches.
…nsaction replaceMetricSeries deletes the named keys in [from, to] and upserts the new rows atomically, so a recomputed window that drops a key cannot leave the old row behind or expose an empty window between two writes.
…ears, daily - One resolver builds the 182-day, 30-day and monthly windows from stored history. - The day scan banks %HRR zone 1–3 / 4–5 minutes (zone_min_hrr_*). - Sleep regularity is the SRI from stored sleep sessions; VO₂max comes from the strap estimate or an import (tagged external); lean mass is a percentage of body weight. - Unlocks at 21 scored days in the last 31. Each unlocked day persists body_age, vitality, pace_of_aging (+ margin), body_age_projected and, per driver, hs_years/value/target/recent. - A healthspan_model marker triggers a one-shot, idempotent re-derivation of two years of history under the v2 model, clearing the Saturday-keyed v1 rows.
Nothing calls it since the pipeline moved to PaceOfAgingEngine.project.
…line HealthspanDrivers.latest reads the hs_* rows written for the same day as the stored Body Age, replacing the 7-day recomputation, so the Healthspan rows and the Health hub's helping-most / holding-back line use exactly the shares the headline was summed from.
- Hero: six-month Body Age vs calendar age with its ± band, and a 21-of-31-scored-days countdown until it unlocks. - Pace dial: −1…3× in four bands (reversing / slowing / steady / accelerating), the margin, and the projected Body Age in six months; "holding steady" whenever the margin covers 1×. - Biggest lever: the years reaching that driver's target would take off. - Drivers: value, target, a signed years chip and a 30-day-vs-6-month arrow; tapping opens the driver's hazard-ratio curve and its source. - Six-month Body Age and pace trend charts, HRV as unscored context, and a method-and-references sheet. - Every readout comes from one snapshot of the persisted rows for the newest day, so the chips, lever, dial and chart cannot disagree with the headline.
Healthspan v2 (3/3): rebuilt Healthspan tab
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR 2 of 3 for Healthspan v2. Stacked on #12; review and merge that one first.
What changes
Strand/Data/HealthspanPipeline.swiftbuilds the 182-day Body Age window, the 30-day recent window and the six monthly windows, all fromhealthspanInputs(history:endDay:windowDays:…).zone_min_hrr_1_3andzone_min_hrr_4_5. They use the night's resting HR and the profile's max HR, via the newHRZones.heartRateReserveZones. The %HRmaxzone_min_2_3/_4_5rows are still written for the Metric Explorer, but no longer feed Healthspan.wakesegments instagesJSON. It is computed once over the history, off the main actor.vo2max_est(tagged strap-derived) or an importedvo2max(tagged external, and preferred in its window).lean_massas a percentage of the window's weight, falling back to the profile weight.body_age,vitalitypace_of_aging,pace_of_aging_margin,body_age_projectedhs_years_<key>,hs_value_<key>,hs_target_<key>,hs_recent_<key>healthspan_modelmarker row (day1970-01-01, which no chart shows) triggers a one-shot, idempotent re-derivation of the last 730 days under the v2 model. The re-derivation clears the Saturday-keyed v1 rows, so the trend chart never splices two models. The marker is written only after the rows land, so a failed write retries on the next pass. Ordinary passes refresh the last 7 days.WhoopStore.replaceMetricSeries(_:deviceId:keys:from:to:)deletes the named keys in a range and upserts the new rows in one transaction. A day that loses a driver therefore cannot keep a stale row, and there is no empty window between the two writes.HealthspanDrivers.latestnow reads thehs_*rows for the headline's day instead of recomputing a 7-day window. The Health hub's "helping most / holding you back" line uses exactly the shares the Body Age was summed from.Plan deviation (recorded in the plan doc)
The plan had a Sunday weekly headline plus daily trend points. That would put two different "current" Body Ages on one screen (the headline and the chart's last point), which AGENTS.md's two-readouts rule forbids. The headline is now the newest daily point. The 182-day window already gives the stability the weekly cadence was for.
Verification
HealthspanInputsTestsis rewritten for the new resolver. It covers:MetricSeriesStoreTests.testReplaceMetricSeriesClearsOnlyTheNamedKeysInRangechecks that only the named keys, inside the range, for this device, are cleared.HRZonesTests.testHeartRateReserveZonesandSleepRegularityTests.testSessionFromStoredBlockcover the two new helpers.StrandTestsrun withxcodebuild … test(macOS): 1,937 tests, 0 failures.swift testpasses forStrandAnalyticsandWhoopStore(MetricSeriesStoreTests).xcodebuildsucceeds forNOOPiOS. It was built without the embedded watch app, because the watchOS runtime is not installed locally.doc_comment_lintandi18n_audit --ci mainare clean.No BLE changes and no GRDB migration: metricSeries takes any key.