Une meilleure documentation que la mémoire : un repo pour tous

1. Aperçu

L’agent de codage devient progressivement un « membre » de l’équipe : il lit le code, corrige les bugs, écrit des tests et ouvre des demandes de fusion. Le problème, c’est que l’agent oublie tout entre les sessions. Il existe des outils offrant une fonction « mémoire », mais cette mémoire est généralement liée à une personne, difficile à examiner et ne suit pas la version du code.

Cet article explique pourquoi une documentation dans le dépôt est plus fiable que la mémoire de l’agent. L’article guide également sur la façon d’écrire un dépôt Spring Boot pour que les nouveaux venus et les agents fassent bien leur travail dès la première fois. Une fois lu, vous saurez comment écrire un fichier guide AGENTS.md, consolider les vérifications en une seule commande, transformer les conventions en tests et documenter les décisions architecturales.

2. Pourquoi la mémoire ne suffit pas

Comparons deux façons dont un agent « sait » que la couche domain ne doit pas dépendre de Spring :

  • Mémoire : vous lui en avez parlé la dernière fois, et il l’a mémorisé quelque part.
  • Dépôt : la règle se trouve dans AGENTS.md et il y a un test qui échoue si quelqu’un la viole.

La première approche a plusieurs faiblesses évidentes :

  • La note n’existe que sur la machine ou le compte d’une personne. Les collègues utilisant un autre agent ne l’auront pas.
  • Personne ne révise la note. Si l’agent « se souvient mal », vous aurez du mal à le découvrir.
  • La note ne suit pas les branches. Quand la règle change sur main, l’ancienne mémoire persiste.

La deuxième approche est versionnée, examinée via une demande de fusion et juste pour chaque commit. Plus important, cela aide aussi les humains. Ce que l’agent doit connaître pour bien faire son travail correspond presque exactement à ce dont un nouveau développeur a besoin dans sa première semaine.

C’est pourquoi ce sujet attire l’attention. L’agent ne révèle pas un nouveau problème. Il révèle le coût d’une mauvaise documentation, car l’agent ne peut pas demander « Hé, comment ça marche ? » comme une nouvelle personne le pourrait.

3. Le fichier de guide pour l’agent : AGENTS.md

De nombreux outils d’agent lisent un fichier Markdown à la racine du dépôt pour obtenir des directives. Le nom du fichier dépend de l’outil, mais AGENTS.md est une convention courante. Si votre outil utilise un nom différent, vous pouvez créer un lien symbolique au lieu de dupliquer le contenu.

3.1. Ce que le contenu devrait inclure

# AGENTS.md

## Projet
Service de commande (Spring Boot 3, Java 21). Modules principaux : `order`, `payment`, `customer`.

## Commandes
- Build + tous les contrôles : `./scripts/verify.sh`
- Exécuter un test : `./mvnw -q test -Dtest=DiscountPolicyTest`
- Exécuter l'application localement : `./mvnw spring-boot:run -Dspring-boot.run.profiles=local`

## Conventions obligatoires
- `..domain..` ne doit pas importer `org.springframework..` (test : `ArchitectureTest`).
- Utiliser `BigDecimal` pour les montants, pas `double`.
- Tout changement de comportement métier doit avoir un test dans `src/test/java/.../domain`.

## À ne pas faire
- Ne pas modifier les fichiers fusionnés dans `src/main/resources/db/migration` ; créer une nouvelle migration.
- Ne pas valider `application-local.yml`.

## Lecture supplémentaire
- Décisions architecturales : `docs/adr/`
- Flux de paiement : `docs/payment-flow.md`

Chaque section a un objectif distinct :

  • Commandes : l’agent exécute des commandes tout le temps. Une mauvaise commande lui fera perdre plusieurs essais. Écrivez des commandes exactes et copiables.
  • Conventions obligatoires : chaque ligne devrait indiquer comment vérifier (nom du test, règle de lint). Les conventions sans mécanisme de vérification sont faciles à ignorer.
  • À ne pas faire : énumérez les actions aux conséquences difficiles à annuler.
  • Lecture supplémentaire : pointez vers la documentation détaillée au lieu de la dupliquer ici.

3.2. Rester bref, pointer vers la source de vérité

AGENTS.md doit être bref, assez pour être lu entièrement en une minute. L’agent charge ce fichier dans le contexte à chaque session. Plus le fichier est long, plus il consomme de contexte et plus il risque de contenir des informations obsolètes.

Règle générale : ne redécrivez pas ce que le code ou le build explique déjà. Ne listez pas chaque dépendance, car pom.xml l’a déjà. Ne décrivez pas chaque endpoint, car la spec OpenAPI l’a déjà. Écrivez seulement ce qui ne peut pas être déduit du code.

Remarque : Ne mettez jamais de secrets, de jetons ou d’URL internes sensibles dans AGENTS.md. Ce fichier est lu par des outils externes et reste dans l’historique git indéfiniment.

4. Une seule commande de vérification

L’agent fonctionne mieux avec une boucle de rétroaction claire : modifiez le code, exécutez la commande, voyez passer ou échouer. Si une vérification complète nécessite d’exécuter quatre commandes différentes dans l’ordre, l’agent les manquera. Les humains aussi.

#!/usr/bin/env bash
# scripts/verify.sh — exécute exactement comme CI. Exit code non-zéro signifie pas prêt à fusionner.
set -euo pipefail

echo ">> Compilation + tests unitaires + tests d'architecture"
./mvnw -B -q verify

echo ">> Vérifier qu'il ne reste aucun TODO-AGENT"
if grep -rn "TODO-AGENT" src/; then
  echo "Des TODO-AGENT non traités restent" >&2
  exit 1
fi

echo "OK: prêt pour une demande de fusion"

Explications :

  • set -euo pipefail arrête le script dès qu’une commande échoue. Cela permet à l’agent de recevoir un exit code non-zéro au lieu d’une longue sortie avec un « OK » trompeur à la fin.
  • -B (mode batch) désactive la couleur et les barres de progression. Des logs plus propres rendent les erreurs plus faciles à lire pour l’agent.
  • L’étape grep est un exemple de convention de l’équipe : l’agent est autorisé à marquer les tâches incomplètes avec TODO-AGENT, mais ne peut pas les laisser à la fusion.

Quand il s’exécute avec succès, vous verrez :

>> Compilation + tests unitaires + tests d'architecture
>> Vérifier qu'il ne reste aucun TODO-AGENT
OK: prêt pour une demande de fusion

Le point le plus important : CI doit appeler exactement ce script. Si CI exécute un ensemble de commandes différent, verify.sh s’éloignera progressivement de la réalité et deviendra une documentation incorrecte.

5. Transformer les conventions en tests avec ArchUnit

Une phrase comme « domain ne dépend pas de Spring » dans la documentation est juste un conseil. Si vous la transformez en test, elle devient une règle. ArchUnit vous permet d’écrire des règles architecturales sous forme de tests JUnit.

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>1.3.0</version>
    <scope>test</scope>
</dependency>
package com.nonla.shop;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

@AnalyzeClasses(packages = "com.nonla.shop", importOptions = ImportOption.DoNotIncludeTests.class)
class ArchitectureTest {

    @ArchTest
    static final ArchRule domainNeDependPasDeSpring = noClasses()
            .that().resideInAPackage("..domain..")
            .should().dependOnClassesThat().resideInAPackage("org.springframework..")
            .because("domain doit être testable sans contexte Spring, voir docs/adr/0003-domain-pur.md");

    @ArchTest
    static final ArchRule webNeDoitPasAppelerDirectementRepository = noClasses()
            .that().resideInAPackage("..web..")
            .should().dependOnClassesThat().haveSimpleNameEndingWith("Repository")
            .because("le contrôleur passe par le service pour garder la transaction au même endroit");
}

Le point de valeur le plus important se trouve dans .because(...). Quand la règle est violée, le message d’erreur contient la raison et un lien vers l’ADR. Supposons que quelqu’un ajoute @Component à DiscountPolicy dans le package domain, la sortie ressemblera à :

Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package
'..domain..' should depend on classes that reside in a package 'org.springframework..',
because domain doit être testable sans contexte Spring, voir docs/adr/0003-domain-pur.md'
was violated (1 times):
Class <com.nonla.shop.order.domain.DiscountPolicy> is annotated with <org.springframework.stereotype.Component> ...

Un agent lisant ce message saura immédiatement ce qu’il faut faire : supprimer l’annotation, puis enregistrer le bean dans une classe @Configuration dans la couche infra. Un bon message d’erreur est la documentation au bon moment pour celui qui la lit.

6. Les tests sont la documentation métier

Les règles métier sont ce que l’agent risque le plus de mal interpréter. Il ne peut pas deviner que « la remise de première commande est plafonnée à 50 000 VND ». Laissez le code et les tests le dire.

package com.nonla.shop.order.domain;

import java.math.BigDecimal;
import java.math.RoundingMode;

public record Order(String customerId, BigDecimal subtotal, boolean firstOrder) {}

class DiscountPolicy {

    static final BigDecimal FIRST_ORDER_RATE = new BigDecimal("0.10");
    static final BigDecimal FIRST_ORDER_CAP = new BigDecimal("50000");

    BigDecimal discountFor(Order order) {
        if (!order.firstOrder()) {
            return BigDecimal.ZERO;
        }
        BigDecimal raw = order.subtotal().multiply(FIRST_ORDER_RATE)
                .setScale(0, RoundingMode.DOWN);
        return raw.min(FIRST_ORDER_CAP);
    }
}

Les tests qui l’accompagnent doivent être nommés selon le comportement, pas selon le nom de la méthode :

package com.nonla.shop.order.domain;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static org.assertj.core.api.Assertions.assertThat;

@DisplayName("Remise de commande")
class DiscountPolicyTest {

    private final DiscountPolicy policy = new DiscountPolicy();

    @Test
    @DisplayName("La première commande a une remise de 10% sur le total")
    void firstOrderGetsTenPercent() {
        var order = new Order("c-1", new BigDecimal("200000"), true);

        assertThat(policy.discountFor(order)).isEqualByComparingTo("20000");
    }

    @Test
    @DisplayName("La remise de première commande est plafonnée à 50 000 VND")
    void firstOrderDiscountIsCapped() {
        var order = new Order("c-1", new BigDecimal("2000000"), true);

        assertThat(policy.discountFor(order)).isEqualByComparingTo("50000");
    }

    @Test
    @DisplayName("À partir de la deuxième commande, aucune remise")
    void repeatOrderGetsNoDiscount() {
        var order = new Order("c-1", new BigDecimal("2000000"), false);

        assertThat(policy.discountFor(order)).isZero();
    }
}

Explications :

  • @DisplayName transforme le rapport de test en une spécification lisible. Les responsables produit peuvent aussi comprendre.
  • isEqualByComparingTo compare BigDecimal par valeur, donc 20000 et 20000.00 sont considérés comme égaux. Utiliser isEqualTo ici est une erreur courante, car il compare aussi l’échelle.
  • Si l’agent est demandé « d’augmenter la remise à 15% », il verra immédiatement quel test doit changer. Si elle oublie accidentellement le plafond, le deuxième test échouera.

Lors de l’exécution, le rapport ressemble à ceci :

Remise de commande
  ✔ La première commande a une remise de 10% sur le total
  ✔ La remise de première commande est plafonnée à 50 000 VND
  ✔ À partir de la deuxième commande, aucune remise

7. Documenter le « pourquoi » : ADR et package-info

Le code dit ce que le système fait. Les tests disent ce qu’il doit faire. Mais rien n’explique pourquoi vous avez choisi de cette façon. Sans le « pourquoi », l’agent « nettoie » souvent ce qui semble redondant mais a en réalité une raison.

Architecture Decision Record (ADR) est un petit fichier pour chaque décision :

# 0003. La couche domain ne dépend pas de Spring

Statut : Acceptée

## Contexte
Les tests de domain devaient auparavant démarrer le contexte Spring, ce qui était lent et fragile.

## Décision
Le package `..domain..` n'utilise que le JDK. Les beans sont déclarés dans `..infra.config`.

## Conséquences
- Les tests de domain s'exécutent avec JUnit pur.
- Vous devez écrire une classe de configuration supplémentaire. Vérifiée par `ArchitectureTest`.

Avec une note étroitement liée à un package, package-info.java est l’endroit approprié, car l’agent la voit dès qu’il ouvre le répertoire :

/**
 * Règles métier de commande. Utilise uniquement le JDK, pas Spring (ADR 0003).
 * Tous les montants sont BigDecimal, en VND, arrondis à la VND inférieure.
 */
package com.nonla.shop.order.domain;

8. Erreurs courantes et quand ne pas le faire

Documentation dupliquée. La même règle est écrite dans README.md, AGENTS.md et wiki. Après quelques mois, les trois endroits disent trois choses différentes. Choisissez une source de vérité unique et pointez vers elle depuis ailleurs.

Les instructions contredisent CI. AGENTS.md dit « exécutez mvn test », mais CI exécute aussi des tests d’intégration supplémentaires. L’agent signalera « terminer » pendant que la PR reste rouge.

Écrire pour l’agent dans un ton vague et impératif. La phrase « Écrivez du code propre » n’aide pas. La phrase « Chaque méthode publique dans ..service.. doit avoir un test » est utile, et mieux encore si elle est vérifiable.

Mettre tout dans un seul fichier. Un AGENTS.md de mille lignes dilue le contexte. Les informations importantes seront noyées.

Remarque : Tous les dépôts n’ont pas besoin de toutes ces pièces. Un script unique, un prototype destiné à être abandonné ou un dépôt avec seulement quelques centaines de lignes de code ne nécessite qu’un README.md avec des commandes d’exécution. ArchUnit vaut aussi le coup d’ajouter si vous avez au moins quelques couches ou modules avec des limites réelles à maintenir. Écrire de la documentation pour quelque chose qui n’existe pas encore ne crée que plus de choses à maintenir.

D’un autre côté, la mémoire de l’agent a toujours sa place. Elle est appropriée pour les préférences personnelles comme le ton lors de l’explication ou l’éditeur que vous utilisez. Ces choses ne font pas partie du dépôt. Les règles du projet doivent toujours être dans le dépôt.

9. Conclusion

L’agent n’a pas besoin d’une meilleure mémoire, il a besoin d’un dépôt plus compréhensible. Vous avez vu comment :

  • Écrire un AGENTS.md bref contenant des commandes exactes et des conventions avec un moyen de vérification.
  • Consolider tous les contrôles dans scripts/verify.sh et faire partager ce script avec CI.
  • Transformer les conventions architecturales en tests ArchUnit avec des messages d’erreur qui expliquent pourquoi.
  • Utiliser des tests avec @DisplayName comme spécification métier.
  • Documenter le « pourquoi » avec ADR et package-info.java.

Tout ce qui précède aide d’abord les humains. L’agent est juste la raison pour laquelle nous ne pouvons plus repousser cela. Commencez petit : écrivez verify.sh et les cinq premières lignes de AGENTS.md cette semaine.

ai-agentdocumentationarchunittestingspring-boot