Trois défauts du même chemin, tous vérifiés ligne à ligne avant correction.
1. Toutes les séances d'un lot sauf une étaient perdues DÉFINITIVEMENT.
L'ancre est sauvegardée ligne 298, puis seul `latest` était traité ligne 319 —
or scheduleNotification était le seul appelant de reportToRoutineIfStrength et
reportPlanOrigin. Renfo le matin, vélo le soir, iPhone resté à la maison : au
premier fire la requête ancrée rend les deux, le vélo est notifié, le renfo
n'est jamais POSTé, et l'ancre ayant avancé il ne reviendra jamais. Le log
disait « 2 new workout(s), notifying latest » : exact, et parfaitement muet sur
ce qu'il venait de jeter. Les rapports serveur bouclent désormais sur tout le
lot ; la notification reste unique, un « 🎉 » par séance serait du bruit.
2. Le statut HTTP n'était jamais lu. Les deux POST faisaient
`{ data, _, error in }` et ne regardaient que `error`. Un 401 sur token rotaté,
un 404 ou un 500 n'en produisent pas : reportPlanOrigin journalisait donc
« plan-origin envoyé » sur une requête refusée, et auto-complete sortait en
silence au guard de parsing. Un helper `envoyer(_:libelle:traiter:)` vérifie
maintenant (200..<300) et journalise le code — le pattern existait déjà dans
CoachRoutineBridge.postDay, à deux fichiers de là.
3. Une séance de musculation classique était étiquetée fonctionnelle.
L'activité était écrite en dur alors que le garde accepte aussi
traditionalStrengthTraining. Si la règle de correspondance côté serveur
discrimine sur ce champ, l'appariement était faussé. L'UUID et l'heure de début
sont ajoutés au passage : sans eux le serveur ne peut pas dédupliquer et doit
évaluer sa fenêtre de 12 h depuis l'heure de réception — une séance remontée
avec six heures de retard était datée de son arrivée.
⚠️ PAS corrigé, et assumé : le `defer { completion() }` dit à iOS que la
livraison est traitée alors que les requêtes sont encore en vol, sans
beginBackgroundTask. Le faire proprement demande un import UIKit, une bascule
sur le fil principal et un essai sur appareil — à traiter à part plutôt qu'en
marge. Le statut HTTP étant désormais lu, un échec laissera au moins une trace.
Relevé § 2.1, § 2.2 et § 3.2 de docs/AUDIT-2026-09-22.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
setPaused vidait `pendingRoute` pour « ne pas tracer la pause ». Mais ce tampon
ne contient que des points DÉJÀ acceptés par le filtre et DÉJÀ comptés dans
`distanceMeters` : ils attendent seulement leur écriture, qui ne part qu'au
seuil de routeFlushThreshold (25). À 1 Hz, c'est une douzaine de points en
moyenne, jusqu'à 24.
L'écart était donc visible : la distance affichée les incluait, la
HKWorkoutRoute non. Six arrêts sur une sortie VTTAE — feux, photos, ravito — et
jusqu'à 144 points manquaient à la trace, avec un D+ recalculé depuis elle qui
divergeait d'autant de celui affiché pendant la séance. Aucun log ne
mentionnait l'effacement.
Ils sont désormais écrits, via le même `Task { await flushPendingRoute() }` que
le flush au seuil. Le raisonnement du commentaire d'origine visait le point qui
SUIT la reprise, à des centaines de mètres de là — mais celui-là n'est pas
encore dans le tampon quand la pause commence, et c'est `implausibleSpeed` qui
l'écarte. Nettoyer la pause est l'affaire du filtre, pas du tampon d'écriture.
Pas de course possible : `isPaused` est posé avant, et `ingest` sort dès qu'il
est vrai — aucun point neuf ne peut s'ajouter pendant que le flush s'exécute.
Relevé § 2.5 de docs/AUDIT-2026-09-22.md, seul constat de la section vérifié
ligne à ligne avant correction. 88 tests Linux verts (RouteFilter compris), mais
ils ne couvrent pas ce fichier : CoreLocation n'existe pas sur Linux.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 09:20:30 +00:00
2 changed files with 91 additions and 20 deletions
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.