Le secrétaire de Fernand

Prouver et avouer, ne pas seulement linter

Prouver et avouer, ne pas seulement linter

Pourquoi les règles exécutables ne sont que la moitié facile : les vérifications autour du code écrit par des agents doivent exiger la présence, rapporter leurs angles morts, et être écrites comme un standard plutôt que collectées comme des règles.

Sharing is caring : Partager sur X · Partager sur Bluesky · Commenter sur Hacker News

Idée centrale : Tout le monde est maintenant d’accord pour dire que les règles doivent être exécutables, et les outils qui sortent sont des linters branchés sur des agents. C’est la moitié facile. Ce dont une base de code a besoin, une fois que des agents en écrivent l’essentiel, c’est d’un standard qui dit ce que « juste » veut dire pour elle, le prouve à chaque changement en nommant ce qu’il a prouvé, et dit ce qu’il n’a pas regardé. Cette dernière partie est celle que personne ne livre, et c’est elle qui décide où va l’attention d’un relecteur.

La version courte : 2 min L’article complet : 16 min

La version courte

L’article précédent soutenait qu’une architecture n’est réelle que lorsque ses règles sont exécutables. Cet argument est depuis devenu le consensus, plus vite que je ne l’attendais. Les fichiers d’instructions pour agents de code ont cédé la place aux hooks ; les vérificateurs de frontières se sont branchés dans la boucle de l’agent ; la phrase « une instruction n’est pas un garde-fou » est maintenant prononcée par les éditeurs eux-mêmes. Tant mieux. C’est aussi, presque entièrement, la moitié linter du problème.

Un linter cherche la présence de quelque chose d’interdit. Quand il en trouve, il pointe une ligne. Quand il ne trouve rien, il ne dit rien, et ce silence veut dire exactement une chose : rien trouvé. Il ne veut pas dire que la validation est là. Il ne veut pas dire que le port a son adapter. Il ne peut pas le vouloir dire, parce qu’un linter n’a jamais listé ce qu’il s’attendait à voir.

Trois choses séparent un standard d’un linter. Il exige la présence, ce qui demande un modèle de ce qui devrait exister. Il rapporte le complément, ce qui demande une énumération de ce qu’il a regardé. Et il est écrit, pas collecté : quelqu’un possède le modèle, et ce quelqu’un est un ingénieur qui écrit ce que « juste » veut dire pour cette base de code, une fois.

En trois idées

  1. Le silence n’est jamais une preuve. Une vérification qui ne rapporte rien vous a dit ce qu’elle a trouvé, pas ce qui est là. La valeur d’un standard, c’est qu’il ne laisse pas de silence : pour chaque garantie qu’il déclare, un état explicite.

  2. La présence demande un modèle, et le modèle est le standard. On ne détecte pas un trou sans connaître la forme. Cette forme n’est pas une configuration de linter ; c’est ce que les ingénieurs devraient écrire au lieu de relire la sortie des agents ligne par ligne.

  3. Avouer dans son vocabulaire. Un standard ne peut confesser que l’absence de ce qu’il a déclaré. Une frontière que personne n’a écrite n’est pas « pas regardée » ; elle n’est pas sur la carte. Le dire fait partie du standard.

À retenir

Ne demandez pas ce que votre vérificateur a trouvé. Demandez ce qu’il a regardé, et ce qu’il aurait dit s’il n’avait rien trouvé.


L’article complet

Le dernier article s’achevait sur une affirmation : une architecture qui vit dans des documents est une croyance, une architecture qui vit dans des règles exécutables est une propriété. Je la maintiens. Mais une propriété de quelle sorte ? En lisant ce qui est sorti autour de cette idée depuis un an, je pense que l’affirmation était trop facile à approuver, et que l’approbation cache la partie difficile.

Voici ce qui est sorti. Des bibliothèques d’architecture-as-code pour TypeScript tournent maintenant comme un hook après chaque itération d’agent, renvoyant la violation à l’agent comme feedback déterministe. Des linters d’architecture sur le diff sortent avec une sévérité, un texte de remédiation et un serveur MCP pour que l’agent les interroge. Des travaux académiques compilent les règles en texte libre d’un fichier d’instructions d’agent en requêtes AST et en shims shell, et mesurent la conformité. Une chronique dans le radar d’un grand éditeur réclame une « couche de gouvernance d’ingénierie » qui rende les décisions d’architecture lisibles par machine et applicables, et conclut qu’aucun outil de ce genre n’existe.

Chacun de ces outils a la forme d’une prohibition. Chacun cherche une présence interdite et pointe une ligne. Et ce n’est ni une coïncidence ni un manque d’imagination. Une prohibition se vérifie sur le graphe d’imports, gratuitement, sans modèle du système. Une obligation, non. Alors l’outillage est allé là où la vérification était bon marché, et le consensus s’est formé autour de la moitié facile à construire.

Cet article porte sur l’autre moitié, et sur ce qu’elle coûte.

Le silence d’un linter

Prenez la garantie la plus ordinaire d’une application web : l’entrée est validée à la frontière.

Lancez un linter sur une base de code sans aucune validation. Zéro résultat. Lancez-le sur une base de code où chaque server function parse son entrée contre un schéma. Zéro résultat. Les deux bases de code sont indiscernables pour l’outil, parce que l’outil n’a aucune règle qui pourrait tirer dans l’un ou l’autre cas. Il a des règles de la forme cette chose ne doit pas être ici. La garantie qui nous importe est de la forme cette chose doit être ici, et sa violation est un trou.

C’est la distinction que l’article précédent nommait : et , divergence et absence. Ce que je n’ai pas dit assez fort, c’est ce qu’elle fait au sens d’un résultat vert.

Quand un vérificateur de prohibitions est vert, il vous a dit qu’une certaine classe de mauvaises choses n’est pas présente dans ce qu’il a parcouru. C’est une vraie information, et elle vaut la peine. Mais elle ne dit rien de la présence. Une base de code peut passer toutes les prohibitions et n’avoir aucune validation, aucun câblage, aucun adapter derrière la moitié de ses ports. Le système a l’air terminé et n’est pas câblé, et chaque outil de la boucle est vert, parce que chaque outil de la boucle examine ce qui est là et que le défaut est ce qui n’est pas là.

Le premier geste est donc d’arrêter de lire le silence comme un réconfort. Le silence d’un outil n’est jamais la preuve de quoi que ce soit. Si vous voulez la preuve d’une présence, l’outil doit dire, à voix haute, j’ai cherché ceci, et c’est là.

La présence demande un modèle

Ce qui amène le coût. Pour dire j’ai cherché ceci et c’est là, l’outil doit savoir quoi chercher. Quelque chose doit déclarer qu’un port est censé avoir un adapter, qu’une server function est censée porter un validateur, qu’un formulaire est censé lier un schéma. Cette déclaration est un modèle du système : ses périmètres, ses concepts, les arêtes qui devraient exister entre eux.

Les linters ont bien des règles en forme d’obligation, et il serait faux de prétendre le contraire. On peut exiger un bloc JSDoc, exiger await, exiger que les modules qui correspondent à un motif dépendent des modules qui correspondent à un autre. Mais regardez ce que ces règles savent : un fichier, une convention de nommage, un motif. Elles sont locales. L’obligation qui compte, chaque port du cœur a un adapter câblé dans l’infrastructure, porte sur le système, et un linter n’a pas de système. Il a des fichiers.

Le modèle n’est donc pas un fichier de configuration de linter. C’est une description de ce que « juste » veut dire pour cette base de code : quelles frontières existent, quels contrats s’y trouvent, quelles pièces doivent être présentes pour qu’une fonctionnalité compte comme réelle. Il doit être écrit par quelqu’un qui connaît le système, et maintenu par quelqu’un qui le possède.

Je veux être direct sur qui est ce quelqu’un, parce que toute la scène se trompe discrètement là-dessus. Le cadrage actuel donne aux ingénieurs des règles à collecter : des presets par framework, des packs de bonnes pratiques, des antisèches de règles de lint pour agents. Ce qu’il faudrait donner aux ingénieurs, c’est un standard à écrire. Les ingénieurs aiment écrire ; ils ont toujours écrit l’architecture, dans des schémas et des documents qui ne pouvaient pas refuser un commit. Le changement n’est pas qu’ils arrêtent. Le changement est que ce qu’ils écrivent s’exécute maintenant.

C’est l’objet autour duquel cette série tourne sans le nommer : un . Pas un framework, parce qu’on est mesuré contre lui, on ne construit pas dedans. Pas un harnais, parce qu’il ne pilote pas votre agent ; il se branche sur ce qui le pilote. Pas un linter, parce qu’un linter dit ce qu’il a trouvé et qu’un standard dit ce qu’il a prouvé. C’est la description du système par l’ingénieur, transformée en ce qui juge chaque changement.

Un mot de placement, parce que l’article suivant emploiera un autre terme. Le standard est ce que l’ingénieur écrit : le modèle déclaré et ses règles. Ce qui le vérifie est une composition, vérificateur de types, schémas, tests, audits, traces, que l’article suivant appelle le système de garanties. Le standard est la partie déclarée de ce système ; ce n’est pas un outil, et aucun outil ne l’est à lui seul.

Où une obligation est décidable : le siège

Une obligation n’est vérifiable que s’il y a un endroit où regarder. J’appelle cet endroit le : l’endroit unique, localisable statiquement, où une garantie s’attache. Le validateur d’entrée d’une server function. Les validateurs d’un formulaire. Le schéma des paramètres de recherche d’une route. Le constructeur d’un type de domaine. La fonction de transition d’une machine à états.

L’idée a des noms plus anciens. La programmation orientée aspect appelait ces endroits des points de jonction, et la requête qui les sélectionne une coupe ; Michael Feathers appelait les endroits où l’on peut changer un comportement sans les éditer des coutures. Un siège est un point de jonction choisi pour une garantie, et que la bibliothèque a rendu explicite à dessein.

Un siège est une propriété du design de l’API d’une bibliothèque, pas d’un outil. Une bibliothèque qui met sa validation dans un callback enfoui dans une closure, ou la laisse à une convention, ou la laisse vivre « quelque part dans le handler », n’a pas de siège, et aucune obligation à son sujet n’est décidable sans exécuter le code. C’est un critère de choix des bibliothèques, et je pense que c’est le bon : pas « est-elle populaire » mais « a-t-elle un siège pour la garantie dont j’ai besoin, et combien de sièges devrai-je construire moi-même ».

Quand la bibliothèque n’a pas de siège, on en fabrique un. fetch n’en a pas : le parse de la réponse est ad hoc, partout. Alors la base de code reçoit un client mince qui prend un schéma, et une prohibition qui dit pas de fetch en dehors. Le wrapper est le siège, et la prohibition ferme la chaîne. Mince, supprimable par inlining, et, si une bibliothèque fait plus tard pousser le siège, disparu.

Voici à quoi ressemble un siège quand la bibliothèque en a un. Une server function TanStack Start porte son contrat d’entrée à un seul endroit, et le handler reçoit la valeur parsée :

TypeScript

const createArticle = createServerFn({ method: "POST" })
  .inputValidator(CreateArticleInput)          // le siège, et le contrat qui s’y attache
  .handler(async ({ data }) => {               // `data` est la valeur parsée, rien d’autre
    return articles.create(data);
  });

Trois choses sont décidables depuis ce texte sans l’exécuter : un validateur est attaché, il est appliqué avant le handler, et le handler consomme ce que le parse a produit. Comparez avec la version qui satisfait une règle plus faible :

TypeScript

const createArticle = createServerFn({ method: "POST" })
  .inputValidator(z.any())                     // un validateur est « présent »
  .handler(async ({ data, context }) => {
    const body = await context.request.json(); // et le corps brut est lu quand même
    return articles.create(body);
  });

Maintenant, la partie qui rend les obligations honnêtes. Une obligation seule se contourne par une présence vide. « Un validateur est attaché » est satisfait par un schéma qui accepte n’importe quoi. Chaque siège porte donc trois choses, pas une : l’obligation que le contrat est là, l’obligation qu’il est appliqué (parsé, pas seulement déclaré), et une prohibition selon laquelle rien ne consomme la valeur brute autour. La présence est l’obligation ; la forme est une prohibition au même endroit ; la garantie est la paire. Livrez l’obligation sans sa prohibition et un agent sous pression satisfera la lettre dès le premier commit.

Faites passer ça par la frontière la plus courante et ça se lit ainsi. Ce qui arrive d’un utilisateur : vérifier ce qui est attendu, rejeter ce qui n’a pas été demandé, et rien ne lit le corps brut. Ce qui revient d’une API tierce : pareil, mais dégager l’inconnu plutôt que le rejeter, parce qu’ils ajoutent des champs à chaque déploiement. Ce qui est lu en base de données : pareil encore, parce qu’une base est partagée de fait, un autre service y écrit, une colonne a été typée à la main il y a un an, et la confiance dans un schéma est une hypothèse sur le monde, pas sur le code. Et ce que votre serveur renvoie au client : réduire au contrat déclaré, jamais plus, parce qu’une ligne de base de données renvoyée telle quelle envoie password_hash au navigateur, et c’est le sur-envoi qui fuit.

Parse, don’t validate, comme une chaîne : un siège, un contrat, un parse et un verrou. Chaque frontière est une frontière de confiance, dans les deux sens.

Une frontière est une chaîne, et la chaîne a besoin d’un verrou

Une valeur entre à un siège, un contrat s’y attache, le parse l’applique, et seule la valeur parsée atteint le consommateur. Le verrou est une prohibition : aucun chemin de la valeur brute vers le consommateur.

Chargement du diagramme…

Annoncé tôt, jugé tard

Une propriété des obligations est facile à rater en pratique, et l’article précédent l’énonçait sans dire qui en est responsable.

Une prohibition est fausse dès qu’elle apparaît, donc elle se vérifie à chaque pas. Une obligation est légitimement non remplie pendant que la fonctionnalité s’assemble : l’adapter n’est pas encore là parce que le port a été écrit il y a dix minutes. Vérifiez-la trop tôt et vous noyez le constructeur sous les faux rouges ; ignorez-la et vous livrez le handler non câblé. Les obligations sont donc jugées quand le périmètre qu’elles gouvernent est .

Le point que je veux ajouter : le standard ne sait pas quand c’est. Seul, lancé en CI contre une pull request, il traite chaque périmètre comme clos, ce qui est correct, parce qu’une pull request est complète par définition. À l’intérieur de la boucle de travail d’un agent, quelque chose d’autre doit dire « ce périmètre ne recevra plus de fichiers », et ce quelque chose est le harnais, ce qui pilote l’agent et connaît le plan. « Annoncé tôt, jugé tard » est une propriété du couple, standard plus harnais, pas du standard seul. Un outil qui le promet à lui seul en promet trop.

Rapporter le complément, pas la couverture

Maintenant la partie que personne ne livre.

Une fois qu’un standard énumère ce qui devrait exister, il peut faire quelque chose qu’un linter ne peut structurellement pas faire : pour chaque garantie déclarée, à chaque changement, dire l’une de trois choses. Prouvé présent. Prouvé absent. Pas regardé.

Ce troisième état est le produit. Pas un pourcentage de couverture, qui est un chiffre sur l’outil. Pas un badge vert, qui est une déclaration sur l’absence de résultats. Une liste, cellule par cellule, de ce sur quoi la machine n’avait rien à dire. Parce que cette liste est exactement là où l’attention d’un relecteur humain doit aller, et nulle part ailleurs. C’est le , et sur la server function ci-dessus il se lit ainsi :

Garantie sur createArticleÉtatQui l’a prouvée
Un contrat est attaché à l’entréeprouvé présentrequireAtSeat
Le contrat est strict (pas de passthrough, pas de any)prouvé présentzodSchemaStrictness
Rien ne lit la requête brute autour du parseprouvé présentle verrou
La réponse est réduite à un contrat de sortie déclaréprouvé absentrequireAtSeat (sortie)
Le schéma est le bon pour un articlepas regardépersonne : c’est du sens

Quatre lignes qu’un relecteur peut sauter, une ligne rouge qui dit quoi ajouter, et une ligne honnête : la machine n’a pas d’avis sur la question de savoir si le schéma est le bon schéma. C’est sur cette dernière ligne que le relecteur lit.

Regardez à quoi ressemble la revue sans ça. Un agent produit un changement. Les vérifications sont vertes. Le relecteur doit maintenant décider quelle part du changement lire, et la réponse honnête est : tout, parce que le vert ne lui dit rien de ce qui a été vérifié. Alors il lit le diff, reconstruit les conséquences dans sa tête, et saute les parties dont il est fatigué. L’attention va là où l’œil du relecteur tombe, ce qui est le mauvais endroit par construction.

Avec le complément, le même relecteur ouvre une carte. Ces frontières ont été prouvées tenues. Ces contrats ont été prouvés présents, à tel grade. Ces cellules, le standard ne les a pas regardées, soit parce qu’aucune règle n’existe encore pour elles, soit parce que la règle ne pouvait pas tirer sur ce changement. Le relecteur lit ces cellules et survole le reste. Pas parce que le reste est juste, mais parce que le reste est : chaque garantie y nomme la règle qui l’a prouvée, et une garantie qui a un nom est une garantie que le lecteur n’a pas à redériver.

Un mot sur prouvé, puisque je l’emploie à dessein et que l’article suivant insistera pour dire garantie plutôt que preuve. Ici, il ne signifie que le sens mécanique : une règle a tourné, elle s’appliquait à ce changement, et elle est passée, avec le nom de la règle attaché. C’est une preuve de forme. Ce n’est pas une preuve formelle, et ce n’est jamais une preuve du modèle ; la distinction que trace l’article suivant tient pleinement, et tout ce qui est sur la carte est une garantie en son sens.

Deux règles d’honnêteté en découlent, et je les tiens strictement. Jamais un scalaire : un chiffre de couverture invite le lecteur à se sentir rassuré par une quantité, et le réconfort est ce que nous essayons d’abolir. Et « » veut dire une forme prouvée, jamais un sens prouvé : un schéma strict qui accepte un prix négatif passe. Le standard prouve que le schéma est là et strict ; savoir si c’est le bon schéma est le jugement du responsable du domaine, et la carte doit le dire plutôt que de laisser croire le contraire.

Rien de tout cela n’est nouveau hors de notre champ. L’ingénierie des systèmes critiques construit des dossiers d’assurance depuis des décennies : chaque affirmation attachée à sa preuve, chaque trou dans l’argument un objet nommé que quelqu’un doit signer. Le web n’a jamais importé cette discipline parce que personne n’y était forcé. Des agents qui écrivent l’essentiel du code, c’est la force qui oblige. Le coût d’écrire est tombé à zéro ; le coût de lire, non. Une carte de ce qui n’a pas été prouvé est la façon la moins chère de dépenser la lecture.

Avouer seulement dans son vocabulaire

L’aveu a une limite, et un lecteur sérieux la trouvera en trente secondes, alors je préfère l’écrire.

Les lignes de la carte viennent du modèle. Une frontière que personne n’a déclarée n’est pas « pas regardée ». Elle n’est pas sur la carte du tout. Le standard confesse l’absence de ce qu’il sait attendre, et il ne sait attendre que ce qu’un ingénieur a écrit. Si l’ingénieur a oublié une frontière, la carte se tait à son sujet de la pire façon : pas comme un trou, mais comme rien.

Ce n’est pas un défaut à cacher ; c’est la raison pour laquelle le standard doit être écrit par quelqu’un qui possède le système, et relu comme un document à part entière. L’honnêteté de la carte est bornée par la complétude du modèle, et la complétude du modèle est une responsabilité humaine. Un standard qui prétendrait le contraire vendrait à nouveau le badge, un étage plus haut.

La même limite dit quelque chose des tests. Il est tentant d’ajouter « chaque siège a un test » comme une obligation de plus, et ce serait la plus contournable de toutes : un test qui n’asserte rien la satisfait. Les tests ne sont pas des sièges. Ce sont des oracles : ils prouvent le fond d’une garantie dont le standard a prouvé la forme. Deux mécanismes, et la carte doit les tenir séparés, pour que « prouvé présent » ne veuille jamais dire en douce « il y a un fichier de test ».

Ce que ça fait à l’attention

Reculez et demandez à quoi tout cela sert.

L’économie de la construction logicielle vient de s’inverser. Écrire était la partie chère ; c’est maintenant presque gratuit. Lire du code qu’on n’a pas écrit était la partie bon marché, faite par la personne qui l’avait écrit ; c’est maintenant tout le coût, fait par quelqu’un qui ne l’a pas écrit. Chaque pratique qu’on appelait sur-ingénierie, configurations strictes, contrats explicites, matrices exhaustives, était chère parce que l’écrire était cher. C’est maintenant le côté bon marché de l’échange, parce que chaque garantie explicite et attribuée est une chose que le lecteur n’a pas à vérifier.

Voilà à quoi sert un standard : pas à attraper des bugs, ce que font les tests, ni à imposer un goût, ce que personne ne devrait faire, mais à déplacer le jugement hors de la tête du lecteur vers quelque chose qui tourne et nomme ce qu’il a fait. Il coupe le travail en deux. L’ingénieur écrit le standard et tranche la forme : le contenant, les frontières, les formes. La personne qui possède le domaine, ingénieur ou non, le regarde tourner et tranche le fond : si la chose construite dans le contenant est la bonne. Et ceux qui construisent dedans, qui ou quoi qu’ils soient, produisent un travail qui peut être relu sur le fond, parce que la forme a déjà été prouvée et attribuée.

Construisez votre standard exécutable pour que votre attention aille là où elle compte. Tout le reste de cet article est la mécanique de cette phrase.

Conclusion

L’article précédent disait : faites respecter l’architecture, ne vous fiez pas à l’intention. Celui-ci resserre l’affirmation, parce que « faire respecter » s’est révélé être le mot facile. Les prohibitions font respecter ; l’outillage pour cela existe et est maintenant branché sur des agents. Ce que les agents changent, ce n’est pas si les règles tournent, c’est ce qu’un résultat vert a le droit de vouloir dire.

Un standard exige la présence, ce qui demande un modèle. Il prouve par attribution, ce qui demande des sièges. Il rapporte son complément, ce qui demande la discipline de dire « pas regardé » à voix haute et de ne jamais le résumer en un chiffre. Et il avoue que son honnêteté s’arrête à son propre vocabulaire, ce qui est la raison pour laquelle un ingénieur doit l’écrire et continuer de l’écrire.

Ne demandez pas ce que votre vérificateur a trouvé. Demandez ce qu’il a regardé, et ce qu’il aurait dit s’il n’avait rien trouvé.

Le geste le plus fort reste celui dont parle l’article suivant : ne pas vérifier une violation, mais la rendre inexprimable. Mais entre vérifier et rendre impossible, il y a un vaste pays, et le standard y vit. La plupart des garanties ne deviendront jamais des types. Elles peuvent quand même être déclarées, prouvées à chaque changement, et avouées quand elles ne l’ont pas été.

Pour aller plus loin

  • Gail Murphy, David Notkin et Kevin Sullivan. Software Reflexion Models : divergence et absence, les deux polarités, il y a trente ans.
  • Neal Ford, Rebecca Parsons et Patrick Kua. Building Evolutionary Architectures : les fonctions de fitness architecturales, la moitié prohibition bien faite.
  • Tim Kelly et Rob Weaver. The Goal Structuring Notation : les dossiers d’assurance, où chaque affirmation porte sa preuve et chaque trou est un objet nommé.
  • Gregor Kiczales et al. Aspect-Oriented Programming (1997) : points de jonction et coupes, les noms plus anciens d’un endroit où quelque chose s’attache et de la requête qui le sélectionne.
  • Michael Feathers. Working Effectively with Legacy Code (2004) : coutures et points d’activation, les endroits où l’on peut changer un comportement sans les éditer.
  • Alexis King. Parse, Don’t Validate : le siège, le contrat et le parse comme un seul geste, avant que quiconque n’appelle ça une chaîne.
  • Standard Schema : l’interface que plusieurs bibliothèques de validation partagent désormais, et le vocabulaire dans lequel un siège peut être demandé sans nommer aucun outil.

Sharing is caring : Partager sur X · Partager sur Bluesky · Commenter sur Hacker News

Une remarque après lecture ?

Si vous souhaitez envoyer un mot au sujet de cet article, vous pouvez écrire ici. Je partage ici parce que le sujet m’intéresse et que je veux apprendre des autres. Merci pour vos retours, surtout lorsqu’ils sont formulés avec soin.