Chapter 4 · verification
Handicap scoring only means something if reps and effort are real. Lane 7 ships two verifiers in the first-party app today: camera rep counting (MoveNet pose detection, 100% on-device) and BLE heart-rate straps (any GATT strap, scored as Karvonen %HRR). Both feed fields the engine already scores.
Point the phone at yourself side-on, pick the exercise, do the set — the app counts reps live and logs the set as verified:true. No frame ever leaves the device.

How it works — the pose layer. The verifier lazily injects TensorFlow.js + MoveNet Lightning (~1MB, UMD builds via CDN) the first time a verifier opens — never on app boot, never bundled. Every frame runs through MoveNet locally; the counter consumes only keypoints.
How it works — the counting state machine. Pure logic, unit-tested headlessly in count.ts: one tracked joint angle per exercise, two thresholds (hysteresis) plus a persistence window —
| Exercise | Tracked joint (vertex) | down threshold | up threshold |
|---|---|---|---|
| Push-ups | shoulder → elbow → wrist | < 90° | > 160° |
| Squats | hip → knee → ankle | < 100° | > 150° |
Anti-jitter rules, all enforced in code: keypoint confidence floor 0.35 (both sides averaged when both clear it); the 300ms debounce rejects twitchy half-reps; a tracking dropout longer than the debounce invalidates the phase (the person may have moved anywhere while untracked) — brief dropouts are tolerated. The Good-GYM angle-threshold approach (MIT), kept DOM-free so it can be reused by future verifiers (watch, IMU).
Confirming the counted set logs it straight into the match; the leaderboard's verified % rises with it.
Pair any standard chest/arm strap over Web Bluetooth; the app scores each set's effort as Karvonen %HRR and attaches avgHrrPct to the entry — the exact field the v2 handicap blend consumes.

How it works. Connects to the standard GATT Heart Rate Service (0x180D → measurement characteristic 0x2A37) — Polar H10, Garmin, Wahoo, anything that broadcasts it; no vendor API needed. Per-session it averages notifications into:
Settings (resting HR default 60, age default 35 → max 185) persist in localStorage["rwf.hr.v1"] and are clamped to sane ranges. Graceful by design: unsupported browser (iOS Safari), user cancel, and mid-session disconnect (partial averages are kept) all resolve cleanly — verified reps never block a workout.
The engine side: an entry carrying avgHrrPct plus a learned player baseline switches that entry's multiplier to the 70/30 measured-vs-declared blend (see Game Rules → tier handicap), with baseline drift ≤10% per update as anti-sandbagging.
effortMultiplier · baseline logic in game-core/src/baseline.ts| Phase | Lane | Status |
|---|---|---|
| P1 | In-browser MoveNet camera counting + Web Bluetooth HR straps | BUILT — live in the app, entries log verified:true / avgHrrPct |
| P2 | HealthKit / Health Connect history import | phased (docs/05) — native wrapper required |
| P3 | WHOOP / Garmin cloud cross-check | phased (docs/05) |
| — | Custom ML rep counting | deliberately NOT a blocker — MoveNet covers MVP |
Privacy stance: inference is 100% local, no frame ever leaves the device, and HR is averaged per-set rather than streamed anywhere. The wearable research trail (chest-strap accuracy, camera angles) is documented in docs/05_RESEARCH_WEARABLES.md.