/click-path-audit — Audit des chemins de clics (comportemental)
Trouvez les bugs que la lecture statique du code ne voit pas : effets secondaires d'interactions entre états, conditions de course entre appels séquentiels, et gestionnaires qui s'annulent silencieusement.
Le problème résolu
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'interface correspond-il à ce que promet le label 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 secondaires 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 secondaire 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 SECONDAIRES sur l'état partagé ?
d. Réinitialise-t-il/efface-t-il un état comme effet secondaire ?
4. VÉRIFIER : un appel ultérieur ANNULE-t-il un changement d'état d'un appel précédent ?
5. VÉRIFIER : l'état FINAL correspond-il à ce que l'utilisateur attend du label 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 un point de contact, construisez une carte des effets secondaires de chaque action du store d'état :
Pour chaque store Zustand / contexte React dans le périmètre :
Pour chaque action/accesseur :
- Quels champs définit-il ?
- RÉINITIALISE-t-il d'autres champs comme effet secondaire ?
- Documenter : actionName → {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} RÉINITIALISE : {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 : [Label du bouton] dans [Composant:ligne]
GESTIONNAIRE : onClick → {
appel 1 : fonctionA() → définit {X: true}
appel 2 : fonctionB() → définit {Y: null} RÉINITIALISE {X: false} ← CONFLIT
}
ATTENDU : L'utilisateur voit [description de ce que promet le label du bouton]
RÉEL : X est false car fonctionB l'a réinitialisé
VERDICT : BUG — [description]
Vérifiez chacun de ces motifs de bug :
Motif 1 : Annulation séquentielle
gestionnaire() {
setState_A(true) // définit X = true
setState_B(null) // effet secondaire : réinitialise X = false
}
// Résultat : X est false. Le premier appel était inutile.
Motif 2 : Course asynchrone
gestionnaire() {
fetchA().then(() => setState({ loading: false }))
fetchB().then(() => setState({ loading: true }))
}
// Résultat : l'état loading final dépend de celui qui se résout en premier
Motif 3 : Fermeture périmée (Stale Closure)
const [count, setCount] = useState(0)
const gestionnaire = useCallback(() => {
setCount(count + 1) // capture un count périmé
setCount(count + 1) // même count périmé — 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, ne sauvegarde jamais
// Le bouton dit "Supprimer" mais le gestionnaire définit un flag sans appeler l'API
// Le bouton dit "Envoyer" mais le point d'accès API est supprimé/cassé
Motif 5 : Chemin mort conditionnel
gestionnaire() {
if (someState) { // someState est TOUJOURS false à ce stade
faireLaVraieAction() // jamais atteint
}
}
Motif 6 : Interférence de 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 : [sévérité : CRITIQUE/HAUTE/MOYENNE/BASSE]
Point de contact : [Label du bouton] dans [fichier:ligne]
Motif : [Annulation séquentielle / Course asynchrone / Fermeture périmée / Transition manquante / Chemin mort / Interférence useEffect]
Gestionnaire : [nom de fonction ou inline]
Trace :
1. [appel] → définit {champ: valeur}
2. [appel] → RÉINITIALISE {champ: valeur} ← CONFLIT
Attendu : [ce que l'utilisateur attend]
Réel : [ce qui se passe réellement]
Correctif : [correctif spécifique]
Contrôle du périmètre
Cet audit est coûteux. Délimitez-le correctement :
- Audit complet de l'application : À utiliser lors du lancement ou après une refonte majeure. Lancez des agents en parallèle par page.
- Audit d'une seule page : À utiliser après la construction d'une nouvelle page ou après qu'un utilisateur a signalé un bouton cassé.
- Audit centré sur un store : À utiliser après modification d'un store Zustand — auditez tous les consommateurs des actions modifiées.
Répartition recommandée des agents pour une application complète :
Agent 1 : Cartographier TOUS les stores d'état (Étape 1) — c'est le 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 terminer en premier. Sa sortie est l'entrée de tous les autres agents.
Quand l'utiliser
- Après un débogage systématique qui n'a trouvé "aucun bug" mais que les utilisateurs signalent une interface cassée
- Après modification de toute action d'un store Zustand (vérifiez tous les appelants)
- Après toute refonte touchant l'état partagé
- Avant une release, sur les flux utilisateur critiques
- Quand un bouton "ne fait rien" — c'est L'OUTIL pour cela
Quand NE PAS l'utiliser
- Pour les bugs au niveau API (mauvaise forme de réponse, point d'accès manquant) — utilisez le débogage systématique
- Pour les problèmes de style/mise en page — 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 correctifs 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" dans ThreadList.tsx :
onClick={() => {
useEmailStore.getState().setComposeMode(true) // ✓ définit composeMode = true
useEmailStore.getState().selectThread(null) // ✗ RÉINITIALISE 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 des deux fonctions ne plante (pas d'erreur d'exécution)
- Les types de données sont corrects (pas de mismatch de type)
L'audit des chemins de clics le détecte car :
- Étape 1 : cartographie
selectThreadréinitialisecomposeMode - Étape 2 : trace le gestionnaire : appel 1 définit true, appel 2 réinitialise false
- Verdict : Annulation séquentielle — l'état final contredit l'intention du bouton