--- name: gametime-architecture-exercise-steps description: memory note gametime-architecture-exercise-steps metadata: type: project --- # GameTime — Architecture exercices à plusieurs étapes Décision d'architecture pour le ticket #54, basée sur la mémoire UX `gametime-ux-exercise-steps` et les patterns existants : snapshots, `ActiveSetResult` distinct des résultats détaillés, timers persistés via tables dédiées (`ActiveRestState`, `ActiveScoreStopwatchState`). ## Principe métier Les étapes sont un rythme interne d'un exercice, pas une nouvelle mesure de série. Elles coexistent avec les mesures existantes `Temps`, `Répétitions`, `Score`. - Si `Répétitions` est active sur la série, un passage complet de la séquence d'étapes = une répétition/passsage réalisé. - Si `Temps` est actif sur la série, il reste une durée/fenêtre globale de série, indépendante des timers d'étapes. - Le score de série reste porté par `ActiveSetResult` / `WorkoutHistorySetResult`. - Les résultats d'étapes restent dans des entités dédiées et ne se mélangent jamais avec les résultats de série. ## Domaine exercice Ajouter : ```dart enum ExerciseStepType { time, reps } final class ExerciseStep { final String id; final int position; final String name; final ExerciseStepType type; final int defaultTargetValue; final bool hasScore; final ScoreInputMode scoreInputMode; final String? scoreLabel; final String? scoreUnit; final double? defaultTargetScore; final int? defaultTargetScoreTimeMs; } ``` Ajouter `List steps` sur `Exercise`. Invariants : - `steps.length <= 8`. - positions uniques, contiguës et `>= 0` dans l'ordre affiché. - `name` non vide. - `defaultTargetValue > 0` ; unité métier : secondes si `type=time`, répétitions si `type=reps`. - si `hasScore=false`, aucune valeur/label de score d'étape ne doit être significative. - si `hasScore=true` et `scoreInputMode=manual`, `scoreLabel` et `scoreUnit` obligatoires ; `defaultTargetScore` nullable mais si renseigné `>= 0`. - si `hasScore=true` et `scoreInputMode=stopwatch`, `defaultTargetScoreTimeMs` nullable mais si renseigné `> 0`; pas d'unité libre. - score manuel et score chrono d'étape sont exclusifs. - `type=time` + score chrono d'étape est autorisé mais doit rester un avertissement UX non bloquant. ## Snapshot pattern Les étapes doivent suivre le même pattern que les autres propriétés d'exercice : `Exercise.steps` -> snapshot dans `ProgramExercise` -> inclus dans `ProgramExercise.toSnapshotJson()` -> inclus dans `WorkoutTemplateProgram.programSnapshotJson` -> résolu dans `ActiveWorkoutSession.resolvedTemplateSnapshotJson` -> copié dans l'historique. Recommandation Drift : - source normalisée : table `exercise_steps` liée à `exercises`. - snapshot programme : colonne `exercise_steps_snapshot_json` sur `program_exercises` plutôt qu'une table normalisée de snapshots pour le MVP. - historique : les snapshots utiles sont copiés dans `WorkoutHistoryStepResult`, et le snapshot global reste dans `historySnapshotJson`. Les modifications ultérieures d'un Exercise ne modifient pas les ProgramExercise existants. ## Modèle d'exécution Ne pas étendre `ActiveSetResult` pour porter les détails d'étapes. `ActiveSetResult` reste le résultat global de série. Ajouter une table/entité dédiée pour la progression courante : `ActiveExerciseStepProgressState`. Champs recommandés : - champs sync communs. - `activeWorkoutSessionId`. - `programIndex`, `exerciseIndex`, `setIndex`. - `currentPassageIndex` : `>= 0`. - `currentStepIndex` : `>= 0`. - `currentStepSnapshotId`. - `status` : `notStarted | waitingManual | runningTimer | pausedTimer | stoppedTimer | sequenceComplete`. - `startedAt?` : horodatage du run timer courant. - `accumulatedMs` : `>= 0`, temps déjà accumulé pour l'étape timer courante. - `lastTransitionAt`. Contraintes : - unique `(active_workout_session_id, program_index, exercise_index, set_index)`. - pas de compteur uniquement mémoire ; tout timer d'étape actif est reconstituable depuis `startedAt + accumulatedMs`. - pause séance : un `runningTimer` devient `pausedTimer` en figeant `accumulatedMs`. - kill app : à la reprise, recalculer depuis les horodatages et avancer automatiquement les étapes chronométrées écoulées jusqu'à la première étape manuelle ou fin de séquence. Ajouter une table/entité de résultats actifs : `ActiveExerciseStepResult`. Champs recommandés : - champs sync communs. - `activeWorkoutSessionId`. - `programSnapshotId`, `exerciseSnapshotId`. - `programIndex`, `exerciseIndex`, `setIndex`. - `passageIndex`, `stepIndex`, `stepSnapshotId`. - snapshots : `stepNameSnapshot`, `stepTypeSnapshot`, `targetValueSnapshot`, `hasScoreSnapshot`, `scoreInputModeSnapshot`, `scoreLabelSnapshot?`, `scoreUnitSnapshot?`, `targetScoreSnapshot?`, `targetScoreTimeMsSnapshot?`. - `status` : `completed | skipped`. - `startedAt?`, `completedAt?`. - `actualTimeMs?` pour étape `time`. - `actualReps?` pour étape `reps` si correction future ; MVP peut enregistrer la cible quand validée ou laisser null avec statut completed selon choix UI, mais l'historique doit rester lisible. - `actualScore?` pour score manuel d'étape. - `actualScoreTimeMs?` pour score chrono d'étape. - `note?` optionnel. Contraintes : - unique `(active_workout_session_id, program_index, exercise_index, set_index, passage_index, step_index)`. - `skipped` implique toutes les valeurs `actual*` nulles. - `actualTimeMs` seulement pour `stepType=time`. - `actualReps` seulement pour `stepType=reps`. - `actualScore` seulement si `hasScoreSnapshot=true` et `scoreInputModeSnapshot=manual`. - `actualScoreTimeMs` seulement si `hasScoreSnapshot=true` et `scoreInputModeSnapshot=stopwatch`. - score manuel et score chrono jamais remplis simultanément. ## Historique Ajouter `WorkoutHistoryStepResult`, distinct de `WorkoutHistorySetResult`. Champs analogues à `ActiveExerciseStepResult`, avec `workoutHistoryId` au lieu de `activeWorkoutSessionId` et snapshots complets pour affichage autonome. À la clôture d'une séance : - créer `WorkoutHistory` et `WorkoutHistorySetResult` comme aujourd'hui pour les résultats globaux de série. - copier tous les `ActiveExerciseStepResult` de la session vers `WorkoutHistoryStepResult`. - ne jamais recalculer `WorkoutHistorySetResult.actualReps` depuis les step results sans décision explicite du use case ; les passages réalisés peuvent alimenter l'UI, mais le résultat de série reste sa propre source. ## Drift / migration Le schéma actuel est `schemaVersion = 7`. Le ticket #54 doit passer à `schemaVersion = 8`. Ajouts recommandés : - table `exercise_steps`. - colonne `exercise_steps_snapshot_json` sur `program_exercises`. - table `active_exercise_step_progress_states`. - table `active_exercise_step_results`. - table `workout_history_step_results`. - index de session sur les tables actives. - index history sur `workout_history_step_results(workout_history_id)`. - contraintes CHECK pour types, statuts, valeurs positives/non négatives. ## Audio / bips Choix recommandé : introduire un port applicatif/presentation `ExerciseStepAudioCuePlayer` ou équivalent, avec méthodes métier `playShortCountdownBeep()` et `playLongCompletionBeep()`. Adapter Flutter recommandé : `audioplayers` avec deux assets très courts bundlés (`short_beep`, `long_beep`), préchargés et joués en mode faible latence si possible. Raison : solution mature et multiplateforme iOS/Android, fiable pour distinguer bip court et bip long. `SystemSound` est plus simple mais ne garantit pas un bip long distinct ni un contrôle suffisant. Une génération synthétique pure éviterait les assets mais augmente la complexité native/test. Invariants de test : - les widgets/use cases dépendent du port, jamais directement du lecteur audio réel. - tests unitaires/widget avec fake player uniquement. - l'absence/échec audio ne doit pas bloquer la progression d'étape. ## Tickets créés - #56 `[DevBackend] Modèle domain + Drift pour exercices à étapes`. - #58 `[DevBackend] Exécution persistante des étapes et résultats par passage`, dépend de #56. - #59 `[DevFrontend] Éditeur d'exercice avec séquence d'étapes`, dépend de #56. - #60 `[DevFrontend] Exécution de séance avec module séquence et bips`, dépend de #58 et #59. - #61 `[DevFrontend] Plan de séance et historique avec résultats d'étapes`, dépend de #58. - #62 `[QA] Validation exercices à étapes, persistance et historique`, dépend de #60 et #61. Ordre recommandé : #56 -> #58 -> #59 -> #60 et #61 -> #62. Note orchestration : le sprint dédié `Exercice editor enhancement` existe avec id `1bb8bdf2-9c35-4f53-9a31-1390a47bec63`, mais l'outil de création de ticket exposé à Architect ne permet pas de renseigner `sprintId`. Les tickets ont donc été créés liés à #54 et devront être rattachés au sprint par l'orchestrateur si nécessaire.