Skip to content

price strain above the cost of being awake, and rebase the coach on it - #45

Open
svssathvik7 wants to merge 1 commit into
OpenStrap:mainfrom
svssathvik7:fix/strain-scale-recalibration
Open

price strain above the cost of being awake, and rebase the coach on it#45
svssathvik7 wants to merge 1 commit into
OpenStrap:mainfrom
svssathvik7:fix/strain-scale-recalibration

Conversation

@svssathvik7

@svssathvik7 svssathvik7 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

The problem

The 0–21 headline was min(21, ln(TRIMP+1)/ln(1.5)) over whole-waking-day Banister TRIMP. Two defects, and they compounded.

A huge floor. Whole-day TRIMP counts every waking minute above resting, so ~16 h of ordinary living accrues ~180 TRIMP before any exercise. Log base 1.5 is steepest near zero, so that overhead alone bought ~13 of the 21 points. Verified against real bundles (formula reproduces the stored scalars bit-for-bit):

Day TRIMP Old strain
2026-07-09 — 2,641 steps 177.8 12.79
2026-07-10 — 23 steps, 24 kcal 23.4 7.88
Inactive full-wear day ~190 ~13

The number moved with how long the band was worn, not with training.

No reachable ceiling. Each further point cost 1.5× the load, so 21 sat at TRIMP ~4987 — roughly 35 h at 80 % HRR. A marathon reads ~15.8. The usable range was about 8 to 16, and neither end meant anything.

The fix

Strain is now the load earned above a quiet-waking baseline — 20 % of HRR, priced through the same banisterY the TRIMP was scored with, scaled by the wake window actually observed. The net maps by 21·ln(1+u·14)/ln(15), u = net/400.

Scaling the baseline by the observed wake window is what stops a partial-wear day being charged a full day's overhead: 2026-07-10 was worn 135 waking minutes, and lending it a 16 h allowance would read as effort.

Anchors, on days built from per-minute HR and run through the real banisterTrimp:

Day Strain
Inactive, full wear ~0
Rest + a light walk 2–4
45-min moderate run 8–11
90-min hard session 14–17
5 h at 160 bpm 21

wakeMinutes is required, not assumed — the subtraction is meaningless without it, and guessing silently mis-scores every partial-wear day, so strainScoreMetric abstains instead.

The coach

strainTarget's bands were sized for a distribution this package never produced. "recover 4–8" sat below what an inactive worn day scored, so a low-recovery day asked for a number the user had already passed before getting out of bed; "push 14–18" needed more than a marathon. Rebased onto the distribution above.

Its load adjustment was also dimensionally wrong: ctl/atl/tsb arrive as raw daily TRIMP (hundreds), but the thresholds were atl − ctl > 10 and tsb > 5 — magnitudes sized for the 0–21 scale. On TRIMP-scale inputs those fire on ordinary noise (a 320-vs-300 acute:chronic pair is a 6.7 % lift, not fatigue). Both are now ratios against CTL, guarded on ctl > 0.

Tests

The old test pinned strainScore(335) ≈ 14.347 — the formula as its own expectation, which let the miscalibration pass forever. Replaced with behavioural anchors in strain_calibration_test.dart that constrain the whole pipeline, not just the map.

435 tests pass, dart analyze lib test clean.

Notes for review

  • The sleep-need strain bonus needs no change and self-corrects: it is linear in strain, so an inactive day used to earn ~28 min of extra "training recovery" sleep need and now earns 0.
  • OpenStrap/edge has a companion PR that cannot build against upstream until this merges and its pinned SHA is bumped — see the sequencing note there.

Summary by CodeRabbit

  • New Features

    • Introduced baseline-adjusted strain scoring on a bounded 0–21 scale.
    • Added support for wake duration and optional sex-specific calibration.
    • Strain results now abstain when TRIMP or valid wake duration is unavailable.
  • Improvements

    • Retuned recovery targets to align with the updated strain scale.
    • Improved fatigue and freshness adjustments using relative training-load measures.
    • Expanded validation across activity intensity, wear duration, and edge cases.

The 0-21 headline was `min(21, ln(TRIMP+1)/ln(1.5))` over whole-waking-day
Banister TRIMP. Two things were wrong with that, and they compounded.

Whole-day TRIMP counts every waking minute above resting, so ~16 h of ordinary
living accrues ~180 TRIMP before any exercise happens. Log base 1.5 is steepest
near zero, so that overhead alone bought ~13 of the 21 points. On a real bundle
(2026-07-09, 2641 steps) the day scored 12.79; a day with 23 steps and 24 kcal
scored 7.88. An INACTIVE full-wear day reads 12.8 — the number moved with how
long the band was worn, not with training.

The other half: each further point cost 1.5x the load, so 21 sat at TRIMP ~4987,
roughly 35 h at 80 % HRR. The top third of the scale was unreachable by any
human day — a marathon reads ~15.8. The usable range was about 8 to 16, and
neither end meant anything.

Strain is now the load earned ABOVE a quiet-waking baseline. The baseline is
20 % of HRR priced through the same banisterY the TRIMP was scored with, scaled
by the wake window actually observed, so a partial-wear day is not charged a
full day's overhead — 2026-07-10 was worn 135 waking minutes, and borrowing a
16 h allowance for it would have read as effort. The net is mapped by
21*ln(1+u*14)/ln(15) with u = net/400.

Anchored on real days rather than on the formula restated: inactive ~0, rest
plus a walk 2-4, a 45-min moderate run 8-11, a 90-min hard session 14-17, 5 h at
160 bpm 21. strain_calibration_test.dart builds each day from per-minute HR and
runs it through the real banisterTrimp, so the anchors constrain the pipeline
and not just the map. The old test pinned strainScore(335) ~ 14.347 — the
formula as its own expectation, which let the miscalibration pass forever.

wakeMinutes is REQUIRED rather than assumed. The baseline subtraction is
meaningless without a wake window, and guessing one silently mis-scores every
partial-wear day, so strainScoreMetric abstains instead.

SAME COMMIT, the coach. strainTarget's bands were sized for a distribution the
package never produced: "recover 4-8" sat below what an inactive worn day
scored, so a low-recovery day asked for a number the user had already passed
before getting out of bed, while "push 14-18" needed more than a marathon. They
are rebased onto the distribution above.

Its load adjustment was also dimensionally wrong. ctl/atl/tsb arrive as raw
daily TRIMP (hundreds), but the thresholds were `atl - ctl > 10` and `tsb > 5`,
magnitudes sized for the 0-21 scale. On TRIMP-scale inputs those fire on
ordinary week-to-week noise — a 320-vs-300 acute:chronic pair is a 6.7 % lift,
not fatigue. Both are now ratios against CTL, guarded on ctl > 0.

The sleep-need strain bonus needs no change and self-corrects: it is linear in
strain, so an inactive day used to earn ~28 min of extra "training recovery"
sleep need and now earns 0.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces raw TRIMP strain mapping with a wake-adjusted 0–21 scale. It requires valid wake duration, retunes coaching target adjustments using ATL, TSB, and CTL ratios, and adds calibration and regression tests.

Changes

Strain scoring and coaching

Layer / File(s) Summary
Wake-adjusted strain model
lib/src/onehz/clinical/load_trimp.dart, test/onehz/clinical_test.dart
The strain API subtracts wake-duration baseline TRIMP and applies a bounded 0–21 logarithmic scale. The metric requires valid TRIMP and positive wake duration.
Normalized coaching targets
lib/src/onehz/human/coaching.dart, test/onehz/coaching_test.dart
Recovery bands use revised ranges. Fatigue and freshness adjustments compare ATL and TSB with positive CTL.
End-to-end strain calibration
test/onehz/strain_calibration_test.dart
Tests cover heart-rate pipeline anchors, monotonicity, clamping, partial wear, missing inputs, and estimate-tier results.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HeartRateData
  participant BanisterTRIMP
  participant strainScoreMetric
  HeartRateData->>BanisterTRIMP: calculate daily TRIMP
  BanisterTRIMP->>strainScoreMetric: provide TRIMP and wake_minutes
  strainScoreMetric->>strainScoreMetric: subtract baseline and clamp to 0–21
Loading

Possibly related PRs

  • OpenStrap/analytics#30: Shares changes to the TRIMP and strain calculation pipeline, including abstention and integration behavior.

Suggested reviewers: abdulsaheel

🚥 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 clearly describes recalibrating strain above a waking baseline and updating coach targets.
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.

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 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/src/onehz/clinical/load_trimp.dart`:
- Around line 99-122: Update strainScore in
lib/src/onehz/clinical/load_trimp.dart:99-122 to reject non-finite or
non-positive wakeMinutes and non-finite or negative trimp before calculating the
score. Also update the Metric construction at
lib/src/onehz/clinical/load_trimp.dart:137-143 to require finite trimp and
wakeMinutes before creating a present Metric.
- Around line 112-115: Update the package version in pubspec.yaml from 1.0.0 to
2.0.0 to publish the required breaking API change for strainScore and
strainScoreMetric, and add migration documentation describing the required
wakeMinutes argument before release.
🪄 Autofix

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2922f803-06f7-4e06-9144-923ce53719fd

📥 Commits

Reviewing files that changed from the base of the PR and between e047c59 and 914410e.

📒 Files selected for processing (5)
  • lib/src/onehz/clinical/load_trimp.dart
  • lib/src/onehz/human/coaching.dart
  • test/onehz/clinical_test.dart
  • test/onehz/coaching_test.dart
  • test/onehz/strain_calibration_test.dart

Comment on lines +99 to +122
double baselineTrimp(double wakeMinutes, {bool female = false}) =>
wakeMinutes *
quietWakingHrr *
StrainScorer.banisterY(quietWakingHrr, female: female);

/// Log-map the TRIMP EARNED ABOVE baseline into a 0–21 headline "strain" score.
///
/// Check-points: 0 → 0; 335 → ln(336)/ln(1.5) ≈ 14.34 (cap not hit).
double strainScore(double trimp) {
if (trimp <= 0) return 0.0;
final s = math.log(trimp + 1) / math.log(1.5);
return math.min(21.0, s);
/// net = trimp − baselineTrimp(wakeMinutes)
/// u = min(1, net / maximalNetTrimp)
/// strain = 21 · ln(1 + u·(C−1)) / ln(C), C = [strainCurvature]
///
/// [wakeMinutes] is the observed waking wear window that produced [trimp] — it
/// sets the baseline, so it is required rather than assumed.
double strainScore(
double trimp, {
required double wakeMinutes,
bool female = false,
}) {
final net = trimp - baselineTrimp(wakeMinutes, female: female);
if (net <= 0) return 0.0;
final u = math.min(1.0, net / maximalNetTrimp);
final s =
21.0 * math.log(1 + u * (strainCurvature - 1)) / math.log(strainCurvature);
return math.min(21.0, math.max(0.0, s));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject non-finite and invalid strain inputs.

double.nan passes the relational checks and produces a present metric with a NaN value. double.infinity can silently map to strain 21, and infinite wake time can map to strain 0. The direct APIs also accept zero or negative wake windows, despite the required positive wake-window contract.

  • lib/src/onehz/clinical/load_trimp.dart#L99-L122: reject non-finite or non-positive wakeMinutes, and reject non-finite or negative trimp before calculating a score.
  • lib/src/onehz/clinical/load_trimp.dart#L137-L143: require finite trimp and finite wakeMinutes before constructing a present Metric.
Proposed fix
-double baselineTrimp(double wakeMinutes, {bool female = false}) =>
-    wakeMinutes *
-    quietWakingHrr *
-    StrainScorer.banisterY(quietWakingHrr, female: female);
+double baselineTrimp(double wakeMinutes, {bool female = false}) {
+  if (!wakeMinutes.isFinite || wakeMinutes <= 0) {
+    throw ArgumentError.value(
+      wakeMinutes,
+      'wakeMinutes',
+      'must be finite and greater than zero',
+    );
+  }
+  return wakeMinutes *
+      quietWakingHrr *
+      StrainScorer.banisterY(quietWakingHrr, female: female);
+}

 }) {
+  if (!trimp.isFinite || trimp < 0) {
+    throw ArgumentError.value(
+      trimp,
+      'trimp',
+      'must be finite and non-negative',
+    );
+  }
   final net = trimp - baselineTrimp(wakeMinutes, female: female);

-  if (trimp == null || trimp < 0 || wakeMinutes == null || wakeMinutes <= 0) {
+  if (trimp == null ||
+      !trimp.isFinite ||
+      trimp < 0 ||
+      wakeMinutes == null ||
+      !wakeMinutes.isFinite ||
+      wakeMinutes <= 0) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
double baselineTrimp(double wakeMinutes, {bool female = false}) =>
wakeMinutes *
quietWakingHrr *
StrainScorer.banisterY(quietWakingHrr, female: female);
/// Log-map the TRIMP EARNED ABOVE baseline into a 0–21 headline "strain" score.
///
/// Check-points: 0 → 0; 335 → ln(336)/ln(1.5) ≈ 14.34 (cap not hit).
double strainScore(double trimp) {
if (trimp <= 0) return 0.0;
final s = math.log(trimp + 1) / math.log(1.5);
return math.min(21.0, s);
/// net = trimp − baselineTrimp(wakeMinutes)
/// u = min(1, net / maximalNetTrimp)
/// strain = 21 · ln(1 + u·(C−1)) / ln(C), C = [strainCurvature]
///
/// [wakeMinutes] is the observed waking wear window that produced [trimp] — it
/// sets the baseline, so it is required rather than assumed.
double strainScore(
double trimp, {
required double wakeMinutes,
bool female = false,
}) {
final net = trimp - baselineTrimp(wakeMinutes, female: female);
if (net <= 0) return 0.0;
final u = math.min(1.0, net / maximalNetTrimp);
final s =
21.0 * math.log(1 + u * (strainCurvature - 1)) / math.log(strainCurvature);
return math.min(21.0, math.max(0.0, s));
double baselineTrimp(double wakeMinutes, {bool female = false}) {
if (!wakeMinutes.isFinite || wakeMinutes <= 0) {
throw ArgumentError.value(
wakeMinutes,
'wakeMinutes',
'must be finite and greater than zero',
);
}
return wakeMinutes *
quietWakingHrr *
StrainScorer.banisterY(quietWakingHrr, female: female);
}
/// Log-map the TRIMP EARNED ABOVE baseline into a 0–21 headline "strain" score.
///
/// net = trimp − baselineTrimp(wakeMinutes)
/// u = min(1, net / maximalNetTrimp)
/// strain = 21 · ln(1 + u·(C−1)) / ln(C), C = [strainCurvature]
///
/// [wakeMinutes] is the observed waking wear window that produced [trimp] — it
/// sets the baseline, so it is required rather than assumed.
double strainScore(
double trimp, {
required double wakeMinutes,
bool female = false,
}) {
if (!trimp.isFinite || trimp < 0) {
throw ArgumentError.value(
trimp,
'trimp',
'must be finite and non-negative',
);
}
final net = trimp - baselineTrimp(wakeMinutes, female: female);
if (net <= 0) return 0.0;
final u = math.min(1.0, net / maximalNetTrimp);
final s =
21.0 * math.log(1 + u * (strainCurvature - 1)) / math.log(strainCurvature);
return math.min(21.0, math.max(0.0, s));
Suggested change
double baselineTrimp(double wakeMinutes, {bool female = false}) =>
wakeMinutes *
quietWakingHrr *
StrainScorer.banisterY(quietWakingHrr, female: female);
/// Log-map the TRIMP EARNED ABOVE baseline into a 0–21 headline "strain" score.
///
/// Check-points: 0 → 0; 335 → ln(336)/ln(1.5) ≈ 14.34 (cap not hit).
double strainScore(double trimp) {
if (trimp <= 0) return 0.0;
final s = math.log(trimp + 1) / math.log(1.5);
return math.min(21.0, s);
/// net = trimp − baselineTrimp(wakeMinutes)
/// u = min(1, net / maximalNetTrimp)
/// strain = 21 · ln(1 + u·(C−1)) / ln(C), C = [strainCurvature]
///
/// [wakeMinutes] is the observed waking wear window that produced [trimp] — it
/// sets the baseline, so it is required rather than assumed.
double strainScore(
double trimp, {
required double wakeMinutes,
bool female = false,
}) {
final net = trimp - baselineTrimp(wakeMinutes, female: female);
if (net <= 0) return 0.0;
final u = math.min(1.0, net / maximalNetTrimp);
final s =
21.0 * math.log(1 + u * (strainCurvature - 1)) / math.log(strainCurvature);
return math.min(21.0, math.max(0.0, s));
if (trimp == null ||
!trimp.isFinite ||
trimp < 0 ||
wakeMinutes == null ||
!wakeMinutes.isFinite ||
wakeMinutes <= 0) {
return const Metric<double>.absent(
tier: Tier.estimate,
inputs_used: inputs,
note: 'strain needs a TRIMP and the wake window it was measured over',
);
}
📍 Affects 1 file
  • lib/src/onehz/clinical/load_trimp.dart#L99-L122 (this comment)
  • lib/src/onehz/clinical/load_trimp.dart#L137-L143
🤖 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/src/onehz/clinical/load_trimp.dart` around lines 99 - 122, Update
strainScore in lib/src/onehz/clinical/load_trimp.dart:99-122 to reject
non-finite or non-positive wakeMinutes and non-finite or negative trimp before
calculating the score. Also update the Metric construction at
lib/src/onehz/clinical/load_trimp.dart:137-143 to require finite trimp and
wakeMinutes before creating a present Metric.

Comment on lines +112 to +115
double strainScore(
double trimp, {
required double wakeMinutes,
bool female = false,

Copy link
Copy Markdown

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

# Find repository call sites and inspect each argument list.
rg -n -C 3 '\bstrainScore(?:Metric)?\s*\(' lib test

# Inspect the package version before publishing this required-parameter change.
fd -a '^pubspec\.yaml$' . -x sh -c 'printf "\n--- %s ---\n" "$1"; rg -n "^version:" "$1"' sh {}

Repository: OpenStrap/analytics

Length of output: 7887


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- all tracked references ---'
rg -n -C 2 '\bstrainScore(?:Metric)?\s*\(' . --glob '!build/**' --glob '!dist/**' --glob '!node_modules/**' || true

printf '%s\n' '--- exports and declarations ---'
rg -n -C 4 '\b(strainScore|strainScoreMetric)\b|load_trimp' lib/onehz.dart lib/src/onehz/clinical/load_trimp.dart

printf '%s\n' '--- release metadata ---'
for f in $(git ls-files | rg '(^|/)(pubspec\.yaml|CHANGELOG(\.md)?|README(\.md)?|.*ชangelog.*)$' || true); do
  printf '\n--- %s ---\n' "$f"
  rg -n -C 2 '^(version:|## |# |-\s)|breaking|BREAKING|strainScore|strainScoreMetric' "$f" || true
done

printf '%s\n' '--- package status summary ---'
git diff --stat

Repository: OpenStrap/analytics

Length of output: 14304


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- all tracked references ---'
rg -n -C 2 '\bstrainScore(?:Metric)?\s*\(' . --glob '!build/**' --glob '!dist/**' --glob '!node_modules/**' || true

printf '%s\n' '--- exports and declarations ---'
rg -n -C 4 '\b(strainScore|strainScoreMetric)\b|load_trimp' lib/onehz.dart lib/src/onehz/clinical/load_trimp.dart

printf '%s\n' '--- release metadata ---'
for f in $(git ls-files | rg '(^|/)(pubspec\.yaml|CHANGELOG(\.md)?|README(\.md)?|.*changelog.*)$' || true); do
  printf '\n--- %s ---\n' "$f"
  rg -n -C 2 '^(version:|## |# |-\s)|breaking|BREAKING|strainScore|strainScoreMetric' "$f" || true
done

printf '%s\n' '--- package status summary ---'
git diff --stat

Repository: OpenStrap/analytics

Length of output: 14304


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- signature change and package metadata diff ---'
git diff --unified=8 -- lib/src/onehz/clinical/load_trimp.dart pubspec.yaml

printf '%s\n' '--- call-site verifier ---'
python3 - <<'PY'
import subprocess
from pathlib import Path

tracked = subprocess.check_output(["git", "ls-files", "*.dart"], text=True).splitlines()
needle_names = ("strainScore", "strainScoreMetric")

def matching_close(text, opening):
    depth = 0
    quote = None
    escaped = False
    for i in range(opening, len(text)):
        ch = text[i]
        if quote:
            if escaped:
                escaped = False
            elif ch == "\\":
                escaped = True
            elif ch == quote:
                quote = None
            continue
        if ch in ("'", '"'):
            quote = ch
        elif ch == "(":
            depth += 1
        elif ch == ")":
            depth -= 1
            if depth == 0:
                return i
    return None

found = []
for filename in tracked:
    text = Path(filename).read_text()
    i = 0
    while i < len(text):
        hit = next((text.find(name, i) for name in needle_names
                    if text.find(name, i) != -1), -1)
        if hit < 0:
            break
        name = next(name for name in needle_names if text.startswith(name, hit))
        j = hit + len(name)
        while j < len(text) and text[j].isspace():
            j += 1
        if j < len(text) and text[j] == "(":
            end = matching_close(text, j)
            if end is not None:
                args = text[j + 1:end]
                line = text.count("\n", 0, hit) + 1
                found.append((filename, line, name, "wakeMinutes:" in args))
                i = end + 1
                continue
        i = j

for filename, line, name, has_wake in found:
    print(f"{filename}:{line}: {name}: wakeMinutes={'yes' if has_wake else 'no'}")
print(f"calls={len(found)}")
print(f"missing_wakeMinutes={sum(not item[3] for item in found)}")
PY

Repository: OpenStrap/analytics

Length of output: 1158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- signature change and package metadata diff ---'
git diff --unified=8 -- lib/src/onehz/clinical/load_trimp.dart pubspec.yaml

printf '%s\n' '--- call-site verifier ---'
python3 - <<'PY'
import subprocess
from pathlib import Path

tracked = subprocess.check_output(["git", "ls-files", "*.dart"], text=True).splitlines()
needle_names = ("strainScore", "strainScoreMetric")

def matching_close(text, opening):
    depth = 0
    quote = None
    escaped = False
    for i in range(opening, len(text)):
        ch = text[i]
        if quote:
            if escaped:
                escaped = False
            elif ch == "\\":
                escaped = True
            elif ch == quote:
                quote = None
            continue
        if ch in ("'", '"'):
            quote = ch
        elif ch == "(":
            depth += 1
        elif ch == ")":
            depth -= 1
            if depth == 0:
                return i
    return None

found = []
for filename in tracked:
    text = Path(filename).read_text()
    i = 0
    while i < len(text):
        candidates = [(text.find(name, i), name) for name in needle_names]
        candidates = [(pos, name) for pos, name in candidates if pos >= 0]
        if not candidates:
            break
        hit, name = min(candidates)
        j = hit + len(name)
        while j < len(text) and text[j].isspace():
            j += 1
        if j < len(text) and text[j] == "(":
            end = matching_close(text, j)
            if end is not None:
                args = text[j + 1:end]
                line = text.count("\n", 0, hit) + 1
                found.append((filename, line, name, "wakeMinutes:" in args))
                i = end + 1
                continue
        i = j

for filename, line, name, has_wake in found:
    print(f"{filename}:{line}: {name}: wakeMinutes={'yes' if has_wake else 'no'}")
print(f"calls={len(found)}")
print(f"missing_wakeMinutes={sum(not item[3] for item in found)}")
PY

Repository: OpenStrap/analytics

Length of output: 1158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re
import subprocess
from pathlib import Path

files = subprocess.check_output(["git", "ls-files", "*.dart"], text=True).splitlines()

def mask_comments(text):
    out = list(text)
    i = 0
    state = "code"
    quote = ""
    while i < len(text):
        ch = text[i]
        nxt = text[i + 1] if i + 1 < len(text) else ""
        if state == "code":
            if ch == "/" and nxt == "/":
                out[i] = out[i + 1] = " "
                i += 2
                state = "line_comment"
                continue
            if ch == "/" and nxt == "*":
                out[i] = out[i + 1] = " "
                i += 2
                state = "block_comment"
                continue
            if ch in ("'", '"'):
                quote = ch
                state = "string"
            i += 1
        elif state == "line_comment":
            if ch == "\n":
                state = "code"
            else:
                out[i] = " "
            i += 1
        elif state == "block_comment":
            if ch == "*" and nxt == "/":
                out[i] = out[i + 1] = " "
                i += 2
                state = "code"
            else:
                if ch != "\n":
                    out[i] = " "
                i += 1
        else:
            # Mask string contents so documentation text cannot look like a call.
            if ch == "\\":
                if i + 1 < len(text):
                    out[i] = out[i + 1] = " "
                    i += 2
                else:
                    out[i] = " "
                    i += 1
            elif ch == quote:
                state = "code"
                i += 1
            else:
                if ch != "\n":
                    out[i] = " "
                i += 1
    return "".join(out)

def matching_close(text, opening):
    depth = 0
    for i in range(opening, len(text)):
        if text[i] == "(":
            depth += 1
        elif text[i] == ")":
            depth -= 1
            if depth == 0:
                return i
    return None

patterns = [
    re.compile(r"\bstrainScoreMetric\s*\("),
    re.compile(r"\bstrainScore\s*\("),
]
results = []
for filename in files:
    original = Path(filename).read_text()
    masked = mask_comments(original)
    for pattern in patterns:
        for match in pattern.finditer(masked):
            opening = masked.find("(", match.start(), match.end())
            closing = matching_close(masked, opening)
            if closing is None:
                continue
            args = masked[opening + 1:closing]
            line = original.count("\n", 0, match.start()) + 1
            declaration = bool(re.search(r"\b(?:double|Metric<double>)\s+$",
                                         masked[max(0, match.start() - 24):match.start()]))
            results.append((filename, line, match.group(0).split("(")[0],
                            "declaration" if declaration else "call",
                            "yes" if re.search(r"\bwakeMinutes\s*:", args) else "no"))

for item in sorted(results):
    print(f"{item[0]}:{item[1]}: {item[2]}: {item[3]}: wakeMinutes={item[4]}")

calls = [item for item in results if item[3] == "call"]
print(f"calls={len(calls)}")
print(f"calls_missing_wakeMinutes={sum(item[4] == 'no' for item in calls)}")
PY

Repository: OpenStrap/analytics

Length of output: 1602


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re
import subprocess
from pathlib import Path

files = subprocess.check_output(["git", "ls-files", "*.dart"], text=True).splitlines()

def mask_comments(text):
    out = list(text)
    i = 0
    state = "code"
    quote = ""
    while i < len(text):
        ch = text[i]
        nxt = text[i + 1] if i + 1 < len(text) else ""
        if state == "code":
            if ch == "/" and nxt == "/":
                out[i] = out[i + 1] = " "
                i += 2
                state = "line_comment"
                continue
            if ch == "/" and nxt == "*":
                out[i] = out[i + 1] = " "
                i += 2
                state = "block_comment"
                continue
            if ch in ("'", '"'):
                quote = ch
                state = "string"
            i += 1
        elif state == "line_comment":
            if ch == "\n":
                state = "code"
            else:
                out[i] = " "
            i += 1
        elif state == "block_comment":
            if ch == "*" and nxt == "/":
                out[i] = out[i + 1] = " "
                i += 2
                state = "code"
            else:
                if ch != "\n":
                    out[i] = " "
                i += 1
        else:
            if ch == "\\":
                if i + 1 < len(text):
                    out[i] = out[i + 1] = " "
                    i += 2
                else:
                    out[i] = " "
                    i += 1
            elif ch == quote:
                state = "code"
                i += 1
            else:
                if ch != "\n":
                    out[i] = " "
                i += 1
    return "".join(out)

def matching_close(text, opening):
    depth = 0
    for i in range(opening, len(text)):
        if text[i] == "(":
            depth += 1
        elif text[i] == ")":
            depth -= 1
            if depth == 0:
                return i
    return None

patterns = [
    re.compile(r"\bstrainScoreMetric\s*\("),
    re.compile(r"\bstrainScore\s*\("),
]
results = []
for filename in files:
    original = Path(filename).read_text()
    masked = mask_comments(original)
    for pattern in patterns:
        for match in pattern.finditer(masked):
            opening = masked.find("(", match.start(), match.end())
            closing = matching_close(masked, opening)
            if closing is None:
                continue
            args = masked[opening + 1:closing]
            line = original.count("\n", 0, match.start()) + 1
            declaration = bool(re.search(
                r"\b(?:double|Metric<double>)\s+$",
                masked[max(0, match.start() - 24):match.start()],
            ))
            results.append((
                filename,
                line,
                match.group(0).split("(")[0],
                "declaration" if declaration else "call",
                "yes" if re.search(r"\bwakeMinutes\s*:", args) else "no",
            ))

for item in sorted(results):
    print(f"{item[0]}:{item[1]}: {item[2]}: {item[3]}: wakeMinutes={item[4]}")

calls = [item for item in results if item[3] == "call"]
print(f"calls={len(calls)}")
print(f"calls_missing_wakeMinutes={sum(item[4] == 'no' for item in calls)}")
PY

Repository: OpenStrap/analytics

Length of output: 1602


Publish this breaking API change under a major version.

strainScore and strainScoreMetric are publicly exported. All repository callers pass wakeMinutes, but external callers will break. Change pubspec.yaml from 1.0.0 to 2.0.0 and document the migration before publishing.

🤖 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/src/onehz/clinical/load_trimp.dart` around lines 112 - 115, Update the
package version in pubspec.yaml from 1.0.0 to 2.0.0 to publish the required
breaking API change for strainScore and strainScoreMetric, and add migration
documentation describing the required wakeMinutes argument before release.

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