Artiste : heure de passage ; vœux d'artistes pris en compte par le solveur

Artist gagne start_time/end_time (end <= start ⇒ finit le lendemain,
même convention que Shift/DayPart/Availability). Migration 0008.

Le solveur (app/solver/) résout maintenant les fenêtres de passage des
artistes que chaque bénévole veut voir (VolunteerArtist) et pénalise
toute suggestion qui chevaucherait un passage souhaité — le bénévole
raterait l'artiste. Comme le reste du scoring, c'est un terme de
l'objectif, pas un blocage : pour un créneau urgent sans autre
candidat, le solveur propose quand même, quitte à ce que ce soit un
pis-aller.

- SolverVolunteer.wished_artist_windows (fenêtres ShiftInfo précalculées
  par le loader depuis Artist + VolunteerArtist).
- Nouveau malus _ARTIST_CONFLICT_PENALTY dans engine.py.
- Tests : moteur (conflit détecté/ignoré hors chevauchement/toujours
  suggéré en dernier recours) + intégration API bout en bout.

176 tests, ruff et alembic check au vert. CLAUDE.md mis à jour (§2 et §4).

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 18:13:54 +02:00
co-authored by Claude Sonnet 5
parent e47c29d35b
commit fea9d37019
12 changed files with 212 additions and 35 deletions
+8 -1
View File
@@ -9,7 +9,7 @@ au niveau base (`ondelete="CASCADE"`) ; les relations ORM utilisent
import datetime as dt
import uuid
from sqlalchemy import Boolean, Column, Integer
from sqlalchemy import Boolean, Column, Integer, Time
from sqlalchemy import Enum as SAEnum
from sqlmodel import Field, Relationship, SQLModel, UniqueConstraint
@@ -117,6 +117,13 @@ class Artist(TimestampMixin, table=True):
name: str
# Jour de passage, dans les dates de l'édition.
performance_date: dt.date = Field(index=True)
# Heure de passage. end_time <= start_time ⇒ le passage finit le lendemain.
start_time: dt.time = Field(
sa_column=Column(Time, nullable=False, server_default="20:00:00")
)
end_time: dt.time = Field(
sa_column=Column(Time, nullable=False, server_default="21:00:00")
)
fans: list["Volunteer"] = Relationship(
back_populates="wished_artists",