L’historique doit vivre dans Git, pas dans les noms de fichiers
Une méthode pour faire évoluer un produit sans empiler `v2`, `fixes`, overrides et copies parallèles jusqu’à transformer chaque nouvelle modification en travail d’archéologie.
Sur Brand OS, j’ai réussi en quelques itérations à recréer un problème extrêmement classique : v02.css, v03.css, v04.css, v05.css, puis un fichier de fixes.
Chaque modification était défendable prise isolément. Il fallait aller vite, ne pas casser ce qui fonctionnait et corriger une nouvelle série de retours. Mais l’ensemble produisait une architecture absurde : pour comprendre le style final d’un composant, il fallait connaître l’ordre historique des corrections.
La règle que j’en ai tirée est simple : Git garde l’histoire ; le code actif décrit l’état actuel du système.
Cette phrase paraît évidente. Elle change pourtant beaucoup la manière d’itérer.
Comment naît la dette d’itération
La dette d’itération apparaît rarement sous la forme d’une énorme mauvaise décision.
Elle ressemble plutôt à ceci :
styles.css
styles-v2.css
styles-v3.css
mobile-fixes.css
final.css
final-fixes.cssOu côté composants :
ProjectCard.tsx
ProjectCardV2.tsx
ProjectCardNew.tsx
ProjectCardFinal.tsxOu encore côté logique :
calculateScore()
calculateScoreNew()
calculateScoreLegacySafe()
calculateScoreFinal()Le motif est toujours proche : au lieu de modifier la source qui possède la responsabilité, on ajoute une couche capable de contourner l’existant.
À court terme, cette couche paraît moins risquée. À long terme, elle rend la responsabilité impossible à localiser.
Le problème n’est pas seulement esthétique
On peut penser qu’un dossier rempli de v2 et de fixes est surtout désagréable à lire.
Le vrai coût est ailleurs.
Chaque modification doit comprendre la chronologie
Un nouvel agent ou développeur doit déterminer :
- quel fichier est encore actif ;
- quelle règle écrase quelle autre ;
- quelle version est historique ;
- quelles exceptions sont encore nécessaires ;
- ce qui peut être supprimé sans régression.
Le coût de compréhension augmente avec chaque patch.
Les sources de vérité se multiplient
Deux fonctions calculent presque la même chose. Trois fichiers déclarent la même couleur. Deux composants représentent le même concept avec des props différentes.
À partir de ce moment, une correction peut être faite au mauvais endroit tout en ayant l’air correcte localement.
Les tests deviennent moins fiables comme documentation
Si plusieurs chemins historiques restent actifs, les tests peuvent protéger un comportement sans expliquer quel chemin est réellement canonique.
On finit par tester la compatibilité avec sa propre dette plutôt que le produit cible.
Identifier le propriétaire d’une responsabilité
Avant de modifier un système existant, je cherche maintenant son owner sémantique.
Exemples :
- les tokens partagés appartiennent au design system ;
- le responsive d’une surface appartient au fichier responsive défini par la convention du repo ;
- une règle de scoring appartient au moteur ou au contrat de scoring ;
- la lecture de contenu appartient au content loader ;
- la présentation d’un statut appartient à une primitive de présentation réutilisable.
L’important n’est pas le nom exact du fichier. L’important est qu’une personne puisse répondre à la question : si je veux changer cette règle, où dois-je aller ?
Si la réponse est « ça dépend de quelle version du composant tu regardes », le système a déjà un problème de responsabilité.
Modifier la source plutôt qu’ajouter une surcouche
Prenons un exemple CSS.
Mauvaise trajectoire :
base.css -> règle originale
v04.css -> nouvelle taille
v05.css -> correction desktop
v05-fixes.css -> exception iPadLa page peut être visuellement correcte. Mais son comportement dépend de l’ordre d’import de quatre couches.
La consolidation consiste à déterminer la règle actuelle, puis à la remettre dans le propriétaire adéquat :
design-system.css -> tokens et échelle
base.css -> structure publique
projects.css -> composants projet
responsive.css -> adaptations de viewportLes fichiers d’itération disparaissent. Git conserve leur histoire si l’on a besoin de comprendre pourquoi une valeur a changé.
Supprimer fait partie de la feature
Un anti-pattern très courant est :
« On implémente la nouvelle version maintenant, puis on nettoiera l’ancienne plus tard. »
Parfois une migration progressive est réellement nécessaire. Mais lorsque le nouveau chemin remplace entièrement l’ancien dans le périmètre de la tâche, le nettoyage fait partie du changement.
Sinon le projet conserve deux états :
- celui que l’on veut utiliser ;
- celui que l’on n’ose pas encore supprimer.
Et la prochaine session doit payer le coût de cette hésitation.
J’utilise donc cette règle :
si ma modification rend du code obsolète et que rien ne dépend encore de lui, sa suppression fait partie de la Definition of Done.
DRY ne signifie pas « tout abstraire »
À l’autre extrême, vouloir éviter toute duplication peut produire des abstractions beaucoup trop tôt.
Deux composants qui se ressemblent visuellement ne partagent pas forcément la même responsabilité. Deux règles métier proches peuvent diverger volontairement.
Je distingue donc deux situations.
Duplication accidentelle
Même règle, même raison d’exister, deux copies qui doivent évoluer ensemble.
Exemple : deux tables différentes qui traduisent le statut active en « Actif ».
Ici, factoriser réduit clairement le risque.
Similarité provisoire
Deux choses se ressemblent aujourd’hui mais possèdent des responsabilités différentes.
Exemple : une carte de projet publique et une carte Workbench peuvent partager quelques tokens tout en ayant des besoins différents.
Créer immédiatement un composant universel à vingt variantes peut rendre le système plus complexe que la duplication temporaire.
La règle que j’utilise est : factoriser une responsabilité répétée, pas une ressemblance visuelle.
Attendre le deuxième cas concret
Pour les abstractions non évidentes, j’aime attendre qu’un deuxième cas réel apparaisse.
Le premier cas montre qu’un besoin existe.
Le deuxième permet de comparer :
- ce qui est vraiment commun ;
- ce qui est spécifique ;
- quelle API aurait du sens ;
- quelles différences doivent rester visibles.
Cette méthode évite de construire un framework interne à partir d’une seule feature.
Elle ne s’applique évidemment pas aux primitives déjà évidentes : un token de couleur, une fonction de parsing commune ou un type métier canonique n’ont pas besoin de deux implémentations divergentes avant d’être partagés.
Une migration temporaire a besoin d’une sortie
Certaines surcouches sont légitimes.
Exemples :
- migration de données progressive ;
- support temporaire d’un ancien format ;
- coexistence V1/V2 pendant une bascule ;
- feature flag de transition ;
- compatibilité API avec plusieurs clients.
La différence avec une dette accidentelle est que la couche transitoire possède :
- une raison explicite ;
- un propriétaire ;
- une condition de suppression ;
- idéalement une validation qui permet de savoir quand la retirer.
Sans condition de sortie, « temporaire » devient souvent une architecture permanente.
Le rôle des fichiers sémantiques
Je préfère des fichiers nommés selon leur responsabilité plutôt que selon leur histoire.
Bon :
design-system.css
projects.css
billing.ts
content-loader.ts
runtime-contract.mdMauvais :
v06.css
final-projects.css
billing-new.ts
content-loader-fixed.ts
runtime-contract-v3-final.mdLe premier groupe explique ce que contient le fichier.
Le second explique seulement quand il a été créé.
Or la prochaine personne veut savoir où vit la responsabilité, pas dans quel sprint elle a été corrigée.
Git doit assumer son rôle d’archive
Une raison pour laquelle on conserve les anciennes versions dans le code est la peur de perdre l’histoire.
Mais Git est précisément conçu pour répondre à :
- qu’est-ce qui existait avant ?
- qui a changé cette ligne ?
- dans quel commit ?
- pourquoi cette PR a-t-elle été mergée ?
Le code actif n’a donc pas besoin d’embarquer sa propre archéologie.
Cela ne signifie pas supprimer les décisions durables. Le rationale important doit rester dans la documentation appropriée. Mais l’ancienne implémentation n’a pas besoin de rester active pour prouver qu’elle a existé.
La qualité de production ne veut pas dire sur-architecture
Quand je parle d’une logique « production-grade », je ne parle pas de microservices, de Design Patterns partout ou de pipelines disproportionnés.
Pour un petit projet, une base de qualité peut être très simple :
- responsabilités lisibles ;
- une source de vérité par règle ;
- conventions stables ;
- suppression du code mort ;
- tests là où une régression coûte réellement ;
- lint et typecheck ;
- build reproductible ;
- responsive regardé sur les devices concernés ;
- documentation minimale de reprise.
La sophistication n’est pas la qualité. La capacité à modifier le système sans surprise est un meilleur indicateur.
Automatiser les invariants faciles à vérifier
Une règle importante mais entièrement manuelle finit souvent par être oubliée.
Sur Brand OS, après avoir supprimé les feuilles v02.css à v05-fixes.css, j’ai ajouté un petit contrôle de repo qui refuse les stylesheets nommées comme des couches d’itération.
Le build exécute ce contrôle avant Next.
Ce script ne garantit évidemment pas que l’architecture est bonne. Il protège seulement un invariant mécanique : ne pas recréer exactement la forme de dette que nous venons de supprimer.
J’essaie d’appliquer la même logique ailleurs :
- vérifier les fichiers inattendus ;
- valider les schémas de contenu ;
- empêcher les imports interdits ;
- tester les invariants métier importants ;
- faire échouer le build quand une règle structurelle simple est violée.
Les humains gardent les jugements complexes. La machine protège les rails évidents.
Exemple : frontière client / serveur
Une autre forme de dette arrive lorsqu’un module pratique devient propriétaire de trop de responsabilités.
Sur Brand OS, le loader de contenu utilise node:fs pour lire les fichiers Markdown. Un composant client avait besoin d’une simple fonction de présentation du statut et l’importait depuis ce même module.
Résultat : le navigateur se retrouvait indirectement dépendant d’un module serveur.
La correction durable n’était pas d’ajouter une exception de bundling. C’était de séparer :
- le loader serveur ;
- les helpers de présentation browser-safe.
Cette séparation rend le propriétaire plus précis et empêche la même frontière de casser ailleurs.
Une Definition of Done orientée dette
Avant de considérer une évolution significative terminée, je vérifie maintenant :
- La nouvelle fonctionnalité ou correction est-elle complète ?
- Ai-je modifié la source propriétaire plutôt qu’ajouté une surcouche ?
- Le code devenu inutile a-t-il été supprimé ?
- Ai-je créé une nouvelle source de vérité concurrente ?
- Une abstraction est-elle justifiée par des cas réels ?
- Les validations pertinentes passent-elles ?
- Les devices ou états concernés ont-ils été réellement vérifiés ?
- Le contexte durable a-t-il été mis à jour sans recopier l’historique ?
Cette checklist évite qu’une feature soit « fonctionnelle » tout en rendant la suivante plus difficile.
Les anti-patterns que je surveille
« On nettoiera dans une autre PR »
Valable seulement si le nettoyage a une raison claire d’être séparé. Sinon c’est souvent une dette créée volontairement.
Les overrides qui deviennent l’API
Si l’on doit connaître l’ordre de cinq fichiers CSS pour modifier un composant, la cascade a pris la place du design system.
Les versions parallèles sans migration
Foo, FooV2, FooNew et FooLegacy qui restent tous actifs sont un symptôme d’une bascule jamais terminée.
Le helper « temporaire » utilisé partout
Une compatibilité locale finit par devenir une dépendance centrale parce qu’elle était plus facile à importer que la source canonique.
L’abstraction de prestige
Créer une architecture générique avant que le produit n’ait deux cas concrets transforme des hypothèses en API difficile à retirer.
Quand accepter une dette explicite
Toute dette n’est pas interdite.
On peut volontairement accepter un compromis lorsqu’il permet de tester une hypothèse importante beaucoup plus vite.
La différence est qu’une dette saine est nommée et bornée.
Je veux pouvoir écrire :
- pourquoi elle existe ;
- ce qu’elle permet de valider ;
- quel risque elle crée ;
- quand elle doit être supprimée.
Une dette invisible n’est pas un compromis. C’est simplement une surprise différée.
Une règle applicable aux petits comme aux grands projets
Cette méthode n’est pas réservée aux grosses codebases.
Elle est même particulièrement utile dans les petits projets assistés par IA, parce qu’un agent peut produire beaucoup de changements en peu de temps. Sans discipline d’ownership, on peut construire en une journée la dette que plusieurs développeurs auraient mis des semaines à accumuler.
L’avantage est symétrique : avec des sources de vérité claires et quelques garde-fous, les agents sont également très efficaces pour consolider, supprimer et maintenir une base propre.
Checklist de revue avant livraison
- Existe-t-il un fichier
v2,fix,final,newoucopyqui ne décrit pas une vraie responsabilité ? - Une règle a-t-elle désormais deux implémentations ?
- La modification aurait-elle pu être faite directement dans la source propriétaire ?
- Du code obsolète reste-t-il actif « au cas où » ?
- Une abstraction a-t-elle été créée pour un seul cas hypothétique ?
- Les migrations temporaires ont-elles une condition de sortie ?
- Les validations structurelles et fonctionnelles passent-elles ?
- Git et la documentation suffisent-ils à retrouver l’histoire sans la laisser dans le code actif ?
Ce que cette discipline cherche à préserver
L’objectif n’est pas d’avoir un repository parfaitement élégant à chaque commit.
L’objectif est que chaque nouvelle itération reste presque aussi facile à raisonner que la précédente.
Un bon système peut grandir. Il peut changer de direction. Il peut même accepter ponctuellement une couche transitoire.
Mais il ne devrait pas obliger la prochaine personne à connaître toute son histoire simplement pour savoir où modifier la règle actuelle.