Bisección del estado: reducir hasta el mínimo reproducible
El método que rescata cuando el árbol de diagnóstico no converge, y la técnica que más tiempo ahorra en toda la carrera de un desarrollador. Esta lección defiende una tesis incómoda: cuando un bug de reactividad se resiste, eliminar código a ciegas es estrictamente superior a razonar sobre él, porque cada eliminación aporta un bit de información garantizado mientras que cada hipótesis puede aportar cero. Formaliza el protocolo de bisección con su invariante y su condición de parada, distingue las tres dimensiones independientes sobre las que se puede bisecar —la superficie del árbol de componentes, el volumen del estado y la historia de commits—, y establece la regla de oro que casi todo el mundo viola: reproducir antes de reducir, porque bisecar un fallo intermitente produce conclusiones falsas. Cierra convirtiendo el caso mínimo en un artefacto duradero: el test de regresión y el informe que la librería puede aceptar.
Llega un momento en toda depuración seria en que el árbol de diagnóstico deja de converger. Has recorrido los cuatro nodos, ninguno acusa con claridad, y el fallo solo se manifiesta con este cliente, en esta ruta, después de esta secuencia de clics. Es el punto donde la mayoría de los desarrolladores se instala en la peor estrategia disponible: leer código y pensar mucho. Esta lección defiende lo contrario con toda la firmeza que permite la evidencia. Cuando el espacio de causas es grande y tu comprensión del sistema es incompleta, eliminar código a ciegas es estrictamente superior a razonar sobre él, y la razón es informacional, no anecdótica. Cada eliminación que preserva o destruye el fallo aporta un bit garantizado de información; cada hipótesis brillante puede aportar cero, y a menudo aporta menos que cero porque te enamora de una explicación que resulta falsa. La bisección convierte la depuración de un ejercicio de intuición, donde el mejor razonador gana, en un procedimiento mecánico donde gana quien lo ejecuta con disciplina. Y su producto final no es solo la causa: es un artefacto reproducible que sobrevive al bug y lo impide para siempre.
- Justificar por qué la reducción sistemática vence al razonamiento cuando el espacio de causas es grande.
- Ejecutar el protocolo de bisección con su invariante explícito y su condición de parada.
- Bisecar en las tres dimensiones independientes: superficie de componentes, volumen de estado e historia de commits.
- Convertir el caso mínimo en un test de regresión y en un informe que otro pueda reproducir.
Bisecar la superficie
Quita ramas del árbol de componentes hasta que el fallo viva en el mínimo de vista posible. Cada corte descarta la mitad.
Bisecar el estado
Reduce el volumen de datos y el número de campos. Un bug que sobrevive con una fila es un bug lógico, no de escala.
Bisecar el tiempo
Cuando funcionaba antes, git bisect sobre la historia da la causa exacta sin leer una sola línea.
Por qué reducir vence a razonar
El argumento a favor de la bisección es cuantitativo y conviene tenerlo claro porque contradice el prestigio profesional del razonamiento. Supón que el fallo puede estar en cualquiera de dieciséis piezas. Si razonas, formulas una hipótesis, la compruebas, y en el mejor caso eliminas una pieza por intento: dieciséis intentos en el peor escenario, y solo si tu comprensión del sistema es buena. Si bisecas, cada prueba elimina ocho piezas, luego cuatro, luego dos: cuatro intentos, y el número no depende en absoluto de lo bien que entiendas el sistema. Esa independencia respecto a la comprensión previa es lo que hace la técnica tan poderosa precisamente en los casos que más duelen, que son aquellos en los que no entiendes lo que está pasando.
La cuenta anterior es además optimista con el razonamiento, porque supone que cada hipótesis es comprobable de forma barata y concluyente. En un sistema reactivo rara vez lo es: comprobar que un efecto concreto es el culpable exige a menudo modificarlo, y modificarlo altera el comportamiento que estabas midiendo. La bisección no sufre ese problema porque su unidad de prueba no es una hipótesis sino una ausencia, y una ausencia no puede interactuar con nada.
Hay además una asimetría psicológica que la bisección neutraliza. Una hipótesis equivocada no solo desperdicia el intento: dirige los siguientes, porque el sesgo de confirmación hace que interpretes las evidencias posteriores a su favor. Un corte, en cambio, no tiene opinión. El fallo persiste o no persiste, y esa respuesta binaria es incontestable. Cambiar hipótesis por cortes es cambiar un proceso creativo y falible por uno mecánico y fiable, y en depuración el mecánico gana casi siempre.
Hay una condición de aplicabilidad que conviene enunciar para no idealizar la técnica: la bisección exige que las piezas sean razonablemente independientes entre sí. Si eliminar un componente rompe la compilación de otros cinco, ese corte no es un experimento sino una demolición, y el resultado no informa de nada. Cuando eso ocurre con demasiada frecuencia, la dificultad para bisecar es en sí misma un diagnóstico arquitectónico: significa que las fronteras del código no existen o están en el sitio equivocado. Un sistema bien modularizado se biseca con facilidad, y uno que se resiste a la bisección te está diciendo, con evidencia, que el acoplamiento es el problema de fondo.
Esta es la regla que más gente viola y la que invalida más sesiones de bisección. El protocolo entero descansa sobre una premisa: que sabes reproducir el fallo de forma determinista, con una secuencia fija que lo provoca siempre. Si el fallo aparece siete de cada diez veces, cada corte tiene un treinta por ciento de probabilidad de parecer una corrección sin serlo, y la bisección te llevará con total confianza a una conclusión falsa que después defenderás con la autoridad de haberla medido. Antes de cortar nada, invierte el tiempo que haga falta en fijar la reproducción: congela la hora, siembra el generador aleatorio, sustituye la red por respuestas fijas, elimina la concurrencia real. Un fallo no reproducible no se biseca; se convierte primero en reproducible, y ese paso es la mitad del trabajo.
El protocolo: invariante, corte y condición de parada
Conviene también fijar de antemano el oráculo, es decir, la comprobación exacta que dictamina si el fallo sigue ahí. Debe ser objetiva, rápida y siempre la misma: una aserción concreta sobre un valor, no la impresión de que la pantalla se ve rara. Un oráculo ambiguo contamina cada iteración posterior, porque a mitad de la sesión ya no recuerdas con qué criterio juzgaste los cortes anteriores.
La bisección es un algoritmo y como tal tiene tres piezas obligatorias que hay que enunciar antes de empezar. El invariante: en todo momento debes tener un caso que reproduce el fallo. El corte: cada paso elimina aproximadamente la mitad de lo que queda, y si tras el corte el fallo desaparece, restauras y cortas por otro sitio. La condición de parada: te detienes cuando cualquier eliminación adicional hace desaparecer el fallo, porque eso significa que todo lo que queda es necesario para producirlo, y por definición eso es el caso mínimo.
// Bisecar la superficie sustituyendo ramas por marcadores inertes.
export function Panel() {
return (
<>
<Cabecera />
{/* <ListaCompleja /> */}
<Marcador texto="lista fuera" />
<PieDePagina />
</>
)
}
// Bisecar el estado: mismo componente, dato reducido al minimo.
const datosMinimos = [{ id: 1, nombre: 'a', activo: false }]
El detalle que más rendimiento da en la práctica es sustituir cada rama eliminada por un marcador inerte en vez de borrarla sin más. La razón es que borrar cambia dos cosas a la vez —desaparece el componente y desaparece su hueco en el diseño—, y si el fallo depende del segundo efecto habrás obtenido una respuesta falsa. Un marcador que ocupa el mismo lugar sin ejecutar lógica aísla la variable que te interesa y deja el resto igual, que es lo único que un experimento controlado exige.
Merece la pena insistir en la condición de parada porque es donde más gente abandona antes de tiempo. La tentación es detenerse en cuanto la causa parece evidente, y esa parada anticipada es exactamente lo que produce las correcciones que no corrigen: arreglas la pieza que quedaba a la vista, el fallo desaparece del caso reducido, y reaparece en producción porque la causa real era otra que solo se manifestaba junto a algo que ya habías eliminado. La parada correcta es demostrable, no intuitiva: se alcanza cuando cada una de las piezas restantes, retirada por separado, hace desaparecer el fallo. Solo entonces tienes la prueba de que el conjunto es mínimo.
El fallo de ejecución más frecuente no es cortar mal, es perder la cuenta. Tras quince o veinte iteraciones nadie recuerda qué combinaciones ya se probaron, cuál fue el último estado que reproducía el fallo ni qué se restauró después de un corte fallido, y la sesión degenera en un revuelto donde ya no se sabe qué se está midiendo. La disciplina que lo evita cuesta muy poco: mantén una bitácora de dos columnas, qué eliminaste y si el fallo persistió, y trabaja siempre sobre una rama desechable con confirmaciones pequeñas para poder volver con exactitud a cualquier punto anterior. Esa bitácora tiene además un segundo valor que aparece al terminar: es la lista probada de lo que resultó irrelevante, y esa lista es conocimiento del sistema que puedes reutilizar la próxima vez que algo falle en la misma zona.
Las tres dimensiones y cuál elegir primero
Antes de recorrer los tres ejes conviene subrayar que son verdaderamente independientes: puedes reducir el estado sin tocar un solo componente, y puedes recorrer la historia sin entender nada del código actual. Esa independencia es lo que permite combinarlos en cascada, empezando por el más barato y usando su resultado para acotar los siguientes, en lugar de atacar los tres a la vez y perder la trazabilidad de lo que cada cambio demostró.
Se puede bisecar sobre tres ejes independientes y elegir el correcto ahorra la mitad del trabajo. La superficie es el árbol de componentes y los efectos que cuelgan de él; se biseca comentando ramas. El volumen es el estado: cuántos registros, cuántos campos, qué valores; se biseca reduciendo los datos hasta una sola entidad con los campos imprescindibles. El tiempo es la historia del repositorio; se biseca con git bisect y es, con mucha diferencia, el eje más rentable cuando existe, porque no requiere entender nada del código.
El criterio de elección es simple y casi nadie lo aplica. Si el fallo funcionaba antes, empieza siempre por el tiempo: la historia te da el cambio exacto en unos pocos pasos automáticos y te ahorra las otras dos dimensiones por completo. Si el fallo nunca funcionó, empieza por el volumen, porque reducir datos es reversible, rápido y no toca el código. Deja la superficie para el final, que es la dimensión más laboriosa y la que más riesgo tiene de introducir fallos nuevos mientras la manipulas.
# Bisecar el tiempo con un guion que responda por ti: cero intervencion manual.
git bisect start
git bisect bad HEAD
git bisect good v2.4.0
git bisect run npm test -- --testNamePattern="total del carrito"
La bisección del volumen tiene además un poder diagnóstico que las otras dos no tienen, y consiste en lo que revela el punto en que el fallo desaparece. Si el bug sobrevive con una sola fila y dos campos, es un fallo lógico y estará en el grafo de dependencias; si necesita cien filas para manifestarse, es un fallo de escala y estará en la granularidad o en una virtualización mal ajustada; si solo aparece con un valor concreto —una cadena vacía, un cero, un nulo—, es un fallo de tratamiento de casos límite y no de reactividad en absoluto. Reducir datos no solo acorta el caso: clasifica el bug antes de que sepas dónde está.
La forma automatizada del comando merece conocerse porque cambia el coste de la técnica por completo. Si logras expresar el fallo como un guion que devuelve cero cuando el código está sano y distinto de cero cuando falla —y el test de regresión que produce esta lección sirve exactamente para eso—, el repositorio se biseca solo mientras tú haces otra cosa, y devuelve el commit culpable con su autor y su mensaje. Diez minutos de escribir el guion sustituyen a una tarde de arqueología, y el resultado es una respuesta exacta en lugar de una sospecha razonable.
flowchart TD B[el fallo se resiste al arbol de diagnostico] --> R[se reproduce siempre] R -->|no| FIJ[congela hora azar red y concurrencia primero] R -->|si| T[funcionaba en alguna version anterior] T -->|si| GIT[bisecar el tiempo con git bisect] T -->|no| VOL[bisecar el volumen de estado] VOL --> SUP[bisecar la superficie de componentes] GIT --> MIN[caso minimo reproducible] SUP --> MIN MIN --> TEST[test de regresion permanente] style B fill:#f38ba8,color:#11111b style FIJ fill:#f9e2af,color:#11111b style MIN fill:#a6e3a1,color:#11111b style TEST fill:#a6e3a1,color:#11111b
El caso mínimo es el producto, no el desecho
Antes de llegar al producto conviene señalar una parada intermedia que muchos se saltan: extraer el caso reducido fuera del proyecto. Mientras el mínimo siga viviendo dentro de tu repositorio, arrastra configuración, alias de importación, proveedores globales y una versión concreta de cada dependencia, y ninguno de esos factores ha sido probado como irrelevante. Trasladarlo a un archivo aislado o a un espacio de pruebas en blanco es la última reducción y la más informativa de todas, porque si el fallo no sobrevive al traslado acabas de descubrir que la causa estaba en el entorno y no en el código, que es un diagnóstico completamente distinto y muchísimo más rápido de corregir.
Cuando la bisección termina, la mayoría de la gente arregla el bug y tira el caso reducido a la papelera, y ahí se pierde casi todo el valor de la sesión. El caso mínimo no es un residuo del proceso: es su producto más duradero. Tienes en la mano la secuencia más corta conocida que produce el fallo, y eso es exactamente la definición de un buen test de regresión: la lógica de estado aislada de la vista, con los datos mínimos y la aserción precisa que falla antes de la corrección y pasa después.
// El caso minimo se convierte en el test que impide la reaparicion.
test('anadir una linea notifica a los suscriptores del total', () => {
const store = crearStore({ lineas: [] })
const vistos: number[] = []
store.subscribe((s) => vistos.push(s.lineas.length))
store.anadir({ id: 1, precio: 10 })
expect(vistos).toEqual([1]) // fallaba con mutacion en sitio: quedaba vacio
})
El caso mínimo es además la única forma de informe de error que una librería puede aceptar. Un mantenedor no puede reproducir tu aplicación, tu ruta ni tu cliente concreto; puede reproducir treinta líneas. Y hay un efecto secundario que aparece con una frecuencia sorprendente: mientras reduces, el bug se explica solo. Al llegar a la penúltima eliminación, la causa suele volverse evidente antes de que ninguna herramienta la señale, porque el ruido que la ocultaba ya no está.
Ese efecto tiene una consecuencia estratégica que conviene aprovechar deliberadamente: la reducción es útil incluso cuando ya crees saber la causa. Reducir confirma la hipótesis en lugar de suponerla, y en la mitad de los casos la refuta y te ahorra una corrección equivocada que habrías defendido durante días. Por eso la bisección no es únicamente el recurso al que se acude cuando el árbol de diagnóstico falla; es también el procedimiento con el que se verifica una conclusión que el árbol ya te dio, y en un sistema donde las dependencias son implícitas esa verificación vale su coste casi siempre.
Hay una resistencia cultural a bisecar que merece nombrarse, porque explica por qué una técnica tan barata se usa tan poco. Bisecar se siente como rendirse. Comentar código a ciegas parece lo que hace quien no comprende el sistema, mientras que razonar sobre la causa parece lo que hace el ingeniero competente, y la profesión premia la apariencia de comprensión por encima del resultado. Pero la comparación honesta va justo al revés. El razonamiento escala con tu conocimiento del sistema y con la exactitud de tu modelo mental, y en un sistema reactivo grande —con dependencias implícitas, efectos que se disparan en cascada y una librería cuyo interior no escribiste— ese modelo mental es casi siempre incompleto en el punto exacto donde vive el bug. Precisamente ahí razonar rinde menos, porque razonas sobre un sistema imaginario que se parece al real salvo en el detalle que importa. La bisección, en cambio, no consulta tu modelo mental: interroga al sistema real y le arranca un bit por pregunta, y ese bit es verdadero aunque tu comprensión sea nula. Por eso rinde más justo donde el razonamiento fracasa. Y hay algo más profundo que la mera eficiencia. Reducir un caso hasta el mínimo no es solo encontrar la causa: es descubrir cuál era la esencia del fenómeno y cuánto de lo que creías relevante era decorado. Cada eliminación que preserva el fallo es una demostración formal de que esa pieza era irrelevante, y al final tienes una lista probada de lo que sí importa. Eso es conocimiento del sistema obtenido experimentalmente, no supuesto. La ironía final es que quien biseca con disciplina acaba entendiendo la arquitectura mejor que quien la razonaba, porque su comprensión está construida sobre resultados y no sobre memoria.
- Toma un fallo de estado abierto y, antes de tocar nada, escribe la secuencia exacta que lo reproduce siempre; si no lo consigues, fíjalo congelando hora, azar y red.
- Decide la dimensión de bisección con el criterio de la lección y justifica por escrito por qué descartaste las otras dos.
- Si el fallo funcionaba antes, ejecuta
git bisecthasta el commit culpable y compara ese tiempo con lo que estimabas tardar leyendo código. - Corta por la mitad al menos cuatro veces sustituyendo cada rama eliminada por un marcador inerte, no borrándola.
- Alcanza la condición de parada real: comprueba que cualquier eliminación adicional hace desaparecer el fallo, y anota la lista probada de piezas necesarias.
- Convierte el caso mínimo en un test de regresión que falle sin la corrección, y consérvalo aunque el bug ya esté arreglado.