Skip to content

log(hrv): describe the main night the gate judged in the hrv diag line - #2427

Merged
ryanbr merged 1 commit into
ryanbr:mainfrom
pipiche38:fix/hrv-diag-main-night-verdict
Sep 24, 2026
Merged

ryanbr merged 1 commit into
ryanbr:mainfrom
pipiche38:fix/hrv-diag-main-night-verdict

Conversation

@pipiche38

@pipiche38 pipiche38 commented Sep 23, 2026 •

Copy link
Copy Markdown

Stacked on #2426. This branch contains #2426's commit (c7a678c32) plus one commit (ea58148ef). Review the second commit; once #2426 merges, this diff reduces to it alone.

Problem

This is the second half of #2425. The always-on hrv diag line pooled every sleep session of the day and divided their beat-time by first-to-last beat, so the hours between a night and a nap entered the denominator. On a real day whose main night the #1118 gate refused at 1.31 coverage, the line printed:

hrv diag day=2026-09-20 … coverage=0.48 … rrIntegrity=underCovered

The log called a night refused for an over-count "not an over-count", and it sent triage of that report the wrong way first. Two other readers use the same rows: hrvOverCounted (the HRV card's "unverified" flag) and the RSA respiration gate. Both sided with the log against the gate.

Change

  • analyzeDay exposes the main-night group it already scores from, as DayResult.mainNightBlocks (the same mainGroup, not re-derived). It's trailing and defaulted on both platforms, and each has a single construction site.
  • A new pure helper, AnalyticsEngine.hrvDiagnosticRows, selects that group's beats over the gate's own inclusive [start, end] window. On a one-block night that's exactly the set of beats SleepStager.sessionHrvOverCounted judged, so the over-count half of the verdict cannot disagree with the gate.
  • A day with no main night keeps the old pooled, half-open set.
  • Swift and Kotlin IntelligenceEngine read the rows through the helper. The line's format is unchanged; its fields now describe the main night.
  • Known limit: a bridged multi-fragment night still spans its short bridge gap, which can only pull coverage down.

Verification

  • HrvDiagnosticRowsTests (Swift) and its Kotlin twin HrvDiagnosticRowsTest, same fixtures and expected values:
    • DayResult carries the scoring main group.
    • On a refused night, the selected rows are the night's alone and classify as an over-count, agreeing with the gate. The old pooled selection on the same day reads plausible, which is pinned so the test fails if the fixture stops reproducing the disagreement.
    • With no main night, the half-open fallback is kept.
  • swift test StrandAnalytics: 2056/0. Kotlin twin: 3/0.
  • Full Android unit suite: the same 8 failures as untouched upstream/main (RecoveryDrivers, StandardHrSensorFormat, StepsDetailIntegration, TodayExplainability), none in touched code.
  • App-target Swift changed (IntelligenceEngine.swift), so both app targets were built locally: macOS Strand and iOS NOOPiOS (generic Simulator) both build.
  • doc_comment_lint: OK. No UI strings.

Refs #2425, #1118

The always-on `hrv diag` line pooled every sleep session of the day and
divided their beat-time by first-to-last beat, so the hours between a
night and a nap entered the denominator. On a real day whose main night
the ryanbr#1118 gate refused at 1.31 coverage, the line printed
`coverage=0.48 rrIntegrity=underCovered`: it called a night refused for
an over-count "not an over-count". `hrvOverCounted` (the HRV card's
unverified flag) and the RSA resp gate read the same rows, so they sided
with the log against the gate.

`analyzeDay` now exposes the main-night group it already scores from
(`DayResult.mainNightBlocks`, the same `mainGroup`), and a pure
`AnalyticsEngine.hrvDiagnosticRows` selects that group's beats over the
gate's own inclusive [start, end] window. On a one-block night that is
exactly the set `SleepStager.sessionHrvOverCounted` judged, so the
over-count half of the verdict cannot disagree with the gate. A day with
no main night keeps the old pooled, half-open set. The line's format is
unchanged; its fields now describe the main night.

Stacked on the refused-main-night fix, which introduces
`sessionHrvOverCounted`.

Tests: HrvDiagnosticRowsTests (Swift) and its Kotlin twin, same fixtures
and expected values: the DayResult carries the scoring main group; on a
refused night the selected rows are the night's only and classify as an
over-count, agreeing with the gate, while the old pooled selection reads
`plausible`; no main night keeps the half-open fallback. StrandAnalytics
2056/0; Kotlin twin 3/0; full Android suite shows the same 8
pre-existing failures as upstream/main. macOS `Strand` and iOS `NOOPiOS`
both build. doc_comment_lint OK.

Refs ryanbr#2425, ryanbr#1118

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvMtCweTWqqM9iyDBz88XX
@ryanbr
ryanbr force-pushed the fix/hrv-diag-main-night-verdict branch from ea58148 to 4dee916 Compare September 24, 2026 01:30
@ryanbr
ryanbr merged commit f412fa0 into ryanbr:main Sep 24, 2026
16 checks passed
@pipiche38
pipiche38 deleted the fix/hrv-diag-main-night-verdict branch September 24, 2026 16:08
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.

2 participants