Solveur : retire le plafond de cumul journalier, ajoute un malus de chaînage

- Retiré : le solveur ne pénalise plus un bénévole qui dépasserait 8 h sur
  la journée (`_daily_overage_minutes`/`_DAILY_OVERAGE_PENALTY_PER_MIN`,
  côté back et mock). Seule la validation le signale encore, à la
  confirmation (`DAILY_HOURS_EXCEEDED`, inchangée). `SolverInput.day_parts`
  et son chargement sont retirés au passage : ils ne servaient qu'à ça.
- Ajouté : malus (-40) si le bénévole a déjà (affectation décidée) le
  créneau précédent de ce même poste, le même jour
  (`SolverShift.predecessor_shift_id`, calculé par le loader à partir de
  tous les créneaux du poste — pas seulement ceux à compléter). Favorise la
  rotation plutôt que d'enchaîner toujours la même personne sur un poste.
  Reste un malus, pas une exclusion : le seul candidat pour un créneau
  urgent est quand même proposé.
- Miroir côté mock (`runSolver`) pour que la démonstration se comporte pareil.
- CLAUDE.md à jour (§4).

Tests : remplacé le test du plafond journalier par un test de non-régression
explicite (le cumul n'est plus pénalisé) + un test isolant le repos (qui,
lui, reste vérifié) ; ajouté 3 tests pour le chaînage (préfère un bénévole
frais, reste proposé si c'est le seul candidat urgent, un créneau existant
non-prédécesseur ne déclenche pas le malus).

Vérifié : 183 tests backend, ruff, alembic check ; typecheck/lint/build/smoke
frontend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
AdamThomas
2026-09-15 21:12:30 +02:00
co-authored by Claude Sonnet 5
parent e405590bbc
commit 64e2f86647
6 changed files with 176 additions and 94 deletions
+14 -7
View File
@@ -274,18 +274,25 @@ affectation n'est jamais persistée par cet endpoint.
- bonus si le poste fait partie des vœux du bénévole
(`VolunteerPositionWish`) ;
- malus proportionnel aux minutes hors des fenêtres de disponibilité
déclarées (réutilise `validation.engine.uncovered_minutes`), au dépassement
du cumul de 8 h ce jour-là, et si le repos avec une affectation déjà
décidée serait `< 8 h 00` ;
déclarées (réutilise `validation.engine.uncovered_minutes`), et si le
repos avec une affectation déjà décidée serait `< 8 h 00` ;
- malus si le bénévole a déjà (affectation décidée) le créneau précédent de
ce même poste, le même jour (`SolverShift.predecessor_shift_id`) —
favorise la rotation plutôt que d'enchaîner toujours la même personne sur
un poste ;
- malus si le créneau chevauche le passage d'un artiste que le bénévole
veut voir (`VolunteerArtist` + `Artist.start_time`/`end_time`) — lui
ferait manquer le passage ;
- léger malus pour les bénévoles déjà bien chargés (répartition de la
charge).
- Simplification assumée : cumul de 8 h et repos ne sont vérifiés que
contre les affectations **déjà décidées**, pas entre deux suggestions du
même calcul — la validation finale à la confirmation reste seule juge de
ce cas limite.
- **Pas de plafond de cumul journalier côté solveur** : contrairement à la
validation (`DAILY_HOURS_EXCEEDED`, § 3), le solveur ne pénalise pas un
bénévole qui dépasserait 8 h sur la journée — seule la validation le
signale, à la confirmation.
- Simplification assumée : repos et chaînage ne sont vérifiés que contre
les affectations **déjà décidées**, pas entre deux suggestions du même
calcul — la validation finale à la confirmation reste seule juge de ce
cas limite.
**Classement par défaut, sans lancer le solveur** — `frontend/src/lib/planning.ts`
(`candidateChanceScore`) porte une version simplifiée du même scoring (vœu de