Modernizar código legacy con IA: base de conocimiento primero, código después

Pedirle a un agente que reescriba un legacy no funciona. Nuestro método: documentar el sistema, fijar invariantes y reimplementar por aggregate.

Cover for Modernizar código legacy con IA: base de conocimiento primero, código después
Actualizado: 31 oct 2026

Cuando modernizamos una aplicación legacy con un agente, la versión nueva no recibe una sola línea de código hasta que el sistema antiguo está descrito en una carpeta de ficheros markdown y alguien del equipo ha leído esa descripción y la ha dado por buena.

La tentación contraria es enorme. Un agente como Claude Code lee un repositorio entero y genera el equivalente en Kotlin de un controlador de hace diez años en minutos. Escribir «migra esto a Spring Boot» y ver cómo aparecen ficheros resulta muy convincente. Lo que no aparece no se ve hasta que alguien en producción echa de menos una validación que vivía escondida en un evento de formulario.

En el equipo mantenemos un plugin de Claude Code con skills y comandos (lo cuento en cómo organizar un plugin de equipo). Una de sus piezas es un comando de modernización de legacy, con una skill de apoyo que destilamos de modernizar varias aplicaciones reales de integración con plataformas educativas. Aquí explico el método que codifica, por qué está ordenado así y dónde sigue cojeando.

Lo que pasa cuando le pides al agente que reescriba sin más

Un equipo de la Universidade Federal de Minas Gerais y Ontario Tech publicó un caso de estudio de migración de un ERP en Visual Basic 6 a C# .NET 10 con Claude Code (el PDF se distribuye como artículo de SCAM 2026). El ERP lleva más de veinte años en producción y da servicio a más de 200 clientes. Migraron 12 funcionalidades de dos módulos con una estrategia de generación única: el agente recibe un prompt estándar, hace la migración completa y nadie le corrige durante la sesión.

No le dieron el código a pelo: cada directorio de trabajo tenía un CLAUDE.md con objetivo, alcance y convenciones, otro describiendo el código VB6 y un tercero con la arquitectura destino. Con ese contexto, los números fueron estos:

  • Equivalencia media del 70 % entre lo migrado y el original, medida sobre 331 instrucciones catalogadas antes de empezar (reglas de negocio y operaciones de base de datos).
  • Una asimetría muy marcada por complejidad: 92 % en funcionalidades sencillas, 81 % en medias y 47 % en las complejas.
  • Coste bajo: 50,29 dólares en total, unos 0,15 dólares por instrucción migrada, y menos de cinco minutos de media por funcionalidad.

Lo interesante está en el análisis de los fallos. La mayoría no son código mal escrito, sino código que falta. El agente dejó fuera 42 «módulos adyacentes» (importaciones batch, informes, notificaciones por email), 22 operaciones de base de datos y 20 reglas concretas. En una funcionalidad de solicitudes de compra, la versión migrada creaba la solicitud siempre en estado abierto y nunca enviaba el email del que dependía la aprobación jerárquica: el código compilaba y funcionaba, pero implementaba otro proceso.

Los autores señalan dos causas. La primera es la fragmentación: cuando una regla está repartida entre varios ficheros, el agente la pierde, «incluso cuando los ficheros se proporcionan completos». La segunda, que el agente migra bien las reglas explícitas e ignora las implícitas: un filtro IdMarca > 0 escrito en el formulario que carga la lista de marcas, un parámetro booleano que excluye un tipo de consumo, una variable global que se carga de base de datos al arrancar. Su recomendación es no delegar en el agente la inferencia de restricciones importantes y dejarlas escritas de la forma más clara y explícita posible.

El otro caso que suele citarse apunta en la misma dirección, con menos rigor. Peter Winston, CEO de ICS, cuenta en Embedded Computing Design cómo tres desarrolladores intentaron durante dos semanas portar código Motif a Qt 6 con Claude Code. Los tres obtuvieron el 80-90 % de la funcionalidad muy rápido y ninguno terminó en plazo. El agente tendía a generar patrones de Qt 5, porque hay mucho más código público de esa versión, y los errores se acumulaban si no se cazaban pronto. El que más automatizó (en torno al 95 %, sobre aplicaciones de ejemplo con poca lógica de negocio, como reconoce el propio autor) siguió un proceso ordenado: documentar el código, crear tests, portar y validar.

Ninguno de los dos casos dice que la IA no sirva para modernizar. Lo que muestran es un agente rápido y barato en la parte visible del sistema y poco fiable en la parte que nadie escribió nunca. Nuestro método sale de tomarse eso en serio.

El método en cuatro fases

El comando recibe la ruta del proyecto legacy y recorre cuatro fases en orden, con dos paradas en las que el agente espera aprobación humana. Lo gobiernan cuatro reglas: documentar antes de codificar, preguntar en lugar de asumir, preservar la funcionalidad original salvo que se acuerde lo contrario, y reflejar en la documentación cada decisión que se tome.

La segunda parece obvia, pero va contra la inclinación natural de un modelo, que tiende a rellenar un hueco con algo plausible antes que admitir que no sabe por qué una columna se llama flag2.

Fase 1: la base de conocimiento del legacy

Antes de tocar nada, el agente crea una carpeta de referencias dentro del proyecto legacy y documenta el sistema actual en ficheros markdown separados, a partir de plantillas vacías que trae la skill. Cada fichero cubre un aspecto: arquitectura y flujo de ejecución, modelo de datos, API expuesta con su autorización, entidades de dominio, integraciones externas, seguridad, infraestructura de despliegue y, por último, problemas conocidos. Si el dominio lo pide se añaden más, por ejemplo uno dedicado al flujo de lanzamiento LTI.

Separarlo así permite que, durante la reimplementación, el agente consulte solo la parte de la API cuando trabaja en un controlador o solo la de integraciones cuando toca un adaptador, sin cargar el sistema entero en contexto. Es el mismo patrón de router con references que usamos en las skills, aplicado al proyecto. También reparte la revisión humana: quien conoce las integraciones revisa ese fichero y quien conoce el modelo de datos, el suyo.

El documento que más valor aporta es el de problemas conocidos. Recoge bugs, deuda técnica y riesgos para la migración, y tiene una sección de decisiones pendientes de confirmar con una persona. Esa sección pone por escrito justo lo que el estudio de la UFMG encontró que el agente se salta. Cuando el agente da con un filtro raro en un formulario o un comportamiento que depende de una variable global, no tiene que decidir si es una regla de negocio o un accidente: lo apunta como pregunta. Alguien del equipo responde, y la respuesta queda documentada para la fase siguiente.

Una regla implícita en el código pasa así a ser explícita en un documento que el agente leerá cuando reimplemente esa parte. No garantiza que la respete, pero convierte un fallo silencioso en algo comprobable. Al terminar la fase, el agente para y pide validación: es la primera puerta.

En esa revisión, lo que más atención merece es lo que el estudio de la UFMG encontró que se pierde: importaciones batch, informes, notificaciones e integraciones que no forman parte del flujo principal, y reglas que el agente describe con más seguridad de la que permite el código. Un documento de arquitectura que no menciona ningún proceso programado en un sistema de diez años debería despertar sospechas antes que confianza.

Fase 2: stack objetivo e invariantes

La segunda fase fija a dónde se va. El comando trae por defecto el stack con el que trabaja el equipo (Kotlin, Spring con WebFlux reactivo, DDD con capas de dominio, aplicación e infraestructura, API documentada con OpenAPI y arranque local con docker-compose), y cualquier desviación se consulta.

Si no se le fija el stack, el agente elige lo que más ha visto en su entrenamiento (le pasó al equipo de ICS con Qt 5). Fijarlo hace además que todas las versiones nuevas se parezcan entre sí, que es lo que quieres cuando el mismo equipo las va a mantener todas.

Lo más útil de esta fase son las invariantes de salida: condiciones que la v2 tiene que cumplir para dar el trabajo por terminado y que se comprueban en cada iteración del bucle. Las nuestras son cinco:

  1. Multitenancy, si el legacy no lo era.
  2. Dependencias al día y build en verde.
  3. Separación en módulos si el legacy ejecuta tareas programadas.
  4. Guardarraíles de arquitectura activos como tests.
  5. Documentación coherente del legacy y de la v2, con arranque local y API documentada.

La primera es la que más cambia el diseño. Nuestras aplicaciones se integran con plataformas educativas vía LTI 1.3, y una misma instalación sirve a varios clientes. En LTI, cada cliente se identifica por la combinación de issuer, client_id y deployment_id. Muchas aplicaciones antiguas nacieron para un único cliente y se replicaban o se configuraban por instancia. La invariante obliga a que la v2 tenga un registro por cliente con esa clave, que la configuración, las credenciales y la imagen de marca queden aisladas por cliente, y que el identificador de tenant viaje por el dominio, los casos de uso y los procesos batch, en lugar de resolverse a mitad de camino o aceptarse como parámetro libre de una petición.

Lo que hace que esto funcione con un agente es formular cada invariante como algo que se puede comprobar en el código. «Que sea multitenant» es una intención. «Toda consulta de repositorio sobre datos de negocio recibe el identificador de tenant como parámetro» es algo que se revisa en un diff o se convierte en una regla automática.

La tercera invariante sale del mismo razonamiento: si el legacy tiene tareas programadas, la v2 separa la API y los jobs en módulos distintos, y cada job se ejecuta como un proceso independiente por cliente, de modo que una tarea pesada de uno no comparte proceso con la API de otro.

Usamos invariantes y no una lista de tareas porque un agente declara victoria con mucha facilidad. Con «migrar los endpoints» como criterio, migra los endpoints y da el trabajo por terminado. Con «estas cinco condiciones en verde», hay algo contra lo que comparar en cada iteración, y casi todo se verifica con una herramienta en lugar de con la palabra del agente.

Fase 3: el plan, con el legacy todavía en producción

La tercera fase produce una hoja de ruta: el orden de los bounded contexts y aggregates, qué se reescribe y qué se reutiliza, los riesgos y cómo se va a validar la paridad funcional. El principio que la guía es que el legacy sigue en producción durante toda la migración. No hay big bang: cada fase se despliega por separado.

Incluye una matriz de gaps entre legacy y v2, funcionalidad por funcionalidad, las diferencias de diseño aceptadas como mejora y los cambios de modelo que exigen migrar datos (por ejemplo, la nueva referencia al cliente en cada tabla), que es la lista que evita sorpresas en el cutover. Junto a ella van un plan de rollback y otro de despliegue sin cortes.

Aquí está la segunda puerta. El agente propone el plan y espera aprobación antes de generar código. Si se parece a escribir una spec antes de implementar, es porque lo es: la base de conocimiento y la hoja de ruta funcionan como la especificación de la v2, solo que extraída de un sistema que ya existe en lugar de escrita desde cero.

Fase 4: el bucle por aggregate

Con el plan aprobado, el agente trabaja en un proyecto nuevo (el legacy no se toca) y repite el mismo bucle por cada aggregate de la hoja de ruta:

  1. Extraer el aggregate en tres capas: dominio sin framework y con sus puertos, aplicación con los casos de uso, infraestructura con adaptadores y controladores finos.
  2. Escribir tests de dominio sin Spring, de aplicación con los puertos simulados y de integración con Testcontainers.
  3. Verificar la paridad funcional contra lo documentado del legacy.
  4. Pasar el quality gate: formato, tests (incluidos los de arquitectura) y umbral de cobertura.
  5. Documentar la decisión en la carpeta de la v2 y marcar la casilla en la hoja de ruta.

Trabajar por aggregate responde a la otra conclusión del estudio de la UFMG: el agente rinde mucho mejor con piezas pequeñas y acotadas. Los autores proponen como trabajo futuro descomponer las funcionalidades complejas antes de migrar, que es lo que el bucle hace por diseño.

El cuarto paso es el que lo sostiene sin depender de la memoria del agente. El gate se ejecuta en bucle de corrección hasta quedar en verde, y los guardarraíles de arquitectura son tests que corren con el resto de la suite. Con Konsist, una regla de capas tiene este aspecto (ejemplo ilustrativo, con un paquete inventado):

@Test
fun `el dominio no depende de otras capas`() {
    Konsist.scopeFromProject().assertArchitecture {
        val domain = Layer("Domain", "com.example.app..domain..")
        val application = Layer("Application", "com.example.app..application..")
        val infrastructure = Layer("Infrastructure", "com.example.app..infrastructure..")

        domain.dependsOnNothing()
        application.dependsOn(domain)
        infrastructure.dependsOn(domain, application)
    }
}

Si el agente mete una dependencia de Spring en el dominio, el test falla y el gate no pasa, sin depender de que recuerde la regla que leyó hace cuarenta ficheros.

El quinto paso alimenta una bitácora de la reimplementación: cómo levantarla en local, los módulos migrados con su estado de paridad y de gate, las decisiones y su porqué, y la deuda pendiente. Se parece a un registro de decisiones de arquitectura, salvo que lo escribe el agente mientras trabaja y lo revisa una persona. Si cambia quien lleva la migración, o se abre una sesión nueva sin el contexto de la anterior, ese documento permite continuar.

Por qué el orden importa más que el prompt

Visto en conjunto, el método no hace nada que un desarrollador senior no hiciera ya en una migración manual. Lo que cambia con un agente es el precio de saltarse las fases: dejan de ser un buen hábito que se abandona cuando hay prisa y pasan a ser lo único que impide entregar un 70 % de sistema con aspecto de 100 %.

El agente es muy bueno generando código y bastante malo sabiendo qué código falta. Las fases 1 y 3 existen para que lo que falta se vea antes de generar nada, y las invariantes y el gate para que lo que se genera no se degrade mientras avanza el bucle. Cambiar de modelo o afinar el prompt de reescritura mueve poco esa ecuación; cambiar el orden del trabajo la mueve mucho.

Por eso lo empaquetamos como comando con una skill detrás y no como una página de wiki. El comando impone el orden y las paradas; la skill lleva el detalle (patrones DDD con ejemplos, el modelo multitenant, la separación de jobs, la estrategia de tests) y solo se carga cuando hace falta; las plantillas hacen que cada migración produzca la misma documentación. El proceso de convertir lo aprendido en varias migraciones en esa skill da para otro artículo: cómo destilar el conocimiento de un equipo en skills.

Lo que el método no resuelve

Antes de copiarlo, hay que saber dónde flojea.

La paridad funcional se verifica contra la documentación del legacy, no contra el legacy en ejecución. Si la base de conocimiento omite una regla y nadie la detecta en la revisión, la v2 también la omitirá, y el paso 3 del bucle lo dará por bueno. El método traslada el riesgo del código a la documentación, donde es más fácil de revisar, pero no lo elimina. Lo que lo cerraría sería capturar el comportamiento real del legacy (peticiones y respuestas, efectos en base de datos) y reproducirlo contra la v2 como tests de caracterización. Eso hoy no forma parte del método.

La revisión humana es el cuello de botella, y debe serlo. Las dos puertas de aprobación solo sirven si quien aprueba conoce el sistema; si nadie recuerda por qué existe un flujo, la base de conocimiento lo describirá y nadie podrá confirmar si es una regla o un accidente.

El coste se desplaza al principio. Las fases 1 a 3 no producen código desplegable, y quien espera ver avances en forma de endpoints las percibe como tiempo perdido. El estudio de la UFMG muestra que generar código es barato; lo caro es decidir qué código tiene que existir.

Lo que compensa ese coste no lo puedo contar en plazos ni en cifras, pero sí en lo que queda después. Las modernizaciones hechas así nos han dado cohesión entre los desarrollos, porque todas las v2 comparten stack, invariantes y forma de documentarse; capas de seguridad que los legacy no tenían; y una estructura que además permite introducir automatizaciones sobre ella.

Si vas a modernizar un legacy con un agente

Empezaría por la lista de problemas conocidos: pídele al agente que recorra el código y apunte como pregunta todo lo que no entiende, sin intentar resolverlo. Esa lista te dirá cuánto conocimiento implícito tiene el sistema y cuánto se perdería en una reescritura directa.

Después, escribe las invariantes de salida como condiciones comprobables y convierte en tests todas las que puedas. Un agente respeta mucho mejor lo que le hace fallar el build que lo que le pides en el prompt.

Y trabaja por aggregate, con el gate en verde antes de pasar al siguiente. Es más lento que pedir la migración entera de una vez, y es la única forma que conozco de saber, en cada momento, qué parte del sistema nuevo está terminada de verdad.

Si te interesa cómo encaja este comando con el resto de skills del equipo, el artículo pilar sobre agent skills da la visión de conjunto.

OshyTech

Ingeniería backend y de datos orientada a sistemas escalables, automatización e IA.

Navegación

Copyright 2026 OshyTech. Todos los derechos reservados