Skip to content

fix irregular-rhythm window diffs + kill dup clamp helper - #58

Closed
abdulsaheel wants to merge 2 commits into
audit/fixes-round3-analyticsfrom
audit/fixes-round4-analytics
Closed

fix irregular-rhythm window diffs + kill dup clamp helper#58
abdulsaheel wants to merge 2 commits into
audit/fixes-round3-analyticsfrom
audit/fixes-round4-analytics

Conversation

@abdulsaheel

@abdulsaheel abdulsaheel commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

found the per-window irregular-rhythm check was diffing the compacted beat array positionally instead of skipping across a dropped artifact beat -- same bug the aggregate path already got fixed for in 6e11781, just not carried into the window pass. carries adjacency info through now.

also:

  • deleted the free clamp() in util.dart, it's a dupe of num.clamp -- swapped ~70 call sites over
  • _windowSdnn in cardio_stager now calls the shared stddev() instead of hand-rolling it
  • fixed a stale doc comment in onehz.dart pointing at a file that got deleted
  • added a quick-start snippet to the readme so there's one runnable example

ran dart analyze --fatal-infos and the full test suite, both clean.

Summary by Sourcery

Preserve beat adjacency in irregular-rhythm window analysis and consolidate shared numeric utilities.

New Features:

  • Add a runnable quick-start example to the README for computing an HRV metric.

Bug Fixes:

  • Correct irregular-rhythm window analysis to avoid calculating differences across dropped artifact beats.

Enhancements:

  • Replace the duplicated utility clamp helper with Dart's built-in num.clamp throughout the analytics code.
  • Reuse the shared standard-deviation implementation for cardio staging window calculations.
  • Update the one-hertz library documentation to reflect the removal of the legacy minute-resolution library.

Documentation:

  • Refresh the one-hertz library documentation and add a user-facing quick-start example.

Tests:

  • Remove obsolete tests for the deleted clamp helper.

…amp helper

per-window sustained-irregularity check was diffing the compacted nn array
positionally, so a dropped artifact beat inside a window manufactured a
spurious jump just like the aggregate path used to before 6e11781 -- carry
adjacency through into the window pass now.

also killed the free clamp() helper in util.dart since it's just
math.max(lo, math.min(hi,x)) and num.clamp already does that -- swapped all
call sites to the native method. replaced a hand-rolled stddev in
cardio_stager's _windowSdnn with the shared stddev() helper. fixed a stale
doc comment in onehz.dart pointing at a deleted file, and added a quick-start
snippet to the readme.
@sourcery-ai

sourcery-ai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR fixes windowed irregular-rhythm detection by carrying original-series adjacency through beat compaction, removes and replaces the duplicated clamp helper throughout the package, reuses shared standard-deviation logic in cardio staging, and refreshes documentation with corrected package guidance and a runnable quick start.

Sequence diagram for artifact-aware irregular-rhythm window detection

sequenceDiagram
    participant Caller
    participant Screen as irregularBeatScreen
    participant Windows as _sustainedAcrossWindows
    participant Stddev as stddev

    Caller->>Screen: irregularBeatScreen(rrMs, rrTimesMs, ...)
    Screen->>Screen: Build keep, nn, and nnAdjacent
    Screen->>Windows: _sustainedAcrossWindows(nn, nnTimes, nnAdjacent, ...)
    Windows->>Windows: Group beats into time windows
    Windows->>Windows: Keep diffs only when bucketAdjacent[i]
    Windows->>Stddev: stddev(diffs)
    Windows->>Stddev: stddev(bucket)
    Stddev-->>Windows: Window variability
    Windows-->>Screen: Sustained irregularity result
    Screen-->>Caller: Metric<IrregularRhythm>
Loading

Flow diagram for preserving beat adjacency across artifact removal

flowchart LR
    A[Original rrMs] --> B{Beat passes keep filter?}
    B -->|yes| C[Compacted nn beat]
    B -->|no| D[Dropped artifact beat]
    C --> E[Record nnAdjacent]
    D --> E
    E --> F[Build time window bucket]
    F --> G{Adjacent to prior original beat?}
    G -->|yes| H[Include successive difference]
    G -->|no| I[Skip difference across artifact]
    H --> J[Window stddev and irregularity flag]
    I --> J
Loading

File-Level Changes

Change Details Files
Preserve original beat adjacency when evaluating irregular-rhythm windows.
  • Build an adjacency flag aligned with the compacted valid-beat array.
  • Pass adjacency through the windowed evaluator and exclude diffs spanning dropped artifact beats.
  • Retain fail-closed validation for the new aligned metadata.
lib/src/onehz/clinical/irregular_rhythm.dart
Replace the duplicated clamp utility with Dart's native numeric clamp API.
  • Delete the free clamp helper and migrate callers to num.clamp().
  • Remove imports that were only needed for the helper.
  • Remove the obsolete helper test.
lib/src/onehz/util.dart
lib/src/onehz/clinical/cardiac_coherence.dart
lib/src/onehz/clinical/cosinor.dart
lib/src/onehz/clinical/hrv_freq.dart
lib/src/onehz/clinical/hrv_time.dart
lib/src/onehz/clinical/irregular_rhythm.dart
lib/src/onehz/clinical/load_trimp.dart
lib/src/onehz/clinical/nocturnal.dart
lib/src/onehz/clinical/prsa.dart
lib/src/onehz/clinical/readiness_lnrmssd.dart
lib/src/onehz/clinical/stress_si.dart
lib/src/onehz/human/associations.dart
lib/src/onehz/human/circadian_lifestyle.dart
lib/src/onehz/human/event_detection.dart
lib/src/onehz/human/percentile_of_you.dart
lib/src/onehz/human/readiness_glassbox.dart
lib/src/onehz/human/sleep_regularity.dart
lib/src/onehz/human/weekday_effect.dart
lib/src/onehz/motion/energy_fusion.dart
lib/src/onehz/motion/enmo.dart
lib/src/onehz/motion/orientation.dart
lib/src/onehz/motion/steps.dart
lib/src/onehz/respiration/brv_trend.dart
lib/src/onehz/respiration/cvhr_apnea.dart
lib/src/onehz/respiration/relative_odi.dart
lib/src/onehz/respiration/resp_rate.dart
lib/src/onehz/sleep/cardio_stager.dart
lib/src/onehz/sleep/circadian_np.dart
lib/src/onehz/sleep/nap.dart
lib/src/onehz/sleep/night_hrv_shape.dart
lib/src/onehz/sleep/segment.dart
lib/src/onehz/sleep/sri.dart
lib/src/onehz/sleep/van_hees.dart
lib/src/onehz/wellness/anomaly.dart
lib/src/onehz/wellness/cycle_lengths.dart
lib/src/onehz/wellness/readiness_composite.dart
lib/src/onehz/wellness/temp_circadian.dart
lib/src/onehz/workout/hr_recovery.dart
lib/src/onehz/workout/observed_max_hr.dart
test/onehz/util_test.dart
Reuse shared standard-deviation behavior in cardio staging.
  • Replace the local SDNN calculation with the shared stddev() helper while preserving the insufficient-data fallback.
lib/src/onehz/sleep/cardio_stager.dart
Update package documentation and usage guidance.
  • Correct the onehz.dart comment describing the removed minute-resolution library.
  • Add a runnable HRV quick-start example and explain the common Metric API shape.
lib/onehz.dart
README.md
Apply formatter-only cleanup across touched code.
  • Reflow long expressions, collection literals, declarations, and comments without changing their behavior.
lib/src/onehz/clinical/cardiac_coherence.dart
lib/src/onehz/clinical/load_trimp.dart
lib/src/onehz/human/associations.dart
lib/src/onehz/motion/steps.dart
lib/src/onehz/sleep/cardio_stager.dart
lib/src/onehz/wellness/anomaly.dart
test/onehz/util_test.dart

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1aa0a530-b3f0-4336-9eaf-7ffa42d61b8a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="lib/src/onehz/clinical/cosinor.dart" line_range="127-134" />
<code_context>
     ssRes += (y[i] - fit) * (y[i] - fit);
   }
-  final r2 = ssTot == 0 ? 0.0 : clamp(1 - ssRes / ssTot, 0, 1);
+  final double r2 = ssTot == 0 ? 0.0 : (1 - ssRes / ssTot).clamp(0, 1);
   // Adjusted for the 3 fitted parameters (M, β, γ). Confidence MUST come from
   // the adjusted value: the raw R² of a 3-parameter fit is upward-biased
</code_context>
<issue_to_address>
**issue (bug_risk):** The native `num.clamp` API returns `num`, so these replacements produce static type errors where the result is assigned to a `double` or returned from a function declared to return `double`; the package no longer analyzes or compiles.

**Suggested fix:** Call `.toDouble()` after `clamp`, or add a typed helper that returns `double`.

```suggestion
  final double r2 = ssTot == 0 ? 0.0 : (1 - ssRes / ssTot).clamp(0, 1).toDouble();
  // Adjusted for the 3 fitted parameters (M, β, γ). Confidence MUST come from
  // the adjusted value: the raw R² of a 3-parameter fit is upward-biased
  // (E[R²] = 2/(n−1) under the null), so a handful of noise points used to
  // score confidence 0.95 at tier HIGH.
  final double r2Adj = (1 - (1 - r2) * (n - 1) / (n - 3)).clamp(0, 1).toDouble();

  final conf = r2Adj.clamp(0.1, 0.95).toDouble();
```
</issue_to_address>

### Comment 2
<location path="README.md" line_range="73-74" />
<code_context>

+## Quick start
+
+```dart
+import 'package:openstrap_analytics/onehz.dart';
+
+// nnMs: cleaned beat-to-beat RR intervals in ms (see foundations/rr_correction.dart
+// for turning raw RR into this). nnTimesMs: elapsed ms per beat, same length.
+final Metric<HrvTime> hrv = hrvTime(nnMs, nnTimesMs: nnTimesMs, artifactFraction: 0.04);
+if (hrv.value != null) {
+  print('RMSSD ${hrv.value!.rmssd} ms (confidence ${hrv.confidence}, tier ${hrv.tier})');
</code_context>
<issue_to_address>
**issue:** The advertised runnable quick-start snippet references `nnMs` and `nnTimesMs` without declaring or initializing either variable, so copying the example fails to compile immediately.

**Triggers:** When a user copies the quick-start example as provided.

**Suggested fix:** Declare concrete sample lists in the snippet, or show how to obtain them from `RrCorrectionResult`.

```suggestion
final nnMs = <double>[800, 810, 795, 805];
final nnTimesMs = <double>[0, 800, 1610, 2405];
```
</issue_to_address>

### Comment 3
<location path="README.md" line_range="73-75" />
<code_context>
+```dart
+import 'package:openstrap_analytics/onehz.dart';
+
+// nnMs: cleaned beat-to-beat RR intervals in ms (see foundations/rr_correction.dart
+// for turning raw RR into this). nnTimesMs: elapsed ms per beat, same length.
+final Metric<HrvTime> hrv = hrvTime(nnMs, nnTimesMs: nnTimesMs, artifactFraction: 0.04);
+if (hrv.value != null) {
+  print('RMSSD ${hrv.value!.rmssd} ms (confidence ${hrv.confidence}, tier ${hrv.tier})');
</code_context>
<issue_to_address>
**issue (bug_risk):** The quick-start comment says `nnTimesMs` contains elapsed milliseconds per beat, but `hrvTime` expects cumulative beat timestamps; passing RR durations makes its gap check compare differences between durations rather than elapsed beat times and incorrectly retains or drops successive-difference pairs.

**Triggers:** When a caller follows the documented meaning and supplies one elapsed-duration value per beat.

**Suggested fix:** Describe `nnTimesMs` as cumulative beat times in milliseconds and provide a timestamp construction example.
</issue_to_address>

Sourcery assessment

Approval pending. 3 findings to address first.

Blocking findings: lib/src/onehz/clinical/cosinor.dart:134, README.md:74, README.md:75


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +127 to +134
final double r2 = ssTot == 0 ? 0.0 : (1 - ssRes / ssTot).clamp(0, 1);
// Adjusted for the 3 fitted parameters (M, β, γ). Confidence MUST come from
// the adjusted value: the raw R² of a 3-parameter fit is upward-biased
// (E[R²] = 2/(n−1) under the null), so a handful of noise points used to
// score confidence 0.95 at tier HIGH.
final r2Adj = clamp(1 - (1 - r2) * (n - 1) / (n - 3), 0, 1);
final double r2Adj = (1 - (1 - r2) * (n - 1) / (n - 3)).clamp(0, 1);

final conf = clamp(r2Adj, 0.1, 0.95);
final conf = r2Adj.clamp(0.1, 0.95);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): The native num.clamp API returns num, so these replacements produce static type errors where the result is assigned to a double or returned from a function declared to return double; the package no longer analyzes or compiles.

Suggested fix: Call .toDouble() after clamp, or add a typed helper that returns double.

Suggested change
final double r2 = ssTot == 0 ? 0.0 : (1 - ssRes / ssTot).clamp(0, 1);
// Adjusted for the 3 fitted parameters (M, β, γ). Confidence MUST come from
// the adjusted value: the raw R² of a 3-parameter fit is upward-biased
// (E[R²] = 2/(n−1) under the null), so a handful of noise points used to
// score confidence 0.95 at tier HIGH.
final r2Adj = clamp(1 - (1 - r2) * (n - 1) / (n - 3), 0, 1);
final double r2Adj = (1 - (1 - r2) * (n - 1) / (n - 3)).clamp(0, 1);
final conf = clamp(r2Adj, 0.1, 0.95);
final conf = r2Adj.clamp(0.1, 0.95);
final double r2 = ssTot == 0 ? 0.0 : (1 - ssRes / ssTot).clamp(0, 1).toDouble();
// Adjusted for the 3 fitted parameters (M, β, γ). Confidence MUST come from
// the adjusted value: the raw R² of a 3-parameter fit is upward-biased
// (E[R²] = 2/(n−1) under the null), so a handful of noise points used to
// score confidence 0.95 at tier HIGH.
final double r2Adj = (1 - (1 - r2) * (n - 1) / (n - 3)).clamp(0, 1).toDouble();
final conf = r2Adj.clamp(0.1, 0.95).toDouble();

Comment thread README.md Outdated
Comment thread README.md
@abdulsaheel

Copy link
Copy Markdown
Contributor Author

squashed into #59 for one clean review — closing this round.

@abdulsaheel
abdulsaheel deleted the audit/fixes-round4-analytics branch August 29, 2026 13:16
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