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.

Ingeniero de plataforma trabajando tarde en la noche en un dashboard de despliegue mostrando un gráfico de porcentaje de implementación

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.

Close-up of hands reviewing a code diff with nested indentation on a laptop screen

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:

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.

Two engineers reviewing a branching flowchart drawn on a glass whiteboard

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:

  1. 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.

  2. 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.

  3. 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.

Server room corridor at night with status LEDs and a lone engineer walking away holding a tablet

¿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".

Preguntas frecuentes

¿Qué mide exactamente la complejidad ciclomática?
La complejidad ciclomática cuenta el número de caminos linealmente independientes a través de una función. Se calcula basándose en el número de decisiones (if, for, while, case, operadores booleanos) en el código. Una función con complejidad 25 necesita al menos 25 casos de prueba para cubrir todos sus caminos posibles.
¿Por qué NIST recomienda un máximo de 10 de complejidad ciclomática?
NIST establece un máximo de 10 porque es el punto en el que la complejidad comienza a afectar significativamente la capacidad humana de entender y probar código. Las puntuaciones de 11-20 son moderadas, 21-50 son altas, y 50+ son extremadamente altas y muy riesgosas.
¿Cuál es la diferencia entre usar la complejidad ciclomática absoluta versus el delta de complejidad?
La complejidad absoluta es la puntuación total de una función en un punto dado. El delta de complejidad es el *cambio* introducido por un nuevo diff. El delta es más valioso para decidir el riesgo de un despliegue específico, porque predice qué función recién modificada podría fallar en producción.
¿Cómo afecta el delta de complejidad al riesgo de un canary?
Un delta de complejidad alto sin crecimiento paralelo en las pruebas indica una carga de prueba insuficiente. Si una función salta de complejidad 8 a 19 pero solo se agregan 2 pruebas nuevas, has creado una brecha de cobertura que no aparecerá hasta que el canary golpee una rama no probada en producción.
¿Qué es un 'camino irregular' (bumpy road) en el contexto de complejidad?
Un camino irregular es una función con múltiples bolsas de condicionales anidados dispersos a través de ella, cada uno varios niveles profundo. Aunque puede tener una puntuación de complejidad idéntica a un switch simple bien estructurado, es mucho más riesgoso porque la profundidad de anidamiento hace que sea más fácil pasar por alto casos límite.
¿Cómo pueden los agentes de IA de codificación agravar los problemas de complejidad?
Los agentes como Devin tienden a agregar ramas en lugar de refactorizar código existente. Esto es porque agregar una rama es el camino de menor resistencia para pasar tests rápidamente. En un equipo que fusiona muchos diffs autorizados por IA, el delta de complejidad por PR aumenta más rápido, aumentando el riesgo de despliegue.
¿Debería bloquear PRs con alto delta de complejidad en la revisión de código?
No necesariamente. Código legítimamente complejo (máquinas de estado, parsers de protocolo) siempre tendrá alta complejidad. En lugar de bloquear en revisión, usa el delta de complejidad para controlar el ritmo de tu canary: mantén tráfico bajo y extiende el tiempo de cocción en diffs de alto delta hasta que tengas confianza en la cobertura de pruebas.
¿Hay investigación que desafíe la utilidad de la complejidad ciclomática?
Sí. Algunos estudios argumentan que optimizar solo para un puntaje bajo de complejidad ciclomática puede simplemente trasladar la complejidad a otro lugar sin reducir el riesgo real. Sin embargo, cuando usas el delta de complejidad como una entrada de despliegue (no como una puntuación absoluta de repositorio), sigue siendo una señal útil para el riesgo.