/click-path-audit — Auditoría de Flujo de Comportamiento
Encuentra errores que la lectura de código estático no detecta: 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 en tiempo de ejecución)
- ¿Devuelve el tipo correcto? (flujo de datos)
Pero NO verifica:
- ¿El estado final de la interfaz coincide con lo que promete la etiqueta del botón?
- ¿La función B deshace silenciosamente lo que la función A acaba de hacer?
- ¿El estado compartido (Zustand/Redux/context) tiene efectos secundarios que cancelan la acción deseada?
Ejemplo real: Un botón "Nuevo Correo" llamaba a setComposeMode(true) y luego selectThread(null). Ambas funcionaban individualmente. Pero selectThread tenía un efecto secundario que reseteaba composeMode: false. El botón no hacía nada. La depuración sistemática encontró 54 errores — este se pasó por alto.
Cómo Funciona
Para CADA punto de interacción en el área objetivo:
1. IDENTIFICAR el manejador (onClick, onSubmit, onChange, etc.)
2. TRAZAR cada llamada a función en el manejador, EN ORDEN
3. Para CADA llamada a función:
a. ¿Qué estado LEE?
b. ¿Qué estado ESCRIBE?
c. ¿Tiene EFECTOS SECUNDARIOS en estado compartido?
d. ¿Resetea/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 según la etiqueta del botón?
6. VERIFICAR: ¿Hay condiciones de carrera (llamadas asíncronas que resuelven en orden incorrecto)?
Pasos de Ejecución
Paso 1: Mapear Stores de Estado
Antes de auditar cualquier punto de interacción, construye un mapa de efectos secundarios de cada acción del store de estado:
Para cada store Zustand / contexto React en el ámbito:
Para cada acción/setter:
- ¿Qué campos establece?
- ¿RESETEA otros campos como efecto secundario?
- Documentar: nombreAcción → {establece: [...], resetea: [...]}
Esta es la referencia crítica. El error del botón "Nuevo Correo" era invisible sin saber que selectThread resetea composeMode.
Formato de salida:
STORE: emailStore
setComposeMode(bool) → establece: {composeMode}
selectThread(thread|null) → establece: {selectedThread, selectedThreadId, messages, drafts, selectedDraft, summary} RESETEA: {composeMode: false, composeData: null, redraftOpen: false}
setDraftGenerating(bool) → establece: {draftGenerating}
...
RESETEOS PELIGROSOS (acciones que limpian estado que no les pertenece):
selectThread → resetea composeMode (perteneciente a setComposeMode)
reset → resetea todo
Paso 2: Auditar Cada Punto de Interacción
Para cada botón/alternador/envío de formulario en el área objetivo:
PUNTO DE INTERACCIÓN: [Etiqueta del botón] en [Componente:línea]
MANEJADOR: onClick → {
llamada 1: funciónA() → establece {X: true}
llamada 2: funciónB() → establece {Y: null} RESETEA {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 reseteó
VEREDICTO: ERROR — [descripción]
Verifica cada uno de estos patrones de error:
Patrón 1: Deshacer Secuencial
handler() {
setState_A(true) // establece X = true
setState_B(null) // efecto secundario: resetea X = false
}
// Resultado: X es false. La primera llamada fue inútil.
Patrón 2: Carrera Asíncrona
handler() {
fetchA().then(() => setState({ loading: false }))
fetchB().then(() => setState({ loading: true }))
}
// Resultado: el estado final de loading depende de cuál resuelve primero
Patrón 3: Clausura Obsoleta
const [count, setCount] = useState(0)
const handler = 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
// 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 fue eliminado/roto
Patrón 5: Ruta Muerta Condicional
handleMouseEnter() {
if (someState) { // someState es SIEMPRE false en este punto
doTheActualThing() // nunca se alcanza
}
}
Patrón 6: Interferencia de useEffect
// El botón establece stateX = true
// Un useEffect observa stateX y lo resetea a false
// El usuario no ve que pase nada
Paso 3: Reportar
Para cada error encontrado:
CLICK-PATH-NNN: [severidad: CRÍTICO/ALTO/MEDIO/BAJO]
Punto de Interacción: [Etiqueta del botón] en [archivo:línea]
Patrón: [Deshacer Secuencial / Carrera Asíncrona / Clausura Obsoleta / Transición Faltante / Ruta Muerta / Interferencia de useEffect]
Manejador: [nombre de la función o inline]
Traza:
1. [llamada] → establece {campo: valor}
2. [llamada] → RESETEA {campo: valor} ← CONFLICTO
Esperado: [lo que el usuario espera]
Real: [lo que realmente sucede]
Solución: [solución específica]
Control de Alcance
Esta auditoría es costosa. Delimí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 página nueva o cuando un usuario reporte un botón roto.
- Auditoría centrada en un store: Úsala después de modificar un store Zustand — audita todos los consumidores de las acciones modificadas.
División recomendada de agentes para aplicación completa:
Agente 1: Mapear TODOS los stores de estado (Paso 1) — este es contexto compartido para todos los demás agentes
Agente 2: Dashboard (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 Usarlo
- 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 store Zustand (verifica todos los llamantes)
- 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 Usarlo
- 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 funcionan) - Alimenta
/superpowers:test-driven-development— cada error encontrado aquí debería 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) // ✗ RESETEA composeMode = false
}}
Definición del store:
selectThread: (thread) => set({
selectedThread: thread,
selectedThreadId: thread?.id ?? null,
messages: [],
drafts: [],
selectedDraft: null,
summary: null,
composeMode: false, // ← este reseteo 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 falta cableado)
- Ninguna función falla (sin error en tiempo de ejecución)
- Los tipos de datos son correctos (sin desajuste de tipos)
La auditoría de click-path lo detecta porque:
- Paso 1 mapea que
selectThreadreseteacomposeMode - Paso 2 traza el manejador: llamada 1 establece true, llamada 2 resetea false
- Veredicto: Deshacer Secuencial — el estado final contradice la intención del botón