Aller au contenu
Garder la main
Sommaire
04Chapitre 4·45 min·Intermédiaire / Avancé

Legacy

Reprendre la main sur ce que personne ne comprend plus

Ton vrai codebase n'a ni tests, ni conventions écrites, ni personne qui sache pourquoi. Ce qui change, et dans quel ordre s'y prendre.

Ce que tu sauras faire
  • 01Voir pourquoi la méthode du module précédent ne tient pas telle quelle sur du legacy
  • 02Se servir de l'agent pour reconstituer la connaissance que plus personne n'a
  • 03Poser un filet de tests là où il n'y a rien à vérifier
  • 04Extraire les conventions d'un codebase au lieu de les inventer

1. Pourquoi la méthode ne tient pas telle quelle

Le module précédent se termine sur cinq règles d'or. Sur du legacy, trois tombent d'un coup.

« Donne toujours à l'IA un moyen de vérifier son travail. » Il n'y a pas de tests. Ou il y en a, et ils sont rouges depuis 2019 parce que personne n'a jamais eu le budget pour les réparer.

« Pas de code sans plan validé par un humain. » Valider un plan suppose que tu saches ce que le code fait aujourd'hui. Sur un module que tu ouvres pour la première fois, tu n'en sais rien : tu vas valider un plan que tu n'es pas en mesure de juger.

« Les specs vivent dans le repo. » Il n'y a pas de spec. La seule spec, c'est le code, et il ment : il contient autant de décisions volontaires que d'accidents que personne n'a jamais nettoyés.

Et il y a un piège de plus, spécifique au legacy. Sur un projet moderne, quand le modèle se trompe, ça se voit : il propose un pattern qui jure avec le reste du code. Sur du legacy, c'est l'inverse. Ses propositions ont l'air meilleures que l'existant. Plus propres, plus modernes, mieux nommées. Ce n'est pas le signe qu'il a raison, c'est le signe qu'il applique le web moyen à un code qui, lui, a des raisons. Le if bizarre en haut de la fonction est peut-être un contournement pour un client qui envoie encore du XML depuis 2011. L'agent ne peut pas le deviner, et il le supprimera avec l'assurance de quelqu'un qui nettoie.

Ce que supposait le Module 3Ce que tu as vraimentCe qu'on fait à la place
Des tests qui passentPas de tests, ou rouges depuis longtempsOn pose un filet de caractérisation (§4)
Des conventions écritesRien, ou un wiki abandonnéOn les extrait du code (§5)
Quelqu'un qui sait pourquoiL'auteur est parti il y a trois ansOn interroge l'historique Git (§3)
Un périmètre de changement clairTout est couplé à toutAnalyse d'impact avant la moindre ligne (§6)

2. Sur du legacy, l'agent lit avant d'écrire

Sur du code neuf, la valeur de l'IA c'est qu'elle écrit vite. Sur du legacy, sa première valeur c'est qu'elle lit vite, et c'est là que le rapport risque/rendement est le meilleur de tout le guide.

Regarde les deux côtés. En lecture seule, le risque est nul : l'agent ne peut rien casser, tu n'as même pas besoin de relire un diff. Et le gain est énorme : reconstituer en une heure la carte d'un module que plus personne ne comprend, c'est un travail qui prend des jours à un humain seul, et que personne ne fait jamais parce que personne n'a le temps.

Alors la première session sur un codebase inconnu se passe entièrement en lecture seule. Chaque outil a sa façon de le garantir : un mode dédié qui interdit l'écriture, un profil de permissions restreint, ou à défaut une branche jetable sur laquelle tu peux tout jeter. Chez Claude Code, c'est le plan mode :

claude --permission-mode plan

Et si tu veux une garantie plus dure qu'un mode qu'on peut quitter par mégarde, ferme les outils d'écriture au niveau des permissions le temps de l'exploration. Le principe vaut partout, la syntaxe est propre à chaque outil :

// .claude/settings.local.json — le temps de la phase d'archéologie
{
  "permissions": {
    "deny": ["Edit(**)", "Write(**)", "Bash(git commit:*)"]
  }
}

Si ton outil n'offre ni l'un ni l'autre, la version rustique fonctionne : tu explores sur une branche dédiée, et tu la supprimes en sortant. Ce qui compte n'est pas le mécanisme, c'est qu'aucune écriture de cette phase ne puisse atteindre ton code.


3. L'agent comme archéologue

Cartographier

Le premier livrable, ce n'est pas de la doc. C'est une carte : par où ça entre, par où ça sort, et où sont les mines.

Cartographier un module dont personne ne sait plus rien
Tu explores un module legacy que plus personne dans l'équipe ne connaît. Personne ne pourra corriger tes erreurs : signale ce dont tu n'es pas sûr.Analyse @[répertoire/module] et livre : Les points d'entrée : qui appelle ce code depuis l'extérieur, et par où. Le flux principal, en 10 lignes maximum. Pas de paraphrase du code. Les effets de bord : écritures en base, fichiers, appels réseau, état global, événements. Les endroits qui te surprennent : code mort probable, contournements, valeurs en dur, commentaires qui contredisent le code. Ce que tu n'as pas réussi à comprendre. Cette liste est la plus importante des cinq : ne la laisse pas vide pour faire bonne figure. N'écris aucun code. N'invente aucun comportement : si tu n'as pas lu la ligne qui le prouve, écris « hypothèse » devant.

Retrouver le pourquoi

Le code dit ce que ça fait. Il ne dit jamais pourquoi. Sur du legacy, la seule trace qui reste du pourquoi est dans l'historique Git, et c'est une source que l'agent sait exploiter beaucoup mieux qu'un humain pressé.

Interroger l'historique pour comprendre une bizarrerie
Cette portion de code me paraît absurde : @[fichier], lignes [x-y]. Avant de conclure qu'elle est inutile, cherche pourquoi elle existe.Utilise l'historique : git log -L [x],[y]:[fichier] — l'évolution de ces lignes précises git log -S "[chaîne caractéristique]" — le commit qui a introduit ce comportement git log --follow [fichier] — l'historique malgré les renommages Puis dis-moi : quel commit l'a introduite, quel message l'accompagne, et quel problème elle avait l'air de résoudre. Si les messages de commit ne disent rien d'utile, dis-le franchement plutôt que d'inventer une justification plausible.

Reconstituer le vocabulaire métier

Les vieux codebases sont pleins de noms que plus personne ne sait lire : flagStatutB, traiterLot2, une table PARAM_GEN. Ce vocabulaire est la vraie clé du domaine, et il n'est écrit nulle part.

Demande à l'agent le lexique avant la doc : « Liste les termes métier récurrents dans ce module, avec pour chacun ce que le code laisse deviner de sa signification, et ton niveau de confiance. » Tu obtiens en dix minutes une liste que tu peux aller faire valider par la personne du métier qui est encore là. C'est souvent la conversation la plus rentable du projet.


4. Poser le filet : les tests de caractérisation

C'est l'idée centrale de Working Effectively with Legacy Code de Michael Feathers, et elle est plus utile que jamais maintenant qu'on a une machine pour faire le travail fastidieux.

Un test de caractérisation ne teste pas ce que le code devrait faire. Il fige ce qu'il fait, aujourd'hui, bugs compris. Ce n'est pas un test de qualité, c'est une alarme : si le comportement change, tu le sais. C'est exactement ce qui te manque pour refactorer sans trembler.

L'IA est très bonne à ce jeu, parce que c'est mécanique, volumineux et pénible : trois qualificatifs qui décrivent ce qu'on veut déléguer.

Générer un filet de caractérisation
Objectif : figer le comportement actuel de @[fichier/fonction], pas l'améliorer.Écris des tests [Vitest/Jest/pytest/Pest] qui capturent ce que le code fait réellement aujourd'hui : Un cas nominal représentatif. Les branches conditionnelles : un test par chemin, y compris ceux qui ont l'air absurdes. Les valeurs limites et les entrées dégénérées (null, vide, zéro, négatif). Ce que la fonction fait quand elle échoue : exception, valeur de retour, effet de bord partiel. IMPORTANT: si un comportement te semble être un bug, tu l'encodes tel quel dans le test et tu ajoutes un commentaire // comportement actuel, à discuter. Tu ne le corriges pas. Tu ne l'améliores pas. Un test qui décrit le code voulu au lieu du code réel ne sert à rien ici.Nomme chaque test d'après le comportement observé, pas d'après l'intention supposée.

Vérifier que le filet en est un

Un filet de tests qui reste vert quoi qu'il arrive ne protège rien. Le contrôle prend deux minutes et il n'est pas négociable : va casser le code exprès.

# Change un opérateur au hasard dans la fonction couverte (> devient >=,
# && devient ||, un + devient un -), puis relance la suite.
npm test -- [fichier]

# Au moins un test doit passer au rouge. Si tout reste vert, tes tests
# n'observent rien de ce qui compte. Reprends-les avant d'aller plus loin.
git checkout [fichier]   # on remet la mutation en place

C'est le principe du test de mutation, appliqué à la main. Sur un module critique, ça vaut la peine de brancher un vrai outil (Stryker en JS, mutmut en Python, Infection en PHP), mais la version manuelle suffit à disqualifier un filet inutile.


5. Extraire les conventions au lieu de les inventer

Le module suivant t'apprend à écrire ton fichier de contexte. Il suppose que tu connais tes conventions. Sur du legacy, tu ne les connais pas : elles n'ont jamais été écrites, et une partie n'a jamais été décidée.

Alors on inverse le geste : l'agent fait un premier jet à partir du code réel, et toi tu tranches.

Extraire un premier jet de conventions
Lis un échantillon représentatif du code : @[3 ou 4 fichiers de couches différentes].Déduis les conventions réellement appliquées : nommage, structure des fichiers, gestion des erreurs, accès aux données, validation des entrées, style de tests.Pour chaque convention trouvée, indique : Sa formulation en une ligne. Sur combien de fichiers de l'échantillon elle tient. Si elle est systématique, majoritaire ou ponctuelle. Ne me dis pas ce qui serait une bonne pratique. Dis-moi ce que ce code fait. Les deux listes ne se ressemblent pas, et c'est la seconde qui m'intéresse.

Ce que tu récupères est une matière première, pas un fichier de contexte. Le tri qui suit, personne ne peut le faire à ta place, parce qu'il demande de savoir ce que l'équipe veut devenir. Trois catégories :

  • Ce qu'on garde et qu'on impose. Les conventions qui tiennent, même si elles ne sont pas à la mode.
  • Ce qu'on tolère sans l'étendre. Le code existant reste comme il est, mais on n'en écrit plus de nouveau.
  • Ce qu'on interdit désormais. Les patterns qu'on a décidé de faire disparaître.

Cette deuxième catégorie est ce qui distingue un fichier de contexte de legacy d'un fichier de contexte de projet neuf. Sans elle, l'agent va « améliorer » l'ancien code à chaque passage, et tes diffs deviendront illisibles.

## Conventions de ce codebase

### On fait comme ça, on n'y touche pas
- Les repositories retournent des tableaux associatifs, pas des entités.
  C'est ancien et ce n'est pas ce qu'on ferait aujourd'hui. Ne le change pas
  au passage : une centaine d'appelants en dépendent.
- La casse des colonnes en base est incohérente selon les tables. C'est ainsi.

### Nouveau code uniquement
- Toute nouvelle requête passe par le QueryBuilder, jamais par du SQL concaténé.
- Tout nouveau service a ses tests. On ne rattrape pas l'existant, on ne l'aggrave pas.

IMPORTANT: ne refactorise jamais du code qui n'est pas dans le périmètre
demandé, même s'il te paraît mauvais. Signale-le, ne le touche pas.

6. Modifier : un seul type de changement par diff

La règle tient en une phrase : un diff, une nature de changement. Refactoring, ou correction de bug, ou nouvelle fonctionnalité. Jamais deux ensemble.

Ce n'est pas de la coquetterie de reviewer. Sur du legacy, c'est ce qui rend le rollback possible. Quand un diff qui mélange un refactoring et un correctif casse la prod à 18 h, tu ne sais pas quoi annuler : tu perds le correctif en annulant le refactoring. Quand les deux sont séparés, tu annules l'un et tu gardes l'autre.

L'ordre qui marche, sur du code non testé :

Avec méthode
  1. Filet de caractérisation (tests verts)
  2. Refactoring à comportement constant (tests toujours verts)
  3. Le changement voulu (un test rouge, puis vert)
  4. Nettoyage, si on en a encore envie

L'agent tient très bien cette discipline si tu la lui donnes explicitement. Il ne la tient jamais tout seul : par défaut, il améliore ce qu'il croise.

Cadrer une modification sur du legacy
Contexte : code ancien, peu testé, que je connais mal. La prudence prime sur l'élégance.À faire : [le changement, précisément]. Périmètre autorisé : @[fichier] uniquement.Contraintes : Ne modifie aucun autre fichier. Si le changement en impose un autre, arrête-toi et dis-le-moi avant. Ne renomme rien, ne réindente rien, ne réorganise aucun import. Ne « corrige » aucun code alentour, même manifestement fautif. Liste-le en fin de réponse, je déciderai. Les tests existants restent verts. S'ils ne le sont pas déjà, dis-le avant de commencer. Livre le diff le plus petit qui fasse le travail.

Quand le module est trop abîmé pour être réparé

Il arrive qu'un module ne se refactore pas : trop couplé, trop long, personne ne comprend. Dans ce cas on n'y touche pas, on construit à côté et on bascule les appelants un par un. C'est le strangler fig, et c'est le seul mode où l'IA travaille confortablement sur du legacy, parce que le code neuf qu'elle écrit est un projet propre : tu peux lui donner des tests, des conventions et une spec.

La bascule reste ton travail, appelant par appelant, avec l'ancien module intact tant que le dernier n'est pas passé. Le jour où plus rien ne l'appelle, tu le supprimes, et l'agent est très bon pour te prouver que plus rien ne l'appelle.


7. Le cas de la stack morte

Angular 1.x, PHP 5.6, Struts 2, un ORM maison écrit en 2014. Ici le knowledge cutoff joue contre toi dans un sens auquel on ne pense pas : on retient qu'un modèle est faible sur ce qui est trop récent, on oublie qu'il l'est aussi sur ce qui est trop ancien. Sur un framework mort, ce qui domine ses données d'entraînement, ce sont de vieux tutoriels approximatifs et des réponses Stack Overflow contradictoires, pas une doc à jour.

Trois réflexes :

  • Épingle la version dans le contexte, explicitement. « PHP 5.6, pas 8. Les fonctions et la syntaxe introduites après 5.6 n'existent pas ici. » Sans ça, le modèle glisse vers la version moderne sans prévenir, et le code ne tourne pas.
  • Injecte les signatures plutôt que d'espérer. C'est l'injection just-in-time du Module 2 : pour ton ORM maison, le modèle n'a strictement rien. Colle-lui l'interface, et interdis-lui toute méthode absente de la liste.
  • Méfie-toi des améliorations spontanées. Sur une stack morte, une suggestion qui « modernise » est souvent une suggestion qui ne compile pas.

8. Ce que tu ne délègues pas

Le legacy déplace la frontière posée au Module 0. Certaines zones se pilotent, elles ne se délèguent pas.

Checklist0 / 5

Le Lab 1 du Module 10 déroule tout ce module sur un vrai projet, en une heure : la session en lecture, l'extraction des conventions, le premier filet, et la mesure de ce que ça coûte en contexte. C'est le meilleur endroit pour commencer, et de préférence sur ton projet le moins reluisant.