From 5d4cdc1d4936b753d8fa5d42e98b2eb3856bd0f5 Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Wed, 15 Jul 2026 22:33:10 +0530 Subject: [PATCH 1/3] chore: add dummy firebase_options.dart for local development --- .gitignore | 2 +- lib/firebase_options.dart | 68 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 lib/firebase_options.dart diff --git a/.gitignore b/.gitignore index fe01b834..1c629f93 100644 --- a/.gitignore +++ b/.gitignore @@ -71,7 +71,7 @@ pubspec_overrides.yaml # Firebase configurations android/app/google-services.json ios/Runner/GoogleService-Info.plist -lib/firebase_options.dart + firebase.json # one-off local debug scratch files - had a handful of these lying around diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 00000000..195b11d3 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,68 @@ +// File generated by FlutterFire CLI (Dummy version). +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'dummy_api_key_for_android', + appId: '1:000000000000:android:0000000000000000000000', + messagingSenderId: '000000000000', + projectId: 'dummy-project', + storageBucket: 'dummy-project.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'dummy_api_key_for_ios', + appId: '1:000000000000:ios:0000000000000000000000', + messagingSenderId: '000000000000', + projectId: 'dummy-project', + storageBucket: 'dummy-project.appspot.com', + iosBundleId: 'wtf.openstrap.dummy', + ); +} From 4456c0f7149be3c065960b164ee4acaaf9f7bc74 Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Wed, 15 Jul 2026 22:34:17 +0530 Subject: [PATCH 2/3] feat: update to v42 stager and derivation engine integration --- lib/compute/derivation_engine.dart | 65 +++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/lib/compute/derivation_engine.dart b/lib/compute/derivation_engine.dart index 6f075541..dea14c30 100644 --- a/lib/compute/derivation_engine.dart +++ b/lib/compute/derivation_engine.dart @@ -193,7 +193,22 @@ import 'substrate.dart'; // `_localNextDayLabelToSec` asks DateTime for the actual start of the next // day. Bump so recent days recompute onto the corrected readiness baseline; // only matters for history on the rare day that crossed a DST transition. -const int kAlgoVersion = 38; +// v39: night-tail sleep runs shorter than the 60-min standalone floor are no +// longer dropped when they continue the overnight chain (advanced_stager +// detectSleep) — a pre-dawn arousal that split off a <60-min tail was +// truncating the sleep-window offset at the arousal. Bump so affected days +// recompute the corrected (later) offset and downstream sleep/readiness metrics. +// v42: PERSONALIZED, self-improving cardio stager. (1) REM feature upgrades in +// cardioStager — LF/HF from the RR Lomb–Scargle spectrum + R(k)=mean|ΔIHR|, +// OR-combined with the RMSSD drop and gated by atonia + an HR floor (recovers +// under-called REM), plus a 3-epoch median flicker filter. (2) A rolling +// per-user sleep profile (baselines key `sleep_user_profile`) EWMA-folded after +// each finalized night and blended (bounded ≤0.5, growing with nights, 0 at +// cold start) with tonight's per-night-local baselines — so staging gets better +// over time while per-night-local always leads. Deep stays a low-confidence +// NREM sub-split (deep_low_confidence). The profile self-seeds across this +// re-derivation sweep; no explicit migration. Bump so every day re-stages. +const int kAlgoVersion = 42; /// Raw is kept this many days past derivation, then pruned (derived stays). const int rawRetentionDays = 3; @@ -815,21 +830,69 @@ class DerivationEngine { dayId: dayId, stats: stats, ); + // PERSONALIZED STAGER (v42): arm the sleeper's rolling profile before + // staging, and record this night's observed baselines for the fold below. + // `cardioStager` (inside segmentSleep) reads the ambient profile and blends + // it — bounded ≤0.5 — with tonight's per-night-local baselines. Runs in the + // main isolate here, so the ambient globals are in-scope for the call. + await _loadSleepUserProfile(); + ana.cardioRecordObservations = true; + ana.resetCardioObservations(); final candidate = prepareSleepSessionCandidate( searchSub, targetDay: dayId, override: override, ); + ana.cardioRecordObservations = false; if (override == null) { await LocalDb.putSleepSessionCandidate( dayId: dayId, algoVersion: kAlgoVersion, payloadJson: jsonEncode(candidate.toJson()), ); + // Fold the MAIN sleep (most epochs) of a freshly-staged night into the + // rolling profile. Skipped for overrides and cached reuse (this branch is + // the fresh-stage path). EWMA self-seeds across the v42 re-derivation + // sweep, so no explicit migration is needed. + await _foldSleepUserProfile(); + } else { + ana.resetCardioObservations(); } return candidate; } + /// Load the persisted per-user sleep profile into the analytics ambient slot + /// (`baselines` key `sleep_user_profile`). Absent/corrupt ⇒ cold start (null). + Future _loadSleepUserProfile() async { + ana.cardioUserProfile = null; + final row = await LocalDb.baseline('sleep_user_profile'); + final raw = row?['payload_json']; + if (raw is String && raw.isNotEmpty) { + try { + final decoded = jsonDecode(raw); + if (decoded is Map) { + ana.cardioUserProfile = + ana.SleepUserProfile.fromJson(decoded.cast()); + } + } catch (_) { + // Cold start on unreadable payload. + } + } + } + + /// EWMA-fold the just-staged main sleep into the per-user profile and persist. + Future _foldSleepUserProfile() async { + final obs = ana.takeCardioObservations(); + if (obs.isEmpty) return; + obs.sort((a, b) => b.epochs.compareTo(a.epochs)); + final main = obs.first; + if (main.epochs < 120) return; // require ≥60 min — not a nap + final base = ana.cardioUserProfile ?? const ana.SleepUserProfile(); + final updated = base.fold(main); + await LocalDb.putBaseline( + 'sleep_user_profile', jsonEncode(updated.toJson())); + } + Future _loadSubstrateRange( int fromRecTs, int toRecTs, { From 321ce9acac5c32b75ef57cfcbd7190b94d651863 Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Wed, 15 Jul 2026 23:31:12 +0530 Subject: [PATCH 3/3] fix(compute): reset cardioRecordObservations in a finally (v42 stager) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap sleep-candidate prep + persistence in try/finally so the analytics recording flag is always cleared, even if staging or a DB write throws — otherwise it leaks into the next day's derivation. Derivation is sequential in one isolate, so only the reset is needed (no lock). Override and fresh-stage fold behavior preserved. The MAIN-sleep duration gate (main.epochs < 120) was verified correct: epochs are 30-s (analytics _epochSec = 30), so 120 = 60 min; left unchanged. --- lib/compute/derivation_engine.dart | 46 +++++++++++++++++------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/lib/compute/derivation_engine.dart b/lib/compute/derivation_engine.dart index dea14c30..3212d6f9 100644 --- a/lib/compute/derivation_engine.dart +++ b/lib/compute/derivation_engine.dart @@ -838,27 +838,33 @@ class DerivationEngine { await _loadSleepUserProfile(); ana.cardioRecordObservations = true; ana.resetCardioObservations(); - final candidate = prepareSleepSessionCandidate( - searchSub, - targetDay: dayId, - override: override, - ); - ana.cardioRecordObservations = false; - if (override == null) { - await LocalDb.putSleepSessionCandidate( - dayId: dayId, - algoVersion: kAlgoVersion, - payloadJson: jsonEncode(candidate.toJson()), + // `finally` guarantees the recording flag is cleared even if staging or + // persistence throws — otherwise it leaks into the next day's derivation + // (this isolate is sequential, so no lock is needed, only the reset). + try { + final candidate = prepareSleepSessionCandidate( + searchSub, + targetDay: dayId, + override: override, ); - // Fold the MAIN sleep (most epochs) of a freshly-staged night into the - // rolling profile. Skipped for overrides and cached reuse (this branch is - // the fresh-stage path). EWMA self-seeds across the v42 re-derivation - // sweep, so no explicit migration is needed. - await _foldSleepUserProfile(); - } else { - ana.resetCardioObservations(); - } - return candidate; + if (override == null) { + await LocalDb.putSleepSessionCandidate( + dayId: dayId, + algoVersion: kAlgoVersion, + payloadJson: jsonEncode(candidate.toJson()), + ); + // Fold the MAIN sleep (most epochs) of a freshly-staged night into the + // rolling profile. Skipped for overrides and cached reuse (this branch + // is the fresh-stage path). EWMA self-seeds across the v42 + // re-derivation sweep, so no explicit migration is needed. + await _foldSleepUserProfile(); + } else { + ana.resetCardioObservations(); + } + return candidate; + } finally { + ana.cardioRecordObservations = false; + } } /// Load the persisted per-user sleep profile into the analytics ambient slot