On l'a tous croisé : le monolithe de dix ans, sans tests, où toucher UserManagerImpl fait tomber la facturation pour des raisons que personne n'explique. Michael Feathers en donnait déjà la meilleure définition en 2004 : le legacy, ce n'est pas du vieux code, c'est du code sans feedback. Vous ne pouvez pas le modifier parce que rien ne vous dira si vous l'avez cassé.
Jusqu'ici, réécrire coûtait si cher qu'on maintenait sous perfusion. L'IA générative change l'équation économique, mais pas la physique du problème. Le développeur augmenté est déjà une réalité sur les projets neufs , reste à savoir ce que ça donne sur le terrain le plus hostile qui soit : une base de code sans documentation et sans tests. L'approche naïve (coller 4 000 lignes de Java 8 dans un chat et demander du Kotlin) produit systématiquement la même chose : du code qui compile, qui a l'air moderne, et dans lequel trois règles de gestion ont silencieusement disparu.
L'IA n'est pas un transpileur. Elle amplifie votre méthode : si la méthode est bonne, le gain est réel. Sinon, elle amplifie aussi les erreurs.
Étape 0 : décider ce qui ne sera pas migré
Avant de moderniser, supprimez. Sur un monolithe mature, 20 à 40 % du code est mort : endpoints appelés par aucun client, branches rendues inatteignables par un flag figé en 2019, batchs dont le cron a été commenté.
C'est le premier usage rentable de l'IA, et le moins glamour. Croisez une analyse statique du graphe d'appels avec les logs de production sur douze mois, et faites trier le LLM par catégorie : actif / suspect / probablement mort. Chaque classe supprimée est une classe que vous n'aurez ni à documenter, ni à tester, ni à réécrire, ni à relire.
La ligne de code la moins chère à migrer est celle que vous effacez.
Étape 1 : l'archéologie logicielle
Le risque majeur d'une refonte n'est pas technique, il est fonctionnel : perdre la règle obscure codée en dur par quelqu'un qui est parti depuis.
Utilisez les modèles à grande fenêtre de contexte pour rétro-documenter avant d'écrire la moindre ligne. L'astuce Craft : ne demandez pas « que fait ce code ? », vous obtiendrez une paraphrase. Demandez des livrables structurés :
- des critères d'acceptation Gherkin (Given / When / Then), une ligne par branche d'exécution ;
- des ADR rétroactifs : « quelle décision d'architecture explique cette structure ? » révélateur sur les contournements ;
- une liste de questions ouvertes : forcer le modèle à déclarer ce qu'il ne comprend pas. C'est là que se cachent vos vraies règles métier.
Le prompt d'archéologie
[RÔLE] Tu es un analyste chargé de rétro-documenter un composant legacy
avant sa migration. Tu ne proposes AUCUNE amélioration, AUCUNE réécriture.
[CONTEXTE]
Domaine métier : [ex. facturation B2B, gestion de sinistres]
Langage et version : [ex. Java 8, Spring 3.2]
Code source du composant : {code_source}
Éléments connexes (schéma de base, constantes, config) : {contexte_annexe}
[MISSION]
Produis, dans cet ordre exact :
1. RÈGLES DE GESTION
Une ligne par règle, formulée en langage métier, sans vocabulaire technique.
Pour chacune, cite le numéro de ligne dont elle est tirée.
2. CRITÈRES D'ACCEPTATION
Format Gherkin (Given / When / Then), un scénario par branche d'exécution,
y compris les branches d'erreur et les cas de repli implicites.
Nomme explicitement les valeurs de frontière (seuils, bornes, cas nuls).
3. VALEURS EN DUR
Tout littéral porteur de sens métier (seuil, taux, code, date pivot).
Pour chacun : sa valeur, son emplacement, et l'hypothèse métier qu'il encode.
4. INCOHÉRENCES
Tout écart entre ce que le code fait et ce que son nom, sa signature,
sa Javadoc ou ses commentaires annoncent. Ne tranche pas : signale.
5. ZONES D'OMBRE
Ce que tu ne peux pas déterminer à partir du code fourni, et ce qu'il
te faudrait pour trancher. Sois exhaustif : une omission ici coûtera
cher en aval.
[CONTRAINTES]
- Aucune inférence présentée comme un fait. Si tu supposes, écris "HYPOTHÈSE :".
- Aucune règle inventée par analogie avec d'autres systèmes du même domaine.
- Si une section est vide, écris "NÉANT" plutôt que de la remplir.
Les sections 4 et 5 sont celles qui rentabilisent le prompt. Un modèle à qui on n'ouvre pas explicitement la porte du doute produit un document lisse, sans aspérités et ce sont précisément les aspérités que vous cherchez. La section 4 alimente directement l'étape 2 : chaque incohérence signalée est une frontière à capturer dans le golden master.
Conseil de terrain : faites tourner ce prompt deux fois sur le même composant, dans deux sessions distinctes. Les règles présentes dans les deux sorties sont fiables. Celles qui n'apparaissent qu'une fois méritent une lecture humaine c'est un détecteur d'hallucination gratuit.
Attention enfin sur les stacks rares. Sur COBOL, RPG, ABAP ou PL/I, la qualité chute nettement : moins de données d'entraînement, plus d'inventions plausibles. Paradoxalement, c'est là que la rétro-documentation a le plus de valeur, et c'est là qu'elle exige le plus de relecture par un sachant.
L'IA ne doit pas commencer par écrire le nouveau code. Elle doit commencer par écrire la spécification de l'ancien.
Étape 2 : le filet de sécurité, et le piège que tout le monde se tend
On ne refactorise pas sans tests. Le legacy n'en a pas. On génère donc des tests de caractérisation : l'objectif n'est pas de vérifier que le code est juste, mais de figer son comportement actuel, bugs compris.
Et c'est ici que la plupart des pipelines IA se sabotent.
Si vous demandez au LLM de produire des tests complets à partir du code legacy, il génère aussi les valeurs attendues. Or ces valeurs sortent de sa lecture du code, pas de son exécution. Prenez cette méthode, dont le nom annonce clairement l'intention :
// Remise de 10 % à partir de 10 articles
public double appliquerRemiseVolume(int quantite, double prixUnitaire) {
double total = quantite * prixUnitaire;
if (quantite > 10) {
total = total * 0.9;
}
return total;
}
Commandez exactement 10 articles : aucune remise. Le commentaire dit « à partir de 10 », le code dit > 10. Un >= manquant, écrit il y a douze ans, et jamais remonté par personne.
Demandez maintenant à un LLM d'écrire les tests. Il lit le nom de la méthode, lit le commentaire, et assert une remise pour quantite = 10. C'est le comportement correct mais ce n'est pas le comportement actuel. Le test passe au rouge sur le code legacy. Un développeur pressé corrige le > en >= pour verdir la suite, et vous venez de changer une règle de facturation en production, avant même d'avoir commencé la migration.
Le problème n'est pas que le modèle soit mauvais. Il est que personne ne sait si c'est un bug. Peut-être que le seuil réel négocié avec le métier est bien 11 articles et que c'est le commentaire qui ment. Peut-être que tout le canal B2B s'est aligné sur ce comportement depuis dix ans. Cette question ne se tranche ni par lecture, ni par déduction : elle se tranche en exécutant le code, puis en allant voir le métier.
D'où l'inversion : le LLM propose les entrées, le code legacy produit les sorties. Le modèle est excellent pour repérer que quantite = 10 est une frontière à tester.Il l'aurait proposée spontanément. Il est disqualifié pour dire ce qui doit en sortir. Cette autorité appartient au binaire qui tourne en production.
"""
Golden Master : le LLM génère les ENTRÉES.
Le code legacy, lui seul, génère les SORTIES attendues.
"""
import json, os
from pathlib import Path
from openai import OpenAI
# base_url configurable : API cloud pour l'analyse, vLLM/Ollama local pour le volume
client = OpenAI(
base_url=os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1"),
api_key=os.environ["LLM_API_KEY"],
)
def propose_input_vectors(legacy_code: str, signature: str, n: int = 50) -> list[dict]:
prompt = f"""Tu analyses une fonction legacy. NE DÉDUIS AUCUN RÉSULTAT ATTENDU.
Ta seule mission : proposer {n} jeux d'entrées maximisant la couverture des branches
et des cas limites (zéro, négatif, null, valeurs aux frontières des conditions,
débordements, valeurs par défaut non gérées).
Signature : {signature}
Code : {legacy_code}
Réponds uniquement par un objet JSON {{"vectors": [{{nom_du_paramètre: valeur}}, ...]}}."""
resp = client.chat.completions.create(
model=os.environ["LLM_MODEL"],
messages=[{"role": "user", "content": prompt}],
temperature=0.7, # ici on VEUT de la diversité, pas du déterminisme
response_format={"type": "json_object"},
)
return json.loads(resp.choices[0].message.content)["vectors"]
def capture_golden_master(vectors: list[dict], runner) -> list[dict]:
"""runner = appel réel du code legacy (JPype, subprocess, service de test...)."""
snapshot = []
for v in vectors:
try:
observed = {"returns": runner(**v)}
except Exception as e:
# Une exception fait partie du contrat. On la fige aussi.
observed = {"raises": type(e).__name__, "message": str(e)}
snapshot.append({"input": v, "observed": observed})
return snapshot
# Le LLM n'intervient plus : on sérialise la vérité terrain.
vectors = propose_input_vectors(
legacy_code, "appliquerRemiseVolume(int quantite, double prixUnitaire)"
)
Path("golden_master.json").write_text(
json.dumps(capture_golden_master(vectors, runner=call_legacy_remise), indent=2)
)
Trois garde-fous à ajouter avant de déclarer le filet posé :
Neutralisez les sources d'indéterminisme. LocalDateTime.now(), Random, appels réseau, singletons statiques : tant qu'ils sont là, votre golden master n'est pas reproductible. Créez les seams de Feathers (injection, wrapper, horloge paramétrable). L'IA est très efficace pour repérer ces points et proposer l'extraction minimale, à condition de lui interdire tout autre changement dans le même commit.
Ne vous fiez pas au coverage. 90 % de lignes couvertes ne dit rien de la qualité des assertions. La vraie métrique est le score de mutation (PIT en Java, mutmut en Python) : on injecte des bugs, et on regarde combien votre suite en détecte. C'est le seul chiffre qui mesure un filet plutôt qu'un décor.
Aspirez de la vraie donnée. Le meilleur golden master reste l'enregistrement du trafic de production : capturez les couples entrée/sortie réels sur une période représentative. L'approval testing (ApprovalTests, snapshots) industrialise très bien ça, et aucun LLM n'inventera vos cas tordus mieux que vos utilisateurs.
Étape 3 : la réécriture incrémentale, composant par composant
Règles métier (étape 1) et filet (étape 2) en place, la réécriture peut commencer. Toujours sans Big Bang : Strangler Fig, module par module, avec une façade de routage devant l'ancien système. Chaque composant migré est débranché de l'ancien, jamais dupliqué.
Une découpe agentique fonctionne bien ici :
la méthode BMAD formalise ce type d'orchestration par rôles
- Agent Architecte : propose la structure cible (classe statique géante → fonctions pures injectées, mutation d'état → immuabilité).
- Agent Développeur : reçoit le code legacy et le golden master, produit l'implémentation cible.
- Boucle d'exécution : les tests tournent, les échecs repartent en entrée. C'est cette boucle qui fait la différence, pas la qualité du prompt initial.
- Validation humaine : un développeur arbitre, notamment sur les écarts « le nouveau code est plus juste que l'ancien ». Ces écarts sont des décisions produit, pas des décisions techniques. Ils remontent au métier.
L'étape qui sécurise réellement la bascule : le shadow traffic. Avant de couper, routez la production vers les deux implémentations en parallèle, ne renvoyez que la réponse de l'ancienne, et loguez les divergences. Une semaine de trafic réel vaut mille tests générés. Le jour où le taux de divergence tombe à zéro, la bascule derrière feature flag devient un non-événement.
Et une règle non négociable : aucune nouvelle fonctionnalité pendant la migration d'un composant. Dès que vous mélangez migration et évolution, vous perdez votre référentiel de comparaison, et avec lui toute capacité à dire qui a cassé quoi.
Le prompt de traduction
[RÔLE] Ingénieur Staff, migration [Langage Source] → [Langage Cible].
[CONTEXTE]
Code source : {code_source}
Golden master (entrées/sorties observées en production) : {golden_master}
[MISSION]
Réécris ce code en [Langage Cible] :
1. Idiomes modernes du langage cible (pattern matching, types optionnels, immuabilité).
2. Supprime le code mort ou inatteignable, et LISTE-LE séparément.
3. Nommage explicite (fini les `int t` et `boolean isV`).
4. Le comportement DOIT être strictement identique au golden master,
y compris pour les cas qui te semblent être des bugs.
5. Si un cas du golden master te paraît erroné, ne le corrige pas :
signale-le dans une section "ÉCARTS À ARBITRER".
[OUTPUT] Le code, puis la liste du code mort, puis les écarts à arbitrer.
Le point 5 est le plus important du prompt. Sans lui, le modèle corrige en silence, et vous découvrez la « correction » au premier ticket client.
Où ça s'insère dans votre SDLC
Rien de ce qui précède ne crée un cycle de vie parallèle. Un chantier de migration reste un SDLC mais le poids de ses phases se déplace, et ça a des conséquences très concrètes sur la façon de le piloter.
Votre spécification acquiert un second lecteur, et il ne pardonne rien. Les specs exécutables ne datent pas de l'IA : Gherkin et la living documentation sont des acquis du BDD depuis quinze ans, et un SDLC bien tenu les versionne déjà avec le code. Ce qui change, c'est qui les consomme. Jusqu'ici, la spec était lue par des humains, qui comblaient les trous avec leur connaissance du domaine, une conversation au café, un « on sait tous que ça marche comme ça ». Ce liant tacite disparaît : l'agent lit la spec à la lettre et comble les trous par extrapolation statistique, pas par expérience. Une ambiguïté qu'un développeur senior aurait levée en trois secondes devient une décision d'implémentation arbitraire, appliquée silencieusement sur tout le composant. Conséquence pratique : le niveau de précision exigé d'une spec monte d'un cran, et les zones d'ombre de l'étape 1 cessent d'être une annexe pour devenir un bloquant, vous les traitez avant de lancer la boucle, ou l'agent les tranchera à votre place.
La documentation devient une entrée du système de production. Vos conventions, vos ADR, votre glossaire métier : tout ce que vous n'écrivez pas, l'agent l'invente. Sur une migration, un fichier de contexte projet maintenu à jour (CLAUDE.md, AGENTS.md, un dossier /docs/adr ; le format importe peu) a plus d'impact sur la qualité du code produit que le choix du modèle. C'est vérifiable en une demi-journée : générez le même composant avec et sans, comptez les écarts.
Les tests changent de rôle. Ils ne valident plus seulement en aval : ils constituent le contrat que l'agent doit satisfaire, en amont. Une boucle de génération sans oracle fiable n'a pas de critère d'arrêt, et un agent sans critère d'arrêt produit du code jusqu'à ce que quelqu'un se lasse.
Les indicateurs de pilotage ne sont plus les mêmes. « 34 points livrés ce sprint » ne veut plus rien dire quand le débit d'écriture n'est plus la contrainte. Ce qui pilote réellement un chantier de migration : le score de mutation, le taux de divergence en shadow par composant, et le nombre d'itérations avant convergence.
Deux artefacts s'ajoutent enfin à votre dépôt, et méritent le même traitement que le code : les prompts et rôles d'agents, revus en PR , un prompt modifié change votre chaîne de production autant qu'un changement de compilateur ; et le registre des écarts arbitrés, où chaque « plus juste que l'ancien » tranché avec le métier est consigné. C'est la mémoire du chantier, et la réponse au ticket client dans six mois.
FinOps et confidentialité : le vrai calcul
Reste la contrainte d'exécution. Envoyer 500 000 lignes à une API de frontière, c'est une facture à cinq chiffres pour un résultat médiocre : noyer un modèle sous du contexte non pertinent dégrade sa précision autant que votre budget.
Les leviers, par ordre de rendement :
- Ne jamais envoyer le monolithe. Extraction AST (tree-sitter), repo map, découpage par graphe d'appels. Vous envoyez un composant et ses dépendances directes, pas un répertoire.
- Routing de modèles. Raisonnement architectural et extraction de règles métier : modèles haut de gamme par API. Génération de vecteurs de test, traduction ligne à ligne, renommage : SLM orienté code auto-hébergé. Le coût devient marginal et la latence s'effondre.
- Prompt caching. Sur une migration, le contexte partagé (conventions, schéma, ADR) est identique sur des milliers d'appels. C'est souvent le gain le plus rapide à obtenir.
- L'argument qui tranche en comité, et qui n'est pas financier : votre code legacy contient vos règles de pricing, vos algorithmes de scoring, parfois de la donnée de test non anonymisée. Sur un secteur régulé, l'auto-hébergement n'est pas une optimisation de coût, c'est une condition d'accès au projet.
Ce qui n'a pas changé
L'IA ne remplace pas les concepts d'architecture logicielle des vingt dernières années. Elle les rend enfin applicables à grande échelle. Les tests de caractérisation existaient, mais coûtaient des semaines ; le Strangler Fig existait, mais butait sur l'absence de filet ; la rétro-documentation existait, mais personne ne la finançait.
Ce qui change, c'est le prix du feedback. Ce qui ne change pas, c'est qu'il faut le construire avant de toucher au code, et qu'un test dont la valeur attendue a été imaginée par un modèle n'est pas un test : c'est une opinion avec une assertion autour.