Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 81 additions & 12 deletions lib/compute/derivation_engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Comment on lines +893 to +895

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.

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

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.

Future<Substrate> _loadSubstrateRange(
Expand Down
68 changes: 68 additions & 0 deletions lib/firebase_options.dart
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

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.

}