Vœu de compagnon : traité comme réciproque par le solveur

Le vœu reste à sens unique en base (A choisit B sur sa fiche, B n'a rien à
faire) mais si A souhaite B, B doit désormais profiter aussi de la présence
de A — dans les deux sens : affecter A d'abord fait remonter B comme
candidat, et affecter B d'abord fait remonter A, même si B n'a rien demandé.

- Solveur (`loader.py`) : le vœu (A → B) alimente maintenant l'ensemble
  effectif des deux bénévoles (A et B), pas seulement celui du demandeur.
  Aucun changement dans `engine.py` : il ne fait que lire un ensemble déjà
  symétrisé.
- Mock (`buildEffectiveCompanions`) et classement par défaut
  (`buildEffectiveCompanionIds` dans `lib/planning.ts`, utilisé par
  `AssignmentPanel.tsx`) : même symétrisation, pour rester cohérents avec le
  vrai solveur.
- La fiche bénévole (vœux affichés/édités) n'est pas touchée : c'est le seul
  endroit qui reste à sens unique, par construction (la réciprocité n'est
  qu'une lecture, jamais une donnée enregistrée — sinon éditer sa propre
  liste risquerait d'effacer le vœu de quelqu'un d'autre).
- Nouveau test d'intégration : le bénévole qui n'a rien demandé profite quand
  même de la présence de celui qui l'a choisi.
- CLAUDE.md à jour.

Vérifié : 193 tests backend pertinents (dont le nouveau), 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 22:32:02 +02:00
co-authored by Claude Sonnet 5
parent 166e2c3366
commit 11bc8cbe36
8 changed files with 115 additions and 16 deletions
+10 -8
View File
@@ -85,11 +85,13 @@ les tables sauf les tables d'association.
voudrait tenir »), distinct de `Assignment` (décision) et de `Position.
lead_account_id` (référent).
- Vœux de compagnon : `volunteer_companion_wish` (« bénévoles avec qui je
voudrais être affecté », 0 à N, à sens unique — A peut souhaiter B sans que
B ne le souhaite en retour). `CHECK (volunteer_id <> companion_id)` : un
bénévole ne peut pas se souhaiter lui-même. Le solveur (section 4)
favorise un candidat dont l'un des compagnons souhaités est déjà affecté
au créneau.
voudrais être affecté », 0 à N). **Saisi à sens unique** (A souhaite B sans
que B fasse le geste en retour) **mais traité comme réciproque par le
solveur** (section 4) : si A souhaite B, B profite aussi de la présence de
A, même s'il ne l'a rien demandé — la réciprocité est artificielle, calculée
à la volée (le loader du solveur, pas la table elle-même, qui reste à sens
unique et éditable indépendamment par chacun). `CHECK (volunteer_id <>
companion_id)` : un bénévole ne peut pas se souhaiter lui-même.
- **`Position`** : `name`, `location`, `category` (`PositionCategory`),
`lead_account_id` (→ `Account`, `SET NULL` à la suppression du compte).
- **`Shift`** : `position_id`, `day_part_id` (nullable ; le créneau type dont
@@ -291,9 +293,9 @@ affectation n'est jamais persistée par cet endpoint.
- 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 ;
- bonus par compagnon souhaité (`VolunteerCompanionWish`) déjà affecté
(affectation décidée) au créneau — plusieurs compagnons présents
cumulent le bonus ;
- bonus par compagnon souhaité (`VolunteerCompanionWish`, **réciproque** :
si A souhaite B, B en profite aussi) déjà affecté (affectation décidée)
au créneau — plusieurs compagnons présents cumulent le bonus ;
- léger malus pour les bénévoles déjà bien chargés (répartition de la
charge).
- **Pas de plafond de cumul journalier côté solveur** : contrairement à la