IntervalEngine : le commentaire sur les pauses disait l'inverse d'Apple

L'en-tête affirmait que `HKLiveWorkoutBuilder.elapsedTime` « exclut déjà les
pauses », et en tirait que le moteur n'avait rien à savoir de la pause ni de
l'auto-pause.

La doc Apple dit le contraire, vérifié à la source le 2026-08-20 :
« The elapsed time for the workout based on the builder's current contents,
including pauses. »

Conséquence si on câblait le moteur dessus telle quelle : une pause de 5 min
ferait avancer le déroulé de 5 min d'effort — un fractionné mis en pause pour
traverser une route se déroulerait à l'arrêt.

La propriété qui exclut réellement les pauses est
`HKWorkoutBuilder.elapsedTime(at:)` : « The duration of a workout doesn't
include intervals between pause and resume events. » Les deux textes d'Apple
se contredisent frontalement ; le choix de la source de temps reste à faire
avant le câblage.

Le moteur lui-même reste correct : il n'intègre que ce qu'on lui pousse.
Aucun comportement modifié, 57 tests Swift toujours verts.

COWORK gagne l'analyse complète du câblage (3 couches, le serveur sait déjà
produire les étapes via _blocks_from_session) pour reprise à froid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Bettinelli
2026-08-20 16:52:21 +00:00
parent 19fbfd116e
commit 891a8bddce
2 changed files with 79 additions and 3 deletions

View File

@@ -129,6 +129,59 @@ armer `allowsBackgroundLocationUpdates` sans elle termine l'app. Un garde-fou
la vérifie au démarrage, mais si elle disparaissait du build, le suivi la vérifie au démarrage, mais si elle disparaissait du build, le suivi
retomberait silencieusement au premier plan seul. retomberait silencieusement au premier plan seul.
## 🔜 Câbler IntervalEngine — analyse du 2026-08-20 (rien de codé)
`IntervalEngine` **compile sur device** (build validé le 20/08, l'app tourne),
mais **aucune vue ne l'appelle** : le moteur est un guide sans itinéraire.
### Le chaînon manquant
Le moteur attend un `CoachSessionPlan` (liste d'étapes : durée, zone, bornes
bpm). **Rien ne le lui fournit**`ConnectivityManager` reçoit les zones, le
`CoachWidgetSnapshot` et la routine, mais aucun plan structuré. Vérifié par
lecture du fichier, pas supposé.
En revanche **le serveur sait déjà produire ces étapes** :
`coach_sportif/web/fitness_export.py::_blocks_from_session` déplie une séance
en blocs chronométrés, répétitions comprises (un bug de prod du 12/08 avait
fait disparaître un 7×(1'C/1'M) : les `repeat` sont désormais dépliés), et
`_zone_bpm(zone, hr_zones)` convertit une zone en bornes bpm. Ces briques
servent déjà à l'export TCX Workout.
Indice que le chemin était prévu : les `CodingKeys` de `CoachPlanStep` sont en
snake_case (`duration_sec`, `hr_zone`, `hr_bpm_min`…) — le type a été écrit
pour décoder du JSON venant du serveur.
### Les trois couches, dans l'ordre
1. **Serveur (Python, testable sans Mac)** — une route rendant la séance du
jour au format `CoachSessionPlan`, en réutilisant `_blocks_from_session` et
`app.current_zones()` (règle 9 du CLAUDE.md : jamais un % de FCmax
générique). ✅ Commencer par là : le JSON se juge dans un navigateur avant
qu'une ligne de Swift n'en dépende.
2. **iPhone** — pousser ce plan vers la montre par `WCSession`, à côté de ce
qui part déjà.
3. **Montre**`ConnectivityManager` publie un `sessionPlan` ; une vue appelle
`engine.update(elapsed:distance:)` à chaque tick et joue une haptique sur
`events.transitions`.
### ⚠️ Le piège à trancher AVANT de câbler
L'en-tête d'`IntervalEngine.swift` affirmait que
`HKLiveWorkoutBuilder.elapsedTime` « exclut déjà les pauses ». **La doc Apple
dit l'inverse**, vérifié à la source le 20/08 : *« The elapsed time for the
workout based on the builder's current contents, including pauses. »*
Câbler le moteur dessus telle quelle ferait avancer le déroulé **pendant les
pauses** — un fractionné mis en pause pour traverser une route se déroulerait
à l'arrêt. La propriété qui exclut réellement les pauses est
`HKWorkoutBuilder.elapsedTime(at:)`. Les deux textes d'Apple se contredisent
frontalement ; le commentaire du fichier a été corrigé, **le choix de la source
de temps reste à faire**.
⚠️ Et jamais un `Timer` de vue SwiftUI : il ne tourne pas écran éteint, donc
pendant l'essentiel d'une séance (même famille de piège que `VmaTestView`).
## 👉 À reprendre ## 👉 À reprendre
-**Phase 4 login — FAIT & DÉPLOYÉ** (révocation Apple à la suppression de compte) : code complet côté backend `coach_sportif` (commit `5ae6e2d`) + clé `.p8` déployée sur le VPS prod (vérifié 2026-06-26). Rien à coder. Détails : `coach_sportif/COWORK.md`. -**Phase 4 login — FAIT & DÉPLOYÉ** (révocation Apple à la suppression de compte) : code complet côté backend `coach_sportif` (commit `5ae6e2d`) + clé `.p8` déployée sur le VPS prod (vérifié 2026-06-26). Rien à coder. Détails : `coach_sportif/COWORK.md`.
- **iOS — bloqué Mac** : builder + uploader TestFlight le natif accumulé (login Apple+Google natif, watchOS live, HeartRateRangeAlert). Sur le Mac : `cd ~/coach-ios && git pull && npm install && npx cap sync ios && open ios/App/App.xcodeproj` → Clean Build Folder → Archive → Upload. - **iOS — bloqué Mac** : builder + uploader TestFlight le natif accumulé (login Apple+Google natif, watchOS live, HeartRateRangeAlert). Sur le Mac : `cd ~/coach-ios && git pull && npm install && npx cap sync ios && open ios/App/App.xcodeproj` → Clean Build Folder → Archive → Upload.

View File

@@ -15,9 +15,32 @@ import Foundation
* *
* - il se teste intégralement sur Linux (cf. `tests-linux/`), là où * - il se teste intégralement sur Linux (cf. `tests-linux/`), là où
* `WorkoutManager` ne le peut pas ; * `WorkoutManager` ne le peut pas ;
* - le temps de référence est `HKLiveWorkoutBuilder.elapsedTime`, qui * - le temps de référence est poussé par l'appelant ;
* **exclut déjà les pauses** : le moteur n'a donc rien à savoir de la *
* pause, ni de l'auto-pause de la montre ; * **CE COMMENTAIRE AFFIRMAIT LE CONTRAIRE DE SA SOURCE** (relevé le
* 2026-08-20, avant tout câblage). Il disait que
* `HKLiveWorkoutBuilder.elapsedTime` « exclut déjà les pauses », donc que
* le moteur n'avait rien à savoir de la pause. La doc Apple dit
* l'inverse, mot pour mot : « The elapsed time for the workout based on
* the builder's current contents, **including pauses**. »
* (developer.apple.com/documentation/healthkit/hkliveworkoutbuilder/elapsedtime)
*
* Conséquence si on câble le moteur sur cette propriété telle quelle :
* une pause de 5 min ferait avancer le déroulé de 5 min d'effort. Un
* fractionné mis en pause pour traverser une route se déroulerait tout
* seul, à l'arrêt.
*
* La propriété qui exclut réellement les pauses est
* `HKWorkoutBuilder.elapsedTime(at:)` « The duration of a workout
* doesn't include intervals between pause and resume events. » Ce n'est
* PAS la même API, et les deux textes d'Apple se contredisent
* frontalement sur ce point : à trancher avant le câblage.
*
* Le moteur, lui, reste correct : il ne fait qu'intégrer ce qu'on lui
* pousse. C'est l'appelant qui devra fournir un temps réellement actif ;
*
* - il ignore donc l'auto-pause de la montre, à condition que la source de
* temps ci-dessus soit correcte ;
* - il est rejouable : réinjecter la même suite de ticks redonne le même * - il est rejouable : réinjecter la même suite de ticks redonne le même
* déroulé, ce qui rend la reprise après crash triviale * déroulé, ce qui rend la reprise après crash triviale
* (`handleActiveWorkoutRecovery`). * (`handleActiveWorkoutRecovery`).