Article 2 de la série “De la spécification à l’exécution, un workflow pour fiabiliser le code à l’ère IA”. L’article précédent a posé le contexte. Avec l’IA, une partie du travail s’est déplacée de l’écriture du code vers la spec et le jugement. Ici, je commence par la spec.

En 2025, une équipe d’ingénieurs a mesuré ce que produit GPT-4 Turbo quand on lui demande de générer des tests d’acceptance Cypress à partir de scénarios Gherkin. 60 % des tests étaient corrects à la première génération, 92 % après une correction mineure ou un ajout de contexte. Sur 20 cas problématiques, 12 partageaient la même cause, un contexte insuffisant dans la user story. Ces résultats sont publiés dans Acceptance Test Generation with Large Language Models. Le point à retenir est clair : le goulet d’étranglement n’est pas la génération, c’est la spécification.

Je pars donc d’une idée simple. Une spec utile n’est pas un texte décoratif. Elle doit dire la même chose à l’humain et à l’agent, sans laisser de blancs à compléter.

Pourquoi la spec change avec l’IA

Martin Fowler décrit bien le mécanisme dans Spec-Driven Development (2025). Les modèles complètent très bien un pattern. Ils devinent beaucoup moins bien une intention. Ils n’ont pas accès au contexte métier que j’ai en tête, sauf si je le rends explicite.

Plus la spec est précise, plus elle laisse peu de place à l’interprétation. Plus elle est vague, plus l’agent invente. Le résultat peut sembler plausible, ce qui est précisément le problème. Un bug plausible se repère plus tard, et souvent plus cher.

Prenons une demande du type : “fais-moi un endpoint qui retourne les utilisateurs”. L’agent peut produire du code sans difficulté. Il doit pourtant choisir une pagination, des filtres, un format JSON, des codes d’erreur, une politique de cache. S’il manque ces éléments, il comble les blancs avec des patterns appris ailleurs. Ils sont parfois bons. Ils sont souvent étrangers au système que je construis.

Si je fournis à la place un scénario Gherkin précis, un exemple de requête, un exemple de réponse et, quand c’est utile, un type OpenAPI, la marge d’invention baisse fortement. L’agent complète ce qui est déjà cadré.

Le rôle change. Le travail consiste moins à produire du texte qu’à formuler une intention claire. C’est là que se joue la qualité.

Les quatre niveaux de formalisation

Une spec n’est pas binaire. Elle ne se réduit pas à “présente” ou “absente”. J’y vois plutôt un curseur.

Niveau 1, la user story en langage naturel

Le format classique reste utile :

En tant que <rôle>, je veux <action> afin de <bénéfice>.

Ce format répond à la question du pourquoi. Il ne suffit pas pour cadrer le comportement attendu. Une user story seule n’est pas une spec exécutable. C’est un point de départ.

Niveau 2, les scénarios Gherkin

Le Gherkin donne une forme lisible par le métier et exploitable par des outils.

Scenario: Tarification d'un appel standard vers une destination connue
  Given un tarif de 0.02 €/min pour la destination "33"
  When je tarifie un CDR de 120 secondes vers "+33612345678"
  Then le coût doit être de 0.04

Il reste lisible côté métier. Il est aussi exécutable par des outils comme Cucumber, godog, behave ou cucumber-js.

L’étude de 2025 citée plus haut l’utilisait comme étape intermédiaire. Le flux tenait en trois étapes, user story, Gherkin généré par GPT-4, puis tests Cypress générés à partir du Gherkin. Cette étape compte. Elle laisse une correction possible avant que la génération de tests n’aille trop loin.

Niveau 3, les exemples typés

Quand le Gherkin reste trop verbal, je passe à des exemples concrets dans le code.

EXAMPLES = [
    # (duration_s, destination, expected_cost_millicents)
    (60,  "+33612345678", 2000),
    (120, "+33612345678", 4000),
    (1,   "+33612345678", 2000),   # arrondi à la minute supérieure
    (0,   "+33612345678", 0),
    (61,  "+33612345678", 4000),   # bord
]

Les exemples typés apportent deux choses. Ils alimentent directement des tests paramétrés ou des tests table-driven. Ils forcent aussi à expliciter les cas limites. C’est souvent là que j’identifie les zones floues laissées dans la user story.

Niveau 4, les propriétés et les contrats

C’est le niveau le plus formel. C’est aussi celui qui cadre le mieux l’IA quand le code est critique.

# Propriétés du calcul de coût d'un appel
# 1. cost(d, t) >= 0 pour tout d, t
# 2. cost(d1, t) <= cost(d2, t) si d1 <= d2
# 3. cost(d, 2*r) == 2 * cost(d, r)

Ici, je ne décris plus un cas particulier. Je décris un invariant métier. Ces propriétés se traduisent directement en tests property-based, avec Hypothesis en Python, fast-check en TypeScript ou rapid en Go. J’y reviens dans l’article 5.

Les contrats jouent un rôle voisin au niveau des signatures. icontract en Python, pydantic pour les modèles, Zod en TypeScript, les types OpenAPI, tout cela réduit la place laissée à l’interprétation. Une fonction typée def rate_call(duration: int, tariff: Tariff) -> Cost est déjà une spec partielle. L’agent ne peut pas décider seul qu’elle renvoie une string.

Quel niveau pour quel sujet

NiveauAudienceOutilsCoût d’écriturePouvoir contraignant
1. User storyTousConfluence, Jira, READMEFaibleFaible
2. GherkinMétier + devCucumber, godog, behaveMoyenMoyen
3. Exemples typésDevpytest.parametrize, table-drivenMoyenÉlevé
4. Propriétés + contratsDevHypothesis, icontract, typesÉlevéTrès élevé

La règle pratique est simple. Plus le coût d’un bug est élevé, plus je monte dans la formalisation. Une page de réglages utilisateur peut vivre avec les niveaux 1 et 2. Une bibliothèque de calcul de prix, un parseur de protocole ou un moteur de routage dialplan a besoin du niveau 4. Sur du code critique, rester trop bas dans l’échelle finit par coûter plus cher que la formalisation elle-même.

Trois pièges classiques

Trois erreurs reviennent souvent lors de la rédaction d’une spec destinée à être lue par un agent.

Piège 1, le contexte implicite

La user story dit : “l’utilisateur peut se connecter”. Il manque pourtant des éléments concrets, méthode d’authentification, politique de mot de passe, comportement après trois échecs, règle de rate limit.

Quand je garde ce contexte dans ma tête, je remplace un blanc par une hypothèse. L’agent, lui, invente. C’est exactement ce que montre l’étude de 2025. Sur 20 cas problématiques, 12 venaient d’un contexte insuffisant dans la user story. Le problème ne vient pas du modèle. Il vient du texte que je lui donne.

Le remède tient en une liste. Je note les préconditions, l’état du système, la configuration, les variables d’environnement et les données déjà présentes. Quand une feature en dépend, je préfère même un fichier partagé qui fixe le contexte une fois pour toutes.

Piège 2, les exemples trop sages

Je rédige facilement des scénarios qui passent, avec un utilisateur valide, des données propres et un chemin nominal. Les cas limites restent de côté. L’agent complète le pattern au lieu de le questionner, puis produit des tests du même genre. La couverture monte, mais la robustesse ne suit pas toujours.

Pour chaque feature, je vise au moins trois cas d’échec, une entrée invalide, une ressource manquante, un état incompatible. Si je n’ai que des scénarios qui réussissent, la spec est incomplète. Une spec utile ne décrit pas seulement le chemin qui fonctionne. Elle décrit aussi ce que le système doit refuser.

Piège 3, la propriété tautologique

Quand je veux monter au niveau 4, je peux écrire une propriété qui répète simplement le résultat.

# Propriété : la fonction de tri retourne une liste triée.

Cette propriété ne teste presque rien. Une bonne propriété décrit une relation entre deux états ou entre deux exécutions. Pour la même fonction de tri, je préfère par exemple :

  • len(sort(xs)) == len(xs), la longueur est préservée.
  • sort(sort(xs)) == sort(xs), l’idempotence.
  • set(sort(xs)) == set(xs), les éléments sont conservés.

La règle pratique tient en une phrase. Si je dois lire l’implémentation pour trouver la propriété, elle est trop faible. Une bonne propriété vient avant le code.

Un template applicable

Voici un format SPEC.md que je peux coller dans un ticket et donner tel quel à un agent IA.

# Spec - <nom de la feature>

## Intention (user story)
En tant que <rôle>, je veux <action> afin de <bénéfice>.

## Contexte à préciser
- État du système attendu avant : ...
- Configuration / variables d'environnement : ...
- Dépendances externes : ...

## Scénarios Gherkin

Scenario: <chemin nominal>
  Given ...
  When ...
  Then ...

Scenario: <échec attendu n°1>
  Given ...
  When ...
  Then ... (erreur attendue)

[au moins un, idéalement trois scénarios d'échec]

## Exemples typés
| Entrée | Sortie attendue |
|---|---|
| ... | ... |

## Propriétés candidates, niveau 4 si le code est critique
1. Invariant : ...
2. Round-trip : ...
3. Métamorphique : ...

## Non-goals
- Ce que cette feature ne fait pas.

Je garde ce template dans un fichier versionné, puis je le passe à l’agent quand je veux qu’il travaille à partir d’une base commune. Le but est simple : réduire les interprétations inutiles.

Trois points à retenir

1. La spec n’est plus optionnelle. Avec l’IA, elle devient l’entrée directe du workflow, et sa qualité conditionne celle du code produit.

2. Je peux formaliser à quatre niveaux selon la criticité du sujet, user story, Gherkin, exemples typés, propriétés et contrats. Plus le bug coûte cher, plus je monte en formalisation.

3. Trois pièges reviennent souvent, le contexte implicite, les exemples trop sages et les propriétés tautologiques. Les corriger tôt évite de demander à l’agent de deviner ce que je n’ai pas écrit.

L’article 3 traitera de la répartition des rôles entre humain, IA et outils déterministes, et de la place à leur laisser quand la hype IA les relègue au second plan.


Pour aller plus loin


Cet article est le deuxième d’une série de sept. Si cet article a été utile, l’article précédent pose le contexte général, et le suivant traitera de la répartition des rôles entre humain, IA et outils déterministes dans le workflow.