/click-path-audit — Audit de flux comportemental
Trouvez les bugs que la lecture statique de code manque : effets de bord d'interaction d'état, conditions de course entre appels séquentiels, et gestionnaires qui s'annulent silencieusement.
Le problème résolu par cet audit
Le débogage traditionnel vérifie :
- La fonction existe-t-elle ? (câblage manquant)
- Plante-t-elle ? (erreurs d'exécution)
- Retourne-t-elle le bon type ? (flux de données)
Mais il ne vérifie PAS :
- L'état final de l'UI correspond-il à ce que promet le libellé du bouton ?
- La fonction B annule-t-elle silencieusement ce que la fonction A vient de faire ?
- L'état partagé (Zustand/Redux/contexte) a-t-il des effets de bord qui annulent l'action prévue ?
Exemple réel : un bouton "Nouvel email" appelait setComposeMode(true) puis selectThread(null). Les deux fonctionnaient individuellement. Mais selectThread avait un effet de bord réinitialisant composeMode: false. Le bouton ne faisait rien. 54 bugs ont été trouvés par débogage systématique — celui-ci a été manqué.
Comment ça marche
Pour CHAQUE point de contact interactif dans la zone cible :
1. IDENTIFIER le gestionnaire (onClick, onSubmit, onChange, etc.)
2. TRACER chaque appel de fonction dans le gestionnaire, DANS L'ORDRE
3. Pour CHAQUE appel de fonction :
a. Quel état LIT-IL ?
b. Quel état ÉCRIT-IL ?
c. A-t-il des EFFETS DE BORD sur l'état partagé ?
d. Réinitialise-t-il/efface-t-il un état comme effet de bord ?
4. VÉRIFIER : Un appel ultérieur ANNULE-t-il un changement d'état d'un appel antérieur ?
5. VÉRIFIER : L'état FINAL est-il ce que l'utilisateur attend du libellé du bouton ?
6. VÉRIFIER : Y a-t-il des conditions de course (appels asynchrones qui se résolvent dans le mauvais ordre) ?
Étapes d'exécution
Étape 1 : Cartographier les stores d'état
Avant d'auditer tout point de contact, construisez une carte des effets de bord de chaque action de store d'état :
Pour chaque store Zustand / contexte React dans le périmètre :
Pour chaque action/setter :
- Quels champs définit-il ?
- RÉINITIALISE-t-il d'autres champs comme effet de bord ?
- Documenter : nomAction → {sets: [...], resets: [...]}
C'est la référence critique. Le bug "Nouvel email" était invisible sans savoir que selectThread réinitialise composeMode.
Format de sortie :
STORE: emailStore
setComposeMode(bool) → sets: {composeMode}
selectThread(thread|null) → sets: {selectedThread, selectedThreadId, messages, drafts, selectedDraft, summary} RESETS: {composeMode: false, composeData: null, redraftOpen: false}
setDraftGenerating(bool) → sets: {draftGenerating}
...
RÉINITIALISATIONS DANGEREUSES (actions qui effacent un état qu'elles ne possèdent pas) :
selectThread → réinitialise composeMode (possédé par setComposeMode)
reset → réinitialise tout
Étape 2 : Auditer chaque point de contact
Pour chaque bouton/bascule/soumission de formulaire dans la zone cible :
POINT DE CONTACT : [Libellé du bouton] dans [Composant:ligne]
GESTIONNAIRE : onClick → {
appel 1: fonctionA() → sets {X: true}
appel 2: fonctionB() → sets {Y: null} RESETS {X: false} ← CONFLIT
}
ATTENDU : L'utilisateur voit [description de ce que le libellé promet]
RÉEL : X est faux car fonctionB l'a réinitialisé
VERDICT : BUG — [description]
Vérifiez chacun de ces motifs de bugs :
Motif 1 : Annulation séquentielle
handler() {
setState_A(true) // sets X = true
setState_B(null) // effet de bord : réinitialise X = false
}
// Résultat : X est faux. Le premier appel était inutile.
Motif 2 : Course asynchrone
handler() {
fetchA().then(() => setState({ loading: false }))
fetchB().then(() => setState({ loading: true }))
}
// Résultat : l'état de chargement final dépend de celui qui se résout en premier
Motif 3 : Fermeture obsolète
const [count, setCount] = useState(0)
const handler = useCallback(() => {
setCount(count + 1) // capture un count obsolète
setCount(count + 1) // même count obsolète — incrémente de 1, pas de 2
}, [count])
Motif 4 : Transition d'état manquante
// Le bouton dit "Enregistrer" mais le gestionnaire ne fait que valider, jamais sauvegarder
// Le bouton dit "Supprimer" mais le gestionnaire définit un indicateur sans appeler l'API
// Le bouton dit "Envoyer" mais le point de terminaison API est supprimé/cassé
Motif 5 : Chemin mort conditionnel
handler() {
if (someState) { // someState est TOUJOURS faux à ce stade
doTheActualThing() // jamais atteint
}
}
Motif 6 : Interférence useEffect
// Le bouton définit stateX = true
// Un useEffect surveille stateX et le réinitialise à false
// L'utilisateur ne voit rien se produire
Étape 3 : Rapport
Pour chaque bug trouvé :
CLICK-PATH-NNN : [gravité : CRITIQUE/HAUTE/MOYENNE/BASSE]
Point de contact : [Libellé du bouton] dans [fichier:ligne]
Motif : [Annulation séquentielle / Course asynchrone / Fermeture obsolète / Transition manquante / Chemin mort / Interférence useEffect]
Gestionnaire : [nom de la fonction ou en ligne]
Traçage :
1. [appel] → sets {champ: valeur}
2. [appel] → RESETS {champ: valeur} ← CONFLIT
Attendu : [ce que l'utilisateur attend]
Réel : [ce qui se passe réellement]
Correction : [correction spécifique]
Contrôle du périmètre
Cet audit est coûteux. Adaptez son périmètre :
- Audit complet de l'application : À utiliser lors du lancement ou après une refactorisation majeure. Lancez des agents en parallèle par page.
- Audit d'une seule page : À utiliser après avoir construit une nouvelle page ou après qu'un utilisateur signale un bouton cassé.
- Audit centré sur un store : À utiliser après avoir modifié un store Zustand — auditez tous les consommateurs des actions modifiées.
Répartition d'agents recommandée pour une application complète :
Agent 1 : Cartographier TOUS les stores d'état (Étape 1) — contexte partagé pour tous les autres agents
Agent 2 : Tableau de bord (Tâches, Notes, Journal, Idées)
Agent 3 : Chat (DanteChatColumn, JustChatPage)
Agent 4 : Emails (ThreadList, DraftArea, EmailsPage)
Agent 5 : Projets (ProjectsPage, ProjectOverviewTab, NewProjectWizard)
Agent 6 : CRM (tous les sous-onglets)
Agent 7 : Profil, Paramètres, Coffre, Notifications
Agent 8 : Suite de gestion (toutes les pages)
L'Agent 1 doit se terminer en premier. Sa sortie est l'entrée de tous les autres agents.
Quand utiliser
- Après un débogage systématique qui ne trouve "aucun bug" mais que les utilisateurs signalent une UI cassée
- Après avoir modifié une action de store Zustand (vérifiez tous les appelants)
- Après toute refactorisation qui touche l'état partagé
- Avant la sortie, sur les flux utilisateur critiques
- Quand un bouton "ne fait rien" — c'est L'OUTIL pour ça
Quand ne PAS utiliser
- Pour les bugs au niveau API (mauvaise forme de réponse, point de terminaison manquant) — utilisez le débogage systématique
- Pour les problèmes de style/disposition — inspection visuelle
- Pour les problèmes de performance — outils de profilage
Intégration avec d'autres compétences
- Exécuter APRÈS
/superpowers:systematic-debugging(qui trouve les 54 autres types de bugs) - Exécuter AVANT
/superpowers:verification-before-completion(qui vérifie que les corrections fonctionnent) - Alimente
/superpowers:test-driven-development— chaque bug trouvé ici devrait avoir un test
Exemple : Le bug qui a inspiré cette compétence
Bouton "Nouvel email" de ThreadList.tsx :
onClick={() => {
useEmailStore.getState().setComposeMode(true) // ✓ sets composeMode = true
useEmailStore.getState().selectThread(null) // ✗ RESETS composeMode = false
}}
Définition du store :
selectThread: (thread) => set({
selectedThread: thread,
selectedThreadId: thread?.id ?? null,
messages: [],
drafts: [],
selectedDraft: null,
summary: null,
composeMode: false, // ← cette réinitialisation silencieuse a tué le bouton
composeData: null,
redraftOpen: false,
})
Le débogage systématique l'a manqué car :
- Le bouton a un gestionnaire onClick (pas mort)
- Les deux fonctions existent (pas de câblage manquant)
- Aucune fonction ne plante (pas d'erreur d'exécution)
- Les types de données sont corrects (pas d'incompatibilité de type)
L'audit de chemin de clic l'attrape car :
- L'étape 1 cartographie que
selectThreadréinitialisecomposeMode - L'étape 2 trace le gestionnaire : l'appel 1 définit true, l'appel 2 réinitialise false
- Verdict : Annulation séquentielle — l'état final contredit l'intention du bouton