Le jour où lib/ a cessé d'être lisible
Un dimanche après-midi, je lance ls lib/ dans Rembrandt, l'ERP que je code seul pour L'Atelier Palissy depuis un mois. Il y a un mois, quand j'ai démarré, lib/ tenait dans un écran de MacBook. Je pouvais parcourir les noms d'un seul coup d'œil et savoir ce que chacun faisait. Ce dimanche-là, j'en compte quarante-et-un. Treize sont des adapters vers des services tiers — Supabase, Gmail, Brevo, Slack, Stripe, Meta CAPI, QStash, Push, PennyLane — et chacun fait entre 120 et 260 lignes de plumbing honnête. Rien ne plante, tout marche. Pourtant le dossier qui m'inspirait une lecture me demande maintenant un effort de scroll.
Je m'aperçois que chaque nouvelle intégration que j'ai demandée à Claude Code s'est cristallisée en un fichier d'adapter de cette taille, parce qu'un adapter est facile à générer : signature claire, pas d'invariant métier à protéger, pas de test à écrire. Mon agent a fait exactement ce que je lui demandais, à chaque fois, et par sédimentation quotidienne il a produit le genre de base dont Sculley et ses co-auteurs de Google écrivaient en 2015 qu'elle finit, dans les systèmes pathologiques, par représenter 95 % de code de glue pour 5 % de logique métier.
Il y a quelques semaines, Gaspard, notre prestataire informatique de longue date, était passé au bureau pour une raison qui m'échappe aujourd'hui. Je lui ai montré un début de lib/ sur l'écran, fier de la progression. Il a fait défiler trois secondes, sans s'asseoir, et lâché sans lever les yeux : « C'est de la plomberie, ça. » J'avais acquiescé comme on acquiesce à un commentaire technique qu'on ne comprend pas encore, en me disant qu'il parlait d'un détail. Je comprends six semaines plus tard qu'il venait de nommer en deux mots ce que Sculley et la littérature sur la dette technique essaient d'articuler depuis dix ans.
Si tu as 30 secondes. Le glue code (adapters, format conversions, plumbing vers des APIs externes) prolifère silencieusement quand on code vite, et encore plus vite quand on code avec un LLM qui produit volontiers des adapters. La parade : mesurer le ratio glue/business sur
lib/avec un script de 130 lignes, et brancher une CI sur la non-régression plutôt que sur un seuil absolu. Cet article donne le script, le pattern CI, et pourquoi la non-régression vaut mieux qu'un plafond. Utile si tu pilotes une base qui parle à beaucoup de services externes.
Le cadre qui m'a manqué pendant trois semaines
L'article Hidden Technical Debt in Machine Learning Systems (Sculley et al., NIPS 2015) décrit une dette particulière aux systèmes ML : le code utile au modèle est une toute petite boîte au centre d'un grand écosystème de plumbing — ingestion de données, normalisation, serving, monitoring. Le ratio type qu'ils constatent en production, 5/95. Les auteurs ne prétendent pas que le glue est mauvais en soi, ils prétendent que quand il n'est pas nommé, il se paie en coûts cachés : chaque refacto devient acrobatique, chaque migration se négocie avec dix fichiers qui ne devraient pas être concernés.
Le cadre est ML mais la forme dépasse largement. Dès qu'un système parle à cinq ou six services externes, il en produit, du glue. Un ERP vertical coché de six intégrations tierces est structurellement condamné à en fabriquer, et le risque n'est pas qu'il y en ait — il y en aura — mais qu'il soit compté comme du code métier dans l'équation mentale du développeur. Le jour où je relis lib/supabase-paginate.ts en me disant que c'est une brique métier, j'ai perdu. C'est un adapter, il doit rester un adapter, il doit être nommé tel, et son volume doit entrer dans une métrique dont la courbe a le droit de m'inquiéter.
Neuf ans avant Sculley, Moseley et Marks avaient posé dans Out of the Tar Pit (2006) la distinction fondatrice qui donne sa grille au problème : complexité essentielle, qui vient du métier, et complexité accidentelle, qui vient de la solution technique choisie. Le glue, dans cette grille, est de la complexité accidentelle à l'état pur. Il ne sert aucune exigence métier, il résout seulement le fait que deux systèmes ne parlent pas la même langue. C'est cette asymétrie — essentiel se paie une fois, accidentel se paie à chaque lecture, à chaque refacto, à chaque migration — qui explique que le glue devient dangereux bien avant d'être majoritaire.
Le script, cent trente lignes
J'ai écrit scripts/glue-ratio.sh un après-midi, un peu contre moi-même. Deux listes en dur : la liste des fichiers lib/*.ts qui sont du glue, la liste de ceux qui sont de la logique métier. Tout nouveau fichier que j'ajoute doit être classé consciemment dans l'une des deux. Rien n'est automatique, et c'est le seul moyen que chaque décision d'ajout soit une décision nommée.
GLUE_FILES=(
"lib/supabase.ts" "lib/supabase-admin.ts" "lib/supabase-server.ts"
"lib/supabase-paginate.ts" "lib/gmail.ts" "lib/gmail-api.ts"
"lib/brevo.ts" "lib/slack.ts" "lib/stripe.ts"
"lib/meta-capi.ts" "lib/pennylane.ts" "lib/qstash.ts"
"lib/push.ts" "lib/rate-limit.ts" "lib/cache.ts"
"lib/webhook-idempotency.ts" "lib/wordpress.ts" "lib/utils.ts"
"lib/database.types.ts"
)
BUSINESS_FILES=(
"lib/rembrandt.ts" "lib/rembrandt-tool-defs.ts"
"lib/rembrandt-tool-handlers.ts" "lib/lead-pipeline.ts"
"lib/email-outbox.ts" "lib/email-templates.ts"
"lib/permissions.ts" "lib/contacts.ts"
"lib/calendrier.ts" "lib/segments.ts"
)
Le reste du script additionne les lignes, calcule deux pourcentages (global et hors database.types.ts), et écrit un verdict court. Le mode --metric sort uniquement le ratio hors-types, prévu pour être comparé en CI.
Le piège des types auto-générés
lib/database.types.ts est un fichier auto-généré par Supabase à partir du schema. Il pèse plus de vingt mille lignes sur Rembrandt, et comme il est entièrement du glue (définitions TypeScript des tables, rien de métier), il fait basculer le ratio global au-delà de 60 % si on le compte. Ce serait juste, et ce serait inutile, parce que personne ne décide rien en relisant ce fichier. La règle que j'ai fini par poser est : le ratio de référence est hors database.types.ts. Le script expose les deux chiffres, le global pour mémoire et le hors-types pour piloter. Ratio actuel du repo : 28 % hors types sur main. Cible que je me donne : sous 25 % durablement.
Ratio glue/business — lib/
==========================
Glue: 22 183 lignes (64%)
Business: 12 487 lignes (36%)
Total: 34 670 lignes
(hors database.types.ts : 2 183 glue / 14 670 total = 28%)
OK: glue hors-types sous le seuil d'alerte 30% (cible 25%)
La CI qui bloque la régression, pas l'absolu
Voici le choix que j'ai mis du temps à faire, et qui compte plus que le script lui-même. On écrit souvent un garde-fou CI avec un seuil absolu : if (glue > 30%) fail. C'est séduisant parce que c'est simple, et c'est une mauvaise idée. Un projet mature à 35 % de glue qui se tient peut être parfaitement sain. Un projet à 18 % qui monte à 22 % en une semaine est en train de dériver. Le seuil absolu ne voit pas la dérive, il ne voit que l'arrivée.
J'ai branché la CI sur la non-régression entre HEAD et origin/main, avec une tolérance de zéro point. Chaque PR qui fait monter le ratio plus haut que main fail, et le message lève la vraie question : « est-ce que tu ajoutes de la logique métier qui justifie plus de plumbing, ou est-ce que tu ajoutes un adapter qui n'a rien demandé à personne ? ». Si c'est le premier cas, tu ajoutes du business en face, le ratio baisse, la PR passe. Si c'est le second, tu cherches à extraire, à mutualiser, à renommer.
# scripts/glue-ratio-check.sh (extrait)
current=$(bash scripts/glue-ratio.sh --metric)
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/scripts"
cp scripts/glue-ratio.sh "$tmp/scripts/"
git archive "$BASE_REF" lib/ | tar -x -C "$tmp"
base=$(cd "$tmp" && bash scripts/glue-ratio.sh --metric)
delta=$((current - base))
if [ "$delta" -gt "$TOLERANCE" ]; then
echo "ECHEC: le ratio glue a augmente de ${delta} pts (tolerance +${TOLERANCE})."
echo "Regarder si du glue peut etre extrait dans lib/mappings/ ou lib/adapters/,"
echo "ou si un nouveau fichier est mal categorise dans scripts/glue-ratio.sh."
exit 1
fi
Un filet secondaire, pour les cas pathologiques : au-delà de 40 %, le script sort en mode alerte dans la sortie humaine, ce qui impose un débat d'équipe même si la non-régression passe. Mais c'est un filet, pas la métrique principale.
Pourquoi une règle écrite dans CLAUDE.md ne suffit pas
J'avais d'abord écrit une règle dans mon CLAUDE.md, formulée à peu près comme « privilégier la logique métier aux adapters, garder lib/ mince ». Cette règle n'a rien empêché. Elle ne se heurtait à aucun fait, et un adapter qui semble nécessaire sur le moment l'emporte toujours sur une phrase abstraite lue en haut du fichier de contraintes. La métrique chiffrée, elle, renvoie un fait matériel à la tête du rédacteur : +3 points sur cette PR. Le débat devient concret, la règle devient opposable, et le rédacteur — humain ou LLM — prend conscience de ce qu'il est en train de faire. C'est exactement ce que le CLAUDE.md ne peut pas produire tant qu'il reste du texte.
Il y a là une leçon qui dépasse la métrique elle-même. Les disciplines qui tiennent ont toutes un chiffre que la machine calcule à ta place. Pas une intention, pas un principe, pas un vœu — un chiffre. Le reste s'érode au rythme de la fatigue du développeur et de la complaisance de l'agent.
Ce que tu peux copier dans ton projet
Les deux scripts et un exemple de workflow CI vivent dans le repo compagnon, licence MIT : github.com/michelfaure/rembrandt-samples.
Quatre gestes directement applicables si ta base a beaucoup d'intégrations externes :
- Poser deux listes en dur dans un script shell, glue et business, et obliger toute nouvelle addition à être classée dans l'une ou l'autre. Pas de détection automatique — la friction est le point
- Exclure les fichiers auto-générés du dénominateur. Les exposer en chiffre global pour mémoire, mais piloter sur le ratio hors-types
- Brancher la CI sur la non-régression, pas sur un seuil absolu. Tolérance zéro point, message qui pose la vraie question au rédacteur de la PR
- Filet secondaire à 40 % pour les cas pathologiques, mais c'est un filet, pas la règle
Et une discipline plus large : tout ce qui n'est pas mesuré dérive. Une règle dans un fichier de contraintes est lue, puis oubliée ; une métrique chiffrée qui bloque une PR est contournée consciemment ou non, mais elle est vue. Les LLM ne font pas exception à cette règle — ils la rendent même plus urgente, parce qu'ils produisent plus vite ce qu'on ne leur demande pas de modérer.
Et vous, quelles métriques pilotent réellement vos PR, et lesquelles sont restées des intentions ? Je lis les commentaires.
Code compagnon : rembrandt-samples/glue-ratio/ — le script de mesure, le filet de non-régression CI, et le workflow GitHub Actions, licence MIT.

Top comments (0)