Skip to content

Healthspan v2 (1/3): target-anchored Body Age engine, SRI, projected pace - #12

Merged
ayiskakov merged 18 commits into
mainfrom
feat/healthspan-engine-v2
Sep 23, 2026
Merged

ayiskakov merged 18 commits into
mainfrom
feat/healthspan-engine-v2

Conversation

@ayiskakov

Copy link
Copy Markdown
Owner

PR 1 of 3 for the Healthspan v2 plan (docs/superpowers/plans/2026-09-23-healthspan-v2.md, added here). This PR changes the engine. The pipeline and tab PRs are stacked on top of it.

What changes

VitalityEngine is upgraded in place, so it can be compared with WHOOP's Healthspan white paper (rev. 2025-09-04):

  • Referent. Zero years now means meeting health targets (WHOOP's anchor), not "average for your age".
  • Years. Years are 10 · ln(HR) (Spiegelhalter 2016). The old factor was 8/ln2 · ln(HR).
  • Drivers, targets and curves. Each curve cites its source in a doc comment.
    • SRI, a hinge curve (Windred 2024).
    • Sleep duration, asymmetric around 7–9 h. It counts at half weight when SRI is also scored.
    • %HRR zone 1–3 and zone 4–5 minutes, with age-declining 100→70 and 10→7 min/wk targets (Ekelund 2019, Ahmadi 2022).
    • Strength: target ≥ 40 min/wk, benefit only.
    • Steps: target 8,000, or 5,600 from age 60 (Paluch 2022).
    • VO₂max: an age/sex target (44 M / 38 F at 30).
    • Resting HR: target < 60 M / < 64 F.
    • Lean-mass percentage: penalty only.
  • HRV is no longer scored. It is non-causal and U-shaped. The age norm stays, for context.
  • Overlap. A strap-derived VO₂max folds into resting HR, because it is computed from it. An external VO₂max keeps the √n rule.
  • New outputs:
    • biggestLever
    • the unlock gate (21 scored days in 31, age 18+)
    • Inputs.restricted(to:)
    • Result.unclampedBodyAge
  • SleepRegularity. A new pure helper for the timing-based Sleep Regularity Index, computed over 30-second epochs. Local noon-to-noon days with no session drop out.
  • PaceOfAgingEngine.project. WHOOP's definition: hold the last 30 days for six months, so pace = 1 + (Δ30d − Δ6mo) / 0.5 y, clamped to −1…3×.
    • Both windows are compared over the drivers they share, so a newly connected data source cannot read as aging.
    • The margin comes from the spread of the monthly windows (a prior until three windows exist).
    • The old least-squares fit stays until PR 2 drops its last caller.

The one calibrated constant

The cross-domain overlap shrink is raised from 0.75 to 0.9. This makes the average US 30-year-old man read +5.9 y and woman +7.4 y. WHOOP's Table 2 gives +6 / +7.5. At 0.75 they read +4.9 / +6.2.

Verification

  • The oracle Packages/StrandAnalytics/Tests/StrandAnalyticsTests/oracles/healthspan_v2.json is generated by compiling the three engines standalone with Tools/oracle-twins/healthspan_v2_twin.swift (swiftc -O). It covers:
    • every curve over its domain (665 points)
    • 12 engine cases, including at-target people (Δ = 0) and the two calibration people
    • 7 SRI schedules, hand-checked: alternating 23:00 / 03:00 gives 33.3, a one-hour wake bout gives 91.7, unworn nights are skipped
    • 10 pace-projection edge cases
  • VitalityEngineTests, PaceOfAgingEngineTests and HealthspanOracleTests were updated on purpose. The reason for each change is written in the tests. The v1 oracle is replaced, not edited.
  • swift test in Packages/StrandAnalytics: 2,119 tests, 0 failures.
  • App adapter: lean mass needs body weight instead of height. xcodebuild succeeds for Strand (macOS) and NOOPiOS (built without the embedded watch app, because the watchOS runtime is not installed locally). The Healthspan StrandTests pass.
  • Between this PR and PR 2, the app does not yet feed SRI or %HRR zones. They are wired in PR 2, so merge the stack in order.

No BLE, schema-migration or UI-copy changes.

SRI = 200 · P(same sleep/wake state at t and t + 24 h) − 100 over 30-second
epochs, from sleep sessions and their wake stretches. Local noon-to-noon days
with no session drop out, so an unworn night is not scored as irregular.
Per-pair agreement is exposed so a caller can compute it once and re-window it.
…model)

- Referent moves from "average for your age" to "meeting health targets",
  WHOOP's anchor; years are 10 · ln(HR) (Spiegelhalter 2016).
- Device-scale targets and curves: SRI hinge (Windred 2024), asymmetric sleep
  duration down-weighted beside SRI, %HRR zone 1–3 / 4–5 minutes (Ekelund,
  Ahmadi), strength ≥ 40 min/wk, age-dependent steps (Paluch), VO₂max age/sex
  target, resting HR 60 / 64, lean-mass percentage.
- HRV is no longer scored (non-causal, U-shaped); the norm stays for context.
- A strap-derived VO₂max folds into resting HR; an external one scores alone.
- Adds biggestLever, the unlock gate (21 scored days in 31, age 18+) and
  Inputs.restricted(to:).
- The cross-domain shrink is calibrated to 0.9 so the average US 30-year-old
  man / woman read +5.9 / +7.4 (WHOOP white paper Table 2: +6 / +7.5).
PaceOfAgingEngine.project holds the last-30-day averages for six months:
pace = 1 + (Δrecent − Δsix-month) / 0.5 y, clamped −1…3×, with both windows
compared over the drivers they share. The ±margin comes from the spread of the
monthly windows (a prior until three exist). The legacy OLS fit stays until the
pipeline moves off it.
…acle

healthspan_v2.json is produced by compiling the three engines standalone with
Tools/oracle-twins/healthspan_v2_twin.swift: every curve over its domain,
at-target people (Δ = 0), the average US 30 M / 30 F calibration people, SRI on
synthetic schedules and pace-projection edge cases. It replaces the v1 oracle,
which pinned the retired population referent.
Lean mass now needs body weight rather than height, and HRV / the duration-CV
regularity no longer feed Body Age. Sleep regularity (SRI) and %HRR zones are
wired in by the pipeline change that follows.
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
Healthspan v2 (2/3): six-month windows, %HRR zones, SRI, persisted per-driver years
@ayiskakov
ayiskakov merged commit b4c506e into main Sep 23, 2026
15 checks passed
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