-
-
Notifications
You must be signed in to change notification settings - Fork 94
Feat/v42 stager #81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/v42 stager #81
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,19 +830,73 @@ class DerivationEngine { | |
| dayId: dayId, | ||
| stats: stats, | ||
| ); | ||
| final candidate = prepareSleepSessionCandidate( | ||
| searchSub, | ||
| targetDay: dayId, | ||
| override: override, | ||
| ); | ||
| if (override == null) { | ||
| await LocalDb.putSleepSessionCandidate( | ||
| dayId: dayId, | ||
| algoVersion: kAlgoVersion, | ||
| payloadJson: jsonEncode(candidate.toJson()), | ||
| // 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(); | ||
| // `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, | ||
| ); | ||
| 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 | ||
| /// (`baselines` key `sleep_user_profile`). Absent/corrupt ⇒ cold start (null). | ||
| Future<void> _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<String, dynamic>()); | ||
| } | ||
| } catch (_) { | ||
| // Cold start on unreadable payload. | ||
| } | ||
| } | ||
| return candidate; | ||
| } | ||
|
|
||
| /// EWMA-fold the just-staged main sleep into the per-user profile and persist. | ||
| Future<void> _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())); | ||
| } | ||
|
|
||
|
Comment on lines
+833
to
+901
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Confirm whether the ana.cardio* symbols are global/static mutable state.
fd -e dart . | xargs rg -n -A2 -B2 'cardioUserProfile|cardioRecordObservations|resetCardioObservations|takeCardioObservations' 2>/dev/nullRepository: OpenStrap/edge Length of output: 3039 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== candidate files =="
fd -e dart . | rg 'analytics|openstrap|derivation_engine|compute' || true
echo
echo "== state definitions / use sites =="
rg -n -A3 -B3 'cardioUserProfile|cardioRecordObservations|resetCardioObservations|takeCardioObservations|class .*Ana|class .*Analytics|final ana|var ana|static .*cardio' lib . 2>/dev/null
echo
echo "== concurrency path =="
rg -n -A4 -B4 'runWithConcurrency|_maxDeriveConcurrency|runDays|run\(' lib/compute/derivation_engine.dart lib 2>/dev/nullRepository: OpenStrap/edge Length of output: 50370 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== ana definition / type =="
rg -n -A6 -B6 '(^|[^A-Za-z0-9_])ana\b|class .*Ana|class .*Analytics|sleep_user_profile|cardioUserProfile|cardioRecordObservations' lib/compute lib | head -n 250
echo
echo "== _sleepCandidateForDay and surrounding flow =="
sed -n '780,900p' lib/compute/derivation_engine.dart
echo
echo "== runWithConcurrency implementation =="
sed -n '395,440p' lib/compute/derivation_engine.dartRepository: OpenStrap/edge Length of output: 25283 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== package config entry for openstrap_analytics =="
cfg=$(fd -a package_config.json .dart_tool 2>/dev/null | head -n 1 || true)
if [ -n "${cfg:-}" ]; then
jq -r '.packages[] | select(.name=="openstrap_analytics") | {name, rootUri, packageUri}' "$cfg"
else
echo "package_config.json not found"
fi
echo
echo "== possible package source locations =="
for p in \
"$(pwd)/.dart_tool/package_config.json" \
"$(pwd)/../.dart_tool/package_config.json" \
"$HOME/.pub-cache/hosted" \
"$HOME/.pub-cache/git"
do
[ -e "$p" ] && echo "$p"
done
echo
echo "== locate onehz.dart or cardio symbols in cache/workspace =="
fd -a 'onehz.dart' . "$HOME/.pub-cache" 2>/dev/null | head -n 50
rg -n --hidden --no-messages 'cardioUserProfile|cardioRecordObservations|resetCardioObservations|takeCardioObservations' "$HOME/.pub-cache" . 2>/dev/null | head -n 200Repository: OpenStrap/edge Length of output: 1005 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== prepareSleepSessionCandidate definition/usages =="
rg -n -A6 -B6 'prepareSleepSessionCandidate\s*\(' lib/compute lib 2>/dev/null
echo
echo "== any async/await inside the staging path =="
rg -n -A2 -B2 'await .*segmentSleep|await .*cardio|await .*prepareSleepSessionCandidate|Future<.*prepareSleepSessionCandidate|segmentSleep\s*\(' lib 2>/dev/null
echo
echo "== lock / serialization already present around sleep staging =="
rg -n -A4 -B4 'Lock\(|synchronized\(|mutex|serialize|cardioRecordObservations|cardioUserProfile' lib 2>/dev/null | head -n 200Repository: OpenStrap/edge Length of output: 11592 Serialize the sleep-profile section. 🤖 Prompt for AI Agents |
||
| Future<Substrate> _loadSubstrateRange( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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', | ||
| ); | ||
|
Comment on lines
+52
to
+67
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n## Files\n'
git ls-files 'lib/firebase_options.dart' 'lib/main.dart' 'lib/compute/background_derivation.dart' '.gitignore' 'lib/.gitignore'
printf '\n## firebase_options.dart (range)\n'
cat -n lib/firebase_options.dart | sed -n '1,220p'
printf '\n## main.dart (relevant parts)\n'
cat -n lib/main.dart | sed -n '1,260p'
printf '\n## background_derivation.dart (relevant parts)\n'
cat -n lib/compute/background_derivation.dart | sed -n '1,260p'
printf '\n## gitignore files\n'
for f in .gitignore lib/.gitignore; do
if [ -f "$f" ]; then
echo "--- $f"
cat -n "$f"
fi
doneRepository: OpenStrap/edge Length of output: 18109 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n## Locate currentPlatform usage and dummy markers\n'
rg -n "currentPlatform|dummy_api_key|dummy-project|FirebaseOptions\\(" lib
printf '\n## AST outlines\n'
ast-grep outline lib/firebase_options.dart --view expanded || true
ast-grep outline lib/main.dart --view expanded || true
ast-grep outline lib/compute/background_derivation.dart --view expanded || trueRepository: OpenStrap/edge Length of output: 1284 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for path in [Path('lib/firebase_options.dart'), Path('lib/main.dart'), Path('lib/compute/background_derivation.dart'), Path('.gitignore'), Path('lib/.gitignore')]:
if path.exists():
print(f'--- {path}')
txt = path.read_text()
for i, line in enumerate(txt.splitlines(), 1):
if any(k in line for k in ['currentPlatform', 'dummy_api_key', 'dummy-project', 'Firebase.initializeApp', 'FirebaseOptions', 'Firebase.initialize']):
print(f'{i}: {line}')
PYRepository: OpenStrap/edge Length of output: 1535 🏁 Script executed: #!/bin/bash
set -euo pipefail
for f in lib/firebase_options.dart lib/main.dart lib/compute/background_derivation.dart .gitignore lib/.gitignore; do
[ -f "$f" ] || continue
echo "=== $f ==="
nl -ba "$f" | sed -n '1,220p' | grep -E 'currentPlatform|dummy_api_key|dummy-project|Firebase.initializeApp|FirebaseOptions|catch|try|ignore|firebase_options' || true
doneRepository: OpenStrap/edge Length of output: 418 Replace the placeholder Firebase options before release. 🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: OpenStrap/edge
Length of output: 6137
🏁 Script executed:
Repository: OpenStrap/edge
Length of output: 50371
🏁 Script executed:
Repository: OpenStrap/edge
Length of output: 8865
🏁 Script executed:
Repository: OpenStrap/edge
Length of output: 50371
🏁 Script executed:
Repository: OpenStrap/edge
Length of output: 156
🏁 Script executed:
Repository: OpenStrap/edge
Length of output: 5280
🏁 Script executed:
Repository: OpenStrap/edge
Length of output: 5391
🏁 Script executed:
Repository: OpenStrap/edge
Length of output: 7164
Main-sleep gate is 2 minutes, not 60 minutes.
epochsis a 1 Hz sample count (epoch_sec: 1inlib/compute/onehz_pipeline.dart), somain.epochs < 120only skips ~120 seconds. Raise the threshold or compare on elapsed seconds if the goal is to exclude naps.🤖 Prompt for AI Agents