Modernitzar codi legacy amb IA: primer la base de coneixement, després el codi
Demanar a un agent que reescrigui un legacy no funciona. El nostre mètode: documentar el sistema, fixar invariants i reimplementar per aggregate.

Quan modernitzem una aplicació legacy amb un agent, la versió nova no rep ni una línia de codi fins que el sistema antic està descrit en una carpeta de fitxers markdown i algú de l’equip ha llegit aquesta descripció i l’ha donada per bona.
La temptació contrària és enorme. Un agent com Claude Code llegeix un repositori sencer i genera en pocs minuts l’equivalent en Kotlin d’un controlador de fa deu anys. Escriure «migra això a Spring Boot» i veure com apareixen fitxers resulta molt convincent. El que no apareix no es veu fins que algú a producció troba a faltar una validació que vivia amagada en un esdeveniment de formulari.
A l’equip mantenim un plugin de Claude Code amb skills i comandes (ho explico a com organitzar un plugin d’equip). Una de les peces és una comanda de modernització de legacy, amb una skill de suport que vam destil·lar després de modernitzar diverses aplicacions reals d’integració amb plataformes educatives. En aquest article explico el mètode que codifica, per què està ordenat així i on encara coixeja.
Què passa quan demanes a l’agent que reescrigui sense més
Un equip de la Universidade Federal de Minas Gerais i d’Ontario Tech va publicar un cas d’estudi de migració d’un ERP en Visual Basic 6 a C# .NET 10 amb Claude Code (el PDF es distribueix com a article de SCAM 2026). L’ERP fa més de vint anys que és a producció i dona servei a més de 200 clients. Van migrar 12 funcionalitats de dos mòduls amb una estratègia de generació única: l’agent rep un prompt estàndard, fa la migració completa i ningú el corregeix durant la sessió.
No li van donar el codi tal qual: cada directori de treball tenia un CLAUDE.md amb l’objectiu, l’abast i les convencions, un altre que descrivia el codi VB6 i un tercer amb l’arquitectura de destinació. Amb aquest context, els números van ser aquests:
- Equivalència mitjana del 70 % entre el que s’havia migrat i l’original, mesurada sobre 331 instruccions catalogades abans de començar (regles de negoci i operacions de base de dades).
- Una asimetria molt marcada segons la complexitat: 92 % en les funcionalitats senzilles, 81 % en les mitjanes i 47 % en les complexes.
- Cost baix: 50,29 dòlars en total, uns 0,15 dòlars per instrucció migrada, i menys de cinc minuts de mitjana per funcionalitat.
El més interessant és l’anàlisi dels errors. La majoria no són codi mal escrit, sinó codi que falta. L’agent va deixar fora 42 «mòduls adjacents» (importacions batch, informes, notificacions per correu), 22 operacions de base de dades i 20 regles concretes. En una funcionalitat de sol·licituds de compra, la versió migrada creava la sol·licitud sempre en estat obert i no enviava mai el correu del qual depenia l’aprovació jeràrquica: el codi compilava i funcionava, però implementava un altre procés.
Els autors apunten dues causes. La primera és la fragmentació: quan una regla està repartida entre diversos fitxers, l’agent la perd, «fins i tot quan els fitxers es proporcionen complets». La segona, que l’agent migra bé les regles explícites i ignora les implícites: un filtre IdMarca > 0 escrit al formulari que carrega la llista de marques, un paràmetre booleà que exclou un tipus de consum, una variable global que es carrega de la base de dades en arrencar. La seva recomanació és no delegar a l’agent la inferència de restriccions importants i deixar-les escrites de la manera més clara i explícita possible.
L’altre cas que se sol citar apunta en la mateixa direcció, amb menys rigor. Peter Winston, CEO d’ICS, explica a Embedded Computing Design com tres desenvolupadors van intentar durant dues setmanes portar codi Motif a Qt 6 amb Claude Code. Tots tres van obtenir el 80-90 % de la funcionalitat molt de pressa i cap no va acabar dins del termini. L’agent tendia a generar patrons de Qt 5, perquè hi ha molt més codi públic d’aquella versió, i els errors s’acumulaven si no es caçaven aviat. El que més va automatitzar (al voltant del 95 %, sobre aplicacions d’exemple amb poca lògica de negoci, com reconeix el mateix autor) va seguir un procés ordenat: documentar el codi, crear tests, portar i validar.
Cap dels dos casos diu que la IA no serveixi per modernitzar. El que mostren és un agent ràpid i barat en la part visible del sistema i poc fiable en la part que ningú no va escriure mai. El nostre mètode neix de prendre’s això seriosament.
El mètode en quatre fases
La comanda rep la ruta del projecte legacy i recorre quatre fases en ordre, amb dues parades en què l’agent espera l’aprovació d’una persona. La governen quatre regles: documentar abans de programar, preguntar en lloc de suposar, preservar la funcionalitat original tret que s’acordi el contrari, i reflectir a la documentació cada decisió que es prengui.
La segona sembla evident, però va contra la inclinació natural d’un model, que tendeix a omplir un buit amb alguna cosa plausible abans que admetre que no sap per què una columna es diu flag2.
Fase 1: la base de coneixement del legacy
Abans de tocar res, l’agent crea una carpeta de referències dins del projecte legacy i documenta el sistema actual en fitxers markdown separats, a partir de plantilles buides que porta la skill. Cada fitxer cobreix un aspecte: arquitectura i flux d’execució, model de dades, API exposada amb la seva autorització, entitats de domini, integracions externes, seguretat, infraestructura de desplegament i, finalment, problemes coneguts. Si el domini ho demana se n’afegeixen més, per exemple un de dedicat al flux de llançament LTI.
Separar-ho així permet que, durant la reimplementació, l’agent consulti només la part de l’API quan treballa en un controlador o només la d’integracions quan toca un adaptador, sense carregar tot el sistema al context. És el mateix patró de router amb references que fem servir a les skills, aplicat al projecte. També reparteix la revisió humana: qui coneix les integracions revisa aquell fitxer, i qui coneix el model de dades, el seu.
El document que aporta més valor és el de problemes coneguts. Recull bugs, deute tècnic i riscos per a la migració, i té una secció de decisions pendents de confirmar amb una persona. Aquesta secció posa per escrit just el que l’estudi de la UFMG va trobar que l’agent se salta. Quan l’agent topa amb un filtre estrany en un formulari o amb un comportament que depèn d’una variable global, no ha de decidir si és una regla de negoci o un accident: ho anota com a pregunta. Algú de l’equip respon, i la resposta queda documentada per a la fase següent.
Així, una regla implícita al codi passa a ser explícita en un document que l’agent llegirà quan reimplementi aquella part. No garanteix que la respecti, però converteix un error silenciós en una cosa que es pot comprovar. En acabar la fase, l’agent s’atura i demana validació: és la primera porta.
En aquesta revisió, el que mereix més atenció és allò que l’estudi de la UFMG va trobar que es perd: importacions batch, informes, notificacions i integracions que no formen part del flux principal, i regles que l’agent descriu amb més seguretat de la que permet el codi. Un document d’arquitectura que no esmenta cap procés programat en un sistema de deu anys hauria de despertar sospites abans que confiança.
Fase 2: stack objectiu i invariants
La segona fase fixa cap a on s’anirà. La comanda porta per defecte l’stack amb què treballa l’equip (Kotlin, Spring amb WebFlux reactiu, DDD amb capes de domini, aplicació i infraestructura, API documentada amb OpenAPI i arrencada local amb docker-compose), i qualsevol desviació es consulta.
Si no se li fixa l’stack, l’agent tria el que més ha vist durant l’entrenament (li va passar a l’equip d’ICS amb Qt 5). Fixar-lo també fa que totes les versions noves s’assemblin entre si, que és el que vols quan el mateix equip les haurà de mantenir totes.
El més útil d’aquesta fase són els invariants de sortida: condicions que la v2 ha de complir per donar la feina per acabada i que es comproven a cada iteració del bucle. Els nostres són cinc:
- Multitenancy, si el legacy no ho era.
- Dependències al dia i build en verd.
- Separació en mòduls si el legacy executa tasques programades.
- Baranes d’arquitectura actives com a tests.
- Documentació coherent del legacy i de la v2, amb arrencada local i API documentada.
El primer és el que més canvia el disseny. Les nostres aplicacions s’integren amb plataformes educatives via LTI 1.3, i una mateixa instal·lació dona servei a diversos clients. A LTI, cada client s’identifica per la combinació d’issuer, client_id i deployment_id. Moltes aplicacions antigues van néixer per a un sol client i es replicaven o es configuraven per instància. L’invariant obliga que la v2 tingui un registre per client amb aquesta clau, que la configuració, les credencials i la imatge de marca quedin aïllades per client, i que l’identificador de tenant viatgi pel domini, els casos d’ús i els processos batch, en lloc de resoldre’s a mig camí o acceptar-se com a paràmetre lliure d’una petició.
El que fa que això funcioni amb un agent és formular cada invariant com una cosa que es pot comprovar al codi. «Que sigui multitenant» és una intenció. «Tota consulta de repositori sobre dades de negoci rep l’identificador de tenant com a paràmetre» és una cosa que es revisa en un diff o es converteix en una regla automàtica.
El tercer invariant surt del mateix raonament: si el legacy té tasques programades, la v2 separa l’API i els jobs en mòduls diferents, i cada job s’executa com un procés independent per client, de manera que una tasca pesada d’un client no comparteix procés amb l’API d’un altre.
Fem servir invariants i no una llista de tasques perquè un agent canta victòria amb molta facilitat. Amb «migrar els endpoints» com a criteri, migra els endpoints i dona la feina per acabada. Amb «aquestes cinc condicions en verd», hi ha alguna cosa amb què comparar a cada iteració, i gairebé tot es verifica amb una eina en lloc de fiar-se de la paraula de l’agent.
Fase 3: el pla, amb el legacy encara a producció
La tercera fase produeix un full de ruta: l’ordre dels bounded contexts i aggregates, què es reescriu i què es reutilitza, els riscos i com es validarà la paritat funcional. El principi que la guia és que el legacy continua a producció durant tota la migració. No hi ha big bang: cada fase es desplega per separat.
Inclou una matriu de gaps entre el legacy i la v2, funcionalitat per funcionalitat, les diferències de disseny acceptades com a millora i els canvis de model que exigeixen migrar dades (per exemple, la nova referència al client a cada taula), que és la llista que evita sorpreses al cutover. L’acompanyen un pla de rollback i un altre de desplegament sense talls.
Aquí hi ha la segona porta. L’agent proposa el pla i espera l’aprovació abans de generar codi. Si s’assembla a escriure una spec abans d’implementar, és perquè ho és: la base de coneixement i el full de ruta fan d’especificació de la v2, només que extreta d’un sistema que ja existeix en lloc d’escrita des de zero.
Fase 4: el bucle per aggregate
Amb el pla aprovat, l’agent treballa en un projecte nou (el legacy no es toca) i repeteix el mateix bucle per a cada aggregate del full de ruta:
- Extreure l’aggregate en tres capes: domini sense framework i amb els seus ports, aplicació amb els casos d’ús, infraestructura amb adaptadors i controladors prims.
- Escriure tests de domini sense Spring, d’aplicació amb els ports simulats i d’integració amb Testcontainers.
- Verificar la paritat funcional contra el que hi ha documentat del legacy.
- Passar el quality gate: format, tests (inclosos els d’arquitectura) i llindar de cobertura.
- Documentar la decisió a la carpeta de la v2 i marcar la casella al full de ruta.
Treballar per aggregate respon a l’altra conclusió de l’estudi de la UFMG: l’agent rendeix molt millor amb peces petites i acotades. Els autors proposen com a treball futur descompondre les funcionalitats complexes abans de migrar, que és el que el bucle fa per disseny.
El quart pas és el que ho sosté sense dependre de la memòria de l’agent. El gate s’executa en un bucle de correcció fins que queda en verd, i les baranes d’arquitectura són tests que corren amb la resta de la suite. Amb Konsist, una regla de capes té aquest aspecte (exemple il·lustratiu, amb un paquet inventat):
@Test
fun `el domini no depèn de cap altra capa`() {
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 l’agent fica una dependència de Spring al domini, el test falla i el gate no passa, sense dependre que recordi la regla que va llegir fa quaranta fitxers.
El cinquè pas alimenta un quadern de bitàcola de la reimplementació: com aixecar-la en local, els mòduls migrats amb el seu estat de paritat i de gate, les decisions i el seu perquè, i el deute pendent. S’assembla a un registre de decisions d’arquitectura, però l’escriu l’agent mentre treballa i el revisa una persona. Si canvia qui porta la migració, o s’obre una sessió nova sense el context de l’anterior, aquest document permet continuar.
Per què l’ordre importa més que el prompt
Vist en conjunt, el mètode no fa res que un desenvolupador sènior no fes ja en una migració manual. El que canvia amb un agent és el preu de saltar-se les fases: deixen de ser un bon hàbit que s’abandona quan hi ha pressa i passen a ser l’única cosa que impedeix lliurar un 70 % de sistema amb aspecte de 100 %.
L’agent és molt bo generant codi i força dolent sabent quin codi falta. Les fases 1 i 3 existeixen perquè el que falta es vegi abans de generar res, i els invariants i el gate, perquè el que es genera no es degradi mentre avança el bucle. Canviar de model o afinar el prompt de reescriptura mou poc aquesta equació; canviar l’ordre de la feina la mou molt.
Per això l’empaquetem com a comanda amb una skill al darrere i no com una pàgina de wiki. La comanda imposa l’ordre i les parades; la skill porta el detall (patrons DDD amb exemples, el model multitenant, la separació de jobs, l’estratègia de tests) i només es carrega quan cal; les plantilles fan que cada migració produeixi la mateixa documentació. El procés de convertir el que vam aprendre en diverses migracions en aquesta skill dona per a un altre article: com destil·lar el coneixement d’un equip en skills.
El que el mètode no resol
Abans de copiar-lo, cal saber on flaqueja.
La paritat funcional es verifica contra la documentació del legacy, no contra el legacy en execució. Si la base de coneixement omet una regla i ningú no ho detecta a la revisió, la v2 també l’ometrà, i el pas 3 del bucle ho donarà per bo. El mètode trasllada el risc del codi a la documentació, on és més fàcil de revisar, però no l’elimina. El que ho tancaria seria capturar el comportament real del legacy (peticions i respostes, efectes a la base de dades) i reproduir-lo contra la v2 com a tests de caracterització. Avui això no forma part del mètode.
La revisió humana és el coll d’ampolla, i ho ha de ser. Les dues portes d’aprovació només serveixen si qui aprova coneix el sistema; si ningú no recorda per què existeix un flux, la base de coneixement el descriurà i ningú no podrà confirmar si és una regla o un accident.
El cost es desplaça al principi. Les fases 1 a 3 no produeixen codi desplegable, i qui espera veure avenços en forma d’endpoints les percep com a temps perdut. L’estudi de la UFMG mostra que generar codi és barat; el car és decidir quin codi ha d’existir.
El que compensa aquest cost no ho puc explicar en terminis ni en xifres, però sí en el que queda després. Les modernitzacions fetes així ens han donat cohesió entre els desenvolupaments, perquè totes les v2 comparteixen stack, invariants i manera de documentar-se; capes de seguretat que els legacy no tenien; i una estructura que, a més, permet introduir-hi automatitzacions.
Si has de modernitzar un legacy amb un agent
Jo començaria per la llista de problemes coneguts: demana a l’agent que recorri el codi i anoti com a pregunta tot el que no entén, sense intentar resoldre-ho. Aquesta llista et dirà quant coneixement implícit té el sistema i quant se’n perdria en una reescriptura directa.
Després, escriu els invariants de sortida com a condicions comprovables i converteix en tests tots els que puguis. Un agent respecta molt millor el que li fa fallar el build que el que li demanes al prompt.
I treballa per aggregate, amb el gate en verd abans de passar al següent. És més lent que demanar tota la migració de cop, i és l’única manera que conec de saber, en cada moment, quina part del sistema nou està acabada de debò.
Si t’interessa com encaixa aquesta comanda amb la resta de skills de l’equip, l’article pilar sobre agent skills en dona la visió de conjunt.


