/click-path-audit — Auditoría de flujo de comportamiento
Encuentra errores que la lectura de código estático pasa por alto: efectos secundarios de interacción de estado, condiciones de carrera entre llamadas secuenciales y manejadores que se deshacen silenciosamente entre sí.
El problema que resuelve
La depuración tradicional verifica:
- ¿Existe la función? (cableado faltante)
- ¿Falla? (errores de tiempo de ejecución)
- ¿Devuelve el tipo correcto? (flujo de datos)
Pero NO verifica:
- ¿El estado final de la UI coincide con lo que promete la etiqueta del botón?
- ¿La función B deshace en silencio lo que la función A acaba de hacer?
- ¿El estado compartido (Zustand/Redux/contexto) tiene efectos secundarios que cancelan la acción prevista?
Ejemplo real: Un botón de "Nuevo correo" llamaba a setComposeMode(true) y luego a selectThread(null). Ambas funcionaban individualmente. Pero selectThread tenía un efecto secundario que restablecía composeMode: false. El botón no hacía nada. La depuración sistemática encontró 54 errores; este uno se pasó por alto.
Cómo funciona
Para CADA punto de contacto interactivo en el área objetivo:
1. IDENTIFICAR el manejador (onClick, onSubmit, onChange, etc.)
2. RASTREAR cada llamada de función en el manejador, EN ORDEN
3. Para CADA llamada de función:
a. ¿Qué estado LEE?
b. ¿Qué estado ESCRIBE?
c. ¿Tiene EFECTOS SECUNDARIOS en el estado compartido?
d. ¿Restablece/limpia algún estado como efecto secundario?
4. VERIFICAR: ¿Alguna llamada posterior DESHACE un cambio de estado de una llamada anterior?
5. VERIFICAR: ¿El estado FINAL es el que el usuario espera de la etiqueta del botón?
6. VERIFICAR: ¿Hay condiciones de carrera (llamadas asíncronas que se resuelven en el orden incorrecto)?
Pasos de ejecución
Paso 1: Mapear almacenes de estado
Antes de auditar cualquier punto de contacto, construye un mapa de efectos secundarios de cada acción del almacén de estado:
Para cada almacén Zustand / contexto de React en el ámbito:
Para cada acción/establecedor:
- ¿Qué campos establece?
- ¿RESTABLECE otros campos como efecto secundario?
- Documentar: nombreDeAcción → {establece: [...], restablece: [...]}
Esta es la referencia crítica. El error de "Nuevo correo" era invisible sin saber que selectThread restablece composeMode.
Formato de salida:
ALMACÉN: emailStore
setComposeMode(bool) → establece: {composeMode}
selectThread(thread|null) → establece: {selectedThread, selectedThreadId, messages, drafts, selectedDraft, summary} RESTABLECE: {composeMode: false, composeData: null, redraftOpen: false}
setDraftGenerating(bool) → establece: {draftGenerating}
...
RESTABLECIMIENTOS PELIGROSOS (acciones que limpian estado que no poseen):
selectThread → restablece composeMode (propiedad de setComposeMode)
reset → restablece todo
Paso 2: Auditar cada punto de contacto
Para cada botón/alternador/envío de formulario en el área objetivo:
PUNTO DE CONTACTO: [Etiqueta del botón] en [Componente:línea]
MANEJADOR: onClick → {
llamada 1: funciónA() → establece {X: true}
llamada 2: funciónB() → establece {Y: null} RESTABLECE {X: false} ← CONFLICTO
}
ESPERADO: El usuario ve [descripción de lo que promete la etiqueta del botón]
REAL: X es false porque funciónB lo restableció
VEREDICTO: ERROR — [descripción]
Verifica cada uno de estos patrones de error:
Patrón 1: Deshacer secuencial
manejador() {
setState_A(true) // establece X = true
setState_B(null) // efecto secundario: restablece X = false
}
// Resultado: X es false. La primera llamada fue inútil.
Patrón 2: Condición de carrera asíncrona
manejador() {
fetchA().then(() => setState({ loading: false }))
fetchB().then(() => setState({ loading: true }))
}
// Resultado: el estado final de loading depende de cuál se resuelva primero
Patrón 3: Cierre obsoleto
const [count, setCount] = useState(0)
const manejador = useCallback(() => {
setCount(count + 1) // captura count obsoleto
setCount(count + 1) // mismo count obsoleto — incrementa en 1, no en 2
}, [count])
Patrón 4: Transición de estado faltante
// El botón dice "Guardar" pero el manejador solo valida, nunca guarda realmente
// El botón dice "Eliminar" pero el manejador establece una bandera sin llamar a la API
// El botón dice "Enviar" pero el endpoint de la API se eliminó/rompió
Patrón 5: Camino muerto condicional
manejador() {
if (algúnEstado) { // algúnEstado SIEMPRE es false en este punto
hacerLaAcciónReal() // nunca se alcanza
}
}
Patrón 6: Interferencia de useEffect
// El botón establece estadoX = true
// Un useEffect observa estadoX y lo restablece a false
// El usuario no ve que suceda nada
Paso 3: Informar
Para cada error encontrado:
CLICK-PATH-NNN: [gravedad: CRÍTICO/ALTO/MEDIO/BAJO]
Punto de contacto: [Etiqueta del botón] en [archivo:línea]
Patrón: [Deshacer secuencial / Condición de carrera asíncrona / Cierre obsoleto / Transición faltante / Camino muerto / Interferencia de useEffect]
Manejador: [nombre de función o en línea]
Traza:
1. [llamada] → establece {campo: valor}
2. [llamada] → RESTABLECE {campo: valor} ← CONFLICTO
Esperado: [lo que el usuario espera]
Real: [lo que realmente sucede]
Corrección: [corrección específica]
Control de alcance
Esta auditoría es costosa. Acótala adecuadamente:
- Auditoría completa de la aplicación: Úsala al lanzar o después de una refactorización importante. Lanza agentes paralelos por página.
- Auditoría de una sola página: Úsala después de construir una nueva página o de que un usuario reporte un botón roto.
- Auditoría centrada en almacén: Úsala después de modificar un almacén Zustand — audita todos los consumidores de las acciones modificadas.
División recomendada de agentes para aplicación completa:
Agente 1: Mapear TODOS los almacenes de estado (Paso 1) — este es contexto compartido para todos los demás agentes
Agente 2: Tablero (Tareas, Notas, Diario, Ideas)
Agente 3: Chat (DanteChatColumn, JustChatPage)
Agente 4: Correos (ThreadList, DraftArea, EmailsPage)
Agente 5: Proyectos (ProjectsPage, ProjectOverviewTab, NewProjectWizard)
Agente 6: CRM (todas las subpestañas)
Agente 7: Perfil, Configuración, Bóveda, Notificaciones
Agente 8: Suite de gestión (todas las páginas)
El Agente 1 DEBE completarse primero. Su salida es la entrada para todos los demás agentes.
Cuándo usar
- Después de que la depuración sistemática encuentre "sin errores" pero los usuarios reporten UI rota
- Después de modificar cualquier acción de un almacén Zustand (verifica todos los llamadores)
- Después de cualquier refactorización que toque estado compartido
- Antes del lanzamiento, en flujos de usuario críticos
- Cuando un botón "no hace nada" — esta es LA herramienta para eso
Cuándo NO usar
- Para errores a nivel de API (forma de respuesta incorrecta, endpoint faltante) — usa depuración sistemática
- Para problemas de estilo/diseño — inspección visual
- Para problemas de rendimiento — herramientas de perfilado
Integración con otras habilidades
- Ejecutar DESPUÉS de
/superpowers:systematic-debugging(que encuentra los otros 54 tipos de errores) - Ejecutar ANTES de
/superpowers:verification-before-completion(que verifica que las correcciones funcionen) - Alimenta
/superpowers:test-driven-development— cada error encontrado aquí debe tener una prueba
Ejemplo: El error que inspiró esta habilidad
Botón "Nuevo correo" en ThreadList.tsx:
onClick={() => {
useEmailStore.getState().setComposeMode(true) // ✓ establece composeMode = true
useEmailStore.getState().selectThread(null) // ✗ RESTABLECE composeMode = false
}}
Definición del almacén:
selectThread: (thread) => set({
selectedThread: thread,
selectedThreadId: thread?.id ?? null,
messages: [],
drafts: [],
selectedDraft: null,
summary: null,
composeMode: false, // ← este restablecimiento silencioso mató el botón
composeData: null,
redraftOpen: false,
})
La depuración sistemática lo pasó por alto porque:
- El botón tiene un manejador onClick (no está muerto)
- Ambas funciones existen (no hay cableado faltante)
- Ninguna función falla (no hay error de tiempo de ejecución)
- Los tipos de datos son correctos (no hay desajuste de tipos)
La auditoría de ruta de clic lo detecta porque:
- El Paso 1 mapea
selectThreadrestablececomposeMode - El Paso 2 rastrea el manejador: la llamada 1 establece true, la llamada 2 restablece false
- Veredicto: Deshacer secuencial — el estado final contradice la intención del botón