Skip to content

Resilience (2/2): Experimental section in the Healthspan tab - #16

Merged
ayiskakov merged 6 commits into
feat/resilience-enginefrom
feat/resilience-ui
Sep 23, 2026
Merged

ayiskakov merged 6 commits into
feat/resilience-enginefrom
feat/resilience-ui

Conversation

@ayiskakov

Copy link
Copy Markdown
Owner

PR 2 of 2 for the Resilience plan. It is stacked on #15, so merge #15 first.

What changes

This PR adds an Experimental Resilience section to the Healthspan screen. It is shared by macOS and iOS. The section is shown only when Settings → Experimental · Resilience is on, and that toggle is off by default (noop.resilienceEnabled).

  • Loader. ResilienceLoader (Strand/Data) is the one funnel. It reads repo.dailyMetrics once and runs ResilienceEngine off the main actor for three signals:

    • steps, whose window ends yesterday, because today's total is still counting;
    • resting HR and HRV, whose windows end today.

    Nothing is persisted. The 12-month trend (one point every 14 days, computed in parallel) is built when the detail screen opens. Its newest point is the snapshot's own headline result, passed through rather than recomputed, so the chart and the hero cannot disagree.

  • Hero. It shows "Recovery time ≈ N days" with its 90 % range. The range is drawn on a log day axis, with faint ticks for the cohort averages (about 2 weeks at age 40 and 8 weeks at age 90), captioned as context and not a score. When the upper bound reaches the 180-day ceiling, the range reads "N days or longer". Two other states replace the headline:

    • No measurable carry-over, when the engine finds no memory in the signal;
    • x of 90 days observed, while the history is still too short.
  • Signal chips. Steps is labelled Published. Resting HR and HRV are labelled Extension. Each chip shows its τ and range; tapping one switches the section to that signal.

  • Detail screen:

    1. How your body settles: the measured C(k) points and the raw exponential fit.
    2. Your last knocks: the three most recent knocks. Each shows its path in SD, the fitted return, the observed days back to the usual range, and the half-life when the fit describes the path.
    3. Trend: τ with its 90 % range as a band, on a log axis.
    4. Day-to-day swing: σ as a percentage for steps and HRV, and in bpm for resting HR.
    5. The number of days observed.
    6. The method sheet.
  • Method sheet. It covers the Langevin/recovery-time idea, the difference between cohort and individual results, the bias correction, the extension labels and the references. The references use Text(verbatim:).

Deviation from the plan. The autocorrelation chart does not draw an interval ribbon. The 90 % range belongs to the bias-corrected τ, and the raw points are biased short against it, so drawing both on one chart would show two readouts that seem to disagree (the AGENTS.md rule). The caption states the raw fit's τ and says that the headline corrects it.

Screenshots

These were rendered off-screen with ImageRenderer from synthetic data: steps with τ = 14, and resting HR with τ = 6 plus an injected illness-like spike. They are not from real history.

Section (steps) How your body settles
section settles
Last knocks (resting HR) Trend Day-to-day swing
knocks trend swing

The first render of the knocks card showed a knock "back in your usual range after 2 days" next to a "half-life ≈ 41.6 days". That half-life came from a flat fit stuck at the search ceiling. The fix is in the engine (#15, fix(analytics): report a knock's half-life only when the fit describes its path). The card above is from after the fix.

Verification

  • ResilienceReadoutTests (StrandTests) covers:
    • which column each signal reads, with missing days left out rather than counted as zero;
    • steps ending yesterday;
    • the trend's end days landing exactly on the headline day;
    • the trend's newest point being the headline estimate itself;
    • the range, headline and chip wording, including the "or longer" ceiling case;
    • σ units, and the Published/Extension labels.
  • Full StrandTests: 1,949 tests, 0 failures, 1 skipped.
  • xcodebuild succeeds for Strand (macOS) and NOOPiOS. NOOPiOS was built without the embedded watch app, because the watchOS runtime is not installed locally.
  • Design tokens only: StrandPalette, StrandFont, NoopMetrics, and the StrandDesign NoopCard, SectionHeader, SourceBadge and ComingSoon.
  • All 55 new strings are in Localizable.xcstrings in all nine shipped locales (de, es, fr, it, pl, pt-PT, ru, zh-Hans, zh-Hant). Format specifiers were checked against each key, including the escaped %% in interpolated keys and positional arguments where the word order changes. i18n_audit --ci main and doc_comment_lint are clean.
  • Real history not checked. The local macOS store on the build machine has no daily rows, because the strap history lives on the phone. The first readout from real data is still to be looked at. The section needs 90 observed days in the last 180 before it shows a recovery time; until then it shows the collecting count.

No BLE, schema, persistence or backup-whitelist changes. The toggle is a local @AppStorage preference and is not added to .noopbak.

…n reads

Reads repo.dailyMetrics once, runs ResilienceEngine off the main actor for
steps (ending yesterday), resting HR and HRV, and computes the rolling
trend on demand with the headline result as its newest point.
…method sheet

Hero with the recovery time and its 90 % range on a log band beside the
cohort ticks, three signal chips (steps published, RHR and HRV labelled
as extensions), and a detail screen with the measured autocorrelation and
its fit, the last three knocks, the 12-month trend with its range, and
the day-to-day swing. Shown only when the Settings toggle is on.
@ayiskakov
ayiskakov merged commit bbd7edb into feat/resilience-engine Sep 23, 2026
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