---
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<ExerciseStep> 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.