/click-path-audit – Verhaltensfluss-Audit
Finde Fehler, die statische Code-Lektüre übersieht: Interaktionseffekte von Zuständen, Wettlaufsituationen zwischen sequenziellen Aufrufen und Handler, die sich stillschweigend gegenseitig rückgängig machen.
Das Problem, das dies löst
Traditionelles Debugging prüft:
- Existiert die Funktion? (fehlende Verdrahtung)
- Stürzt sie ab? (Laufzeitfehler)
- Liefert sie den richtigen Typ? (Datenfluss)
Aber es prüft NICHT:
- Stimmt der endgültige UI-Zustand mit dem überein, was der Button-Text verspricht?
- Macht Funktion B stillschweigend rückgängig, was Funktion A gerade getan hat?
- Hat Shared State (Zustand/Redux/Context) Seiteneffekte, die die beabsichtigte Aktion aufheben?
Realbeispiel: Ein „Neue E-Mail”-Button rief setComposeMode(true) und dann selectThread(null) auf. Beide funktionierten einzeln. Aber selectThread hatte einen Seiteneffekt, der composeMode: false zurücksetzte. Der Button tat nichts. 54 Fehler wurden durch systematisches Debugging gefunden – dieser eine wurde übersehen.
Wie es funktioniert
Für JEDEN interaktiven Berührungspunkt im Zielbereich:
1. IDENTIFIZIERE den Handler (onClick, onSubmit, onChange, etc.)
2. VERFOLGE jeden Funktionsaufruf im Handler, IN REIHENFOLGE
3. Für JEDEN Funktionsaufruf:
a. Welchen Zustand LIEST er?
b. Welchen Zustand SCHREIBT er?
c. Hat er SEITENEFFEKTE auf Shared State?
d. Setzt/löscht er Zustand als Seiteneffekt zurück?
4. PRÜFE: Macht ein späterer Aufruf eine Statusänderung eines früheren Aufrufs RÜCKGÄNGIG?
5. PRÜFE: Entspricht der ENDZUSTAND dem, was der Benutzer vom Button-Text erwartet?
6. PRÜFE: Gibt es Wettlaufsituationen (async-Aufrufe, die in falscher Reihenfolge auflösen)?
Ausführungsschritte
Schritt 1: State-Stores kartieren
Erstelle vor der Prüfung eines Berührungspunkts eine Seiteneffekt-Karte für jede Aktion des State-Stores:
Für jeden Zustand-Store / React-Context im Gültigkeitsbereich:
Für jede Aktion/setter:
- Welche Felder setzt sie?
- Setzt sie andere Felder als Seiteneffekt ZURÜCK?
- Dokumentiere: actionName → {sets: [...], resets: [...]}
Dies ist die entscheidende Referenz. Der „Neue E-Mail”-Fehler war unsichtbar, ohne zu wissen, dass selectThread composeMode zurücksetzt.
Ausgabeformat:
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}
...
GEFÄHRLICHE ZURÜCKSETZUNGEN (Aktionen, die Zustand löschen, der ihnen nicht gehört):
selectThread → setzt composeMode zurück (gehört zu setComposeMode)
reset → setzt alles zurück
Schritt 2: Jeden Berührungspunkt prüfen
Für jeden Button/Toggle/Formular-Absenden im Zielbereich:
BERÜHRUNGSPUNKT: [Button-Text] in [Componente:Zeile]
HANDLER: onClick → {
call 1: functionA() → sets {X: true}
call 2: functionB() → sets {Y: null} RESETS {X: false} ← KONFLIKT
}
ERWARTET: Benutzer sieht [Beschreibung dessen, was der Button-Text verspricht]
IST: X ist false, weil functionB es zurückgesetzt hat
URTEIL: FEHLER — [Beschreibung]
Prüfe jedes dieser Fehlermuster:
Muster 1: Sequenzielles Rückgängigmachen
handler() {
setState_A(true) // setzt X = true
setState_B(null) // Seiteneffekt: setzt X = false zurück
}
// Ergebnis: X ist false. Erster Aufruf war sinnlos.
Muster 2: Asynchrone Wettlaufsituation
handler() {
fetchA().then(() => setState({ loading: false }))
fetchB().then(() => setState({ loading: true }))
}
// Ergebnis: Endzustand von loading hängt davon ab, welcher zuerst auflöst
Muster 3: Stale Closure
const [count, setCount] = useState(0)
const handler = useCallback(() => {
setCount(count + 1) // erfasst veralteten count
setCount(count + 1) // gleicher veralteter count – erhöht um 1, nicht um 2
}, [count])
Muster 4: Fehlender Zustandsübergang
// Button sagt „Speichern”, aber Handler validiert nur, speichert nie
// Button sagt „Löschen”, aber Handler setzt nur ein Flag, ohne die API aufzurufen
// Button sagt „Senden”, aber der API-Endpunkt wurde entfernt/ist defekt
Muster 5: Bedingter toter Pfad
handler() {
if (someState) { // someState ist zu diesem Zeitpunkt IMMER false
doTheActualThing() // wird nie erreicht
}
}
Muster 6: useEffect-Interferenz
// Button setzt stateX = true
// Ein useEffect überwacht stateX und setzt es auf false zurück
// Benutzer sieht nichts passieren
Schritt 3: Bericht
Für jeden gefundenen Fehler:
CLICK-PATH-NNN: [Schweregrad: KRITISCH/HOCH/MITTEL/NIEDRIG]
Berührungspunkt: [Button-Text] in [Datei:Zeile]
Muster: [Sequenzielles Rückgängigmachen / Asynchrone Wettlaufsituation / Stale Closure / Fehlender Übergang / Toter Pfad / useEffect-Interferenz]
Handler: [Funktionsname oder inline]
Ablauf:
1. [Aufruf] → setzt {Feld: Wert}
2. [Aufruf] → SETZT ZURÜCK {Feld: Wert} ← KONFLIKT
Erwartet: [was der Benutzer erwartet]
Ist: [was tatsächlich passiert]
Lösung: [spezifische Lösung]
Umfangskontrolle
Dieses Audit ist aufwendig. Begrenze den Umfang angemessen:
- Komplettes App-Audit: Verwenden beim Start oder nach größerem Refactoring. Starte parallele Agenten pro Seite.
- Einzelseiten-Audit: Verwenden nach dem Erstellen einer neuen Seite oder nachdem ein Benutzer einen defekten Button meldet.
- Store-fokussiertes Audit: Verwenden nach Änderungen an einem Zustand-Store – prüfe alle Konsumenten der geänderten Aktionen.
Empfohlene Agentenaufteilung für die gesamte App:
Agent 1: Kartiere ALLE State-Stores (Schritt 1) – dies ist der gemeinsame Kontext für alle anderen Agenten
Agent 2: Dashboard (Aufgaben, Notizen, Tagebuch, Ideen)
Agent 3: Chat (DanteChatColumn, JustChatPage)
Agent 4: E-Mails (ThreadList, DraftArea, EmailsPage)
Agent 5: Projekte (ProjectsPage, ProjectOverviewTab, NewProjectWizard)
Agent 6: CRM (alle Untertabs)
Agent 7: Profil, Einstellungen, Tresor, Benachrichtigungen
Agent 8: Management-Suite (alle Seiten)
Agent 1 MUSS zuerst abgeschlossen sein. Seine Ausgabe ist die Eingabe für alle anderen Agenten.
Wann verwenden
- Nach systematischem Debugging, das „keine Fehler” findet, aber Benutzer defekte UI melden
- Nach Änderungen an einer Aktion eines Zustand-Stores (alle Aufrufer prüfen)
- Nach jedem Refactoring, das Shared State betrifft
- Vor einem Release, bei kritischen Benutzerabläufen
- Wenn ein Button „nichts tut” – dies ist DAS Werkzeug dafür
Wann NICHT verwenden
- Für API-Fehler (falsche Antwortstruktur, fehlender Endpunkt) – verwende systematisches Debugging
- Für Styling/Layout-Probleme – visuelle Inspektion
- Für Leistungsprobleme – Profiling-Werkzeuge
Integration mit anderen Skills
- Nach
/superpowers:systematic-debuggingausführen (welches die anderen 54 Fehlertypen findet) - Vor
/superpowers:verification-before-completionausführen (welches überprüft, ob Reparaturen funktionieren) - Speist sich in
/superpowers:test-driven-developmentein – jeder hier gefundene Fehler sollte einen Test erhalten
Beispiel: Der Fehler, der dieses Skill inspirierte
ThreadList.tsx „Neue E-Mail”-Button:
onClick={() => {
useEmailStore.getState().setComposeMode(true) // ✓ setzt composeMode = true
useEmailStore.getState().selectThread(null) // ✗ SETZT ZURÜCK composeMode = false
}}
Store-Definition:
selectThread: (thread) => set({
selectedThread: thread,
selectedThreadId: thread?.id ?? null,
messages: [],
drafts: [],
selectedDraft: null,
summary: null,
composeMode: false, // ← dieses stille Zurücksetzen hat den Button getötet
composeData: null,
redraftOpen: false,
})
Systematisches Debugging hat es übersehen, weil:
- Der Button hat einen onClick-Handler (nicht tot)
- Beide Funktionen existieren (keine fehlende Verdrahtung)
- Keine Funktion stürzt ab (kein Laufzeitfehler)
- Die Datentypen sind korrekt (kein Typkonflikt)
Click-Path-Audit fängt es, weil:
- Schritt 1 kartiert
selectThreadsetztcomposeModezurück - Schritt 2 verfolgt den Handler: Aufruf 1 setzt true, Aufruf 2 setzt false zurück
- Urteil: Sequenzielles Rückgängigmachen – Endzustand widerspricht der Buttonabsicht