Solveur de suggestions (OR-Tools CP-SAT) — ne place jamais personne

Nouveau module `backend/app/solver/` (mêmes conventions que
`validation/` : models purs, engine pur testé, loader seul lien avec la
base). Pour chaque créneau encore sous son effectif idéal, calcule une
liste ordonnée de bénévoles candidats — c'est la coordination qui
affecte, dans l'écran habituel ; le solveur fait juste remonter ces
candidats en tête de liste (voir le câblage frontend à suivre).

- Seule contrainte dure : aucun chevauchement de créneaux pour un même
  bénévole (règle SHIFT_OVERLAP), à la fois contre l'existant et entre
  deux suggestions du même calcul. L'effectif idéal est un plafond dur.
- Tout le reste est un terme de l'objectif, pas un blocage : priorité
  forte aux créneaux sous l'effectif minimal, bonus vœu de poste, malus
  disponibilité manquante (réutilise `validation.engine.uncovered_minutes`),
  cumul de 8 h et repos (contre les affectations déjà décidées),
  répartition de la charge.
- `POST /editions/{id}/solver/suggestions` (COORDINATION/référent) :
  recalcule à la demande, ne persiste jamais d'affectation.

Dépendance `ortools`. 18 nouveaux tests (moteur + API), 172 tests au
vert, `ruff` et `alembic check` verts. Testé sur le jeu de démo (120
bénévoles, 100 créneaux) : ~0,3 s.

CLAUDE.md : nouvelle section 4 (solveur), renumérotation 5-10,
compétences retirées de « hors périmètre », journal étape 15.

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-11 08:16:17 +02:00
co-authored by Claude Sonnet 5
parent 36d108b47d
commit 803db5e9a9
12 changed files with 818 additions and 11 deletions
+55 -11
View File
@@ -235,7 +235,48 @@ Sémantique figée :
---
## 4. Périmètre
## 4. Solveur de suggestions (OR-Tools CP-SAT)
Module isolé `backend/app/solver/` (mêmes conventions que `validation/` :
`models.py` types purs, `engine.py` fonction pure `solve()`, `loader.py` seul
point de contact avec la base). **Ne place jamais personne** : il calcule,
pour chaque créneau encore sous son effectif idéal, une liste ordonnée de
bénévoles candidats. La coordination affecte elle-même dans l'écran habituel
(grille des besoins) ; l'appli fait simplement remonter ces candidats en tête
de la colonne des choix quand on ouvre le détail d'un poste. Aucune
affectation n'est jamais persistée par cet endpoint.
- `POST /editions/{id}/solver/suggestions` (COORDINATION ou référent) :
recalcule à la demande (rien n'est mis en cache côté serveur) et renvoie
`{ suggestions: [{ shift_id, volunteer_ids }], shifts_considered,
volunteers_considered }`, `volunteer_ids` du meilleur au moins bon.
- **Seule contrainte dure** : aucun chevauchement de créneaux pour un même
bénévole (règle `SHIFT_OVERLAP`) — à la fois contre les affectations déjà
décidées et entre deux créneaux nouvellement suggérés. L'effectif idéal est
un plafond dur (le solveur ne sur-staffe jamais).
- Tout le reste est un terme de l'objectif (maximisation), pas une
contrainte — cohérent avec le moteur de validation, qui reste seul juge au
moment où la coordination confirme une affectation :
- un créneau encore sous son effectif **minimal** prime largement sur tout
le reste (le solveur doit presque toujours proposer quelqu'un pour un
trou urgent, quitte à ce que ce soit un pis-aller signalé ensuite par un
avertissement) ;
- 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` ;
- 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.
---
## 5. Périmètre
### Premier incrément (en cours)
@@ -252,15 +293,16 @@ Sémantique figée :
`[{ position, shifts: [{ shift, min_staff, ideal_staff, assigned }] }]`.
6. Jeu de données de démonstration : 120 bénévoles, 15 postes, 4 jours,
quelques trous d'effectif, 2–3 avertissements actifs.
7. Solveur de suggestions OR-Tools CP-SAT (section 4) — en cours.
### Hors périmètre (le modèle doit juste pouvoir les accueillir sans refonte)
Solveur OR-Tools · SMS · constructeur de formulaire · pointage sur site ·
SMS · constructeur de formulaire · pointage sur site ·
fonctions d'IA. *(L'envoi d'emails par SMTP est en place — voir § 2.)*
---
## 5. Arborescence
## 6. Arborescence
```
.
@@ -286,12 +328,13 @@ fonctions d'IA. *(L'envoi d'emails par SMTP est en place — voir § 2.)*
│ │ │ # health, auth, accounts, editions,
│ │ │ # day_parts, imports, artists, volunteers,
│ │ │ # availability, positions, shifts,
│ │ │ # assignments, matrix, notifications
│ │ │ # assignments, solver, matrix, notifications
│ │ ├── auth/ # mot de passe + lien magique + JWT + rôles
│ │ ├── email/ # SMTP (sender) + rendu des emails (templates)
│ │ ├── importing/ # import CSV/Excel (parser, text, service)
│ │ ├── models/ # tables SQLModel
│ │ ├── validation/ # moteur pur (models, engine) + loader (base)
│ │ ├── solver/ # suggestions OR-Tools (models, engine, loader)
│ │ ├── bootstrap.py # instance vierge : édition + 1er compte
│ │ ├── seed.py # jeu de démonstration (python -m app.seed)
│ │ ├── i18n/
@@ -306,7 +349,7 @@ fonctions d'IA. *(L'envoi d'emails par SMTP est en place — voir § 2.)*
---
## 6. Commandes
## 7. Commandes
Aucune dépendance locale requise hors Docker.
@@ -326,7 +369,7 @@ Aucune dépendance locale requise hors Docker.
---
## 7. Conventions
## 8. Conventions
- **Code, base, noms de fichiers en anglais.** Le français vit dans les
fichiers de traduction et les commits.
@@ -347,7 +390,7 @@ Aucune dépendance locale requise hors Docker.
---
## 8. Déploiement
## 9. Déploiement
- Dépôt GitHub. VPS avec Docker.
- `.github/workflows/deploy.yml` : sur chaque push `main` → build de l'image
@@ -361,7 +404,7 @@ Aucune dépendance locale requise hors Docker.
---
## 9. Journal des étapes
## 10. Journal des étapes
| Étape | Description | État |
|-------|-------------------------------------------------------|----------|
@@ -379,7 +422,8 @@ Aucune dépendance locale requise hors Docker.
| 12 | Créneaux types (`DayPart` CRUD) + génération auto des créneaux 0/0 à la création d'un poste ; case « à définir » / « sureffectif » sur la grille | à valider |
| 13 | Bénévole : retrait taille de t-shirt / régime ; entité `Artist` (CRUD, par jour) + vœux des bénévoles (artistes, postes) | à valider |
| 14 | Retrait des compétences (`Skill` et règle `MISSING_REQUIRED_SKILL`) ; disponibilités = fenêtres horaires libres par jour (`GET`/`PUT` availability), prises en compte par `OUTSIDE_AVAILABILITY` | à valider |
| 15 | Solveur de suggestions OR-Tools CP-SAT (section 4) : `POST /editions/{id}/solver/suggestions`, candidats remontés en tête de liste dans la grille des besoins — aucune affectation automatique | à valider |
**Reste hors périmètre** (modèle prêt à les accueillir) : solveur OR-Tools,
SMS, constructeur de formulaire, pointage sur site, IA. Frontend :
squelette + 6 écrans + connexion en place (voir `frontend/`).
**Reste hors périmètre** (modèle prêt à les accueillir) : SMS, constructeur
de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans +
connexion en place (voir `frontend/`).