Skip to content

Feat/v42 stager - #81

Merged
abdulsaheel merged 3 commits into
mainfrom
feat/v42-stager
Jul 15, 2026
Merged

Feat/v42 stager#81
abdulsaheel merged 3 commits into
mainfrom
feat/v42-stager

Conversation

@abdulsaheel

@abdulsaheel abdulsaheel commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

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.dart file for Firebase integration.

Personalized sleep staging improvements:

  • Bumped kAlgoVersion to 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.
  • Added methods _loadSleepUserProfile and _foldSleepUserProfile to 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:

  • Added a generated firebase_options.dart file with dummy configuration to enable platform-specific Firebase initialization in the app.

Summary by CodeRabbit

  • New Features

    • Added platform-specific Firebase configuration for Android and iOS.
  • Improvements

    • Sleep staging now learns from individualized cardio and sleep patterns over time to refine future staging.
    • Automatically staged sleep sessions persist and fold newly recorded MAIN sleep observations into the user’s rolling profile.
    • Updated sleep analytics derivation to use the latest algorithm version for recomputation.
  • Compatibility

    • Added runtime safeguards for unsupported platforms when Firebase settings aren’t configured.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9eb09c1a-2414-499e-a430-c08c9fc48c2f

📥 Commits

Reviewing files that changed from the base of the PR and between 4456c0f and 321ce9a.

📒 Files selected for processing (1)
  • lib/compute/derivation_engine.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/compute/derivation_engine.dart

📝 Walkthrough

Walkthrough

The 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.

Changes

Personalized sleep profiling

Layer / File(s) Summary
Sleep staging and algorithm version
lib/compute/derivation_engine.dart
Algorithm version 42 loads the sleep profile, records cardio observations during staging, and applies different persistence behavior for automatic and overridden sleep windows.
Profile loading and folding
lib/compute/derivation_engine.dart
New helpers load the stored profile, select MAIN sleep observations, fold them into the rolling profile, and persist the result.

Firebase platform configuration

Layer / File(s) Summary
Trackable platform options
.gitignore, lib/firebase_options.dart
The generated Firebase options file is no longer ignored and provides Android and iOS options while rejecting unsupported platforms.

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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly points to the v42 staging update, which is the main theme of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v42-stager

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between cf30713 and 4456c0f.

📒 Files selected for processing (3)
  • .gitignore
  • lib/compute/derivation_engine.dart
  • lib/firebase_options.dart

Comment on lines +833 to +895
// 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()));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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/null

Repository: 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/null

Repository: 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.dart

Repository: 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 200

Repository: 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 200

Repository: 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.

Comment on lines +887 to +889
obs.sort((a, b) => b.epochs.compareTo(a.epochs));
final main = obs.first;
if (main.epochs < 120) return; // require ≥60 min — not a nap

Copy link
Copy Markdown
Contributor

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:

#!/bin/bash
# Description: Find CardioObservation's epoch definition/duration.
fd -e dart . | xargs rg -n -A5 -B5 'class CardioObservation|\bepochs\b' 2>/dev/null

Repository: 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 -n

Repository: 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.

Comment thread lib/firebase_options.dart
Comment on lines +52 to +67
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',
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
done

Repository: 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 || true

Repository: 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}')
PY

Repository: 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
done

Repository: 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.
@abdulsaheel
abdulsaheel merged commit 6054d86 into main Jul 15, 2026
1 check passed
@abdulsaheel
abdulsaheel deleted the feat/v42-stager branch July 15, 2026 18:07
abdulsaheel added a commit that referenced this pull request Jul 15, 2026
Release cut of merged main (edge #81 + analytics #25) carrying the v42
personalized cardio stager. Tag v0.9.13 triggers the signed APK + IPA
release build.
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.

1 participant