docs(cardio): freeze v1 implementation contract

This commit is contained in:
fy59 2026-09-23 09:26:14 +02:00
parent e1d6372338
commit c13549338c
3 changed files with 424 additions and 9 deletions

View file

@ -81,7 +81,7 @@ The rollout packager now keeps the legacy request daemon and one-shot helper in
the same relocatable, hashed bundle as the Web generation coordinator, avoiding
mixed-checkout user-service execution during the controlled rollout.
Snapshot date: **2026-09-22**.
Snapshot date: **2026-09-23**.
`TRAINLOG_CODE_READABILITY_V1` normalizes the recent generation-MTP,
generation/ACK, Android-backup, Web Dashboard serialization, tests, doubles and
@ -112,8 +112,8 @@ parallel implementation of its rules.
| Boundary | Current state |
|---|---|
| Frozen project exchange | `TRAINLOG_FORMAT_V1=PASS/FROZEN` |
| Desktop SQLite | schema v29; Sleep Diary V1 is additive in v29 |
| Android SQLite | schema v26; Sleep Diary V1 is additive in v26 |
| Desktop SQLite | schema v29; Sleep Diary V1 is additive in v29; Cardio v30 work is implementation-started but not yet closed |
| Android SQLite | schema v26; Sleep Diary V1 is additive in v26; Cardio v27 work is implementation-started but not yet closed |
| Mobile snapshot | V3 active; V1/V2 readable legacy inputs; explicit V4 codec staged, not selected by transport |
| Desktop terminal backend | Notcurses only |
| Trainlog product version | `0.1.5` development, synchronized across Android and desktop; latest stable release: `v0.1.4` |
@ -147,7 +147,7 @@ parallel implementation of its rules.
| Private full-generation rollout | `TRAINLOG_SYNC_FINALIZATION_AND_ROLLOUT_V1=PASS` (authenticated Bluetooth primary transport with direct MTP recovery on one authorized phone) |
| Local Web | `TRAINLOG_WEB_V1=CONTRACT_FROZEN / IMPLEMENTATION_STARTED` |
| Web Exercises V1 | `TRAINLOG_WEB_EXERCISES_V1=PASS` |
| Current operational cursor | `TRAINLOG_CARDIO_V1_NEXT_CONTRACT`: heart-rate sensor ingestion starts in 0.1.5 and remains distinct from the frozen Bluetooth synchronization transport |
| Current operational cursor | `TRAINLOG_CARDIO_V1=CONTRACT_FROZEN / IMPLEMENTATION_STARTED`: Android-only sensor ownership, training timeline, Sleep one-tap capture, cardio session/calibration and BPM-guidance contract frozen; persistence/sync implementation is the next closeout gate |
| Complete synchronization gap contract | Frozen dependency contract; operational USB/Drive slices required by v0.1.2 are delivered |
| Isolated synchronization test environment | `TRAINLOG_SYNC_TEST_ENV_V1=PASS/FROZEN` |
| Synchronization characterization | `TRAINLOG_SYNC_CHARACTERIZATION_V1=PASS/FROZEN` |

412
docs/design/cardio_v1.md Normal file
View file

@ -0,0 +1,412 @@
# Cardio V1
Status: `TRAINLOG_CARDIO_V1=CONTRACT_FROZEN / IMPLEMENTATION_STARTED`
This document is the implementation contract for Trainlog 0.1.5 cardio,
training-timeline correlation, and Android one-tap sleep capture. It does not
declare the implementation complete.
## Ownership and hard boundaries
Android is the only heart-rate sensor client. It owns BLE discovery,
connection, reconnection, Heart Rate Measurement parsing, local persistence,
capture lifecycle, and the live BPM presentation.
Desktop, TUI and Web never scan, pair with, or connect directly to a
heart-rate sensor. They consume only measurements already recorded by Android
and synchronized through Trainlog.
Heart-rate BLE/GATT and Trainlog Bluetooth synchronization are independent:
```text
heart-rate sensor -- BLE/GATT --> Android
Android <-------- Trainlog full-generation --------> desktop
over
Bluetooth Classic RFCOMM or MTP recovery
```
The cardio implementation must not reuse the frozen Bluetooth Classic service
UUID, framing, peer identity, connection state, or transport protocol.
## Standard sensor target
Cardio V1 targets the Bluetooth LE Heart Rate Service:
- Heart Rate Service: `0x180D`;
- Heart Rate Measurement: `0x2A37`.
The parser must support the standard 8-bit and 16-bit BPM forms, optional
sensor-contact state, optional cumulative energy expended, and zero or more RR
intervals. RR intervals are preserved as raw integer units of 1/1024 second.
Battery Service may be observed as optional device metadata but is not required
to retain a heart-rate sample.
Malformed notifications are rejected. Trainlog never manufactures missing BPM
or RR values.
## Global Android heart-rate indicator
The Android application shell exposes one compact indicator at the top right:
```text
♥ 82 BPM
```
Only the heart and BPM are shown. Sensor name, address, and technical state are
not shown in the global header.
Heart colour describes connection freshness only:
- grey: known sensor unavailable/disconnected;
- orange: connecting, reconnecting, or the last measurement is stale;
- green: connected and receiving fresh valid measurements.
Colour never indicates a medical zone or whether the BPM is high or low.
When no fresh measurement exists, the value is `-- BPM`. A stale previous
number must never remain visible as if it were current.
With a fresh BPM the heart uses a restrained pulse animation derived from
`60 / bpm`. This is only a visual representation of the current measured
rate; it does not claim knowledge of the exact instant of each contraction.
## Capture identities and contexts
One capture has a stable `hrc_<uuid-v4>` identity and exactly one semantic
context:
- `session`: a normal training or MAX session identified by stable
`se_<uuid-v4>`;
- `cardio`: a dedicated cardio session identified by stable
`se_<uuid-v4>`;
- `sleep`: one Sleep Diary entry identified by stable `sl_<uuid-v4>`.
Context is never inferred from a title, clock date, exercise name, or display
position.
A workout/cardio sample may additionally snapshot the stable active occurrence
`sxe_<uuid-v4>` at the instant the sample is received.
## Raw measurement contract
Each received measurement stores at minimum:
- `capture_id`;
- monotonically increasing `sequence` within that capture;
- offset-bearing `observed_at` reception timestamp;
- integer `bpm`.
When actually supplied by the sensor it may also store:
- sensor-contact state;
- cumulative energy-expended value;
- zero or more RR intervals in 1/1024-second units.
For workout/cardio captures it may store the active `entry_id` snapshot.
Raw storage preserves signal gaps. It does not interpolate, smooth, estimate,
diagnose, infer sleep stages, infer a return to sleep, or synthesize missing
measurements.
## Android durability
Android SQLite owns authoritative field capture. An active capture remains
durable across application navigation and ordinary process/lifecycle
transitions supported by Android. Long-running acquisition uses a dedicated
foreground service distinct from synchronization.
Cardio V1 permits one active heart-rate capture at a time.
Sensor disconnect does not delete or close a capture. Reconnection resumes
future measurements and leaves the missing interval as a real gap.
Explicit stop closes the capture with its actual `ended_at`.
## Synchronization contract
Recorded cardio is synchronized Android -> desktop as the separate optional
full-generation companion:
```text
logical_name = heart-rate
format = trainlog-heart-rate
version = 1
filename = heart-rate-v1.json
```
Desktop persists and presents these measurements but is not a sensor producer.
Normal publication contains completed/stopped, not-yet-acknowledged captures.
An active capture is retained locally and is not emitted as a mutable
full-generation snapshot.
Identity is idempotent:
- capture key: `capture_id`;
- sample key: `capture_id + sequence`;
- RR key: `capture_id + sequence + rr_index`.
Exact replay is a no-op. Reusing an identity with different measured content
is rejected.
The exact captured `capture_id -> generation_id` relation is durable. Only a
correlated consumed ACK marks those captures acknowledged. A rejected
generation does not acknowledge them. Acknowledged captures remain in Android
history but are excluded from ordinary future publication.
The companion does not alter `TRAINLOG_FORMAT_V1`, mobile-export V3/V4,
Sleep Diary V1, generation/ACK framing, or Bluetooth Classic sync V1.
## Training timeline correlation
A training session must expose real time boundaries, never reconstructed
timings.
Session start/end use the existing stable session identity and canonical
`started_at` / `ended_at` lifecycle when those values are already the
authoritative timestamps.
Each session occurrence has an explicit execution lifecycle:
```text
not_started -> in_progress -> completed
```
Starting an exercise records its actual start timestamp against
`session_id + entry_id`. Ending it records its actual end timestamp.
Only one occurrence may be active at a time. Starting another occurrence while
one remains active requires an explicit resolution rather than silently
inventing an end.
Samples received while an occurrence is active snapshot that `entry_id`.
Samples between occurrences remain valid session samples with no fabricated
exercise ownership and represent rest/transition.
Ending a session while an occurrence is active requires an explicit UI action
to close the occurrence at the validated finalization instant.
The synchronized history must permit factual curve markers for:
- session start;
- exercise start;
- exercise end;
- session end.
Future set-level `set_start` / `set_end` events may extend this model, but
Cardio V1 must not infer them from reps, duration targets, or form edits.
## Dedicated cardio session type
Trainlog 0.1.5 adds a real third session type:
```text
training
max_test
cardio
```
Every existing boundary that assumes only `training|max_test` must be audited.
A frozen artifact may not be silently widened. If an existing wire codec
cannot safely express `cardio`, add a separately versioned compatible
extension while retaining legacy readers.
A cardio session consists of cardio exercises and optional phases. Initial
exercise families include treadmill, bicycle, elliptical, rower, stepper,
walking, running, and custom cardio.
A phase may express a type, target, duration, and exit condition. Required
phase concepts include warm-up, work, recovery, and cool-down.
The motor must be able to represent fixed duration, reaching a BPM zone,
recovering below a threshold, and bounded combinations of time and
heart-rate conditions.
Machine speed, resistance, incline, and distance may be recorded, but BPM is
the primary guidance signal.
## BPM-guidance motor
Cardio V1 guidance outputs are:
```text
ACCÉLÈRE
MAINTIENS
RALENTIS
```
Guidance is based only on fresh measurements and the phase target.
Hysteresis and a minimum persistence interval prevent oscillation around a
boundary. One isolated sample must not cause rapid instruction changes.
When the signal becomes stale, guidance is suspended. The global heart becomes
orange and BPM becomes unavailable rather than continuing from an old value.
## Calibration cardio exercise
The first dedicated cardio exercise is `Calibration cardio`.
Its purpose is to establish a personal measured reference for later guided
exercise. Trainlog records an **observed peak BPM**, not an automatically
asserted physiological maximum.
The protocol is progressive:
```text
warm-up -> easy stage -> progressive stages -> user-chosen highest tolerated
effort for that day -> stop effort -> recovery observation
```
A manual stop is always immediately available. The motor never attempts to
beat the previous observed peak automatically.
Each calibration is immutable/versioned and retains at minimum:
- stable `calibration_id`;
- protocol version;
- performance timestamp;
- associated heart-rate capture;
- observed peak BPM;
- effort-end timestamp;
- recovery timeline.
Recovery projections may expose +1, +2 and +3 minute facts while the complete
raw curve remains authoritative.
A later calibration creates a new reference. It never rewrites an older
calibration or a prior guided session.
Every guided cardio session snapshots the exact `calibration_id` and target
thresholds used at that time.
Measured, user-configured, and calculated/estimated values remain explicitly
distinguishable.
## Android Sleep entry point
Android adds a first-class `Sommeil` menu entry backed by the existing Sleep
Diary domain. It must not create a parallel sleep database or incompatible
sleep truth.
Night-time interaction is deliberately minimal. The primary screen exposes:
```text
[ COUCHÉ ]
[ médicament 1 ] [ médicament 2 ]
[ médicament 3 ] [ + ]
[ RÉVEIL ]
[ LEVÉ ]
```
Buttons are large, visually consistent with Trainlog, and usable while only
partly awake.
### Couché
One tap records the actual `bed_time` timestamp in the correct stable Sleep
Diary entry. If heart-rate acquisition is available, the nocturnal capture is
linked directly to that `sleep entry_id`.
### Médicaments
Active configured medications are one-tap actions. A tap stores the actual
intake timestamp plus stable medication identity and the effective
name/dose/unit snapshot. Full editing remains available later.
### Réveil
There is no `Rendormi` button.
One tap records one punctual night wake event. Nothing else is required before
the user tries to return to sleep.
Trainlog does not infer a certain return-to-sleep time from BPM or RR data.
An approximate interval may be completed later only if the user chooses to
provide it.
### Levé
One tap records the actual `final_get_up` timestamp, closes the night, and
stops the linked nocturnal heart-rate capture when one is active.
### Immediate undo and later correction
After Couché, medication, Réveil, or Levé, Android shows a short-lived action
such as:
```text
Réveil enregistré à 02:17 [ ANNULER ]
```
Undo is scoped to that exact action and must not roll back unrelated newer
state.
`Corriger la nuit` provides later editing of timestamps, medication
intakes, accidental wake points, forgotten events, and existing subjective
Sleep Diary fields.
The night-time rule is: one tap, one timestamp, then no further required
interaction.
## Sleep + cardio factual timeline
A nocturnal capture is owned by stable `sleep entry_id`, not only a date.
After synchronization, Analyse may superpose factual events and measurements:
- Couché;
- medication intakes;
- wake points;
- Levé;
- BPM;
- RR when supplied;
- real sensor disconnect/reconnect gaps.
Trainlog does not diagnose sleep, invent sleep stages, or claim a certain
return-to-sleep instant from heart rate alone.
## Web/Analyse projection
Web and desktop consume synchronized facts only.
Training views may show the BPM curve, session bounds, exercise intervals,
rest/transition gaps, and factual min/max/mean projections.
Cardio views may additionally show phase targets, chronological guidance,
calibration identity, and measured response versus target.
Sleep views may overlay Sleep Diary events with BPM/RR measurements.
Graphs are projections. Persistent measurements, identities, and timestamps
remain the source of truth.
## Performance bounds
One night can contain tens of thousands of samples. Storage and exchange must
use bounded artifacts, indexed temporal queries, transactional import, and
avoid per-sample N+1 behavior on analysis paths.
Trainlog does not arbitrarily downsample the authoritative raw capture merely
for transport convenience. Presentation may derive bounded projections while
retaining the recorded source data.
## Implementation order
1. audit/freeze this contract;
2. Android + desktop persistence and strict heart-rate companion exchange;
3. standard Heart Rate Measurement parser and real-sensor diagnostics;
4. Android foreground acquisition service and global ♥ BPM indicator;
5. training start/end and exercise start/end timeline;
6. Android one-tap Sleep UI and sleep-linked capture;
7. true `cardio` session type;
8. versioned Calibration cardio exercise;
9. BPM-guided phase motor;
10. Web factual visualization;
11. complete regression, migration, hardware, replay, and restart validation.
No stage permits Web/TUI sensor connection.

View file

@ -59,14 +59,17 @@ mobile snapshot or `TRAINLOG_FORMAT_V1` contracts.
Stable v0.1.4 closes the Analyse/Sleep Diary cycle and freezes the Bluetooth
Classic synchronization transport independently from future sensor work.
The next development cycle is 0.1.5 cardio. Android will own heart-rate sensor
discovery/acquisition and timestamped local capture; synchronized desktop/Web
surfaces will consume recorded measurements and will not connect directly to
the sensor. Heart-rate ingestion must not reuse or overload the frozen Trainlog
The 0.1.5 cycle owns cardio end to end. Android alone discovers and reads the
heart-rate sensor, exposes the global ♥ BPM state, timestamps training
session/exercise boundaries, provides one-tap Sleep capture, and records the
new dedicated cardio session type. Cardio starts with a versioned Calibration
exercise and later drives phase guidance from fresh BPM. Desktop/Web consume
only synchronized recorded measurements and never connect directly to the
sensor. Heart-rate ingestion must not reuse or overload the frozen Trainlog
Bluetooth synchronization transport contract.
```text
CURRENT_OPERATIONAL_CURSOR=TRAINLOG_CARDIO_V1_NEXT_CONTRACT
CURRENT_OPERATIONAL_CURSOR=TRAINLOG_CARDIO_PERSISTENCE_SYNC_V1
```
The focused `TRAINLOG_SYNC_CAUSAL_DELETE_V1_CLOSEOUT` is complete before this