Solveur : le malus de chaînage n'est pas limité au même poste
Correction : le malus ajouté précédemment ne se déclenchait que si le bénévole avait tenu le créneau précédent du *même* poste. L'intention était plus générale — limiter les enchaînements de shifts d'un bénévole, quel que soit le poste, pas seulement faire tourner un poste en particulier. - `SolverShift.predecessor_shift_id: str | None` → `predecessor_shift_ids: frozenset[str]` : l'ensemble des créneaux (n'importe quel poste) du même jour dont la fin coïncide avec le début de celui-ci. Peut contenir plusieurs ids (plusieurs postes se terminant à la même heure). - Loader : le calcul du prédécesseur se fait maintenant sur tous les créneaux de l'édition regroupés par jour (comparaison des heures de fin), plus par poste. - Miroir côté mock (`buildPredecessorMap`, `runSolver`). - Nouveau test d'intégration (`test_api_solver.py`) : deux postes différents, confirme que le chaînage s'applique bien d'un poste à l'autre. - CLAUDE.md à jour. Vérifié : 185 tests backend (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:
co-authored by
Claude Sonnet 5
parent
64e2f86647
commit
b97b95c606
@@ -408,28 +408,32 @@ function volunteerExistingShifts(volunteerId: string): ShiftRead[] {
|
||||
.map((a) => shiftById(a.shift_id));
|
||||
}
|
||||
|
||||
/** Créneau de même poste, même jour, juste avant chaque créneau (indexé par
|
||||
* id de créneau) — sert au malus de chaînage (voir `solverScore`). */
|
||||
function buildPredecessorMap(): Map<string, string> {
|
||||
const byPosition = new Map<string, ShiftRead[]>();
|
||||
/** Créneau(x) — n'importe quel poste — dont la fin coïncide avec le début de
|
||||
* chaque créneau, le même jour (indexé par id de créneau) — sert au malus de
|
||||
* chaînage (voir `solverScore`), pour limiter les enchaînements de shifts
|
||||
* plutôt que cibler la rotation d'un poste en particulier. */
|
||||
function buildPredecessorMap(): Map<string, Set<string>> {
|
||||
const byDay = new Map<string, ShiftRead[]>();
|
||||
for (const s of state.shifts) {
|
||||
const list = byPosition.get(s.position_id) ?? [];
|
||||
const day = s.starts_at.slice(0, 10);
|
||||
const list = byDay.get(day) ?? [];
|
||||
list.push(s);
|
||||
byPosition.set(s.position_id, list);
|
||||
byDay.set(day, list);
|
||||
}
|
||||
const predecessor = new Map<string, string>();
|
||||
for (const positionShifts of byPosition.values()) {
|
||||
const byDay = new Map<string, ShiftRead[]>();
|
||||
for (const s of positionShifts) {
|
||||
const day = s.starts_at.slice(0, 10);
|
||||
const list = byDay.get(day) ?? [];
|
||||
list.push(s);
|
||||
byDay.set(day, list);
|
||||
const predecessor = new Map<string, Set<string>>();
|
||||
for (const dayShifts of byDay.values()) {
|
||||
const idsByEnd = new Map<string, string[]>();
|
||||
for (const s of dayShifts) {
|
||||
const list = idsByEnd.get(s.ends_at) ?? [];
|
||||
list.push(s.id);
|
||||
idsByEnd.set(s.ends_at, list);
|
||||
}
|
||||
for (const dayShifts of byDay.values()) {
|
||||
dayShifts.sort((a, b) => a.starts_at.localeCompare(b.starts_at));
|
||||
for (let i = 1; i < dayShifts.length; i += 1) {
|
||||
predecessor.set(dayShifts[i].id, dayShifts[i - 1].id);
|
||||
const distinctEnds = [...idsByEnd.keys()].sort();
|
||||
for (const s of dayShifts) {
|
||||
const earlierEnds = distinctEnds.filter((e) => e <= s.starts_at);
|
||||
if (earlierEnds.length > 0) {
|
||||
const latest = earlierEnds[earlierEnds.length - 1];
|
||||
predecessor.set(s.id, new Set(idsByEnd.get(latest)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -529,8 +533,8 @@ function runSolver(): SolverRunResult {
|
||||
})
|
||||
.map((v) => {
|
||||
const existing = existingByVolunteer.get(v.id) ?? [];
|
||||
const predecessorId = predecessorByShift.get(shift.id);
|
||||
const chained = predecessorId != null && existing.some((s) => s.id === predecessorId);
|
||||
const predecessorIds = predecessorByShift.get(shift.id);
|
||||
const chained = predecessorIds != null && existing.some((s) => predecessorIds.has(s.id));
|
||||
return {
|
||||
volunteer: v,
|
||||
score: solverScore(
|
||||
|
||||
Reference in New Issue
Block a user