fix(M6): corrections d'audit FIXME_M6 — normalisation, secret, tests, doc

Corrige les 5 points de l'audit FIXME_M6 :

1. Normalisation des matières : fonction normalize_subject (NFKC +
   unification des espaces + suppression ponctuation + minuscule)
   partagée par la génération d'ID et le futur comparateur M8.
2. Expurgation du secret dans l'erreur de collision d'IDs :
   redact_secrets enveloppe l'identifiant dans le message.
3. Test even/odd avec même matière pour isoler la parité comme seul
   différenciateur d'ID ; tests de normalisation (casse, espaces,
   Unicode) ; test de non-fuite de secret.
4. TODO.md M6 : 8 items cochés après validation.
5. GUIDE_DEV §8.4 : bloc de code corrigé (clôture, types Lesson/
   TheoreticalLesson, comparaison des horaires en minutes, début ET
   fin, référence à normalize_subject).

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
This commit is contained in:
2026-09-06 23:30:47 +02:00
parent 29270427ef
commit 1d26d49e74
5 changed files with 194 additions and 23 deletions

View File

@@ -3505,7 +3505,7 @@ def week_parity(
2. **Comparaison exacte** : Les créneaux horaires et la matière normalisée doivent correspondre.
3. **Choix de la première correspondance** : En cas de multiples correspondances admissibles, choisir la **première** après tri déterministe.
**Exemple de tri** :
**Exemple de tri** :
```python
# Tri des cours théoriques par ID stable (pour un matching déterministe)
theoretical_lessons_sorted = sorted(
@@ -3517,24 +3517,43 @@ theoretical_lessons_sorted = sorted(
lesson.subject.lower(),
),
)
```
**Exemple de matching avec départage déterministe** :
```python
def match_theoretical_lesson(
real_lesson: PronoteLesson,
theoretical_lessons: list[TheoreticalLesson],
real_lesson: Lesson,
theoretical_events: list[TheoreticalLesson],
tolerance_minutes: int = 15,
) -> TheoreticalLesson | None:
"""Trouve le cours théorique correspondant, avec départage déterministe."""
"""Trouve la leçon théorique correspondant à une leçon réelle.
:param real_lesson: Leçon réelle depuis Pronote.
:param theoretical_events: Liste des leçons théoriques candidates.
:param tolerance_minutes: Tolérance en minutes pour le créneau horaire.
:return: La leçon théorique correspondante, ou None.
:rtype: TheoreticalLesson | None
"""
real_start = real_lesson.start
real_day = real_start.weekday()
def to_minutes(t: time) -> int:
return t.hour * 60 + t.minute
# ``normalize_subject`` sera défini dans ``sync/diff.py`` (M8) ou dans le
# module théorique ; il normalise les matières pour un matching déterministe.
start_minutes = real_start.hour * 60 + real_start.minute
end_minutes = real_lesson.end.hour * 60 + real_lesson.end.minute
candidates = [
t for t in theoretical_lessons
if t.day_of_week == real_lesson.start.weekday()
and abs((t.start_time - real_lesson.start.time()).total_seconds()) <= tolerance_minutes * 60
t
for t in theoretical_events
if t.day_of_week == real_day
and abs(to_minutes(t.start_time) - start_minutes) <= tolerance_minutes
and abs(to_minutes(t.end_time) - end_minutes) <= tolerance_minutes
and normalize_subject(t.subject) == normalize_subject(real_lesson.subject)
]
if not candidates:
return None
# Tri déterministe par ID stable, puis par créneau
candidates.sort(key=lambda t: (t.id, t.start_time))
return candidates[0]
```