Quietly Wrong

Des règles qui ne se discutent pas

La licence poétique des LLM

Lorsque l’on demande à un LLM de produire du code selon certaines règles, il nous assure que, bien évidemment, toutes ces règles seront respectées. Pas de duplication de code, pas de fichiers trop longs, pas d’« overengineering ». Mais il écrira quand même trois fois la même fonction, ou un fichier de 500 lignes de code qui traite des scénarios impossibles, et se réduit à 20 lignes une fois corrigé.

Nous avons tenté de définir ces règles dans un CLAUDE.md (ou AGENTS.md) : le modèle les lit, les approuve, et les enfreint régulièrement.

Les règles inscrites dans les fichiers d’instructions sont indicatives, et sont souvent ignorées.

Nous définissons donc les règles qui comptent le plus via des scripts automatiques qui vérifient chaque modification produite par le modèle et rejettent celles qui enfreignent une règle. Le modèle n’a pas voix au chapitre et ne peut progresser tant que l’erreur remontée n’est pas corrigée.

Ces règles fonctionnent car il n’y a aucune consigne à oublier, et rien à négocier.

Trois endroits pour arrêter une mauvaise modification

Les contrôles se déclenchent à trois moments, parce que l’instant le moins coûteux pour intercepter une erreur dépend de l’erreur.

Avant l’édition

Certains fichiers ne doivent jamais être modifiés à la main : les objets que jOOQ génère à partir du schéma de la base de données, par exemple. Si l’agent IA tente de modifier ces fichiers, un contrôle s’exécute avant l’écriture et la refuse d’emblée ; le fichier n’est même pas ouvert.

# runs before the edit — a non-zero exit means the write never happens
if [[ "$file" == */generated/* ]]; then
  echo "✗ generated code — change the schema and regenerate, don't edit this" >&2
  exit 2
fi

Après l’édition

La plupart des contrôles lisent le fichier que le modèle vient d’écrire et y testent une règle. C’est là que vivent les règles d’architecture.

Par exemple, certains fichiers ont pour unique fonction d’exposer et documenter une API REST, puis de relayer la demande à un service métier. Des transactions dispersées dans la couche web rendent le modèle transactionnel difficile à suivre.

Nous avons donc des règles qui interdisent à ces objets d’ouvrir des transactions en base de données. La règle est vérifiée à chaque édition d’un fichier situé dans un package rest/ :

# post-edit-gate.sh  (Hook: PostToolUse) - runs after an edit to a file in the rest folder
if echo "$RELATIVE" | grep -qE '/rest/.*\.kt$'; then
  if grep -qE '@Transactional' "$FILE_PATH"; then
    echo "GATE 6 FAILED: REST resources must not use @Transactional." >&2
    echo "Move transaction logic to the service layer." >&2
    exit 2
  fi
fi

Si l’agent introduit une transaction dans un fichier exposant notre API REST, le contrôle rejette la modification, l’échec devient la prochaine chose à corriger, et l’agent déplacera la transaction vers la couche service.

Quand le modèle a fini son travail

Non seulement tous les tests doivent passer, mais un agent mène une revue de code contradictoire, suivant une méthodologie détaillée, et renvoie ses constats à l’agent de développement.

édition suggérée avant l'édition fichiers interdits après l'édition vérifie une règle à l'arrêt compile + tests accepté ok échec : renvoyé au modèle comme prochaine chose à corriger

Quelques exemples de règles

Le hook post-édition applique de nombreuses règles. En voici un échantillon :

Règle Se déclenche sur Pourquoi
Frontière de microservice un microservice qui importe l’implémentation d’un autre les microservices se parlent par leurs API, pas par leur implémentation
Couche @Transactional, catch ou un import de repository dans un fichier rest/ les ressources REST restent minces et testables
Longueur de fichier un fichier de plus de 300 lignes la longueur est un symptôme — le code gère peut-être trop de choses
Code dupliqué une nouvelle classe ou fonction dont le nom existe déjà réutiliser plutôt que réinventer
Emplacement des utilitaires un *Utils / *Helper déposé dans un module métier les utilitaires réutilisables vivent dans un seul module partagé
Sentinelle de constantes un littéral comme 9999-12-31 au lieu d’une constante nommée la constante porte l’intention que le littéral cache
Nommage des migrations une migration Flyway hors de la convention V001__nom les migrations doivent s’appliquer dans un ordre connu

Certains bloquent sans appel : la modification est rejetée. D’autres signalent un simple symptôme — un fichier de 320 lignes n’est pas nécessairement une faute — et se contentent d’un avertissement : la modification passe, le message est affiché.

Bien classer les règles entre ces deux catégories est important : les premières versions bloquaient tout au même niveau, et le modèle restait coincé contre une règle d’hygiène qu’il ne pouvait pas satisfaire, perdant un tour entier sur une limite de 300 lignes.

Un contrôle est un petit script que le modèle ne peut pas esquiver

Le garde-fou des dates sentinelles, reproduit en entier, est représentatif — la plupart des règles tiennent en aussi peu de lignes :

#!/usr/bin/env bash
file="$1"
[[ "$file" == *DateConstants.kt ]] && exit 0   # the one place the date is allowed
if grep -nE '9999[-, ]+12[-, ]+31' "$file"; then
  echo "✗ sentinel ($file): use DateConstants.OPEN_ENDED, not a literal date" >&2
  exit 2
fi

La plupart de ces scripts sont simples. L’ingénierie tient dans les choix : quelles règles implémenter, comment les écrire pour éviter les faux positifs, et comment les brancher pour que le modèle ne puisse pas les éviter.

Le jeu de règles n’est pas figé non plus. Une règle qui ne s’est pas déclenchée depuis des mois est retirée — le plus souvent après un changement de modèle.

Les angles morts

Aucun script post-édition ne compile : compiler le projet à chaque modification rendrait le travail trop lent. Nous demandons au modèle, en prose, de compiler régulièrement et de lancer les tests que sa modification semble affecter. Les modèles récents le font largement d’eux-mêmes, et le choix des tests pertinents relève du jugement du modèle. Mais tout cet article dit que la prose s’oublie ; il lui faut donc un garde-fou supplémentaire.

Chaque édition consigne le module qu’elle touche, et quand le modèle tente de conclure, un dernier garde-fou lance la suite de tests complète de chaque module concerné, permettant de détecter des régressions. Le modèle n’est pas autorisé à terminer tant que tous les tests ne sont pas verts.

Le branchement dans Claude Code

Ce qui rend ces règles robustes, c’est qu’elles ne sont pas sous le contrôle de l’agent. Claude Code, par exemple, expose des hooks : des commandes déclarées dans .claude/settings.json et exécutées à chaque événement de l’agent — avant un outil, après, à l’arrêt. Le modèle ne décide jamais si elles tournent.

"hooks": {
  "PreToolUse":  [{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "hooks/pre-edit.sh" }] }],
  "PostToolUse": [{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "hooks/post-edit.sh" }] }],
  "Stop":        [{ "hooks": [{ "type": "command", "command": "hooks/test-touched-modules.sh" }] }]
}

Chaque script reçoit un JSON qui décrit l’appel en cours, dont tool_input.file_path, le fichier visé, et répond par un code de sortie : 0, l’agent continue ; 2, l’action est bloquée et l’erreur détectée par le script est renvoyée au modèle :

  • Avant l’édition, le blocage empêche l’écriture.
  • Après l’édition, le fichier est déjà écrit et le message devient la prochaine chose à corriger.
  • À l’arrêt, le hook Stop renvoie les erreurs éventuelles au modèle, qui doit alors les corriger pour pouvoir conclure.

Ce qu’un contrôle ne peut pas juger

Ces contrôles permettent de vérifier que le code respecte des règles de base ; ils ne disent pas que le code est correct. Cet aspect relève de la revue contradictoire, puis de notre validation finale. Cela fera l’objet d’un prochain article. Les garde-fous existent pour que cet examen se concentre sur la justesse de la logique, pas sur des transactions égarées dans la mauvaise couche de l’application.


Un projet en tête ?

Un premier entretien rapide suffit en général à déterminer si nous pouvons vous être utiles.

Nous contacter