---
issueRef: "#94"
version: 8
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784936913063
---


---

# Architect — Cadrage point 2 : regroupement des étapes sans chrono (ticket #94)

Périmètre : uniquement le point 2 de l'analyse UX ci-dessus (regroupement/auto-complétion silencieuse des étapes sans chrono). Les points 1/3/4/5 restent des ajustements UI purs, DevFrontend les traite directement sans ce cadrage.

## Ce qui existe déjà et se réutilise tel quel

Code lu : `lib/application/use_cases.dart` (`ActiveExerciseStepUseCases`, ~L3087-3653) et `lib/domain/entities.dart` (`ActiveExerciseStepProgressState`, `ActiveExerciseStepResult`, `ExerciseStep`, `ActiveExerciseStepProgressStatus`).

- `_StepSequenceContext` porte déjà `steps`, `expectedPassages`, `autoStartNextTimedStep` (résolu depuis #73/#76) — tout ce qu'il faut pour scanner la timeline à venir sans lecture DB supplémentaire.
- `_advanceState(context, state, now, {completedStep})` fait déjà avancer d'une étape en traversant les frontières de passage (`nextStep >= steps.length → nextPassage++`) et sait déjà produire `sequenceComplete` en fin de dernier passage. C'est exactement la mécanique de traversée qu'il faut réutiliser en boucle pour le regroupement — **pas besoin de réécrire la logique de frontière de passage**, juste de l'appeler plusieurs fois d'affilée avant l'action déclenchante, comme `_autoAdvanceElapsedTimers` le fait déjà pour les chronos qui s'enchaînent.
- `_stepResult(...)` sait déjà construire un `ActiveExerciseStepResult` `completed` avec `actualReps ?? step.defaultTargetValue` (cf. `completeCurrentStep`, L3301-3303) — c'est la convention à réutiliser pour l'auto-complétion silencieuse (valeur cible enregistrée comme valeur réalisée, jamais `null`, cohérent avec `gametime-architecture-exercise-steps` qui laissait ce choix à l'implémentation).
- `ActiveExerciseStepProgressStatus.waitingManual` reste le statut correct pour une étape reps individuelle ; on ne touche pas à l'enum.

## Ce qui doit changer : nouvelle logique de calcul, pas de nouveau champ persisté

**Pas de changement de modèle de données.** `ActiveExerciseStepProgressState` et `ActiveExerciseStepResult` restent inchangés dans leur forme. La notion de « suite silencieuse » est **dérivée à la volée** depuis `context.steps` + position courante, exactement comme `_isNextTimedStepReady` (présentation, ~L2354-2368) dérive déjà `Chrono suivant prêt` sans champ dédié. Raison : la suite n'est qu'une lecture en avance sur une liste déjà chargée en mémoire (max 8 étapes, cf. `gametime-architecture-exercise-steps`), aucune raison de la persister.

### Règle d'éligibilité au regroupement silencieux

Une étape est éligible au regroupement silencieux si et seulement si :

```dart
bool _isSilentStep(ExerciseStep step) {
  if (step.type == ExerciseStepType.time) return false;
  if (step.hasScore && step.scoreInputMode == ScoreInputMode.stopwatch) return false;
  return true; // reps sans score, ou reps avec score manuel
}
```

C'est la traduction directe du cas limite déjà identifié par UX : une étape reps à score chrono a son propre chrono actif et reste un point d'interaction à part entière.

### Calcul de la suite courante (nouvelle fonction application, pure, sans I/O)

```dart
_SilentStepGroup _resolveSilentGroup(
  _StepSequenceContext context,
  ActiveExerciseStepProgressState state,
) {
  final steps = <ExerciseStep>[];
  var passage = state.currentPassageIndex;
  var index = state.currentStepIndex;
  while (true) {
    final step = context.steps[index];
    if (!_isSilentStep(step)) {
      return _SilentStepGroup(steps: steps, blockingStep: step, blockingPassageIndex: passage, reachesSequenceEnd: false);
    }
    steps.add(step);
    index++;
    if (index >= context.steps.length) {
      index = 0;
      passage++;
      if (passage >= context.expectedPassages) {
        return _SilentStepGroup(steps: steps, blockingStep: null, blockingPassageIndex: passage, reachesSequenceEnd: true);
      }
    }
  }
}
```

Appelée uniquement quand l'étape courante (`_currentStep(context, state)`) est elle-même silencieuse — sinon la vue reste le mode `_CurrentStepPane` classique existant (étape chronométrée, ou étape reps isolée). En pratique une étape reps isolée forme un groupe à 1 élément : le layout groupé et le layout actuel convergent visuellement pour ce cas trivial. DevFrontend affiche le layout groupé dès qu'il y a ≥1 étape silencieuse en tête, pas seulement ≥2, pour ne pas avoir deux présentations différentes selon la longueur de la suite.

### Port/contrat exposé à la présentation

Étendre `ActiveExerciseStepProgressView` (pas de nouveau port séparé — c'est le même port `ActiveExerciseStepUseCases`/vue déjà consommé par l'écran) avec :

```dart
final class ActiveExerciseStepProgressView {
  // ... champs existants inchangés (state, steps, expectedPassages, results)
  final List<ExerciseStep> silentPendingSteps; // vide si l'étape courante n'est pas silencieuse
  final ExerciseStep? silentGroupBlockingStep; // étape chronométrée qui suit la suite, null si fin de séquence
  final bool silentGroupReachesSequenceEnd; // true si la suite va jusqu'à sequenceComplete
}
```

Calculée dans `startOrResumeProgress`/`readProgress` à partir de `_resolveSilentGroup`, en lecture seule — aucune écriture tant que l'utilisateur n'a pas déclenché l'action de clôture. `silentPendingSteps` est la liste à afficher (« Pompes 10 reps / Squats 15 reps / Fentes 10 reps ») ; `silentGroupBlockingStep` alimente la ligne « Chrono suivant : Gainage · 20 s » ; `silentGroupReachesSequenceEnd` sert à afficher le message de fin de série au lieu du bouton `Lancer`.

### Nouvelle méthode d'auto-complétion silencieuse (écriture)

Un seul point d'entrée, appelé par les deux actions déclenchantes :

```dart
Future<ActiveExerciseStepProgressState> completePendingSilentSteps({
  required String sessionId,
  required int programIndex,
  required int exerciseIndex,
  required int setIndex,
}) async {
  final context = await _stepContext(...);
  var state = await _requiredStepProgressState(...);
  final now = clock.now();
  while (_isSilentStep(_currentStep(context, state))) {
    final step = _currentStep(context, state);
    final result = _stepResult(
      context: context, state: state, step: step,
      status: SetResultStatus.completed, now: now,
      actualReps: step.type == ExerciseStepType.reps ? step.defaultTargetValue : null,
    );
    await sessionRepository.saveExerciseStepResult(result);
    state = _advanceState(context, state, now, completedStep: step);
  }
  await sessionRepository.saveExerciseStepProgressState(state);
  return state;
}
```

Réutilise `_advanceState` tel quel pour la traversée multi-passages (aucune modification de cette méthode nécessaire : elle gère déjà `nextPassage++` et `sequenceComplete`).

**Intégration côté « Lancer » (chrono suivant)** : `startTimer(...)` doit appeler `completePendingSilentSteps(...)` en tout premier (si l'étape courante est silencieuse) avant sa logique actuelle — après quoi l'étape courante est bien l'étape chronométrée bloquante, et le reste de `startTimer` fonctionne sans modification.

**Intégration côté « Terminer la série »** : le use case derrière `_finishSet` côté présentation (`recordCurrentSetResult`/`upsertSetResultAtPosition`, `lib/application/use_cases.dart` ~L2435-2487) doit, quand une séquence d'étapes existe pour la série (`context` résoluble sans exception) et que l'état n'est pas déjà `sequenceComplete`, appeler `completePendingSilentSteps(...)` avant d'enregistrer le résultat de série — silencieusement, sans passer par un dialogue supplémentaire (contrairement au dialogue existant `Chrono non lancé` qui gère un autre cas). Point d'attention DevBackend : ne déclencher cet appel que si l'étape courante est silencieuse ; si elle est déjà chronométrée/bloquante, ne rien faire (comportement actuel inchangé, l'utilisateur clôture une série avec une étape chrono en cours comme aujourd'hui).

### Cas limites couverts par ce calcul

- **Traversée de plusieurs passages** : couverte nativement par la boucle `while` de `_resolveSilentGroup`/`completePendingSilentSteps`, qui réutilise `_advanceState` — aucune règle spéciale à écrire, c'est la même mécanique que `_autoAdvanceElapsedTimers` applique déjà pour les chronos.
- **Étape reps à score chrono** : exclue par `_isSilentStep`, casse le regroupement comme demandé — elle redevient un point d'arrêt classique (`waitingManual`), sans changement de comportement pour elle.
- **Séquence entièrement sans chrono** : `silentGroupReachesSequenceEnd == true` dès `startOrResumeProgress`, dès la première étape ; `completePendingSilentSteps` sera appelée uniquement à `Terminer la série`, donc aucune écriture avant cette action (conforme à « l'utilisateur ne touche le téléphone qu'une fois, à la fin »).
- **Reprise après kill/pause** : rien à faire de spécial, `silentPendingSteps` est recalculé à chaque `startOrResumeProgress`/`readProgress` depuis l'état persistant réel (pas de champ volatile en mémoire à perdre).

### Pas de réglage configurable

Confirmé avec la recommandation UX : comportement toujours actif, pas d'override façon `autoStartNextTimedStep` (#73). Contrairement à #73, il n'existe ici aucune fenêtre de temps réel entre étapes silencieuses (pas de chrono qui tourne), donc aucun cas d'usage produit ne justifie une confirmation étape par étape. Ne pas ajouter de champ domaine `Exercise`/`ProgramExercise`/`WorkoutTemplateExerciseOverride` pour ce point. Si un besoin réel émerge après usage, rouvrir un ticket dédié plutôt que d'anticiper.

## Découpage en lots

- **B1 — DevBackend** : `_isSilentStep`, `_resolveSilentGroup`, extension de `ActiveExerciseStepProgressView` (3 nouveaux champs), `completePendingSilentSteps`, intégration dans `startTimer` et dans le use case de clôture de série (`recordCurrentSetResult`/équivalent derrière `_finishSet`). Tests ciblés : suite silencieuse simple, suite traversant 2+ passages, suite bloquée par étape à score chrono, suite qui va jusqu'à `sequenceComplete`, reprise après kill au milieu d'une suite silencieuse.
- **F — DevFrontend** (en plus des points 1/3/4/5 déjà cadrés UI par UX) : nouveau layout du panneau séquence quand `silentPendingSteps.isNotEmpty` (liste « À enchaîner sans interruption » + ligne `Chrono suivant : nom · cible` + bouton `Lancer`, ou message de fin de série sans bouton dédié si `silentGroupReachesSequenceEnd`), réutilisant le format déjà défini par UX dans son cadrage ci-dessus. Dépend de B1.

## Étape suivante

- **Git** : créer la branche de travail pour #94.
- **DevBackend** : lot B1 ci-dessus (point 2 uniquement).
- **DevFrontend** : points 1, 2 (lot F, dépend de B1), 3, 4, 5 — cadrage UX complet disponible plus haut dans ce carnet pour 1/3/4/5, cadrage Architect ci-dessus pour le point 2.
