Repository navigation
analytics: restore the primary-night resting-HR floor - #2557
Conversation
|
@kavemang Reviewed. 17/17 green, including Checked rather than assumed:
One thing the summary understates. Besides mean to floor, the cross-session Deliberate and tested, and narrower than it looks, since One finding, not blocking. That nil still lowers val rhrBaselineTrusted = baselineWindow.count { it.restingHr != null } >= MIN_BASELINE_NIGHTS // 14So a day that used to get a nap-derived RHR no longer counts toward that threshold. Your base is 9 commits behind main. Rebase and I will take it. Thanks for the R1 to R8 framing, and for marking R1 and R5 unavailable rather than reaching for a number. |
f6b47b0 to
2c73b27
Compare
`main` fails its own ratchet at 5de0519, on a clean tree, with no change applied: ERROR: Tools/parity_ledger_baseline.json: accepted finding group regressed test-only-callsite|Packages/StrandAnalytics ERROR: Tools/parity_ledger_baseline.json: accepted finding group regressed test-only-callsite|android/analytics I caused it by merging #2557. That change took the daily resting HR from the primary night's gated five-minute floor instead of the whole-session mean, which replaced the only production call to `primarySessionRestingHR` on both platforms: - ?? primarySessionRestingHR(physiologySessions, hr)?.roundToInt() + ?: primarySession?.restingHR The function is now reached from four test files and nothing else, so the lexical call graph emits one new `test-only-callsite` finding per platform. The checked-in baseline did not record them. This records them: 43 to 44 in `Packages/StrandAnalytics`, 64 to 65 in `android/analytics`, two lines, nothing else moved. Why nothing caught it: #2557 touched no `Tools/parity_*` path, so parity-governance never ran on it. Its seventeen green checks were real and did not include the gate that governs call-site structure. #2575 and #2576 landed the same way today and happened to be harmless. Why my review did not catch it: I checked that `primarySessionRestingHRWithCoverage` still had callers and concluded the old mean was not dead code. That is true of the `WithCoverage` variant and false of the plain function, which is the one #2557 orphaned. I verified the neighbouring symbol. Recording rather than deleting, deliberately. The findings are true, and the baseline exists to hold accepted findings. Whether `primarySessionRestingHR` should survive as a tested-but-unused function, or be deleted with its tests repointed at the `WithCoverage` variant, is a change to code merged an hour ago and belongs in its own review. A governance repair that also deletes product code gets neither part looked at properly. This does not make the debt go away, it makes it visible. The next change in either scope inherits 44 and 65 as the ceiling, so a second orphaned function there fires the gate again. Refs #2557, #2567
`Tools/tests/test_parity_governance_acceptance.py` does not test the scanner. It asserts that the CHECKED-IN authority and baseline still reproduce from the CURRENT product source, so a product change is what invalidates it. It ran only in parity-governance.yml, whose filter names `Tools/parity_*`. So a change could move the declaration and call-site graph the ledger measures without the gate that measures it ever running. That is not hypothetical. Earlier today #2557 took the only production call to `primarySessionRestingHR` on both platforms, turning it test-only, and the two resulting findings went unrecorded. Seventeen green checks, none of them this one. `main` carried a red ratchet until #2581, and it surfaced on #2577, an outside contributor's PR that had caused none of it. parity-governance.yml's own header notes that shape had already happened four times, three of them on an outside contributor. Here, and not behind a filter naming product paths, for the reason the guard test below already gives about a different suite: the list would be whatever the assertions happen to read today, so a filter only moves the trap. #1691 is the precedent, where a `Tools/**` filter silenced a suite that reads `TodayScreen.kt` and `AppModel.swift`, and main carried it red through nine merges. Its own step and its own floor, so the core floor of 114 stays independent, which is the reasoning already written beside that floor. Cost, measured on this runner rather than estimated: 51 s for 49 tests, against roughly 25 s for the rest of the leg. I first assumed this suite cost 4 to 6 minutes and designed a separate parallel workflow around that; the 4 to 6 minutes is `parity_ratchet.py`, which checks out the base tree and derives twice. This suite derives once. The guard is extended to assert the unfiltered leg actually invokes the module. The failure mode is not someone arguing against this, it is someone dropping the step or hanging a filter off it later, which is exactly what #1691 was. Mutation-verified: pointing the step at a different module makes the guard fail. The assertion reads the dotted module path, which appears only in the invocation and not in the prose above it, so a comment cannot satisfy it. Verified: `tests.test_parity_governance_acceptance` plus `tests.test_parity_disposition_kinds`, 51 tests, pass. The workflow parses. Refs #2567, #2557, #1691
`Tools/tests/test_parity_governance_acceptance.py` does not test the scanner. It asserts that the CHECKED-IN authority and baseline still reproduce from the CURRENT product source, so a product change is what invalidates it. It ran only in parity-governance.yml, whose filter names `Tools/parity_*`. So a change could move the declaration and call-site graph the ledger measures without the gate that measures it ever running. That is not hypothetical. Earlier today #2557 took the only production call to `primarySessionRestingHR` on both platforms, turning it test-only, and the two resulting findings went unrecorded. Seventeen green checks, none of them this one. `main` carried a red ratchet until #2581, and it surfaced on #2577, an outside contributor's PR that had caused none of it. parity-governance.yml's own header notes that shape had already happened four times, three of them on an outside contributor. Here, and not behind a filter naming product paths, for the reason the guard test below already gives about a different suite: the list would be whatever the assertions happen to read today, so a filter only moves the trap. #1691 is the precedent, where a `Tools/**` filter silenced a suite that reads `TodayScreen.kt` and `AppModel.swift`, and main carried it red through nine merges. Its own step and its own floor, so the core floor of 114 stays independent, which is the reasoning already written beside that floor. Cost, measured on this runner rather than estimated: 51 s for 49 tests, against roughly 25 s for the rest of the leg. I first assumed this suite cost 4 to 6 minutes and designed a separate parallel workflow around that; the 4 to 6 minutes is `parity_ratchet.py`, which checks out the base tree and derives twice. This suite derives once. The guard is extended to assert the unfiltered leg actually invokes the module. The failure mode is not someone arguing against this, it is someone dropping the step or hanging a filter off it later, which is exactly what #1691 was. Mutation-verified: pointing the step at a different module makes the guard fail. The assertion reads the dotted module path, which appears only in the invocation and not in the prose above it, so a comment cannot satisfy it. Verified: `tests.test_parity_governance_acceptance` plus `tests.test_parity_disposition_kinds`, 51 tests, pass. The workflow parses. Refs #2567, #2557, #1691
Summary
Refs #2522.
.min()fallback: when the selected primary night has no HR samples, daily resting HR stays absent instead of borrowing a nap value. That can lower RHR baseline-night coverage; Android’sV5HealthSignalstrust gate needs 14 nights with RHR when the HRV baseline is also sparse.rhr_primary_sessionas a shadow metric, and update its comments to describe that role.This changes a scored input and may move Charge, Effort, and other downstream values on re-score. It restores the previous floor definition within the primary night; it makes no claim of measured accuracy improvement. Oura's API documents a lowest five-minute HR and a separate average HR for each sleep session.
Verification
PrimaryNightRestingHRDailyTest).PrimarySessionRestingHRTestandAnalyticsEngineRefusedMainNightHrvTestpassed.python3 Tools/doc_comment_lint.pyandgit diff --checkpassed.Validation
cd android && JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./gradlew :app:testFullDebugUnitTest --tests 'com.noop.analytics.PrimaryNightRestingHRDailyTest' --offline --no-daemonand the two existing suites above.AnalyticsEnginepreviously selectedPrimarySessionRestingHR.meanHRbefore the session floor; the new fixture reaches that exact path and distinguishes 55 bpm floor from >69 bpm mean.Command(s): the Android commands above; Swift counterpart is
cd Packages/StrandAnalytics && swift test --filter PrimaryNightRestingHRDailyTestson a Swift-equipped host.Date run / input: 2026-09-28; checked-in synthetic fixtures.
Prediction (pre-registered in the tests): daily RHR is 55 bpm for the mixed main-night/nap fixture, 62 bpm for the provided-value fixture, and absent when the main night has no HR.
Outcome (confirmed): Android fixtures passed. Swift fixtures remain for CI.