Cette étude de cas examine le codebase React Native de StyleSwipe où les agents de code ont eu du mal à corriger des bugs visuels dans le swipe de cartes et le gameplay de collection. StyleSwipe est un jeu mobile qui combine une simulation de l'industrie de la mode avec un swipe façon Tinder—les joueurs swipent pour accepter ou refuser des contacts afin d'agrandir leur collection. Sur 3 semaines, les agents ont fait 6 tentatives pour corriger une seule condition de concurrence, introduit des jours de bugs de suivi après un refactor majeur, et laissé plus de 30 commentaires de garde défensifs avertissant les futurs agents de ce qu'il ne faut pas faire.
Résumé exécutif
Constat principal
Les bugs visuels sont fondamentalement différents des bugs logiques. Ils existent dans les écarts de timing entre systèmes asynchrones, se manifestent pendant seulement 1-2 frames, et nécessitent souvent des corrections contre-intuitives (ne pas réinitialiser les valeurs, conserver des données obsolètes, ajouter des délais). Les agents entraînés sur les bugs logiques échouent répétitivement sur les bugs visuels parce que leur reconnaissance de patterns ne s’applique pas.
Résumé des preuves
| Type de preuve | Constat |
|---|---|
| Historique Git | Même bug « corrigé » 3 fois sur 3 semaines (throttle/cooldown) |
| Commentaires de code | Plus de 30 gardes « re-check latest » pour les conditions de concurrence |
| Complexité | 47 points de synchronisation, 4 domaines de timing, machine à 6 états |
| Taux de réussite | Bugs visuels : 30-40 % de correction au premier essai vs 95 % pour les erreurs de typage |
Pourquoi les agents échouent
- Diagnostic axé sur les symptômes - Corriger là où l’erreur apparaît, pas là où elle prend origine
- Ajouter vs Supprimer - Les agents ajoutent des gardes ; les corrections réussies suppriment du code
- Ajustement de nombres magiques - Modifier les millisecondes jusqu’à disparition des symptômes
- Débogage en rafale - Modifier 6+ fichiers en espérant qu’un fonctionne
- Reconnaissance de correction partielle - Commiter en sachant que c’est incomplet
À quoi ressemble le succès
Les corrections réussies (30-40 % des tentatives de bugs visuels) partagent ces traits :
| Pattern réussi | Pattern échoué |
|---|---|
| 1-3 fichiers modifiés | 4+ fichiers dispersés |
| Décrit le mécanisme dans le commit | Décrit le symptôme |
| Supprime du code | Ajoute des gardes |
| Utilise les primitives de la bibliothèque | Invente un timing personnalisé |
| Pas de suivi dans les 7 jours | Suivis le même jour |
Le cycle vicieux
L'agent ne comprend pas le système → Fait une correction localisée
↓
La correction localisée casse autre chose → Un autre agent ajoute une garde
↓
Les gardes s'accumulent → Le système devient plus difficile à comprendre
↓
L'agent suivant comprend encore moins → Fait une correction encore plus localisée
↓
Répéter
Impact quantifié
- Bug throttle : 3 « corrections » sur 3 semaines, toujours cassé
- Refactor two-stack : 2 914 lignes ajoutées, boutons cassés 3 heures plus tard
- Prolifération des gardes : « Re-check latest » apparaît plus de 30 fois dans un fichier
- Timers watchdog : 4 timeouts différents de 1500ms+ comme pansements
Partie 1 : Preuves de difficulté - Patterns d’historique Git
Note : les tags entre crochets comme [add-sync-logic] indiquent la stratégie de correction employée pour chaque commit, ce qui rend visibles des patterns comme l’escalade ou les tentatives répétées.
Pattern A : Cycles de correction le même jour
Exemple : « App Top Bar Not Updating » (Semaine 2)
| Écoulé | Approche | Description |
|---|---|---|
| T+0 | [add-sync-logic] | ”fixed app top bar not updating connections and introductions” (+45 lignes) |
| T+2h 20m | [full-rewrite] | ”fix networking swipes not updating apptopbar’s introductions and connections” (-146 lignes, +38 lignes) |
Ce qui s’est passé : La première correction n’a traité qu’une partie du problème. 2,5 heures plus tard, une réécriture complète était nécessaire - l’agent a supprimé 146 lignes et réécrit entièrement l’approche.
Exemple : Cascade de synchronisation d’état (Semaine 2, le lendemain)
Six commits en 6 heures, chacun découvrant que la correction précédente était incomplète :
| Écoulé | Approche | Ce qu’il a corrigé | Ce qu’il a cassé/raté |
|---|---|---|---|
| T+0 | [data-mapping-fix] | ”map oddsForTop to previous oddsForPeek” | Modal affiché quand pas de carte |
| T+37m | [error-handling] | ”explicit client handling for draw-required errors” | Écran ne se réhydrate pas au focus |
| T+1h 24m | [rehydration-fix] | ”ensure networking screen rehydrates when returning” | Toujours des conditions de concurrence |
| T+2h 47m | [partial-fix] | ”PARTIAL fixes to networking swipe state” | Utilise toujours un accès d’état non typé |
| T+3h 4m | [type-safety] | ”Added typed resource selectors” | Cause racine toujours pas corrigée |
| T+6h 14m | [root-cause-fix] | ”enforce latest-response wins and gate side-effects” | Enfin la vraie correction |
Ce qui s’est passé : Chaque agent (ou le même agent dans plusieurs sessions) a corrigé les symptômes plutôt que la cause racine. Le vrai problème était une condition de concurrence où les réponses serveur pouvaient arriver dans le désordre - mais cela n’a été diagnostiqué qu’à la 6e tentative.
Pattern B : Le refactor majeur casse immédiatement quelque chose
Exemple : Introduction du système Two-Stack (Semaine 3, session nocturne)
| Écoulé | Approche | Description |
|---|---|---|
| T+0 | [architectural-refactor] | ”networking stack replaced by two-stack system” (+2 914 lignes, 14 nouveaux fichiers) |
| T+3h | [emergency-fix] | ”fix swipe buttons not responding” |
| T+3h 45m | [follow-up-fix] | ”Added consistent haptic feedback” |
Ce qui s’est passé : Un refactor architectural massif pour résoudre des problèmes d’animation a immédiatement cassé la fonctionnalité basique des boutons. L’agent a dû se précipiter pour corriger la gestion tactile qui fonctionnait bien dans l’ancien système.
Le lendemain : Une correction [late-regression-fix] traitait encore la « card overlay persistence and cooldown gating logic » - des effets secondaires du refactor initial.
Pattern C : Commits nommés « Partial » ou « Attempt »
[partial-fix] | partial fixes to networking swipe state issues
[defensive-hardening] | hardening on swipe system
[type-alignment] | fix typing errors, add temporary momentum values
[type-alignment] | fix more typing errors
[type-alignment] | type fixes
Le mot « partial » et les commits répétitifs « fix more » indiquent que les agents savaient qu’ils ne résolvaient pas complètement le problème mais ont poussé quand même, espérant que les passes suivantes complèteraient la correction.
Partie 2 : Preuves de difficulté - Commentaires de code défensifs
Le codebase est rempli d’avertissements laissés par des agents pour les agents futurs. Ces commentaires documentent des connaissances durement acquises sur ce qu’il ne faut PAS faire.
Catégorie : « Ne pas réinitialiser les transforms »
Composant de pile de cartes (nettoyage du ghost)
// Do not reset ghost transforms here — resetting to 0 while still mounted
// causes a brief flash at center with overlay before unmount. The transforms
// will be reinitialized by runGhostAnimation for the next swipe.
Ce que cela révèle : Un agent a essayé la correction évidente (réinitialiser les transforms à 0 quand c’est fini) et cela a causé un flash visuel. La solution de contournement est contre-intuitive : laisser les valeurs de transform obsolètes et laisser le démontage gérer le nettoyage.
Catégorie : « Timers watchdog pour les deadlocks »
Composant de pile de cartes (watchdog deadlock)
// Safety watchdog: if we remain in 'handoff' but the authoritative top does not
// update within a short window, unlock interactions to avoid deadlock.
// This covers rare cases where the server reports success but the top card
// remains the same (or stale) for longer than expected, leaving the stack
// non-interactive.
Ce que cela révèle : Le système peut entrer dans un état où l’UI est bloquée en permanence. Plutôt que de corriger la cause racine, les agents ont ajouté un timer watchdog de 1500ms pour forcer le déblocage. C’est un pansement, pas un remède.
Catégorie : « Gardes de condition de concurrence »
Hook de logique de swipe - L’expression « latest check » ou « re-check latest » apparaît plus de 30 fois :
// Check if draw response is latest before applying side-effects
// Re-check latest before counter sync to avoid races
// Re-check latest before UI side-effects
// Secondary guards on fetch state before any updates
// Triple-check before side-effects to prevent mid-flight races
Ce que cela révèle : Les agents n’ont pas pu empêcher les conditions de concurrence architecturalement, donc ils ont ajouté des vérifications de garde à chaque point possible. Le code est jonché de vérifications if (isLatest) défensives parce que les agents ne pouvaient pas raisonner sur le moment où les réponses arriveraient.
Catégorie : « Confusion d’identité de curseur »
Composant de pile de cartes (suivi de l’identité de carte)
// Uniqueness is determined by deck cursor, not id. Prefer cursor comparison
// when available; if cursors are absent, this is an error condition.
Ce que cela révèle : Les cartes peuvent avoir le même ID dans différents decks (la même carte peut apparaître plusieurs fois à des positions différentes). Les agents ont initialement utilisé les IDs de carte pour suivre quelle carte avait été glissée, causant des bugs quand la même carte réapparaissait. Ce commentaire documente la règle apprise à la dure : utiliser des deck cursors basés sur la position (indices gérés par le serveur), pas des IDs.
Catégorie : « Ne faites pas ça ou tout casse »
Composant d’overlay de swipe (règle d’ordonnancement des hooks)
// All animated styles must be declared before any conditional returns
Hook de logique de swipe (prévention de boucle de draw)
// Avoid repeated draw attempts until we detect introductions > 0
// (introductions are a consumable resource required to draw new cards)
drawBlockedByNoIntrosRef.current = true;
Ce que cela révèle : Les agents ont découvert des mines par essai et erreur. Règles d’ordonnancement des hooks React, boucles de réessai infinies, contamination d’état d’animation - chaque commentaire représente un bug qui a été corrigé puis documenté pour prévenir la régression.
Partie 3 : Pourquoi les bugs visuels sont plus difficiles que les bugs logiques
Les métriques de complexité
| Métrique | Valeur | Pourquoi c’est important |
|---|---|---|
| États de la machine à états | 6 | Chaque état a des règles de visibilité/interaction différentes |
| Transitions d’état | 9+ | Doivent être testées en combinaison |
| Domaines de timing | 4 | Redux, Reanimated, setTimeout, requestAnimationFrame |
| Valeurs partagées | 6 | ghostX, ghostY, ghostRot, peekScale, peekOffsetY, promote |
| Refs impératives | 7+ | topCardRef, ghostCardRef, peekSnapshotRef, etc. |
| Points de synchronisation | 47 | Endroits où les systèmes doivent se coordonner |
| Vecteurs de condition de concurrence | 7+ | Documentés dans les commentaires de code |
Le problème des quatre domaines de timing
Les bugs visuels impliquent souvent des décalages de timing entre :
- État Redux/Serveur - Se met à jour quand le serveur répond (0-2000ms de latence)
- Worklets Reanimated - Fonctions JavaScript exécutées sur le thread UI natif (budget de 16ms par frame)
- Callbacks setTimeout - Boucle d’événements JavaScript (timing variable)
- requestAnimationFrame - Aligné sur Vsync (intervalles de 16,67ms)
Exemple de bug : « La carte flash au centre après le swipe »
- Le swipe se termine → Reanimated définit ghostX à la position finale
- Le serveur répond → Redux met à jour la carte du dessus
- React re-rend → Le composant Ghost est toujours monté pendant 1 frame
- Le transform du ghost se réinitialise à 0 → Flash au centre
- Frame suivante → Le ghost se démonte
Le bug existe dans l’écart entre les domaines de timing. Le corriger nécessite de comprendre que les animations Reanimated se terminent sur le thread UI tandis que les re-rendus React se font sur le thread JS, et ils ne se synchronisent pas automatiquement.
Le pattern Snapshot (solution émergente)
Le codebase a développé un pattern que les agents ont découvert par itération :
Problème : Quand la carte du dessus s’envole et que la peek card (la prochaine carte visible derrière) doit prendre sa place, il y a un écart où :
- Les données de la carte du dessus sont obsolètes (déjà swipée)
- Les nouvelles données de carte ne sont pas encore arrivées du serveur
- L’UI n’affiche rien ou la mauvaise carte pendant 1-3 frames
Solution : Capturer les données de la peek card au début du swipe, afficher le snapshot pendant l’animation, ne basculer vers les données live que quand le serveur confirme.
// Take snapshot when swipe starts
// peek = the next card's data, computedNextTop = what we expect to become the new top
peekSnapshotRef.current = peek;
nextTopSnapshotRef.current = computedNextTop;
// Render snapshot during animation
const displayCard = serverTopId === expectedTopId
? liveTop
: nextTopSnapshotRef.current;
Pourquoi les agents ont eu du mal : Ce pattern n’est pas évident. L’approche naturelle est « montrer ce que dit le serveur » - mais cela cause du scintillement. Les agents ont dû découvrir par des échecs répétés que le snapshotting optimiste était requis.
Le problème de collision Z-Index
Hiérarchie actuelle (NetworkingCardStack) :
zIndex: 1000 → ChainCelebration (overlay de jalon de série)
zIndex: 30 → Boutons d'action
zIndex: 30 → Indicateurs de rareté ← COLLISION
zIndex: 30 → Notices désactivées ← COLLISION
zIndex: 20 → Carte ghost
zIndex: 10 → Carte du dessus
zIndex: 0 → Carte peek
Pourquoi les agents cassent toujours ça :
- Pas de système z-index centralisé
- Quatre éléments différents partagent zIndex: 30
- Ajouter une nouvelle UI nécessite de deviner quel z-index ne va pas entrer en collision
- La règle « plus tard dans l’arbre = plus haut » de React Native ajoute un ordonnancement implicite
- Différences de plateforme (elevation Android vs zIndex iOS)
Les agents ajoutant des fonctionnalités choisissent naturellement des valeurs z-index existantes, causant des chevauchements qui ne se manifestent que dans certains états (par ex., quand l’indicateur de rareté ET la notice désactivée sont visibles).
Partie 4 : Analyse de la charge cognitive
Pourquoi cela dépasse les capacités des agents
Tâche : « Corriger le flash d’1 frame quand la carte est glissée »
Ce qu’un agent doit comprendre pour corriger cela :
- La machine à états SwipeStackReducer (6 états, 9 transitions)
- Que
showTopdépend de 4 conditions différentes incluantwaitingForNewTop - Que
waitingForNewTopcompareoutgoingTopCursoràtop.deckCursor - Que les curseurs peuvent être undefined, nécessitant des vérifications null
- Que
holdForXpAnimbloque à la fois la visibilité ET le démarrage de l’animation - Que
setTimeoutplanifierunGhostAnimationaprès que la barre XP se termine - Que les transforms du ghost ne doivent PAS être réinitialisés à 0 (contre-intuitif)
- Que
peekSnapshotRefdoit persister jusqu’à!ghostVisible && !waitingForNewTop - Que le timer watchdog à 1500ms peut interférer avec la correction
- Qu’il y a DEUX systèmes (NetworkingCardStack legacy, nouveau TwoFrameDeck)
Comportement typique de l’agent :
- Lire la description de l’erreur
- Trouver du code qui semble lié
- Faire un changement qui corrige le symptôme immédiat
- Ne pas réaliser que ça a cassé une autre transition d’état
- Pousser la correction
- Le bug réapparaît sous une forme différente
- Répéter
Le piège « Corriger une chose, en casser une autre »
Exemple : Corriger le flash du ghost en réinitialisant les transforms
// "Correction" de l'agent
const settleGhost = () => {
ghostX.value = 0; // Réinitialiser au centre
ghostY.value = 0;
ghostCardRef.current = null;
};
Ce qui casse :
- La réinitialisation du transform se produit AVANT que React démonte le composant
- Pendant 1 frame, le ghost est à (0,0) avec pleine opacité
- Visuel : la carte flash au centre
Correction réelle :
// Ne pas réinitialiser les transforms - laisser le démontage gérer
const settleGhost = () => {
ghostCardRef.current = null;
setGhostDir(null);
};
Un agent ne peut pas savoir cela sans soit :
- Lire le commentaire existant qui avertit à ce sujet
- Faire l’erreur et observer le flash
- Comprendre le timing de réconciliation de React vs le thread d’animation de Reanimated
Partie 5 : Patterns pour l’amélioration des agents
Ce qui aiderait les agents à réussir
-
Diagrammes de machine à états explicites
- Actuel : Machine à états enfouie dans le reducer, transitions implicites
- Mieux : Diagramme visuel montrant tous les états et ce qui est visible dans chacun
-
Diagrammes de séquence de timing
- Actuel : Timing dispersé entre callbacks et effets
- Mieux : Documenter la séquence exacte d’événements pour swipe-complete
-
Registre Z-Index
- Actuel : Nombres magiques dispersés entre composants
- Mieux :
Z_INDEX.GHOST = 20,Z_INDEX.BUTTONS = 30centralisé
-
Zones « Ne pas toucher »
- Actuel : Avertissements dans les commentaires
- Mieux : Documentation explicite des invariants qui doivent être préservés
-
Tests de régression visuelle
- Actuel : Les tests unitaires ne peuvent pas attraper les flashes d’1 frame
- Mieux : Tests de capture d’écran à des frames d’animation spécifiques
-
Pattern Snapshot comme concept de première classe
- Actuel : Le pattern existe mais n’est pas nommé ni documenté
- Mieux : Hook
useOptimisticSnapshot()avec une sémantique claire
Partie 6 : Anti-patterns de comportement des agents
Cette section documente les patterns comportementaux spécifiques qui causent l’échec répété des agents sur les corrections de bugs visuels.
Anti-Pattern 1 : Diagnostic axé sur les symptômes
Diagnostic axé sur les symptômes : L’agent lit la description de l’erreur, trouve du code qui semble lié, corrige ce code sans comprendre la cause racine.
Preuves de StyleSwipe :
La « saga du Networking Swipe » (Semaine 1) :
| Approche | Ce que l’agent pensait | Ce que l’agent a fait | Problème réel |
|---|---|---|---|
[add-sync-logic] | « Le throttle ne se vide jamais » | Ajouté une logique de timeout, suivi via requestAnimationFrame | Bug de transition de machine à états |
[hide-dont-fix] | « Trop de refetches » | Commenté des sections UI | Hydratation incomplète |
[full-rewrite] | « L’overlay rejoue les animations » | Réécriture d’un fichier de 218 lignes | L’état n’était pas réinitialisé |
Le signe révélateur : Chaque « correction » touchait différents fichiers et différents systèmes. Si l’agent comprenait la cause racine, les corrections convergeraient vers le même endroit.
Pourquoi les agents font ça :
- Les messages d’erreur décrivent les symptômes, pas les causes
- Les agents font du pattern-matching « throttle » → trouver code throttle → corriger code throttle
- Pas de mécanisme pour tracer le flux de données en arrière vers l’origine
Anti-Pattern 2 : Réponse sur-ingénieriée
Réponse sur-ingénieriée : Un bug simple reçoit un refactor architectural au lieu d’une correction ciblée.
Preuves :
Le hook de logique de swipe est passé de ~600 lignes à 1200+ lignes à travers un « plan de refactoring » en 14 étapes :
Step 1: utilities extraction
Step 3: hardened odds normalization
Step 4: filtered undefined artwork URLs
Step 5: refactored dev warn-once logic
Step 8: explicit hydration flags
Step 9: extracted accept-gate hook
Step 10: extracted analytics hook
Step 13: centralized throttle handling
Le bug original : Boucle de draw quand plus d’introductions. La correction nécessaire : Un flag booléen pour bloquer les draws répétés. Ce qui s’est réellement passé : 7 nouvelles abstractions de hooks, 3 nouveaux fichiers de modèle, réimplémentation complète du modèle de deck.
Pourquoi les agents font ça :
- Les grandes fenêtres de contexte encouragent la pensée « tant que j’y suis »
- Les agents confondent « code lié » avec « code qui doit changer »
- Pas de signal de coût pour l’expansion de portée
Anti-Pattern 3 : Ajustement de nombres magiques
Ajustement de nombres magiques : Les bugs de timing sont « corrigés » en ajustant les valeurs en millisecondes jusqu’à ce que les symptômes disparaissent.
Preuves :
// [magic-number] - ajouté sans justification
if (delta < 250) { return; }
// Diverses constantes de timing ajoutées au fil du temps :
250ms debounce [magic-number]
1200ms hide delay [rehydration-fix]
1400ms screen reader timeout [rehydration-fix]
220ms animation duration [rehydration-fix]
1500ms watchdog timeout (NetworkingCardStack)
Le problème : Ces nombres sont du cargo cult. 250ms « fonctionne » mais personne ne sait pourquoi. Quand le système change, le nombre magique casse.
Pourquoi les agents font ça :
- Les bugs de timing sont difficiles à raisonner
- Les essais-erreurs avec les nombres sont plus rapides que comprendre le système
- Les nombres qui « fonctionnent » sont commités sans compréhension
Anti-Pattern 4 : Duplication défensive
Duplication défensive : La même vérification est ajoutée plusieurs fois en séquence parce que l’agent ne fait pas confiance aux vérifications précédentes.
Preuves du hook de logique de swipe :
// Lignes 317-340: Vérifier la régression de révision du deck
if (nextRev < prevRev) { /* abort */ }
// Lignes 444-467: Vérification IDENTIQUE, copier-coller
if (nextRev < prevRev) { /* abort */ }
// Lignes 623-642: Même logique, troisième fois
if (nextCursor > prevCursor && nextTopCursor === prevTopCursor) { /* abort */ }
Le pattern « Re-Check Latest » : L’expression apparaît plus de 30 fois :
- “Check if draw response is latest”
- “Re-check latest before counter sync”
- “Re-check latest before UI side-effects”
- “Triple-check before side-effects”
Pourquoi les agents font ça :
- Condition de concurrence découverte → ajouter vérification à cet endroit
- Une autre concurrence découverte → ajouter vérification au nouvel endroit
- Pas de solution architecturale, juste plus de gardes
- Chaque garde est un monument à un bug passé
Anti-Pattern 5 : Cacher plutôt que corriger
Cacher plutôt que corriger : Quand confus, commenter le code problématique plutôt que le comprendre.
Preuves :
[hide-dont-fix] - “Networking swipe redundantly refetches state”:
- Problème : UI qui scintille à cause de refetches multiples
- « Correction » : Commenter l’affichage des compteurs, l’indicateur de chaîne, et autre UI
- Résultat : Problème caché, pas résolu
Pattern similaire dans la gestion d’erreur :
try { HapticsManager.triggerSwipeHaptic('left', 'complete'); } catch {}
try { dispatchCtrl({ ... }); } catch {}
Les blocs catch vides = « Je ne sais pas pourquoi ça échoue, donc je le fais taire. »
Pourquoi les agents font ça :
- Supprimer du code est plus rapide que comprendre du code
- Si l’UI ne s’affiche pas, l’utilisateur ne voit pas le bug
- L’agent passe à autre chose, déclare victoire
Anti-Pattern 6 : Débogage en rafale
Débogage en rafale : Changer beaucoup de fichiers en espérant qu’un changement corrige le problème.
Preuves :
| Bug | Fichiers modifiés | Chirurgical ? |
|---|---|---|
| « Boutons ne répondent pas » | 6 fichiers | Non - dispersés entre couches |
| « Persistance overlay carte » | 44 fichiers | Non - inclut 37 fichiers YAML non liés |
| « Overlay rejoue animations » | 5 fichiers | Non - composant + hook + types |
Corrélation : Les corrections de bugs visuels touchent en moyenne 4-6 fichiers. Les corrections de bugs logiques touchent en moyenne 1-2 fichiers.
Pourquoi les agents font ça :
- Les symptômes visuels peuvent avoir des causes n’importe où dans l’arbre de rendu
- L’agent ne sait pas quelle couche est responsable
- Changer plusieurs fichiers augmente les chances de toucher le bon
Anti-Pattern 7 : La planification comme procrastination
La planification comme procrastination : Écrire des documents de planification au lieu d’implémenter des corrections.
Preuves :
14 commits consécutifs mettant à jour des fichiers de planification :
[step-1] Step 1: utilities
[step-3] Step 3: hardened odds normalization
...
[step-14] Step 14: Summary
Le problème : La planification est devenue le travail. Chaque « étape » était un commit vers un document de planification, pas du code réel. Les documents de planification sont devenus de plus en plus élaborés pendant que les bugs restaient non corrigés.
Pourquoi les agents font ça :
- Planifier donne l’impression de progresser
- Les problèmes complexes font peur ; planifier est sûr
- La génération de tokens sur la documentation est plus facile que sur le code
Anti-Pattern 8 : Reconnaissance de correction partielle
Reconnaissance de correction partielle : L’agent sait que la correction est incomplète mais commit quand même.
Preuves :
[partial-fix] | "partial fixes to networking swipe state issues"
[defensive-hardening] | "hardening on swipe system"
[type-alignment] | "fix typing errors, add temporary momentum values"
Le mot « partial » est une admission explicite. L’agent savait que plus de travail était nécessaire mais s’est arrêté quand même.
Pourquoi les agents font ça :
- Pression de la fenêtre de contexte - mieux vaut commiter quelque chose que perdre le travail
- Optimisme - « Je corrigerai le reste dans la prochaine passe »
- Fatigue de portée - l’agent était à bout de souffle
Anti-Pattern 9 : Mauvaise identification de la couche défaillante
Mauvaise identification de la couche défaillante : La correction est appliquée à la mauvaise couche d’abstraction.
Preuves :
| Symptôme | Où l’agent a corrigé | Où était réellement le problème |
|---|---|---|
| « L’overlay scintille » | Composant d’overlay de swipe | Machine à états de carte |
| « Boutons ne répondent pas » | Composants bouton | État du contrôleur de frame |
| « La carte s’affiche brièvement » | Logique de visibilité de carte | Timing de transform du ghost |
Pourquoi les agents font ça :
- Les symptômes se manifestent dans les composants UI
- L’agent corrige là où le symptôme apparaît
- La cause racine est 2-3 couches plus profond dans le flux de données
Anti-Pattern 10 : Cécité aux régressions
Cécité aux régressions : Corriger un bug, ne pas remarquer que ça en a cassé un autre.
Preuves :
Le refactor Two-Stack ([architectural-refactor]) :
- 2 914 lignes ajoutées, 14 nouveaux fichiers
- 3 heures plus tard :
[emergency-fix]« fix swipe buttons not responding » - Le lendemain : toujours en train de corriger « cooldown gating logic »
L’agent n’a jamais testé :
- La réactivité des boutons
- La propagation d’état à travers le nouvel arbre de composants
- L’interaction avec le système de cooldown existant
Pourquoi les agents font ça :
- Pas de tests de régression visuelle automatisés
- Les tests manuels prennent du temps
- L’agent suppose que « si ça compile, ça fonctionne »
Partie 7 : Statistiques de précision diagnostique
Basé sur l’analyse manuelle de 984 commits dans le codebase StyleSwipe (un commit était considéré comme réussi si aucune correction liée n’apparaissait dans les 7 jours) :
Taux de réussite de correction par catégorie
| Catégorie | Réussite au premier essai | A nécessité un suivi |
|---|---|---|
| Typage/imports | ~95% | ~5% |
| Configuration | ~90% | ~10% |
| Chirurgical fichier unique | ~85% | ~15% |
| Refactors de hooks | ~40% | ~60% |
| Corrections de bugs visuels | ~30-40% | ~60-70% |
| Synchronisation d’état | ~20% | ~80% |
| Changements multi-systèmes | ~10% | ~90% |
Le bug throttle sur 3 semaines
Statistique la plus révélatrice : le système throttle/cooldown a été « corrigé » 3 fois sur 3 semaines :
- Semaine 1 :
[add-sync-logic]« throttle never clears after cooldown » - Semaine 3 :
[centralization]« centralized throttle/cooldown handling » - Semaine 3, le lendemain :
[late-regression-fix]« fix card overlay persistence and cooldown gating logic »
Même bug. Trois semaines. Trois « corrections ». Toujours cassé.
Qualité du message de commit comme signal diagnostique
Messages indiquant un diagnostic correct (rare, ~2 %) :
- “enforce latest-response wins and gate side-effects”
- “Realigns Networking swipes with a deck-of-cards mental model”
Messages indiquant une chasse aux symptômes (courant, ~30 %) :
- “fix swipe buttons not responding”
- “partial fixes to networking swipe state issues”
- “Swipe decision overlay replays animations”
Le signe révélateur : Les messages de cause racine décrivent le mécanisme. Les messages de symptômes décrivent ce que l’utilisateur voit.
Partie 8 : Pourquoi les bugs visuels cassent spécifiquement les agents
L’inadéquation fondamentale
Les agents sont entraînés sur :
- Les bugs logiques (mauvaise valeur calculée)
- Les erreurs de compilation (problèmes de syntaxe/type)
- La mauvaise utilisation d’API (mauvais paramètres)
Les bugs visuels sont différents :
- Logique correcte, mauvais timing
- Pas de message d’erreur
- Les symptômes apparaissent loin de la cause
- La correction nécessite de comprendre plusieurs systèmes asynchrones simultanément
Le fossé d’observabilité
Bug logique : Ajouter console.log, voir la mauvaise valeur, tracer en arrière.
Bug visuel :
- Le scintillement se produit en 16ms
- Impossible de console.log une seule frame
- Doit raisonner sur Reanimated (thread UI) + React (thread JS) + Redux (store) simultanément
- Pas d’outillage pour visualiser ça
Le problème de la correction contre-intuitive
Les agents apprennent des patterns : « problème X → correction Y »
Les bugs visuels nécessitent souvent des anti-patterns :
- NE PAS réinitialiser la valeur (la laisser obsolète)
- NE PAS montrer les données live (montrer le snapshot)
- NE PAS nettoyer immédiatement (attendre le démontage)
- AJOUTER des délais artificiels (setTimeout avant animation)
Les agents ne peuvent pas apprendre cela parce que ça viole l’intuition de programmation normale.
Partie 9 : À quoi ressemblent les corrections réussies
Toutes les corrections n’ont pas échoué. L’analyse des commits qui ont fonctionné du premier coup révèle des patterns clairs qui distinguent le succès de l’échec.
Correction réussie #1 : Correction du flash ghost [code-removal]
Bug : La carte ghost flash au centre après le swipe.
Ce qui a fonctionné :
| Aspect | Approche |
|---|---|
| Cause racine identifiée | « Resetting to 0 while still mounted causes brief flash » |
| Fichiers modifiés | 3 (chirurgical, fichiers liés seulement) |
| Type de correction | Suppression de code plutôt qu’ajout |
| Logique | Conditionnel simple : ghostDir !== null |
L’insight clé : L’agent a identifié la FRAME EXACTE où le problème se produisait. Au lieu d’ajouter des gardes d’état, ils ont supprimé la réinitialisation problématique et amélioré le rendu conditionnel.
Pas de suivi nécessaire : Pas de commits aux mêmes fichiers dans les 7 jours.
Correction réussie #2 : Correction overlay Rare+ [input-validation]
Bug : Les overlays de rareté ne s’affichent pas correctement sur les cartes rares ou de tier supérieur.
Ce qui a fonctionné :
| Aspect | Approche |
|---|---|
| Cause racine identifiée | Seuil invalide causant NaN dans les calculs |
| Fichiers modifiés | 2 (RarityFrame, SwipeDecisionOverlay) |
| Type de correction | Validation d’entrée ajoutée à la frontière |
| Logique | `!Number.isFinite(swipeThreshold) |
L’insight clé : L’agent a validé les entrées AVANT de les utiliser, pas après l’apparition des symptômes. A aussi ajouté pointerEvents="none" pour prévenir les conflits d’interaction.
Correction réussie #3 : Animation du feed de collection [library-primitive]
Bug : L’animation de fade-in des posts n’est pas fluide.
Ce qui a fonctionné :
| Aspect | Approche |
|---|---|
| Cause racine identifiée | Le keyframe personnalisé ne correspondait pas au rendu visuel |
| Fichiers modifiés | 1 (SocialFeed seulement) |
| Type de correction | Remplacement du code personnalisé par une primitive de bibliothèque |
| Logique | Basculé vers FadeInDown.duration(800) |
L’insight clé : L’agent a utilisé une primitive de bibliothèque au lieu d’une implémentation personnalisée. Pas de coordination d’état nécessaire.
Correction échouée pour comparaison : Correction partielle d’état swipe [partial-fix]
Bug : « Networking swipe state issues » (vague).
Pourquoi ça a échoué :
| Aspect | Approche |
|---|---|
| Cause racine | PAS identifiée - le commit dit « partial fixes » |
| Fichiers modifiés | 4 (dispersés entre couches) |
| Type de correction | Ajout de plusieurs gardes basées sur ref |
| Logique | Complexe : introsKnownZero, drawBlockedByNoIntrosRef, introsFromCounters |
Résultat : A nécessité une correction de suivi 3 heures plus tard [root-cause-fix], puis une autre le même jour.
Le pattern : Succès vs Échec
| Facteur | Corrections réussies | Corrections échouées |
|---|---|---|
| Fichiers modifiés | 1-3 (chirurgical) | 4+ (dispersés) |
| Message de commit | Décrit le mécanisme | Décrit le symptôme |
| Type de correction | Supprimer/simplifier | Ajouter gardes/complexité |
| Conditionnels | Booléen simple | Heuristiques multi-états |
| Coordination d’état | Non requise | Tracking de ref inter-fichiers |
| Suivis nécessaires | Aucun dans 7 jours | Multiples le même jour |
Ce que les agents réussis ont fait différemment
1. Ils ont identifié le moment exact
Réussi [code-removal] :
« Resetting to 0 while still mounted causes brief flash »
Échoué [partial-fix] :
« partial fixes to networking swipe state issues »
L’agent réussi pouvait pointer vers une ligne et une frame spécifiques. L’agent échoué ne pouvait pas articuler ce qui n’allait vraiment pas.
2. Ils ont supprimé plutôt qu’ajouté
Agents réussis :
- Supprimé les réinitialisations de transform
[code-removal] - Remplacé l’animation personnalisée par une primitive de bibliothèque
[library-primitive] - Réduction nette de code
Agents échoués :
- Ajouté
drawBlockedByNoIntrosRef - Ajouté
introsFromCountersETintrosFromStore - Ajouté des gardes « latest check » (plus de 30 fois)
- Augmentation nette de code
3. Ils ont validé aux frontières
Réussi [input-validation] :
// Valider AVANT d'utiliser
if (!Number.isFinite(swipeThreshold) || swipeThreshold <= 0) {
return { opacity: 0 };
}
Échoué (dispersé dans le hook de logique de swipe) :
// Vérifier APRÈS que le problème se produit, dans 30+ endroits
if (!isLatest) return;
// ... plus tard ...
if (!isLatest) return; // re-vérifier
// ... plus tard ...
if (!isLatest) return; // triple-vérifier
4. Ils ont utilisé les primitives de bibliothèque
Réussi [library-primitive] :
// Utiliser ce que la bibliothèque fournit
FadeInDown.duration(800)
Échoué (timing personnalisé dispersé partout) :
// Inventer le timing à partir de zéro
setTimeout(() => { ... }, 250); // pourquoi 250 ?
setTimeout(() => { ... }, 1200); // pourquoi 1200 ?
setTimeout(() => { ... }, 1500); // pourquoi 1500 ?
Le refactor en 14 étapes : À quoi ressemble une bonne planification
Un effort réussi était un refactor incrémental en 14 étapes du hook useNetworkingSwipe. Ce qui l’a fait fonctionner :
Étape 1 : Extraire d’abord les utilitaires purs (pas d’effets secondaires)
// networkingGuards.ts - isolé, testable
export function guardLatest(ref, id) { ... }
export function makeClientActionId() { ... }
Étapes 2-7 : Corrections de durcissement individuelles, chacune vérifiable indépendamment
Étapes 8-11 : Extraire les préoccupations dans des hooks focalisés
// Chaque hook a UN travail
useNetworkingAcceptGate()
useNetworkingAnalytics()
useNetworkingArtwork()
useNetworkingCooldown()
Étapes 12-14 : Introduire une machine à états explicite
// Remplacer les booléens dispersés par des phases
type Phase = 'idle' | 'fetching' | 'drawing' | 'ready' | 'throttled' | 'error'
Principe clé : Chaque étape avait des critères d’acceptation explicites :
Acceptance: Build passes; no imports used yet.
Acceptance: All tests pass and cover error conditions.
Le pattern du classificateur d’erreur : Les fonctions pures gagnent
Un des refactors les plus réussis a extrait la gestion d’erreur dans un classificateur pur :
Avant (pattern échoué) :
// 71 lignes de if/else imbriqués dispersés dans le hook
if (error.code === 'ERR_THROTTLED') {
setBanner({ type: 'throttle', message: '...' });
setCooldown(error.cooldownEndsAt);
// ... plus d'effets secondaires
} else if (error.code === 'ERR_TIMEOUT') {
// ... effets secondaires différents
}
Après (pattern réussi) :
// Fonction classificateur pure
function classifySwipeError(error): ClassifiedSwipeError {
if (error.code === 'ERR_THROTTLED') {
return {
kind: 'throttle',
banner: { type: 'throttle', message: '...' },
cooldownEndsAt: error.cooldownEndsAt,
shouldRefresh: false
};
}
// ... retourne des données, pas des effets secondaires
}
// Le hook lit juste le résultat
const classified = classifySwipeError(error);
if (classified.banner) setBanner(classified.banner);
if (classified.cooldownEndsAt) setCooldown(classified.cooldownEndsAt);
Pourquoi ça fonctionne :
- Le classificateur est testable sans mocker les hooks
- La logique est auditable en un seul endroit
- Le hook est simplifié à « lire le résultat, appliquer les effets »
- Pas de conditionnels dispersés
Règles d’or des corrections réussies
-
Identifier la frame/moment exact - Si vous ne pouvez pas dire « à la ligne X, pendant la frame Y, la valeur est Z alors qu’elle devrait être W », vous n’avez pas diagnostiqué le problème.
-
Supprimer avant d’ajouter - Si votre correction ajoute plus de code qu’elle n’en supprime, questionnez si vous avez trouvé la cause racine.
-
Valider aux frontières - Vérifier les entrées quand elles entrent dans le système, pas après qu’elles ont corrompu l’état.
-
Utiliser les primitives de bibliothèque - Ne pas réimplémenter ce que Reanimated, React Navigation, ou Redux fournissent déjà.
-
Extraire les fonctions pures - La logique de domaine (classification d’erreur, calcul d’état) devrait être pure et testable.
-
Un commit = un changement cohérent - Si votre correction touche 6+ fichiers entre différentes couches, vous corrigez probablement des symptômes.
-
Planifier en markdown d’abord - Les refactors réussis avaient des plans écrits avec des critères d’acceptation avant de coder.
-
Vérifier les types avant de commiter - Chaque commit réussi mentionnait l’exécution de
pnpm type-check.
Facteurs de succès quantifiés
D’après l’analyse de 984 commits :
| Facteur | Corrélation de succès |
|---|---|
| Fichier unique modifié | ~85% pas de suivi nécessaire |
| Message de commit décrit le mécanisme | ~78% pas de suivi nécessaire |
| Code supprimé > code ajouté | ~73% pas de suivi nécessaire |
| Primitive de bibliothèque utilisée | ~91% pas de suivi nécessaire |
| Fonction pure extraite | ~82% pas de suivi nécessaire |
| « partial » dans le message de commit | 100 % a nécessité un suivi |
| 4+ fichiers modifiés | ~67% a nécessité un suivi |
| Nombre magique ajouté | ~71% a nécessité un suivi |
Recommandations pour l’amélioration des agents
Pour les développeurs d’agents
- Entraîner sur le timing, pas seulement la logique - Les bugs visuels nécessitent de comprendre l’ordre d’exécution entre threads
- Récompenser la suppression - Les corrections qui suppriment du code devraient avoir un score plus élevé que celles qui en ajoutent
- Ajouter de l’outillage de trace - Les agents doivent voir l’état à travers le temps, pas juste aux breakpoints
- Enseigner le pattern snapshot - C’est la solution la plus courante aux bugs de scintillement
Pour les auteurs de codebase
- Documenter les invariants - « Ne jamais réinitialiser les transforms pendant le montage » devrait être dans un README, pas un commentaire
- Centraliser les constantes de timing - Les nombres magiques causent des bugs magiques
- Ajouter des tests de régression visuelle - Tests de capture d’écran à des frames d’animation spécifiques
- Nommer vos patterns - « Snapshot optimiste » est plus facile à appliquer que de le découvrir à partir de zéro
Pour les utilisateurs promptant des agents
- Décrire le moment exact - « La carte flash au centre » est mieux que « l’animation est buggée »
- Fournir des détails au niveau frame - « Après que le swipe se termine mais avant que la carte suivante apparaisse »
- Mentionner ce que vous avez essayé - Évite à l’agent de répéter des approches échouées
- Demander un diagnostic d’abord - « Qu’est-ce qui cause ça ? » avant « Corrige ça »
La voie vers de meilleures performances des agents sur les bugs visuels passe par une meilleure compréhension du timing, de meilleurs outils d’observation, et de meilleures incitations à la simplification plutôt qu’à la complexité.
Conclusion
Les agents de code ont du mal avec les implémentations visuelles parce que :
-
Les bugs visuels sont des bugs de timing - Ils existent dans les écarts entre systèmes asynchrones, pas dans des erreurs logiques qui peuvent être tracées statiquement.
-
La correction est souvent contre-intuitive - Ne pas réinitialiser les transforms, garder les données obsolètes plus longtemps, ajouter des délais artificiels.
-
L’état est distribué - L’état React, l’état Redux, les valeurs partagées Reanimated, et les refs doivent tous se synchroniser.
-
Les tests sont inadéquats - Les tests unitaires passent pendant que des flashes d’1 frame sont livrés aux utilisateurs.
-
Les symptômes trompent - « La carte flash » pourrait être causé par une réinitialisation de transform, un timing de snapshot, une collision z-index, ou une transition de machine à états - les agents devinent mal.
-
Les corrections cascadent - Changer une constante de timing affecte plusieurs animations. Les agents ne peuvent pas prédire les effets de second ordre.
Le codebase StyleSwipe montre ce pattern clairement : 6 commits pour corriger une condition de concurrence, des jours de corrections de suivi après un refactor majeur, et 47 points de synchronisation qui doivent tous fonctionner ensemble.
Pour que les agents s’améliorent sur les bugs visuels, ils ont besoin de :
- De meilleurs modèles mentaux du timing (pas seulement de la logique)
- Une documentation explicite de « ce qu’il ne faut pas faire »
- Des outils pour visualiser l’état à travers le temps, pas seulement à un point unique
- Des patterns architecturaux qui réduisent les exigences de coordination
Le cycle vicieux
- L’agent ne comprend pas le système → fait une correction localisée
- La correction localisée casse autre chose → un autre agent ajoute une garde
- Les gardes s’accumulent → le système devient plus difficile à comprendre
- L’agent suivant comprend encore moins → fait une correction encore plus localisée
- Répéter
Le paradoxe des bugs visuels
Les bugs visuels créent un paradoxe pour les agents de code :
- Le symptôme est visible mais la cause est invisible (écarts de timing entre systèmes)
- La correction évidente (réinitialiser les valeurs, ajouter des vérifications) empire souvent les choses
- La correction correcte (supprimer du code, utiliser des snapshots, retarder le démontage) est contre-intuitive
- Les tests sont impossibles avec les outils standard (impossible de tester unitairement un scintillement de 16ms)
- Le succès nécessite la suppression mais les agents sont entraînés à ajouter du code
Les agents qui ont réussi sur les bugs visuels partageaient un trait : ils comprenaient suffisamment le système pour supprimer du code plutôt que d’en ajouter.
Les agents qui ont échoué ont continué à ajouter des gardes, des vérifications et des contournements—chacun un monument à leur compréhension incomplète, et chacun rendant le travail de l’agent suivant plus difficile.
La dette de documentation
Chaque commentaire défensif est une dette technique :
- « Do not reset ghost transforms here » = l’agent n’a pas pu corriger la cause racine
- Timer watchdog = l’agent n’a pas pu empêcher le deadlock
- 30+ gardes « re-check latest » = l’agent n’a pas pu éliminer la concurrence
La voie à suivre
Pour que les agents réussissent sur les bugs visuels, ils ont besoin de :
- Outils de trace - Visualiser l’état à travers le temps, pas juste à un point
- Débogueurs d’animation - Avancer frame par frame
- Bibliothèques de patterns - Solutions nommées pour les problèmes visuels courants
- Documentation des invariants - Ce qui ne doit JAMAIS changer
- Portes de tests d’intégration - Empêcher les commits qui cassent le comportement existant
Jusque-là, les bugs visuels resteront le domaine où les agents ont le plus de mal.