Cuando audité el CI de mi propio SaaS, probé los gates antes que el código. Uno de ellos, un umbral de cobertura de front end del 95% de líneas, 95% de funciones, 90% de ramas y 95% de sentencias, nunca se había evaluado: el job de CI corría los tests sin el flag --coverage. Corregí el job y fijé el piso en 91/93/88/91, un umbral que el pipeline ahora sí aplica, así que falla en cuanto la cobertura baja de ese nivel. El comentario en el pipeline ahora deja constancia del motivo: Previously ran without --coverage = decorative.
Eso es deuda de verificación en una sola línea de YAML. Escribir código se volvió barato. Saber que el código está bien, no. Construyo MailHive con un flujo de trabajo asistido por IA, así que el volumen de código que hay que revisar es alto, y un control que solo parece un control es peor que no tener ninguno, porque dejas de preocuparte.
Qué Es Realmente la Deuda de Verificación
Sonar llama brecha de verificación (verification gap) a la distancia entre desconfiar del código generado por IA y realmente revisarlo, y dice que esa brecha crea lo que el CTO de Amazon, Werner Vogels, ha llamado deuda de verificación (verification debt). La deuda técnica es un atajo que sabes que tomaste. La deuda de verificación es código por el que nadie puede responder. Puede estar bien estructurado, pasar una revisión rápida y aun así estar mal.
La encuesta de Sonar a más de 1.100 desarrolladores, publicada en enero de 2026, le puso números a la brecha: el 96% no confía del todo en que el código generado por IA sea funcionalmente correcto, y solo el 48% dice que siempre lo revisa antes de hacer commit. El 38% dice que revisarlo requiere más esfuerzo que revisar el código de un colega. Sonar vende herramientas de calidad de código, así que toma esas cifras como orientativas.
Tu Sensación de Velocidad No Es Evidencia
El ensayo aleatorizado de METR de 2025 es la mejor advertencia que conozco. 16 desarrolladores open source con experiencia trabajaron en 246 issues reales en repositorios que conocían. Con la IA permitida tardaron un 19% más, y después seguían creyendo que la IA los había hecho un 20% más rápidos. METR ahora considera ese resultado desactualizado, y su seguimiento de febrero de 2026 dice que los datos más nuevos son una señal poco fiable, sobre todo porque los desarrolladores elegían cada vez más no participar si eso implicaba trabajar sin IA. Sus estimaciones más recientes se inclinan hacia una aceleración, con mucha incertidumbre. Así que no cites el 19% como el estado actual de las herramientas de IA. Lo que perdura es la brecha entre percepción y medición. Sentirse rápido no prueba nada.
Dos Hallazgos al Auditar Mis Propios Guardrails
Ninguno era exótico, y los dos se veían bien en un día normal. Los encontré auditando los propios guardrails, no el código que protegen.
- Umbrales de cobertura que nunca se evaluaban. Los números estaban en vitest.config.ts y CI no corría la cobertura. Lo corregí haciendo que el script de CI corriera con la cobertura activada.
- Un linter que solo corría en local. ESLint corría en un hook de pre-push, que --no-verify se salta, así que ningún pipeline evaluaba la regla de límites entre módulos ni no-explicit-any. Lo pasé a un job de CI que hace lint de las dos apps.
Los dos parecían cubiertos. Lo que faltaba era la prueba de que podían fallar.
La Cobertura Te Dice Qué Se Ejecutó, No Qué Se Verificó
Incluso un gate de cobertura que funciona responde la pregunta equivocada. Te dice que una línea se ejecutó. No te dice que un test notaría si esa línea estuviera mal. Aquí va un ejemplo pequeño e ilustrativo, que no viene de ningún codebase real, y llega al 100% de cobertura:
export function isOverLimit(spent: number, limit: number): boolean {
return spent > limit;
}
it('blocks over the limit', () => expect(isOverLimit(150, 100)).toBe(true));
it('allows under the limit', () => expect(isOverLimit(50, 100)).toBe(false));
// Change > to >= and both tests still pass.
// Nothing checks spent === limit, which is the case the business cares about.El mutation testing encuentra exactamente esto. Una herramienta como Stryker, que es open source, cambia tu código de formas pequeñas y vuelve a correr los tests. Si los tests siguen pasando, ese mutante sobrevivió y encontraste un control que no existe. En el servicio que maneja el estado de los emails de MailHive, la primera corrida produjo una lista precisa de mutantes sobrevivientes, cada uno un cambio concreto que los tests no notarían. Un reporte de cobertura, incluso con un piso alto, nunca te va a dar esa lista, y cada entrada señala el próximo test que vale la pena escribir.
No empieces convirtiéndolo en un gate de merge. Las corridas de mutación son lentas y algunos mutantes sobrevivientes son inofensivos. El mío es un job manual, que también puede correr de forma programada, limitado a un servicio, no puede bloquear un merge, y el score es una señal para leer. Amplía el alcance a medida que endureces cada parte.
Pon a Prueba las Propias Protecciones
Una regla puede estar en la configuración y estar armada de una forma que nunca reporte una violación. Una regla que nunca falla se ve idéntica a un codebase que nunca la rompe. Así que el job de lint de MailHive corre un guardrail canary: un script que planta una violación deliberada por cada regla clave y falla si ESLint no la reporta. Un canary cubre las reglas que planta, así que cualquier protección fuera de ESLint, como un grep que busca tests enfocados, necesita su propia violación plantada.
Decide Qué Puede Hacer Cada Control
El error sobre el que más te advertiría es tratar a un revisor de IA como un gate. MailHive tiene un job de revisión con IA en los merge requests que publica una nota con sus hallazgos, y nunca bloquea un merge. Es deliberado. Los controles deterministas pueden bloquear, porque cuando fallan puedes ver por qué y reproducirlo. La opinión de un modelo es un lector extra, útil y a veces agudo, y no tiene derecho a veto ni la última palabra.
Esa división es la misma sobre la que escribí para entornos regulados: las instrucciones son una solicitud, y el pipeline es lo que hace cumplir. Si también usas Copilot o Claude Code con instrucciones de proyecto, mantén la misma línea. Las instrucciones reducen el código malo que llega. No verifican nada.
Qué Hacer Esta Semana
Todo lo que sigue es gratis y funciona en un runner de CI normal. Hazlo en este orden, porque el primer paso es el que deja al descubierto los demás.
- Demuestra que cada gate puede fallar. Abre una rama descartable, rompe una cosa por gate (un test que falla, una caída de cobertura, un error de lint, un secreto) y mira cómo el pipeline se pone en rojo. Cualquier gate que siga en verde es decoración. Después automatiza con un canary los que puedas.
- Asegúrate de que cada control que importa también corra en CI. Los hooks locales son una cortesía contigo mismo. Se pueden saltar por diseño.
- Haz fallar los tests enfocados. En Vitest, verifica que allowOnly no esté activado en CI. En Jest, agrega un grep, y planta una violación para demostrar que el grep corre en el directorio correcto.
- Activa la regla que has estado evitando, usando una línea base. Las bulk suppressions de ESLint dejan que las violaciones existentes de nivel error se queden, contadas por archivo y regla, para que las nuevas fallen. Dejas de hacer crecer la deuda hoy y la pagas después.
- Corre mutation testing una vez en tu módulo más riesgoso. Elige el código de dinero, permisos o estado, lee los sobrevivientes y escribe los tests que señalan.
- Mantén una pequeña suite de smoke tests end-to-end con Playwright para los flujos que nunca deben romperse. Mantenla lo bastante pequeña para que la gente no empiece a ignorarla.
- Dimensiona la revisión humana según el riesgo, no según el diff. Un archivo de tests generado y un cambio en una verificación de autorización no merecen la misma atención, aunque tengan la misma cantidad de líneas.
Dónde Se Detienen los Gates
Incluso cuando todos los gates funcionan, cada uno tiene un límite. El mutation testing es lento, así que lo acoto primero a las partes más riesgosas y lo amplío a medida que se consolidan. Una suite end-to-end que necesita secretos protegidos a menudo no puede correr en cada merge request, así que decide de forma explícita qué flujos tienen que pasar antes de un release. Y ningún gate puede decirte que una regla de negocio es la regla de negocio equivocada. Eso todavía requiere a una persona que conozca el producto.
Un gate que nunca viste fallar es una suposición.
Referencias
- Sonar. Verification gap press release, January 2026. https://www.sonarsource.com/company/press-releases/sonar-data-reveals-critical-verification-gap-in-ai-coding/
- METR. Early-2025 AI and experienced open source developers study. https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/
- METR. February 2026 update on the experiment design. https://metr.org/blog/2026-02-24-uplift-update/
- StrykerJS mutation testing. https://stryker-mutator.io/docs/stryker-js/introduction/
- ESLint bulk suppressions. https://eslint.org/docs/latest/use/suppressions
- Vitest allowOnly. https://vitest.dev/config/allowonly
- Vitest coverage configuration. https://vitest.dev/config/coverage