Repository navigation
Effort funnel: report the longest and mean bridged run - #1603
Merged
Merged
Conversation
A field log showed 22 days of `kept=0` on a WHOOP 4.0 — 2211 candidate runs, 1661 after bridging, 76% dropped as too short, and roughly two hours a day of "active" time above resting+15. The counts alone cannot say which of two things that is. Normal life clearing the floor in one-minute bursts is an honest zero. A real session shattered into sub-threshold fragments is a bug. Both produce exactly the shape above, and the existing line reports every gate except the one number that separates them. Settling it took asking the reporter whether they had trained — the round trip an always-on diagnostic exists to remove. They had not, so the detector was right and nothing needed fixing; but that answer could not be read off the line, and next time the answer may be the other one. The funnel now carries the longest and mean BRIDGED run in seconds, measured before any qualification gate. Against this log it reads longestRunS=74 on a five-minute bar — daily life, unrescuable by any threshold change, and visibly so. A 25-minute run reported alongside kept=0 would be the reading worth chasing. Both platforms, byte-identical, with a test per side pinning that pair of readings. No behaviour change: the detector is untouched and no number moves. Kotlin verified locally (DetectionFunnelTest 10/10, suite 4307/0, assembleFullDebug, doc lint). StrandAnalytics needs macOS — GRDB's CSQLite won't build on Linux — so the Swift twin rides swift-packages CI.
Re-review of my own commit, and the error was in the analysis rather than the code. `droppedShort` is tested first and short-circuits, so `droppedLowIntensity` only ever counts runs that ALREADY cleared the duration bar. The log I justified this change with reports lowIntensity>0 on all 22 days — 390 runs total — which means sustained five-minute-plus efforts were reaching the intensity gate every single day. The previous commit described those same days as one-minute bursts of daily life that no threshold could rescue. That was wrong, and it was wrong in the doc comments and in both tests, which is the worst place for it: a fixture asserting 74 s as the longest run contradicted the counts printed beside it. The corrected reading: 1271 runs never reached minExerciseMin, and 390 did and failed on intensity instead. The counts already said the duration gate was not the whole story. What they cannot say is how substantial the survivors were — and a five-minute stroll rejected on intensity is routine, while an hour-long effort rejected on intensity is a defect. That is what the field is for, so both tests now pin exactly that: two fixtures with IDENTICAL counts taken from the log, separated only by longestRunS. The old pair could be told apart by the counts alone, so they did not test what they claimed to. Detector still untouched; no number moves. Kotlin verified locally (DetectionFunnelTest 10/10, suite 4307/0, assembleFullDebug, doc lint); the Swift twin rides swift-packages CI.
Third re-review pass. The docs told a reader to judge longestRunS by whether it "cleared the duration bar" and described that bar as five minutes. The gate is `minDurS - motionSmoothS` — 290 s, not 300, because the smoothing window costs a run its first samples. Ten seconds sounds immaterial until it is the whole verdict: a 295 s run cleared the gate, and a reader comparing against a round five minutes concludes it did not. This change exists so a line can be read correctly, so an interpretation guide that is itself off by 10 s defeats it. Both docs now name 290 s and where it comes from, and both tests pin the number against the two constants it is derived from, so tuning either one cannot leave the documented figure silently stale. Detector untouched; no number moves. Kotlin verified locally (suite 4307/0, assembleFullDebug); Swift twin rides swift-packages CI.
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.
Diagnostic only. The detector is untouched and no number moves.
What a field log showed
A WHOOP 4.0 strap log, 22 days, every one
kept=0:minExerciseMindroppedShortis tested first and short-circuits, so that second row means sustained five-minute-plus efforts reached the intensity gate on all 22 days — roughly 18 a day. The detector was right to reject them (the reporter confirmed they had not trained; sustained walking is not a workout), and the funnel's arithmetic is self-consistent throughout.I also cleared the plausible bugs:
bridgeGapSis 300 s, andbridgeRunsalready treats an empty gap-HR as a sensor dropout and bridges it, so 4.0 motion sparsity was not silently shattering bouts.The gap
The counts say where candidates died. They cannot say how substantial the best survivor was — and that is the whole difference between:
Both print identical counts. Working the first case out from this log meant inferring run length by dividing
activebyruns, which gave the wrong answer (it suggested one-minute bursts, whilelowIntensity=15proved runs were clearing five minutes every day).The change
DetectionFunnelgainslongestRunSandmeanRunS, measured on the bridged runs before any qualification gate:Verification
longestRunS— 312 s (a walk) against 3600 s (worth chasing). The counts alone cannot tell them apart, which is the point.DetectionFunnelTest10/10, full suite 4307 green,assembleFullDebugand doc lint clean.StrandAnalyticscannot build on Linux (GRDB's CSQLite needssqlite3.h, per CLAUDE.md), so the Swift twin ridesswift-packagesCI rather than a local run.Note on the second commit
The first commit read the same log as one-minute bursts of daily life and wrote that into both the docs and the tests. That contradicted the counts printed beside it —
droppedLowIntensitycannot be non-zero unless runs cleared the duration bar. The second commit corrects the reading and replaces both fixtures, which previously could be distinguished by their counts alone and so did not test what they claimed to.