Repository navigation
Engine Result fields are computed and never read — a sweep, plus 'time in high stress' and a CircadianEngine domain mismatch #982
Description
Auditing NOOP for values that are computed but never reach a user or a score, one pattern kept recurring: an engine's Result struct grows a field, the field is computed with real logic and covered by a test, and nothing ever reads it. The test passes, so it looks wired.
Below: a mechanical sweep, then the two findings I'd act on first, then a suggestion that would catch the next one automatically.
The sweep
For every public let / public var stored property declared in a production Swift file, count .name dereferences in every other production file. 176 come back with zero.
Most are noise — SwiftUI protocol members (animatableData, alignment), init-only parameters, generated FFI (Strand/CloudSync/Generated/), and fields legitimately consumed inside their own file (e.g. NutritionCsvImport.caloriesIn is read at NutritionCsvImport.swift:84 to build metric points — flagged by the sweep, not actually dead). The signal is the subset on engine Result types, where external consumption is the entire point. Those I checked by hand:
| Type | Fields with no reader anywhere | Note |
|---|---|---|
AnalyticsEngine.DayScores |
chargeConfidence, effortConfidence, restConfidence (AnalyticsEngine.swift:87-89) |
all three computed for real (:783, stored :797) — a whole confidence tier the UI never shows |
DaytimeStress.Result |
highStressMinutes (:204), hrOnlyFallback (:210), activityMaskedHours (:215) |
see below |
SleepStager |
tibS (:2150), solS (:2153), remLatencyS (:2154), wasoS (:2155), deepPct/remPct/lightPct (:2161-2163) |
the standard PSG summary set, computed and dropped |
CorrelationEngine |
intercept (:38) |
zero readers repo-wide, tests included |
Baselines.Deviation |
ratio (:89), inNormalRange (:91) |
only .z and .delta are consumed |
FitnessAgeEngine |
chronoAge, deltaYears, bandYears, lowerConfidence (:255-258) |
|
VitalityEngine |
deltaYears, bandYears, chronoAge, factorsUsed, contributions (:66-74) |
HealthView recomputes contributions independently rather than reading this |
DailyStressEngine |
daysBelowBand (:89), rhrBpmAboveLongTerm (:83), usedRHR (:114) |
|
HRZones |
belowZone1 (:78), maxHR (:49) |
|
WorkoutDetector |
zoneTimePct (:49), avgHRRPct (:51), hrmaxSource (:55), caloriesKJ (:57) |
|
SleepStageTotals |
asleepSeconds (:227) |
|
ResonanceEngine.PaceScore |
cleanBeats (:82), scoredCycles (:84) |
|
RhythmScreener |
turningPointRate (:152), agreedAcrossSources (:161), NightRhythmSummary.steady/occasional/variedWindows (:198-204) |
these appear only in a SwiftUI preview fixture (RhythmView.swift:548-554) |
Additive fields are where output goes to die: adding one to an existing struct never breaks a caller, so nothing forces the question "who reads this".
The two I'd act on
1. "Time in high stress" is computed on every render and thrown away
DaytimeStress.swift:529 — let highStressMinutes = scored.filter { $0.1 >= highBandFloor }.count * (bucketSeconds / 60) — real logic, real thresholding, passed into Result at :533, read by nothing. Seven test references, zero production ones.
This is a figure Oura shows directly and users compare against. It already exists, correctly computed, on a screen that's already built. Cheapest user-visible win in the audit — it's a label, not a feature.
2. CircadianEngine is fed heart rate where it documents motion, and the arrhythmia gate is a relative test
ActivityBin.activity is documented as motion volume. The production caller passes mean HR in bpm. The readability gate is relative amplitude — amplitude / |mesor| < 0.10 → .unreadable.
Those two facts don't compose. With motion the mesor sits near zero, so relative amplitude is large and the gate is generous. With HR you get a ~65 bpm DC offset against an ~8–12 bpm circadian swing, landing around 0.12–0.18 — just above a 0.10 floor, and below it for anyone with a flat HR profile. A low-resting-HR user (mine currently runs 41–46 with a very flat curve) sits closest to the edge, so the people the gate is most likely to silence are the fittest ones, which is backwards.
Nothing is currently broken — it clears the floor today. But it's structurally the same shape as the skin-temp baseline bug (a range check meeting a different domain than it was tuned for), and that one ended up permanently unreachable rather than merely marginal. Worth either passing motion as documented, or re-tuning the gate for the HR domain and updating the doc comment.
A suggestion that would catch the next one
Three engines currently assume ~1 Hz banked HR and would silently degrade to nothing if the cadence ever coarsened:
ScoreConfidence.solidEffortReadings = 3600→.solidEffort becomes unreachableDaytimeStress.minHourHRSamples = 300→ the intraday stress timeline goes emptyHeartRateRecovery.maximumContinuousGapSeconds = 10→ HRR returns nil on every workout
I measured it rather than assuming: on my WHOOP 5/MG, hourly hrSample counts run 3548–3599 per 3600-second hour across Jul 29–30, and ~84,500–86,400 per day for Jul 22–28 against a 1 Hz ceiling of 86,400. So the assumption holds on this hardware and all three thresholds are satisfiable — this is not a live bug.
But it's unpinned. One test asserting the observed per-source cadence would convert three independent silent-nothing failures into one loud one, and it's a better deliverable than three separate defensive fixes. Worth doing before the next firmware or the next device family changes the cadence out from under them.
Happy to send PRs for the highStressMinutes surface and the cadence test if either is wanted. The CircadianEngine domain question I'd rather ask than patch, since "pass motion" and "re-tune the gate" are meaningfully different answers.
Environment: WHOOP 5/MG, iOS. Cadence measured on my own cloud mirror; file:line against a fork of main at af44563c.
Activity
ryanbr commentedon Jul 31, 2026
Worked through this against the tree rather than reading it. The central thesis holds, the CircadianEngine finding is correct and is the best thing in here — but finding #1 does not exist in this repo, and that changes what to do first.
Finding #2 is real, in all three parts
Confirmed each independently:
ActivityBin.activityis documented as "the motion volume in that bin (e.g.StepsEstimateEngine.dayMotionIntensityper hour)" —CircadianEngine.swift:42-43.sums[hour] += b.bpmand passessums[h] / counts[h]— mean heart rate — asactivity(AppModel.swift:1624, 1628).minRelativeAmplitude = 0.10, applied asfit.amplitude / abs(fit.mesor)(:30,:161).Your arithmetic follows. A ~65 bpm mesor against an ~8–12 bpm swing lands ~0.12–0.18 against a 0.10 floor, and the flatter the curve the closer to the edge — so the gate is most likely to silence the fittest users. That it clears today is luck of the domain, not design.
Answering your question, since you asked rather than patched: I would keep HR and re-tune, not switch to motion. WHOOP 4.0 motion is documented as too sparse to even stage sleep (#345), so
dayMotionIntensityis the weaker signal here, and HR is a legitimate circadian marker in its own right. But a relative gate is the wrong shape for a signal with a large DC offset — the honest fix is an absolute floor in bpm (an amplitude below ~N bpm is arrhythmic regardless of mesor), plus correcting that doc comment so the next reader is not misled the same way.Finding #1 is not in this tree
highStressMinutesreturns zero matches repo-wide — no production reference, no test reference.DaytimeStress.swiftis 295 lines, so the cited:529and:533cannot exist, and itsResultcarries onlyhours,sustainedHigh,sustainedRun,dayMean,peakand the computedscored. Same for two others in that row:hrOnlyFallbackandactivityMaskedHoursare absent, as isdaysBelowBandfrom theDailyStressEnginerow.Most of the sweep does check out —
chargeConfidenceis exactly atAnalyticsEngine.swift:87as stated, andturningPointRate,cleanBeats,belowZone1,hrmaxSourceand the rest all exist. So this is not a wholesale problem, but four named fields and one set of line numbers came from somewhere other thanmain. Worth checking whether that sweep ran against a fork or a working branch — I would rather ask than guess, because the rest of the report is careful enough that it stands out.The core thesis holds, and there is a better "cheapest win" available
I traced
chargeConfidenceend to end: declaration:87, init parameter:113, init assignment:127, computed for real:770, passed into the struct:784, and one test assertion. Zero production readers, exactly as you describe. Same shape foreffortConfidenceandrestConfidence.So a whole confidence tier is computed every day and shown to nobody — and unlike
highStressMinutes, it is actually here. That is the label-not-a-feature win, and it is arguably a better one: it tells users when a score is still calibrating, which is a recurring source of confusion in the issue tracker.The cadence test
Also verified:
solidEffortReadings = 3600(its own comment says "~1 h at 1 Hz"),minHourHRSamples = 300,maximumContinuousGapSeconds = 10. Three independent silent-nothing failures resting on an unpinned assumption, and your 3548–3599 per hour measurement is the kind of evidence that should be in a test rather than an issue. Agreed it is the better deliverable — one loud failure instead of three quiet ones.Suggested order
Happy to take the PRs for 1 and 2 whenever you want to send them.
Circadian: the input is heart rate, not motion — say so, and pin what…
ryanbr commentedon Jul 31, 2026
Ran the sweep against
mainmyself so this is usable as a backlog. #993 has landed the CircadianEngine half (doc corrected, gate cost pinned on both platforms).Your core claim holds, and it is bigger than the headline suggested. These 22 all have zero production readers on
main— declaration, init, computation, sometimes a test assertion, and nothing else:chargeConfidenceeffortConfidencerestConfidence·tibSsolSremLatencySwasoSdeepPctremPctlightPct·belowZone1zoneTimePctavgHRRPcthrmaxSourcecaloriesKJ·turningPointRateagreedAcrossSources·cleanBeatsscoredCycles·asleepSecondsinNormalRangefactorsUsedlowerConfidenceThree rows do not survive checking, though:
chronoAgehas 6 readers. It is consumed, not dead.interceptis misattributed. It is declared inPpgHr.swift, notCorrelationEngine.swift, and has a reader.bandYears,deltaYears,cleanBeatsandratioeach have one reader.And six named fields are not on
mainat all —highStressMinutes,hrOnlyFallback,activityMaskedHours,daysBelowBand,rhrBpmAboveLongTerm,usedRHR. They are added by #988, which confirms the sweep ran against that branch. Worth re-running againstmainbefore anyone works the list, and worth knowing that finding #1 — the "cheapest user-visible win" — describes a field #988 is itself introducing unread, which is the very pattern this issue is about.Method caveat, since I am asking you to trust my numbers after questioning yours: I counted
.fieldreferences outside the declaring file, excluding tests and previews. That over-counts common words (ratio,intercept,chronoAgecan match unrelated types) and would miss a field consumed by destructuring. Treat the 22 as high-confidence and the low-count rows as "check by hand".On the two items still open
The cadence test is yours — you offered it and you have the per-source measurements. Verified the three constants exist exactly as stated (
solidEffortReadings = 3600, whose own comment says "~1 h at 1 Hz";minHourHRSamples = 300;maximumContinuousGapSeconds = 10). Please do send it.The confidence tier is the better "cheapest win" now that
highStressMinutesturns out not to exist here — it is real onmain, it is a label rather than a feature, and it answers a recurring question in the tracker ("why is my score still blank / is this calibrating?"). But where it goes and what it says is a product call rather than a mechanical fix, so I'd want to settle that before landing anything.One thing I would push back on: deleting the other 22 is churn, not a fix. They cost nothing at runtime, and several are the obvious inputs to features that do not exist yet (the PSG summary set is a sleep-detail screen waiting to happen). The value in your sweep is as a menu of things already computed and free to surface, which is a much better framing than dead-code removal.
Surface the SpO2 candidate mean in the diagnostics both platforms alr…
Diagnostics: a nightly mean for the 5/MG SpO2 candidate, so it can be…
Battery drain: diagnose the reconnect loop, and stop re-asking the ba…
Battery drain: diagnose the reconnect loop, and stop re-asking the ba…
Circadian: the input is heart rate, not motion — say so, and pin what…
Diagnostics: a nightly mean for the 5/MG SpO2 candidate, so it can be…
Don't let the #982 loop detector undo the suppression it cannot see
ble: stop the CLIENT_HELLO from knocking a working 5/MG link off ever…
ble: keep the liveness watchdog alive on a suppressed 5/MG link
ble: port the 5/MG CLIENT_HELLO suppression to iOS/macOS (#1672)
fix: a connection the phone is holding is not a strap refusing to pair
fix: a connection the phone is holding is not a strap refusing to pair
fix: a connection the phone is holding is not a strap refusing to pair
fix: a connection the phone is holding is not a strap refusing to pair
fix: a connection the phone is holding is not a strap refusing to pair
fix: a connection the phone is holding is not a strap refusing to pair (
fix(android): only count bond-watchdog bounces that reached the comma…
fix(android): only count bond-watchdog bounces that reached the comma…
fix(ble): re-prove the WHOOP 5/MG encrypted bond on a re-entered link
fix(ble): ask whether the re-proved bond is this strap's