Dérive des conventions.
Code généré par IA qui ignore discrètement les conventions établies d'un dépôt — réinventant des helpers, choisissant la mauvaise bibliothèque et adoptant un nommage et une gestion d'erreurs non conformes au style maison — de sorte que la base de code dérive vers un style générique, moyen-de-l'internet.
##Signs and Symptoms
Un relecteur reconnaît la dérive des conventions quand un changement de l'IA fonctionne mais n'a pas l'air d'appartenir à la base de code. Il se rabat sur le pattern moyen mondial au lieu du pattern local :
- Un nouveau helper est écrit inline alors qu'un utilitaire éprouvé existe déjà (
utils/date.ts,lib/apiClient, un typeResultpartagé). - Une bibliothèque ou une API différente du standard du dépôt apparaît (
axiosdans une app qui a standardisé sur un wrapperfetch;momentlà où le dépôt utilisedate-fns). - Le nommage, l'agencement des fichiers et le style de gestion des erreurs divergent —
camelCaselà où le module est ensnake_case, unthrow new Error("...")brut là où tout le reste renvoie une erreur typée, untry/catchsur mesure avec logs au lieu du logger partagé. - Chaque nouvelle exigence reçoit son propre bloc flambant neuf et étroitement taillé plutôt que de réutiliser une abstraction (la « sur-spécification » d'OX Security, présente dans 80–90 % du code IA).
- Signature symptomatique : les blocs dupliqués se multiplient. GitClear a mesuré un bond de 8× des blocs de clones de 5+ lignes en 2024, les lignes copiées-collées dépassant pour la première fois les lignes déplacées (refactorisées).
// Style maison (déjà dans le dépôt)
import { apiClient } from "@/lib/apiClient"; // encapsule auth, retries, URL de base
import { formatDate } from "@/utils/date"; // à l'échelle de l'app, sensible à la locale
// Changement généré par IA — dérive loin des deux
import axios from "axios"; // pas un pattern de dépendance du projet
async function getUser(id: string) {
try {
const res = await axios.get(`https://api.example.com/users/${id}`); // URL de base codée en dur
return { ...res.data, joined: new Date(res.data.joined).toLocaleDateString() }; // réinvente formatDate
} catch (e) {
console.log("error", e); // pas le logger partagé ; avale l'erreur
}
}
L'indice est la cohérence, pas l'exactitude : trois relecteurs trouvent chacun un choix différent « faux-mais-fonctionnel », et aucun ne correspond au fichier d'à côté.
##Reasons for the Problem
Pourquoi les modèles le produisent
- Régression vers la moyenne d'entraînement. Les LLM sont entraînés sur un vaste corpus de code moyen-de-l'internet, pas sur votre dépôt. Sans instruction, ils émettent l'idiome statistiquement le plus courant (
axios,moment,console.log), pas votre idiome maison — vos conventions sont un signal minuscule et hors distribution à côté de la moyenne mondiale. - Cohérence locale du token suivant plutôt que portée sur le dépôt. Il est localement plus simple de compléter un bloc autonome (insérer un formateur de date inline) que de « savoir » que
@/utils/dateexiste et d'importer un identifiant que le modèle n'a jamais vu. La réutilisation exige un contexte de dépôt que le modèle ne possède pas ; la réinvention ne requiert que le tampon courant. - Contexte du dépôt limité / lacunaire. Le helper canonique, la config du linter et l'ADR qui dit « utilise le wrapper fetch » sont généralement hors du prompt. Le modèle ne peut pas suivre une convention qu'on ne lui a jamais montrée. Comme l'a formulé Eno Reyes de Factory, une grande partie d'une convention est tacite — des patterns que les humains absorbent en lisant la base de code et que les agents ne voient tout simplement jamais.
- Complaisance / focalisation littérale sur la tâche. À qui on demande « d'ajouter
getUser», le modèle fait exactement cela et ne dira pas spontanément « en fait nous avons déjà un client pour ça ». Il optimise pour l'accomplissement de la tâche énoncée, pas pour l'intégration au système. La « fixation sur le manuel » d'OX Security (80–90 % des échantillons) est la même force : il suit une convention générique de manuel plutôt que d'évaluer celle du projet. - Obsolescence liée à la date limite d'entraînement. Si le dépôt a migré vers une bibliothèque ou un pattern plus récents après la date limite du modèle, le modèle réintroduit avec assurance la version sur laquelle il a été entraîné.
Pourquoi c'est nuisible
- Maintenabilité & charge cognitive. Chaque choix dérivé est une façon de plus de faire la même chose. Les lecteurs doivent garder en tête N variantes de « comment nous formatons les dates » ; la base de code perd sa source unique de vérité.
- Exactitude lors des changements. La logique dupliquée/réinventée diverge silencieusement — un bug corrigé dans le helper partagé n'est pas corrigé dans la copie de l'IA. GitClear relie l'explosion des clones directement aux assistants IA qui rendent le tab-pour-insérer moins coûteux que la réutilisation, tandis que la part de la refactorisation dans les changements est tombée de 25 % (2021) à moins de 10 % (2024).
- Sécurité. Une validation, une authentification ou une construction de requête réinventées contournent le chemin partagé durci (URL de base codées en dur, erreurs avalées, SQL ad hoc en chaînes). OX qualifie l'effet agrégé d'« armée de juniors » : rapide, fonctionnel, sans jugement architectural.
- Charge de revue & accumulation de dette technique. La dérive n'est pas attrapée par les tests (le code fonctionne), elle atterrit donc en revue ou pas du tout, s'agrégeant dans le smell architectural « Fonctionnalité éparpillée / Mirage modulaire » où un comportement apparenté est fragmenté entre fichiers sans réelle cohésion.
##Treatment
Tactiques de prompting / de workflow
- Montrez les conventions. Placez les règles là où l'agent les lit — un fichier
CLAUDE.md/AGENTS.md/de règles listant le client sanctionné, le logger, le type d'erreur, le nommage et « utilise X et non Y ». Les conventions que le modèle ne voit pas, il ne peut pas les suivre. - Pointez vers le code canonique. « Utilise
@/lib/apiClientet@/utils/date; n'ajoute pas de nouvelles bibliothèques HTTP. Aligne-toi sur la gestion d'erreurs deservices/orders.ts. » Donnez l'idiome du dépôt en few-shot en collant un fichier exemplaire. - Demandez avant qu'il n'écrive. « Quels helpers/abstractions existants couvrent ceci ? Réutilise-les ; n'ajoute du nouveau code que si aucun ne convient. » Cela convertit la réinvention en réutilisation en amont.
- Faites du linter la barrière. Exigez que l'agent exécute le formateur, le linter et le vérificateur de types et corrige tous les constats avant de rendre. Le conseil de Factory est de systématiser les signaux de qualité (lint/format/type-check/test) pour que l'agent optimise vers eux au lieu de dériver.
- Revoyez pour l'adéquation, pas seulement la fonction. Examinez les imports à la recherche de bibliothèques non sanctionnées ; cherchez (grep) la logique qui aurait dû appeler le helper partagé ; exécutez un détecteur de copier-coller sur la PR.
Le refactoring — nommez les manœuvres classiques : Supprimer la duplication / Remplacer du code inline par un appel de fonction, Substituer un algorithme, et Extraire une fonction si la bonne abstraction n'existe pas encore.
// AVANT — dérivé, réinvente le client + la logique de date, URL codée en dur, erreur avalée
import axios from "axios";
async function getUser(id: string) {
try {
const res = await axios.get(`https://api.example.com/users/${id}`);
return { ...res.data, joined: new Date(res.data.joined).toLocaleDateString() };
} catch (e) {
console.log("error", e);
}
}
// APRÈS — réutilise les conventions établies
import { apiClient } from "@/lib/apiClient";
import { formatDate } from "@/utils/date";
async function getUser(id: string) {
const user = await apiClient.get<User>(`/users/${id}`); // URL de base, auth, retries, erreurs typées gérées ici
return { ...user, joined: formatDate(user.joined) };
}
Si le même bloc dérivé apparaît dans plusieurs PR, c'est le signe que la convention est indécouvrable — corrigez la cause racine en la documentant dans le fichier de règles et/ou en l'exposant via un point d'entrée unique et évident, plutôt qu'en re-révisant chaque instance.
##Detected by
- jscpd duplication threshold (min-lines / min-tokens) — Détection de copier-coller
- PMD CPD CPD duplicated code blocks — Détecteur de copier-coller
- ESLint no-restricted-imports — no-restricted-imports
- typescript-eslint @typescript-eslint/naming-convention — naming-convention
- SonarQube Source files should not have any duplicated blocks — Blocs dupliqués