Accueil / Articles

Analyser les refacturations majeures en IA : pourquoi réussir les tests ne suffit pas à percer l’intention initiale supprimée

Découvrez pourquoi les refactorisations assistées par l’IA suppriment silencieusement du code essentiel, pourquoi les tests automatisés ne le détectent pas, ainsi que cinq pratiques d’examen qui permettent de livrer en toute sécurité de grandes refactorisations.

Éditeur : REACTAPP.TOP7 min de lecture1618 mots

Sources: I Let Claude Refactor 4,000 Lines. Here’s the Part I Had to Undo.

Les assistants de codage basés sur l’IA sont désormais suffisamment performants pour nettoyer un module négligé en une après-midi, et le résultat est généralement de meilleure qualité que ce qui l’a remplacé. Le danger ne réside pas dans du code bâclé, mais dans un code qui semble correct tout en supprimant discrètement des fonctionnalités que personne n’a documentées. Ce guide décrit un échec réel de ce type, explique pourquoi à la fois le jeu de tests et l’auditeur ne l’ont pas détecté, et propose une méthode concrète d’évaluation pour les refacturations majeures assistées par l’IA.

Un refactoring qui a amélioré le code mais endommagé le système

Prenons un module qui accumule des modifications depuis deux ans : environ 4 000 lignes réparties sur quatre fichiers, la partie du codebase que les ingénieurs évitent de modifier autant que possible. Il présente un traitement incohérent des erreurs, trois méthodes différentes pour parser les dates, et plusieurs fonctions dont personne ne peut affirmer avec certitude qu’elles sont encore appelées.

Six jours plus tard, une intégration avec un partenaire commence à perdre des enregistrements. Aucune erreur notable n’apparaît ; environ quatre pour cent des enregistrements cessent simplement d’arriver. La cause est le refactoring, et plus précisément un changement que le développeur avait lu et approuvé.

Où l’assistant a véritablement excellé

Il est utile d’être précis sur ce qui a bien fonctionné, car la conclusion n’est pas « l’IA est mauvaise pour le refactoring ». L’assistant s’est montré excellent pour toute transformation dépendant uniquement du code qu’il avait sous les yeux :

  • la fusion des trois fonctions de parsing de dates en une seule fonction
  • transformer les conditions imbriquées en clauses de protection
  • annoter l’ensemble du module avec des indications de type
  • identifier deux branches qui étaient réellement inaccessibles
  • choisir des noms plus clairs
  • Pour des modifications locales et mécaniques qui peuvent être vérifiées en les lisant, c’était plus rapide et plus cohérent qu’un être humain, et cela ne perdait pas de vue le contexte après quelques centaines de lignes, là où les refacturations manuelles ont tendance à décliner. Lorsque la tâche consiste à « faire en sorte que ce code exprime le même comportement de manière plus claire », ces outils se révèlent très efficaces. Les problèmes commencent cependant dès que l’on aborde les limites de ce qu’entend réellement « le même comportement ».

    Pourquoi le code essentiel est supprimé

    Un code qui ne présente aucune raison visible ressemble exactement à du bruit inutile. Voici la modification approuvée :

    # before
    def normalise_timestamp(raw):
        ts = raw.strip()
        if ts.endswith("Z") and "." in ts:
            ts = ts.split(".")[0] + "Z"          # partner sends 7-digit microseconds
        return datetime.fromisoformat(ts.replace("Z", "+00:00"))
    
    # after
    def normalise_timestamp(raw):
        return datetime.fromisoformat(raw.strip().replace("Z", "+00:00"))
    

    La branche supprimée ressemble à un cas typique de refactoring : un cas particulier, une vérification de suffixe étrangement spécifique, et une séparation en chaînes. Elle semble redondante, car fromisoformat analyse déjà les timestamps ISO.

    Le problème réside dans les données d’entrée. Un des partenaires envoie des timestamps avec sept chiffres après la virgule. Avant Python 3.11, fromisoformat ne parvenait à interpréter que les formats générés par isoformat() lui-même, ce qui signifie au maximum six chiffres après la virgule ; par conséquent, une valeur à sept chiffres provoquait une erreur ValueError. Python 3.11 et versions ultérieures prennent en charge un sous-ensemble bien plus large de l’ISO 8601 et sont donc plus tolérants à cet égard. Ainsi, le fait que ce changement provoque ou non des problèmes dépend du système d’exécution, ce qui est une raison de plus de vérifier plutôt que de supposer. Cette branche a été créée en réponse à un ticket de support datant de dix-huit mois auparavant, et son bref commentaire en ligne n’a pas suffi pour que quiconque, humain ou modèle, évalue correctement son impact par rapport à son apparence peu problématique. Remarquez également que l’ancienne branche faisait plus qu’il n’y paraissait : elle éliminait complètement la partie décimale, de sorte que même le résultat de la version « mise à jour » diffère pour les données d’entrée qui se prêtent bien à l’interprétation.

    Le problème fondamental est simple : le modèle pouvait voir le code, mais pas l’incident qui l’a généré. Le ticket, l’analyse post-mortem, la conversation en ligne ainsi que les heures passées à déboguer se trouvaient tous en dehors du fichier. L’exécution de git blame aurait révélé un message de commit du type « fix timestamp parsing for partner feed », qui constituait toute la justification, mais personne n’a demandé à l’assistant de vérifier. Le réviseur a approuvé la suppression pour la même raison que l’assistant l’avait proposée : tous deux examinaient le diff, et non l’historique.

    Pourquoi le jeu de tests est resté vert

    Les tests vérifiaient ce que la fonction renvoyait pour des entrées imaginées par quelqu’un, et l’entrée problématique était, par définition, une entrée que personne n’avait envisagée.

    Les fichiers de configuration contenaient des timestamps avec trois décimales, car c’est ce que les gens tapent lorsqu’ils écrivent manuellement des données de test. Des valeurs à sept chiffres n’apparaissaient que dans le flux en temps réel d’un seul partenaire.

    C’est ce qui rend les gros refacteurs risqués, que l’IA soit impliquée ou non : un ensemble de tests qui passe prouve uniquement que le comportement décrit par vos tests a survécu. Il ne dit rien sur les comportements qu’ils ne décrivent pas. Lorsque vous refactorez trente lignes à la main, vous pensez à chacune d’elles. Lorsque quatre mille lignes changent en même temps, l’ensemble de tests finit par effectuer presque toute la vérification, alors qu’il n’a jamais été conçu pour supporter une telle charge.

    Les métriques de couverture n’ont pas aidé. La fonction avait une couverture totale des lignes : chaque ligne était exécutée pendant les tests. Une branche n’a tout simplement jamais été exécutée avec l’entrée pertinente. La couverture des lignes mesure quel code a été exécuté, et non quels types d’entrée ont été testés.

    Tri des modifications de l’IA par niveau de risque

    • Risque faible : annotations de type, renommages, mise en forme, retours prématurés et autres réécritures mécaniques similaires. Elles sont locales et faciles à vérifier par lecture.
    • Risque modéré : consolidation d’aide-mémoires dupliqués. Généralement sans problème, mais il faut s’assurer que la version fusionnée gère les entrées de chaque appelant.
    • Risque élevé : suppression de code qui semble inutilisé, suppression de cas particuliers qui semblent redondants, et unification de comportements qui semblent incohérents.

    Les modifications à haut risque présentent une caractéristique commune : dans chaque cas, le modèle détecte correctement qu’il y a un problème, et c’est précisément parce que quelque chose semble incorrect qu’il existe.

    Dernièrement, ce problème se manifeste également de manière plus discrète. Imaginez un module qui formate les erreurs différemment des autres, et une refonte visant à le mettre en conformité. Cette différence était intentionnelle, car un consommateur ultérieur analyse précisément cette forme d’erreur. Viser l’uniformité était un instinct judicieux ; l’appliquer dans ce cas a été une erreur.

    Cinq pratiques pour la prochaine grande refonte

    Ces pratiques sont classées approximativement en fonction de leur capacité à prévenir l’incident mentionné ci-dessus.

    Garantir que chaque modification reste suffisamment petite pour être réellement examinée

    Quatre mille lignes ne constituent pas une modification pouvant être examinée en détail. Au-delà de quelques centaines de lignes, la plupart des gens se contentent de repérer les problèmes évidents plutôt que d’analyser chaque partie du code, peu importe ce qu’ils se disent. Limitez chaque session à un seul module et chaque demande de fusion aux modifications d’un seul module. Le goulot d’étranglement n’est pas la capacité de traitement du modèle, mais l’attention du réviseur.

    Exiger une justification pour chaque suppression

    Fournir à le modèle l historique

    git blame pour chaque ligne qu il propose de supprimer et de rapporter le message associé au commit. La raison d une branche inhabituelle y est souvent indiquée, et le modèle en tire grand profit lorsqu il dispose de ce contexte ; il ne cherchera pas automatiquement ces informations sans sollicitation.

    Rédiger des commentaires expliquant le "pourquoi" avant de refactorer

    Ajoutez d’abord des commentaires explicatifs, et non après. Un commentaire précis indiquant le partenaire concerné, les fractions à sept chiffres ainsi que la limite de six chiffres du analyseur aurait probablement empêché cette suppression. Une note vague concernant les horodatages du partenaire n’a pas eu cet effet. Si la personne ou l’outil qui lit ensuite le code ne comprend pas pourquoi une ligne existe, cette ligne est sur le point de disparaître lors d’une prochaine opération de nettoyage.

    Comparer l’ancien et le nouveau avec du trafic réel

    Faites passer les données de production d’une journée via tant l’implémentation ancienne que la nouvelle, puis comparez ce que chacune renvoie. Pour des transformations simples comme l’analyse et la normalisation, cela coûte peu d’efforts et permet de détecter précisément le type de problème que les fichiers de test ne repèrent pas, car le trafic réel contient des entrées que personne n’a pensé à consigner par écrit. Si il n’est pas possible de rejouer le trafic réel, l’échantillonnage de charges utiles réelles pour créer des fichiers de test de régression constitue la meilleure alternative.

    Points clés

    La refonte valait toujours la peine d’être effectuée : le module est amélioré, le travail a pu être accompli en une après-midi plutôt qu’en une semaine, et une seule régression sur quatre mille lignes modifiées est bien moins grave que de nombreuses refontes manuelles complexes. qualifier le résultat de « erreur de l’IA » revient à ne pas comprendre la situation. L’IA a tiré une conclusion raisonnable à partir de ce qu’elle pouvait voir, un cas particulier sans explication donnée, et l’humain chargé de la revue a abouti à la même conclusion en examinant le même artefact incomplet.

    • Le répertoire n’est qu’une partie du système ; les raisons derrière des codes étranges se trouvent dans les tickets, les messages de commit, les analyses post-mortem ainsi que dans la mémoire des personnes.
    • Un outil qui ne voit que le code supprimera avec assurance tout élément dont la justification n’a jamais été consignée.
    • Des tests positifs et une couverture complète des lignes ne prouvent pas que le comportement est conservé pour des entrées qui n’ont jamais été modélisées.
  • Trier les différences générées par l’IA en fonction du niveau de risque, exiger des justifications écrites pour les suppressions, et fournir à le modèle l’historique correspondant.
  • Noter les raisons de l’existence de code inhabituel était déjà une bonne pratique bien avant le refactoring par IA ; c’est aujourd’hui la mesure de protection la moins coûteuse dont on dispose.