Resumen. Hacemos muchas auditorías de apps construidas con Cursor, Claude Code, Bolt, Lovable y sesiones largas de ChatGPT. Las bases de código son tremendamente distintas, pero los hallazgos casi nunca lo son. Seis problemas aparecen una y otra vez: sin cobertura de tests (lo que convierte cada despliegue en una apuesta), variables y funciones duplicadas (la misma lógica copiada y pegada con ligeras derivas), sin arquitectura coherente (dos pantallas del mismo proyecto escritas como dos apps distintas), evitar el estado basado en streams (así la lógica asíncrona colapsa en un infierno de callbacks y los formularios complejos nunca acaban de funcionar), ninguna conciencia del entorno de despliegue (estado en memoria y archivos locales que se rompen en cuanto la app corre como más de una instancia) y seguridad completamente abierta (archivos .env subidos al repositorio, tokens incrustados en el código). Ninguno es exótico. Todos son predecibles, y todos se pueden arreglar sin reescribir. Este es el complemento de ingeniería a nuestra guía para fundadores sobre publicar un prototipo de IA; si quieres que nos encarguemos, eso es lo que hace una auditoría de código IA.


Por qué se repiten los hallazgos

Las herramientas de programación con IA optimizan una sola cosa: el camino más corto hasta código que se ejecute. Eso es genuinamente útil: llevas una idea a un estado funcional en horas. Pero «se ejecuta en la demo» y «es mantenible en producción» son objetivos distintos, y la brecha entre ambos es notablemente consistente entre proyectos.

La razón es estructural. Un modelo generando código tiene una ventana de contexto estrecha y ninguna memoria de las decisiones que tomó tres archivos atrás. No puede probar lo que acaba de escribir, no tiene ningún sentido de tu financiación ni de tu postura de seguridad y no tiene incentivo para mantener coherente la base de código con el tiempo. Así que toma la decisión localmente óptima cada vez, y la suma de decisiones localmente óptimas es una base de código que funciona hoy y se resiste a todo cambio mañana.

Tras suficientes auditorías, los fallos se agrupan en los mismos seis cubos. Aquí están, en el orden en que suelen doler.


1. Sin tests: cada despliegue es una apuesta

El hallazgo más común con diferencia: no hay ningún test. Ni una suite fina, ni tests inestables: cero. La app se validó haciendo clics por ella, y esa es toda la red de seguridad.

Esto es invisible hasta justo el momento en que es catastrófico. Sin tests no hay forma de saber si un cambio ha roto algo salvo publicándolo y esperando a que un usuario se queje. Cada despliegue se convierte en una pasada de regresión manual que nadie llega a hacer, así que refactorizar da pánico, las actualizaciones de dependencias se saltan y la base de código se calcifica, no porque el código sea bueno, sino porque nadie se atreve a tocarlo.

Por qué lo hace la IA. Generar una funcionalidad y generar tests para ella son dos peticiones separadas, y nadie hizo la segunda. El modelo escribirá tests encantado si se los pides, pero por su cuenta entrega el camino feliz y para.

Cómo lo arreglamos. No buscamos un 100 % de cobertura el primer día. Añadimos una capa fina donde más rentabiliza: un test de humo de que la app arranca, tests alrededor de la lógica de dinero y de autenticación, y un test de regresión por cada bug que arreglamos durante la auditoría. Solo eso convierte los despliegues de apuesta en rutina. A partir de ahí, la cobertura crece con la base de código en vez de ser un muro que escalar.


2. Variables y funciones duplicadas

Abre una base de código construida con IA y busca el mismo helper de formato de fechas. A menudo lo encontrarás tres o cuatro veces, ligeramente distinto cada vez, porque cada uno se generó de forma aislada para la pantalla que lo necesitaba. Lo mismo pasa con las reglas de validación, los clientes de API, la aritmética de moneda y las constantes de configuración.

La duplicación no es solo fea: es una bomba de relojería de corrección. Cuando la lógica tiene que cambiar —una nueva regla fiscal, un bug de redondeo arreglado, un endpoint actualizado— tienes que encontrar todas las copias. Se te escapará una. Y entonces dos partes de la app discrepan sobre algo en lo que deberían coincidir, y esa discrepancia es el siguiente incidente en producción.

Por qué lo hace la IA. El modelo rara vez busca en la base de código existente un helper que podría reutilizar. Desde su perspectiva es más barato regenerar la función en línea que descubrir e importar la que ya existe. Cada generación es razonable localmente; el agregado es deriva.

Cómo lo arreglamos. Encontramos los grupos de código casi idéntico, extraemos una única fuente de verdad y hacemos pasar por ella todas las llamadas. Es la limpieza de mayor palanca en casi todas las auditorías: encoge la base de código, elimina categorías enteras de bugs de «arreglado aquí pero no allí» y convierte el siguiente cambio en una edición de una línea en vez de en una búsqueda del tesoro.


3. Sin arquitectura coherente

Este choca la primera vez que lo ves. Dos pantallas del mismo proyecto estarán escritas como si las hubieran hecho dos equipos distintos: una pide datos desde el componente, la otra a través de una capa de servicio; una guarda el estado de una forma y la siguiente de otra completamente distinta; los nombres, la estructura de carpetas y la gestión de errores cambian de funcionalidad en funcionalidad. No hay espina dorsal.

Una base de código sin arquitectura coherente es una en la que cada archivo que abres es una sorpresa. Incorporar a un desarrollador lleva semanas porque no hay ningún patrón que aprender, solo cien casos especiales que memorizar. Peor aún, cuando los patrones chocan, las costuras entre ellos son justo donde se crían los bugs: datos pedidos de dos formas distintas, errores tratados en un sitio y tragados en otro.

Por qué lo hace la IA. El modelo no tiene una imagen persistente de «cómo está construida esta app». Cada prompt es un comienzo desde cero, así que recurre al patrón que encaje con esa petición concreta. A lo largo de la vida de un proyecto eso produce un mosaico: cada pieza sensata por sí sola, el conjunto incoherente.

Cómo lo arreglamos. Elegimos una arquitectura que encaje con el proyecto —no una dogmática, una que encaje— y hacemos converger la base de código hacia ella de forma incremental. El acceso a datos, el estado, la gestión de errores y los nombres reciben una única forma acordada. El objetivo no es la pureza; es que un desarrollador que aprenda una funcionalidad pueda predecir cómo funciona la siguiente.


4. Se evita el estado basado en streams: directos al infierno de callbacks

Este es el fallo técnicamente más interesante, y el que rompe en silencio las funcionalidades más difíciles. El código generado por IA tiende a evitar los modelos de estado reactivos y basados en streams en favor de callbacks imperativos. En lugar de modelar «este valor cambia con el tiempo y la interfaz reacciona», conecta un callback, que dispara otro callback, que activa una bandera, que dispara un tercero, y el resultado es el infierno de callbacks.

En pantallas simples apenas se nota. Pero en el momento en que el estado es genuinamente complejo —un formulario de varios pasos con validación cruzada entre campos, un cuadro de mando que se actualiza en vivo, cualquier cosa con debouncing, reintentos, cancelación o actualizaciones optimistas— el enfoque de callbacks se desmorona. El síntoma clásico es el formulario que casi funciona: valida, pero el error se limpia en el momento equivocado; envía, pero un doble toque lo dispara dos veces; un campo se actualiza, pero un campo dependiente va una pulsación por detrás. No son bugs aleatorios. Son el resultado inevitable de coordinar a mano un estado asíncrono complejo en vez de modelarlo como un stream.

Por qué lo hace la IA. Los callbacks imperativos son el patrón más común en sus datos de entrenamiento y el más fácil de generar pieza a pieza. Los modelos reactivos y basados en streams exigen sostener toda la máquina de estados en la cabeza a la vez, que es exactamente aquello en lo que un generador con contexto limitado es peor.

Cómo lo arreglamos. Identificamos las funcionalidades con estado complejo y reconstruimos su capa de estado en condiciones —como streams o como un modelo reactivo apropiado para el stack— para que la interfaz sea una función del estado en vez de un montón de callbacks compitiendo entre sí. Las funcionalidades de infierno de callbacks que «casi funcionaban» empiezan a funcionar del todo, y pasan a ser modificables sin miedo.


5. Ninguna conciencia del entorno de despliegue

El modelo escribe código como si fuera a ejecutarse como un único proceso en una sola máquina, porque desde dentro del prompt ese es el único entorno que puede ver. No tiene ni idea de cuántas instancias van a correr, qué servicios gestionados ya existen, cómo se enruta el tráfico ni qué trabajo se ejecuta en paralelo. Así que recurre a la topología más simple posible, y ese valor por defecto se rompe en silencio en cuanto la app se despliega de verdad.

Los síntomas son siempre los mismos. El estado que vive en la memoria del proceso —una caché, las sesiones, contadores de límite de tasa, un bloqueo en curso— funciona perfectamente en una instancia y diverge en silencio en cuanto una segunda réplica aparece tras el balanceador de carga. Los trabajos en segundo plano y las tareas programadas se disparan en todas las instancias en vez de una sola vez, así que el correo sale tres veces y la limpieza nocturna se ejecuta por triplicado. Los archivos subidos se escriben en un disco local que desaparece en el siguiente despliegue y que nunca se compartió entre instancias de todos modos. El código es correcto para una sola máquina y equivocado para el clúster en el que realmente corre.

Por qué lo hace la IA. No tiene ninguna imagen de tu infraestructura. No sabe que ya tienes Redis, una cola de mensajes, almacenamiento de objetos y una base de datos gestionada, así que los reimplementa en memoria. No sabe que corres tres réplicas, así que asume una. La topología de despliegue es exactamente el contexto que un prompt no puede contener, así que el modelo rellena el hueco con la suposición más simple y sigue adelante.

Cómo lo arreglamos. Mapeamos el despliegue real —cuántas instancias, qué servicios gestionados existen, cómo se paraleliza el trabajo— y movemos el estado compartido a donde pertenece: la caché, las sesiones y los bloqueos a Redis o a la base de datos, los archivos al almacenamiento de objetos, el trabajo recurrente a un planificador o cola de verdad que se ejecuta una vez en lugar de dispararse en cada nodo. El resultado es código que escala horizontalmente: añadir una instancia hace la app más rápida en vez de corromper su estado.


6. Seguridad completamente abierta

Toda auditoría encuentra los mismos olores de seguridad, y son justo los que cuestan dinero de verdad: archivos .env subidos al repositorio, claves de API y tokens incrustados directamente en el código, credenciales horneadas en bundles de cliente. Una sola clave de terceros filtrada puede acumular miles en cargos no autorizados en cuestión de horas, o entregarle a un atacante tu almacén de datos.

Junto a los secretos, los compañeros habituales: autenticación sin autorización real detrás (una pantalla de login, pero endpoints que nunca comprueban quién pregunta), entrada de usuario concatenada directamente en consultas y prompts, y datos sensibles en texto plano o en los logs. Cubrimos la parte de seguridad en profundidad para fundadores en qué se rompe cuando un prototipo de IA llega a producción, y la forma correcta de tratar los datos en nuestra guía sobre cifrado en reposo y en tránsito.

Por qué lo hace la IA. El camino más corto hasta una funcionalidad que funcione casi nunca es el seguro. Incrustar una clave funciona de inmediato; montar un gestor de secretos no. El modelo elige el camino que se ejecuta.

Cómo lo arreglamos. Los secretos salen del código y pasan a una gestión de entorno en condiciones, las claves se rotan (cualquier cosa subida a git ya está comprometida), las comprobaciones de autorización van en cada endpoint que las necesite, y la entrada se valida y se parametriza. Esta es la parte no negociable de cualquier auditoría: una clave filtrada o una elusión de autenticación es una emergencia, y se arregla primero.


El patrón detrás del patrón

Da un paso atrás y los seis hallazgos comparten una causa raíz: la IA optimiza cada generación localmente y nadie está optimizando la base de código globalmente. Los tests, la deduplicación, la arquitectura, el modelado del estado, la conciencia del despliegue y la seguridad son todas propiedades de sistema completo. No pueden emerger de prompt en prompt, porque ningún prompt aislado ve el conjunto. Esa es exactamente la brecha que cierra una revisión humana.

Lo tranquilizador es que nada de esto significa que los cimientos construidos con IA estén desperdiciados. Las funcionalidades funcionan; el producto es real. Lo que falta es el tejido conectivo, y añadirlo es mucho más rápido que reconstruir desde cero.


Preguntas frecuentes

En decenas de bases de código generadas con IA se repiten casi siempre seis problemas de ingeniería: ningún test automatizado, variables y funciones muy duplicadas, ninguna arquitectura coherente en el proyecto, estado asíncrono complejo implementado como infierno de callbacks en lugar de streams, código escrito sin conciencia del entorno de despliegue, y huecos de seguridad como archivos .env subidos al repositorio y tokens incrustados. El código concreto varía de proyecto a proyecto, pero estas seis categorías aparecen una y otra vez.
Porque escribir una funcionalidad y escribir tests para ella son dos peticiones separadas, y la segunda normalmente no llega a hacerse nunca. Las herramientas de IA optimizan el camino más corto hasta código que se ejecute, que es el camino feliz sin ninguna suite de tests detrás. El modelo escribirá tests cuando se los pidas, pero por su cuenta entrega la funcionalidad y para, dejando cada despliegue futuro sin red de seguridad.
El modelo rara vez busca en la base de código existente un helper que podría reutilizar. Regenerar una función en línea para la pantalla que la necesita le sale más barato, desde su perspectiva, que descubrir e importar una que ya existe. Cada generación es razonable localmente, pero el resultado es la misma lógica copiada varias veces con pequeñas diferencias, lo que se convierte en un problema de corrección en cuanto esa lógica tiene que cambiar.
El código generado por IA tiende a evitar los modelos de estado reactivos y basados en streams en favor de callbacks imperativos. En pantallas simples eso está bien, pero el estado complejo —formularios de varios pasos con validación cruzada, debouncing, reintentos o actualizaciones optimistas— colapsa en un infierno de callbacks. El síntoma clásico es un formulario que casi funciona: los errores se limpian en el momento equivocado, un doble toque envía dos veces o un campo dependiente va una pulsación por detrás. Modelar el estado como un stream lo arregla.
Porque el modelo no tiene ninguna imagen de cómo se despliega la app. Escribe código para un único proceso en una sola máquina, así que mantiene el estado en memoria y escribe archivos en disco local. Eso funciona en una instancia pero se rompe en cuanto la app se paraleliza tras un balanceador: las cachés en memoria, las sesiones, los bloqueos y los contadores de límite de tasa divergen entre réplicas, los trabajos en segundo plano se disparan en cada instancia en vez de una vez, y los archivos subidos desaparecen en el siguiente despliegue. La solución es mover el estado compartido a los servicios de respaldo adecuados para que la app escale horizontalmente.
No. Los seis hallazgos se pueden arreglar en el sitio. Se añaden tests donde más rentabilizan, la lógica duplicada se extrae a una única fuente de verdad, la base de código se hace converger hacia una arquitectura coherente, las funcionalidades con estado complejo se reconstruyen con una capa de estado reactiva en condiciones, el estado compartido se mueve a los servicios de respaldo adecuados para que la app escale entre instancias, y los secretos se eliminan y se rotan. Esto conserva los cimientos funcionales que produjo la IA y solo arregla el tejido conectivo que falta, lo cual es mucho más rápido y barato que una reconstrucción.

Consigue los hallazgos de tu base de código

Si tienes una app construida con IA y reconoces alguno de estos seis, no vas retrasado: estás exactamente donde acaba casi toda base de código generada con IA. La solución no es reescribir; es una auditoría enfocada que añade el tejido conectivo que la IA no pudo.

Pasa tu base de código por una auditoría de código IA, o reserva una evaluación gratuita y te diremos cuál de los seis es tu mayor riesgo, qué hace falta para arreglarlo y te daremos un presupuesto de alcance cerrado, no una conjetura.