Feat/v42 stager - #81
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe derivation engine now uses algorithm version 42 with personalized cardio-based sleep profiling. A committed Firebase options file provides Android and iOS configurations and rejects unsupported platforms. ChangesPersonalized sleep profiling
Firebase platform configuration
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant DerivationEngine
participant LocalDb
participant Analytics
DerivationEngine->>LocalDb: Load sleep_user_profile
LocalDb-->>DerivationEngine: Return profile payload
DerivationEngine->>Analytics: Enable cardio observation recording
DerivationEngine->>Analytics: Prepare sleep session candidate
Analytics-->>DerivationEngine: Return staged candidate and observations
DerivationEngine->>Analytics: Fold MAIN sleep into profile
DerivationEngine->>LocalDb: Persist updated sleep_user_profile
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/compute/derivation_engine.dart`:
- Around line 833-895: Serialize the personalized staging sequence in the
surrounding method with a lock covering _loadSleepUserProfile, observation
reset, prepareSleepSessionCandidate, and _foldSleepUserProfile so shared
ana.cardio* state and the sleep_user_profile baseline cannot overlap across
days. Ensure ana.cardioRecordObservations is restored to false in a finally
block, including when candidate preparation or persistence fails, while
preserving override and fresh-stage behavior.
- Around line 887-889: Update the main-sleep gate immediately after selecting
main in the observation derivation flow so it uses the intended 60-minute
duration, accounting for epochs being 1-second samples. Preserve the existing
early-return behavior for observations shorter than that threshold.
In `@lib/firebase_options.dart`:
- Around line 52-67: The FirebaseOptions constants android and ios still contain
placeholder values, and initialization failures are silently ignored. Replace
the dummy configuration with release-provided Firebase values and add a
CI/release validation step that rejects placeholder fields such as dummy API
keys, project IDs, app IDs, and bundle identifiers before release; ensure both
Firebase initialization call sites surface failures instead of swallowing them.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 38e84537-0d6f-4a2a-b068-a50fc2e73c2b
📒 Files selected for processing (3)
.gitignorelib/compute/derivation_engine.dartlib/firebase_options.dart
| // 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<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. | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// 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())); | ||
| } | ||
|
|
There was a problem hiding this comment.
🗄️ 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. runWithConcurrency keeps days in flight together, and this block mutates shared ana.cardio* ambient state plus the single sleep_user_profile baseline row. One day's _loadSleepUserProfile() / observation reset can bleed into another day's prepareSleepSessionCandidate(), and concurrent _foldSleepUserProfile() writes will lose updates. Wrap this whole sequence in a lock and reset ana.cardioRecordObservations in finally.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/compute/derivation_engine.dart` around lines 833 - 895, Serialize the
personalized staging sequence in the surrounding method with a lock covering
_loadSleepUserProfile, observation reset, prepareSleepSessionCandidate, and
_foldSleepUserProfile so shared ana.cardio* state and the sleep_user_profile
baseline cannot overlap across days. Ensure ana.cardioRecordObservations is
restored to false in a finally block, including when candidate preparation or
persistence fails, while preserving override and fresh-stage behavior.
| obs.sort((a, b) => b.epochs.compareTo(a.epochs)); | ||
| final main = obs.first; | ||
| if (main.epochs < 120) return; // require ≥60 min — not a nap |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find CardioObservation's epoch definition/duration.
fd -e dart . | xargs rg -n -A5 -B5 'class CardioObservation|\bepochs\b' 2>/dev/nullRepository: OpenStrap/edge
Length of output: 6137
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the CardioObservation type and any epoch-duration fields/constants.
rg -n -A8 -B8 'class CardioObservation|takeCardioObservations\(|epoch_sec|epochs\b|epochLength|epochLengthSec|epoch_sec' lib . 2>/dev/null | sed -n '1,260p'Repository: OpenStrap/edge
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -e dart . | xargs rg -n -A8 -B8 'class CardioObservation|takeCardioObservations\(|epoch_sec|epochs\b|epochLength|epochLengthSec' 2>/dev/null | sed -n '1,260p'Repository: OpenStrap/edge
Length of output: 8865
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the declaration and construction of CardioObservation, plus any epoch units.
rg -n -A6 -B6 'CardioObservation|takeCardioObservations\(|epoch_sec|epochSec|epochs\s*[:=]' lib test . 2>/dev/null | sed -n '1,220p'Repository: OpenStrap/edge
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search only source files for the CardioObservation type and takeCardioObservations().
fd -e dart lib . | xargs rg -n -A4 -B4 --max-count 3 'CardioObservation|takeCardioObservations\(' 2>/dev/null | sed -n '1,220p'Repository: OpenStrap/edge
Length of output: 156
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git ls-files '*.dart' | xargs rg -n -A4 -B4 --max-count 5 'CardioObservation|takeCardioObservations\(|epoch_sec|epochLength|epochs\b' 2>/dev/null | sed -n '1,240p'Repository: OpenStrap/edge
Length of output: 5280
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '780,895p' lib/compute/derivation_engine.dart | cat -nRepository: OpenStrap/edge
Length of output: 5391
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git ls-files '*.dart' | xargs rg -n -A6 -B6 'class SleepUserProfile|SleepUserProfile\.fold|fold\(main\)|epoch_sec|epochs\b' 2>/dev/null | sed -n '1,220p'Repository: OpenStrap/edge
Length of output: 7164
Main-sleep gate is 2 minutes, not 60 minutes. epochs is a 1 Hz sample count (epoch_sec: 1 in lib/compute/onehz_pipeline.dart), so main.epochs < 120 only skips ~120 seconds. Raise the threshold or compare on elapsed seconds if the goal is to exclude naps.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/compute/derivation_engine.dart` around lines 887 - 889, Update the
main-sleep gate immediately after selecting main in the observation derivation
flow so it uses the intended 60-minute duration, accounting for epochs being
1-second samples. Preserve the existing early-return behavior for observations
shorter than that threshold.
| 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', | ||
| ); |
There was a problem hiding this comment.
🗄️ 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. lib/firebase_options.dart still hardcodes dummy Android/iOS FirebaseOptions, and both init call sites swallow failures, so Firebase-backed features can be disabled silently. Add a CI/release check that rejects placeholder values.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/firebase_options.dart` around lines 52 - 67, The FirebaseOptions
constants android and ios still contain placeholder values, and initialization
failures are silently ignored. Replace the dummy configuration with
release-provided Firebase values and add a CI/release validation step that
rejects placeholder fields such as dummy API keys, project IDs, app IDs, and
bundle identifiers before release; ensure both Firebase initialization call
sites surface failures instead of swallowing them.
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.
This pull request introduces a major update to the sleep staging algorithm and adds support for Firebase initialization. The most significant change is the implementation of a personalized, self-improving cardio sleep stager, which uses a rolling per-user profile to enhance sleep stage detection over time. Additionally, the pull request adds a generated
firebase_options.dartfile for Firebase integration.Personalized sleep staging improvements:
kAlgoVersionto 42 and implemented a personalized cardio stager that uses a rolling per-user sleep profile, blending it with nightly baselines to improve staging accuracy over time. This includes new REM detection features and an EWMA-folded profile that self-seeds across derivations._loadSleepUserProfileand_foldSleepUserProfileto manage loading and updating the per-user sleep profile, ensuring the profile is updated only for freshly staged main sleep sessions (not for overrides or cached results).Firebase integration:
firebase_options.dartfile with dummy configuration to enable platform-specific Firebase initialization in the app.Summary by CodeRabbit
New Features
Improvements
Compatibility