From e60e2a1bfe901b556f4f606af33884a4b11b0daf Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sat, 1 Aug 2026 10:08:09 +0530 Subject: [PATCH 1/2] raise nap cap to 6h so long secondary sleep isn't dropped --- lib/src/onehz/human/coaching.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/onehz/human/coaching.dart b/lib/src/onehz/human/coaching.dart index fee25d7..9cef3b2 100644 --- a/lib/src/onehz/human/coaching.dart +++ b/lib/src/onehz/human/coaching.dart @@ -44,7 +44,12 @@ Metric> detectNaps( }) { const inputs = ['accel_1hz', 'hr_1hz']; const minNapSec = 20 * 60; - const maxNapSec = 3 * 3600; + // Was 3h, which silently dropped a real second sleep block (biphasic/ + // split sleep, shift work) — it's too long to be a nap but also loses to + // the main sleep pick, so it just vanished from every output with no + // signal it ever existed. 6h still excludes anything long enough to be + // arguably its own main sleep, while catching genuine secondary sleep. + const maxNapSec = 6 * 3600; final n = math.min(accel.length, hr.length); if (n < minNapSec) { return const Metric>.absent( From 1b6f7d49d04c1ceae4e1db6c7c38376eb2d29e04 Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sat, 1 Aug 2026 10:43:13 +0530 Subject: [PATCH 2/2] fix stale 3h nap-cap test, add 6h boundary coverage --- test/onehz/coaching_test.dart | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/test/onehz/coaching_test.dart b/test/onehz/coaching_test.dart index c0e3cd9..dce00f2 100644 --- a/test/onehz/coaching_test.dart +++ b/test/onehz/coaching_test.dart @@ -446,14 +446,30 @@ void main() { expect(m.tier, Tier.estimate); expect(m.value, isNotEmpty); for (final nap in m.value!) { - // Every reported nap honors the 20 min – 3 h envelope. + // Every reported nap honors the 20 min – 6 h envelope. expect(nap.durationSec, greaterThanOrEqualTo(20 * 60)); - expect(nap.durationSec, lessThanOrEqualTo(3 * 3600)); + expect(nap.durationSec, lessThanOrEqualTo(6 * 3600)); expect(nap.endSec, greaterThan(nap.startSec)); expect(nap.confidence, inInclusiveRange(0.0, 1.0)); } }); + test('a long secondary sleep block (>3h, <=6h) is now captured, not dropped', () { + final day = buildDay(activeMin: 90, napMin: 4 * 60); + final m = detectNaps(day.accel, day.hr); // no main window → not excluded + expect(m.present, isTrue); + expect(m.value, isNotEmpty); + expect(m.value!.first.durationSec, greaterThan(3 * 3600)); + expect(m.value!.first.durationSec, lessThanOrEqualTo(6 * 3600)); + }); + + test('a block longer than 6h is still rejected as a nap', () { + final day = buildDay(activeMin: 90, napMin: 7 * 60); + final m = detectNaps(day.accel, day.hr); + // 7h exceeds maxNapSec — no qualifying nap reported for that block. + expect(m.value, isEmpty); + }); + test('the main sleep window is carved out (no nap overlaps it)', () { final day = buildDay(activeMin: 90, napMin: 150); // Main window = the whole still block's index range (indices into arrays).