Complejidad Ciclomática: Puerta de Canary, no Regla de Lint
Resumen
La complejidad ciclomática cuenta caminos independientes a través de una función. NIST recomienda un máximo de 10 por función, pero esta métrica estática oculta dónde fallará realmente tu canary en producción. La solución: no gates sobre la puntuación absoluta; en su lugar, gatea el porcentaje de canary en el delta de complejidad del diff, referenciado contra el delta de pruebas.
La complejidad ciclomática cuenta el número de caminos independientes a través de una función. NIST fija el límite seguro en 10 por función, y la mayoría de equipos aplica algo similar en CI. Lo que ese número no te dice es cuál de esos caminos será realmente ejercitado por el canary que estás a punto de enviar al 5% del tráfico en producción, y esa brecha es donde comienzan la mayoría de los incidentes de despliegue. Trata la complejidad ciclomática como una entrada de riesgo de despliegue, no como una regla de linting, y el número comienza a justificar su presencia.
Qué Mide Realmente la Complejidad Ciclomática
La métrica de McCabe es un conteo de grafos: nodos, aristas, componentes conectados. Cada if, for, while, case y operador booleano suma un camino. Una función con complejidad 25 tiene al menos 25 caminos linealmente independientes a través de ella. Eso no es opinión, es matemática de pruebas de caminos básicos, y establece un piso en cuántos casos de prueba necesitarías para cubrir la función de principio a fin. El desglose de Sourcegraph tiene todos los umbrales: 1-10 riesgo bajo, 11-20 moderado, 21-50 alto, 50+ es el nivel de "deberíamos hablar".
La mayoría de equipos no llega a ese piso. Las herramientas de cobertura reportan cobertura de línea, no cobertura de caminos, así que una función puede mostrar 90% de cobertura mientras la mitad de sus ramas nunca se ejecutan en CI. Esa es la parte que nadie pone en la plantilla de PR, y es la razón por la cual "las pruebas pasan" y "esto es seguro para enviar a producción" son dos afirmaciones diferentes que se tratan como una sola.
Hemos revisado suficientes análisis post-mortem para notar el patrón: el review de incidente siempre tiene una línea para el consumo del presupuesto de error y otra para el tiempo de detección. Casi nunca tiene una línea para cómo se veía la complejidad de la función modificada antes de que el diff aterrizara. Esa es una brecha en la documentación, no solo en las herramientas.

Por Qué un Número Plano de Complejidad Oculta el Radio Real de Explosión
Aquí está la parte que todo dashboard de complejidad falla: dos funciones pueden tener la misma puntuación de complejidad ciclomática e implicar riesgos completamente diferentes. Un switch de 20 ramas despachando a handlers bien probados no es el mismo animal que una función con cuatro bolsas separadas de condicionales anidados dispersos en 80 líneas, cada una tres niveles de profundidad. CodeScene llama a esta segunda forma un "camino irregular", y el nombre es preciso. Es la profundidad de anidamiento, no el recuento bruto de ramas, lo que agota la memoria de trabajo y oculta el caso límite que nadie pensó en probar. El artículo de CodeScene camina a través de la comparación en detalle, y se mapea limpiamente a lo que vemos en datos de despliegue.
Esto importa específicamente para nosotros porque un canary no falla en complejidad promedio. Falla en la única función irregular que fue modificada en este diff, a las 2 de la mañana, bajo carga que nadie ha testeado. En los post-mortems que hemos revisado con equipos platform, el patrón se repite: la función raíz del problema casi nunca tiene la puntuación de complejidad más alta en el repositorio. Tiene el delta de complejidad más alto en el diff que fue enviado. Esa es una señal diferente, y casi nadie la instrumenta.
Un dashboard que te dice que la complejidad del repositorio está tendiendo hacia abajo no es lo mismo que un dashboard que te dice que este despliegue, ahora mismo, modificó una función que acaba de ganar tres nuevas ramas anidadas. Uno es un reporte de salud que lees una vez por trimestre. El otro es una puerta que realmente querrías en tu pipeline de despliegue, sentado junto a la revisión del SLO, no enterrado en una herramienta de análisis estático que nadie abre durante un incidente.
Los despliegues basados en anillos y porcentajes ya asumen que algunos diffs son más riesgosos que otros; esa es la premisa completa de un canary. Lo que la mayoría de pipelines no hace es dejar que la forma de complejidad del diff mismo informe qué tan rápido ese canary debería expandirse. Se trata como una preocupación de revisión de código, luego se olvida en el momento en que el PR se fusiona.
La Puerta de CI que Todos Fijan en 10, y Por Qué No Cambia Nada
El consejo estándar, limita la complejidad ciclomática a 10 y falla el build por encima de eso, es el paso "saltar" que casi todos los blogs de ingeniería recomiendan e casi nadie valida contra datos reales de incidentes. No está mal, exactamente. Es incompleto de una manera que permite a los equipos creer que han manejado el riesgo cuando principalmente solo lo han movido.
Dos modos de fallo, ambos comunes en equipos con los que hemos hablado:
Gaming del número. Extract Method es la solución de libro de texto, y funciona: la complejidad ciclomática por función baja. Pero si la extracción no reduce el conteo real de decisiones, solo lo traslada a tres funciones en lugar de una, la complejidad del sistema no cambia. La puerta se pone en verde. El radio de explosión no se encoge, solo se hace más difícil de ver en una vista de diff única.
Ignorar la forma. Un umbral plano trata a un dispatcher de 12 ramas y un camino irregular de 12 como igualmente riesgosos. No lo son. El dispatcher probablemente está bien; es enrutamiento mecánico. El camino irregular es donde vive tu próximo rollback, porque la persona revisándolo dejó de rastrear estado tres niveles de anidamiento dentro.
Los agentes de codificación IA empeoran esto antes de mejorarlo. Devin y agentes similares autónomos envían PRs rápidamente, y "rápido" a menudo significa agregar una rama en lugar de refactorizar la que ya existe. Ese es el camino de menor resistencia para un modelo que optimiza para un conjunto de pruebas que pasa, no para la carga cognitiva de un revisor. Si tu equipo está fusionando diffs autorizados por IA en volumen, delta de complejidad por PR es una métrica que querrías en el dashboard antes de que se convierta en un hallazgo de revisión de incidente, no después.

Qué El Delta de Complejidad Realmente te Dice Sobre la Carga de Prueba
Olvida la puntuación absoluta por un segundo. El número que predice riesgo de incidente es el cambio en complejidad introducido por un único diff, referencia cruzada contra si las pruebas tocando ese diff realmente crecieron para igualarla.
Una función que va de complejidad 8 a complejidad 19 en un PR ha, por la matemática de pruebas de caminos básicos anterior, aproximadamente duplicado su conteo mínimo requerido de pruebas. Si el PR agregó dos pruebas, has enviado una brecha de cobertura disfrazada de una ejecución de CI que pasa. Esa brecha no aparece hasta que el canary golpea el trozo de tráfico del 5% que ejerce la rama no probada, y para entonces es un incidente, no un comentario de revisión de código sin resolver en un hilo de PR.
Esta es la brecha de instrumentación que el Piloto de IA de upstreamapi está construido para cerrar en el lado de despliegue: puerta el porcentaje de canary y tiempo de espera en el delta de complejidad del diff enviado, referencia cruzada contra su delta de pruebas, no solo en el SLO de tasa de error descendente. Un SLO de tasa de error te dice que algo ya se rompió. Una puerta de delta de complejidad te dice que el diff fue más probable que rompa algo antes de servir tráfico en vivo, que es el único punto donde esa información es todavía accionable.
// Puerta de canary simplificada: expande lentamente en diffs de bajo riesgo, aguanta en alto riesgo
function canaryStep(diff: DiffMetrics): CanaryDecision {
const complexityRisk = diff.complexityDelta / Math.max(diff.testDelta, 1);
if (complexityRisk > 3 && diff.slo.errorBudgetBurn > 0.1) {
return { action: "hold", trafficPct: diff.currentTrafficPct };
}
if (complexityRisk > 3) {
return { action: "extend_bake_time", bakeMinutes: 45 };
}
return { action: "advance", trafficPct: diff.currentTrafficPct + 10 };
}¿La Investigación Realmente Está de Acuerdo con Esto?
No, y eso vale la pena decirlo claramente. Parte de la investigación de profesionales repudia fuertemente la complejidad ciclomática como predictor de defectos en absoluto, argumentando que los equipos que optimizan para una puntuación más baja a menudo solo reubican la complejidad en algún lugar menos visible. La crítica de GetDX hace este caso y apunta a los equipos hacia métricas de experiencia de desarrollador en su lugar. No pensamos que ese argumento mate la métrica; pensamos que mata la métrica usada sola, como una puntuación estática de todo el repositorio, desconectada del diff y del despliegue al que está vinculado.
Cómo Realmente Ponerías una Puerta de Canary en Complejidad, No Solo Tasa de Error
Tres cosas, en orden de cuánta fricción agregan a un PR:
Calcula delta de complejidad por diff, no por repositorio. Los promedios de todo el repositorio ocultan la única función que importa esta semana. Delta por PR es económico de calcular en la mayoría de herramientas de análisis estático, y es el número que se correlaciona con lo que realmente se rompe en las próximas 48 horas.
Referencia cruzada contra delta de pruebas, no conteo de pruebas. Una función con 40 pruebas y un salto de complejidad de 8 a 19 con cero nuevas pruebas es un riesgo mayor que una función nueva con complejidad 15 y cobertura igualada desde el día uno.
Alimenta la razón en ritmo de despliegue, no solo en aprobación de fusión. Un diff de alto delta de complejidad no necesita ser bloqueado en revisión; bastante lógica de dominio legítimamente compleja (máquinas de estado, parsers de protocolo) siempre tendrá puntuación alta. Necesita un canary más lento y una correa más corta de presupuesto de error, que es una decisión de despliegue, no una decisión de revisión de código.
Escribe la entrada del runbook antes del despliegue, no después de la página. Si el ingeniero on-call abriendo el canal de incidente a las 3 de la mañana tiene que hacer ingeniería inversa de por qué un PR "limpio" acaba de quemar el presupuesto de error, la documentación falló antes de que el código lo hiciera. Una nota de una línea en el log de despliegue ("delta de complejidad 11, delta de pruebas 1, aguantó al 20%") cuesta nada escribir y ahorra los primeros quince minutos de cada revisión de incidente que sigue.

¿Vale la Pena Hacerle Seguimiento, o Es Solo Otro Número en el Dashboard?
Depende completamente de dónde lo adjuntes. Complejidad ciclomática como una línea de tendencia de salud del repositorio es principalmente decoración: bonito para una diapositiva trimestral, inútil a las 2 de la mañana. Complejidad ciclomática como delta por diff, alimentado en ritmo de canary y referencia cruzada contra crecimiento de pruebas, es uno de los indicadores principales más económicos que hemos encontrado para "este despliegue va a paginar a alguien".
El post-mortem preguntará qué se veía el presupuesto de SLO antes del rollback. Cada vez más, el nuestro también pregunta qué se veía el delta de complejidad en el diff que fue enviado. Vale la pena agregar esa pregunta a tu propio runbook antes de que el incidente force la conversación, no durante el retro cuando la respuesta es un encogimiento de hombros y una promesa de "agregar mejores pruebas la próxima vez".