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
3 changes: 3 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# GitHub funding config only accepts URLs, not raw wallet addresses.
custom:
- https://github.com/OpenStrap/edge/blob/main/DONATE.md
46 changes: 46 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: test

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

# Read-only: this job only builds and tests. Nothing here needs write access,
# and it executes code from pull requests.
permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# pubspec declares `sdk: ^3.5.0`, so 3.5.0 is a claim this package makes
# to anyone depending on it. Test it, rather than only testing whatever
# `stable` happens to be — otherwise the lower bound is a guess.
sdk: ['3.5.0', 'stable']
name: test (Dart ${{ matrix.sdk }})
steps:
- uses: actions/checkout@v4
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
# Don't leave GITHUB_TOKEN in .git/config where PR-authored test code
# could read it. Nothing in this job pushes back to the repo.
persist-credentials: false
- uses: dart-lang/setup-dart@v1
with:
sdk: ${{ matrix.sdk }}

- name: Install dependencies
run: dart pub get

- name: Analyze
run: dart analyze --fatal-infos

# The golden capture (whoop_hist.jsonl) is a real band recording kept
# beside the repo rather than committed to it. The handful of tests that
# replay it SKIP here (they call markTestSkipped) and run locally; every
# synthetic-input and property test runs on each PR.
- name: Test
run: dart test --reporter=expanded
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ build/
.DS_Store
# legacy ts/ tooling scratch, dont want another node_modules commit
node_modules/

# Local dev override redirecting sibling packages to in-tree checkouts.
# Never committed — pubspec.yaml pins real SHAs so CI and fresh clones work.
pubspec_overrides.yaml
92 changes: 92 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Contributing

This package is the math: 1 Hz records in, metrics out. **Pure Dart, zero runtime
dependencies, no I/O, no randomness, no clock.** Same input, same output, every
time. It runs on-device, inside an isolate, on a phone — so it also has to be
cheap.

## What belongs here (and what doesn't)

| Your change | Repo |
|---|---|
| A metric, or how an existing number is computed | **here** |
| A record type, opcode, event, anything byte-level | [protocol](https://github.com/OpenStrap/protocol) |
| Storage, sync, Bluetooth, UI, when things get computed | [edge](https://github.com/OpenStrap/edge) |

## The two rules that matter

**1. Cite the method.** Everything in here implements a published,
peer-reviewed algorithm, with the citation in a comment next to the code and a
row in [`ALGORITHMS.md`](ALGORITHMS.md). The point is that you can go read the
paper and decide for yourself whether to trust the number.

If nothing in the literature fits, that's allowed — mark it `ESTIMATE`, give it
low confidence, and say so plainly. What's not allowed is inventing constants
and presenting them as science, or reverse-engineering WHOOP's scores by
fitting to their output.

**2. Never fabricate a value.** If an input isn't there, return a `Metric` with
a `null` value. Not a default, not a last-known-good, not an interpolation
that'll look plausible on a chart. Cold start returns
`need_baseline:have=H,need=N` and that's the correct behaviour, not a bug to
paper over.

```dart
class Metric<T> {
final T? value; // null when the inputs weren't there
final double confidence; // 0..1
final String tier; // AUTH | HIGH | ESTIMATE | RELATIVE
...
}
```

## Honest ceilings — please don't "fix" these

These are properties of what a WHOOP 4.0 actually hands over, not defects:

- **HRV is PRV**, derived from 1 Hz beat timing. It is not ECG-grade.
- **Deep sleep is a low-confidence HR-flatness overlay.** The band gives no
signal that distinguishes NREM stages properly.
- **SpO₂ and skin temperature are relative ADC values.** There is no calibration
to absolute units and there won't be. `absolute_spo2:false` is deliberate.
- **Cole–Kripke coefficients on 1 Hz data** are a documented bounded exception,
used for the wake spine only and corrected downstream. Read the comment in
`cardio_stager.dart` before touching it.

Making any of these *look* more confident than the underlying signal supports is
the one change guaranteed to be rejected.

## Structure

Each family is a subdirectory under `lib/src/onehz/` with its own sub-barrel:
`foundations`, `clinical`, `sleep`, `respiration`, `motion`, `workout`,
`wellness`, `human`. Put new work with its siblings and export it through the
barrel.

There is **one** sleep source (`segmentSleep`) and **one** headline readiness
(`readinessComposite`). Don't add a second of either — extend the existing one.

## Tests

```bash
dart pub get
dart analyze
dart test
```

Everything runs on synthetic and property-based inputs in CI. A handful of tests
replay `whoop_hist.jsonl`, a real band capture kept *beside* the repo rather
than committed to it; those skip automatically when it's absent.

Any change to a metric needs a test that pins the behaviour, and — because
stored results are versioned and immutable — a note in the PR saying so, since
it means `kAlgoVersion` has to be bumped over in edge. If that bump doesn't
happen, devices keep serving the old numbers.

## Pull requests

- Branch off `main`, one logical change per PR.
- Name the paper you implemented, with enough detail to find it.
- Say whether the output of any existing metric changes. This is the single most
important line in the PR.
- No `Co-Authored-By` trailers.
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# OpenStrap analytics

[![test](https://github.com/OpenStrap/analytics/actions/workflows/test.yml/badge.svg)](https://github.com/OpenStrap/analytics/actions/workflows/test.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

This is the math. Given the always-on 1 Hz substrate a WHOOP 4.0 actually hands over —
beat-to-beat RR intervals, 1 Hz heart rate, 1 Hz tri-axial accel, and a few relative ADC
channels (skin temp, SpO2, ambient light) — it works out the things you care about: how
Expand Down Expand Up @@ -105,7 +108,7 @@ not a feature, no matter how tempting the plausible-looking headline is.
dart test # run from the repo root — some fixtures resolve paths relative to it
```

282 tests, nothing mocked — pure functions, fixture in, assertion out.
290 tests, nothing mocked — pure functions, fixture in, assertion out.

## If you want to add a metric

Expand All @@ -118,3 +121,20 @@ Derive confidence from real coverage, and return absent (`Metric.absent(...)`, n
fabricated fallback) when the inputs genuinely aren't there. And if your idea needs a
cause it can't actually tell apart from three other explanations, report the state, not
the cause.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) — which repo a change belongs in, how to run the
tests, and the rules that keep this package honest. Security issues go through
[SECURITY.md](SECURITY.md), not a public issue.

## Support the work

Free, MIT, no company behind it. If OpenStrap gave your band a second life,
[DONATE.md](https://github.com/OpenStrap/edge/blob/main/DONATE.md) has BTC and EVM
addresses. Protocol findings and bug reports are worth more than money, though.

---

Not affiliated with, endorsed by, or connected to WHOOP. "WHOOP" is their trademark, used
only to say which device this talks to.
59 changes: 59 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Security Policy

## Reporting a vulnerability

Please **don't** open a public issue for a security problem.

Use GitHub's private reporting instead:
[**Report a vulnerability →**](https://github.com/OpenStrap/analytics/security/advisories/new)

That goes straight to the maintainer and stays private until there's a fix.

Rough expectations, set honestly — this is a one-maintainer project, not a
company with an on-call rota:

- Acknowledgement within about a week.
- An assessment, and a fix or a clear "won't fix and here's why", within 30 days
for anything that puts user data at risk.
- Credit in the release notes if you want it.

## What's in scope

- Anything that discloses a user's health data off their device.
- Anything that lets a third party read, write to, or hijack the Bluetooth
session with someone's band.
- Local data-at-rest problems: the database, exports, the App Group container,
widget snapshots.
- The optional companion worker in
[backend](https://github.com/OpenStrap/backend): auth, the import endpoints,
the opt-in telemetry and health-upload paths.
- Anything that causes the app to send data anywhere the user did not agree to.

## What's out of scope

- The band's own firmware. We don't ship it, can't patch it, and won't publish
attacks against it.
- WHOOP's own apps and services. Please report those to WHOOP.
- The fact that sideloaded builds are unsigned, or that a rooted/jailbroken
device can read app storage. Both are known properties of the distribution
model, documented in the README.
- Metric accuracy. Wrong numbers are bugs — open a normal issue.

## Where your data actually is

Worth knowing before you go looking: OpenStrap computes and stores your health
data on-device, and there's no account or server holding it. Two qualifications,
so the boundary is exact:

- **Anonymous diagnostics** (Firebase crash/performance, never health data) are
**on by default in GitHub release builds** and absent from App Store / Play
Store builds. Switchable off in-app.
- **Health-data contribution** uploads the local database, but is opt-in, off by
default, and compiled out of store builds entirely.

Everything else the companion worker does — legacy import, an update pointer —
is optional and carries no health data. See [edge's PRIVACY.md](https://github.com/OpenStrap/edge/blob/main/PRIVACY.md).

That means the realistic attack surface is the phone, the Bluetooth link, and
the local database — not a cloud backend. Reports focused there are the most
useful.
Loading
Loading