Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-152 — Outillage de l’architecture de code : déclaration role et gate check-controleurs

Statut : Proposé (2026-08-05). Réalise ADR-149 D9 et outille D1/D4/D6 ; s’appuie sur l’amendement d’ADR-077.

Catégorie : Outillage. Aucun code livré n’est modifié — la déclaration vit dans [package.metadata], zone inerte pour Cargo, donc aucun descellement.

Contexte

ADR-149 fait un constat sans détour : la dérive architecturale vient d’une règle donnée à l’origine mais jamais rédigée, puis rédigée mais jamais outillée. Ses décisions D3 (le contrôleur borne la responsabilité et les tests), D4 (trois rangs, un seul contractuel) et D6 (une crate porte au moins un contrôleur) sont écrites et vérifiées par rien. L’amendement d’ADR-077 les a mesurées à la main une fois, le 2026-08-03 ; rien n’empêchait le relevé de se périmer dès la PR suivante.

ADR-149 D9 avait identifié le mécanisme — une déclaration role dans le manifeste — et expliqué pourquoi elle ne peut pas être déduite : « toute crate finit par ressembler assez à du vocabulaire pour passer », constat étayé par un classificateur automatique qui rangeait air-uring (43 fonctions publiques) parmi les crates de vocabulaire. Cette déclaration n’existait dans aucune des 83 crates au 2026-08-05.

Décision

D1 — role est obligatoire, et son vocabulaire est fermé

Toute crate air-* déclare son rôle à côté de sa couche :

[package.metadata.air]
layer = 1
role  = "manager"

Une déclaration absente ou hors vocabulaire est une violation immédiate, dans tous les modes du gate. C’est la seule exigence qui ne souffre aucun délai : elle ne demande d’écrire aucun code, seulement de dire ce que la crate est. Les 83 crates la tiennent depuis cet incrément.

D2 — Le vocabulaire : quatre valeurs deviennent sept, et deux changent de nom

Renommages (BDFL, 2026-08-05) — chacun nomme ce que la chose est, là où l’ancien nom disait seulement ce qu’elle n’est pas :

AvantAprèsPourquoi
internecontroleurLe mot dit le rang de D4 : c’est ce sur quoi un manager s’appuie pour tenir son rôle. Un manager est un manager de contrôleurs, et c’est lui la surface publique de la couche. interne ne disait que « pas public »
abi-cbridgeCe sont des ponts vers les API Rust d’Air pour un autre langage. Le nom porte sa règle : un pont mène quelque part, et on peut dire où il a le droit de mener

Trois valeurs ajoutées, parce que trois familles de crates ne participent pas au contrat d’API Rust entre couches — elles participent à un autre contrat :

ValeurLe contrat qu’elle porteEffectif
bridgeLe C ABI (ADR-012, 10 ans). Aucune valeur de D9 n’était vraie : controleur en ferait un détail d’implémentation quand c’est la surface la plus externe et la plus stable ; manager exigerait un Air*Manager qu’un pont ne doit pas porter ; vocabulaire nierait ses 264 opérations28
syscallLe noyau (D2 : le seul endroit où unsafe existe). vocabulaire nierait ses 316 opérations ; manager exigerait un AirSyscallManager que D2 exclut ; controleur produirait ~30 controleur-fuite sur ce qui est la relation N-1 → N légitime1
serviceAucun — un démon système Air est consommé par son protocole4
user-cliAucun — un outil en ligne de commande est consommé par un humain3

Ces deux dernières valeurs portent bien une règle, contrairement à ce que la première version de cet ADR supposait : ni un démon ni un outil n’expose d’API réutilisable, donc aucune autre crate ne doit les lier. La règle a immédiatement attrapé une crate que j’avais mal classée — voir plus bas.

D3 — La déclaration dit l’intention, pas l’état

Une crate qui doit porter un manager déclare role = "manager" même si elle n’en porte pas encore. C’est le registre (D5) qui porte la dette, pas la déclaration.

La règle inverse serait auto-destructrice : si déclarer controleur dispensait de l’obligation de manager, toute crate en écart s’y rangerait, et le gate mesurerait la docilité des déclarations au lieu de l’architecture. La déclaration est un engagement, pas une porte de sortie.

D4 — Huit règles, toutes mécaniques

RègleCe qu’elle refuseOrigine
déclarationrole absent ou hors vocabulaireD1 ci-dessus
manager-absentdéclare manager sans porter de Air*ManagerADR-149 D6
vocabulaire-impurdéclare vocabulaire en exposant des opérations publiquesADR-149 D1
manager-non-declareporte un Air*Manager en déclarant autre chose⚠️ voir ci-dessous
controleur-fuitedéclarée controleur mais consommée depuis la couche N+1ADR-149 D9
bridge-vers-controleurun bridge atteint un contrôleur (ou les syscalls)D5, rendue vérifiable (BDFL 2026-08-05)
syscall-hors-couche-1une crate hors couche 1 consomme les syscallsBDFL 2026-08-05
service-consommeun service ou un user-cli est lié par une autre crateBDFL 2026-08-05

La règle du pont, énoncée par le BDFL : « un bridge n’accède jamais à un contrôleur, toujours au manager de la couche qu’il expose ». C’est D5 — « son corps appelle le manager et rien d’autre » — rendue vérifiable sur le graphe de dépendances. Et la couche 0 se lit en deux natures : les types, qui traversent toutes les couches, et les syscalls, que seule la couche 1 a le droit d’envelopper — toute autre couche qui les atteint se refait une couche 1 en douce.

manager-non-declare n’était pas prévue. Elle a été ajoutée après qu’un test adverse du gate contre lui-même l’a mise en évidence : requalifier air-system — qui porte AirSystemManager — en vocabulaire le faisait passer sans un mot, puisque la règle manager-absent ne s’applique qu’aux crates qui se déclarent manager. Sans cette cinquième règle, D3 était contournable par un seul mot dans un manifeste. C’est la raison pour laquelle un gate doit être attaqué avant d’être cru.

D5 — Un cliquet, jamais un interrupteur

Au premier relevé l’arbre porte 17 écarts : un gate binaire serait donc soit désarmé, soit bloquant pour tout le monde, y compris pour des PR qui ne touchent pas l’architecture. Le gate lit un registre d’écarts connus (xtask/controleurs-reference.toml) et applique la discipline déjà éprouvée par couvrable-vide et par la référence de couverture (#635) :

  • un écart inventorié est toléré et compté ;
  • un écart nouveau est signalé — et échoue en mode --bloquant ;
  • une entrée qui n’est plus en écart est orpheline : le registre doit rétrécir, et le gate le dit.

On n’ajoute jamais une entrée pour faire passer une PR. Un écart nouveau est une régression : il se corrige par du code. Le registre est une dette datée, pas une permission.

Consultatif d’abord, bloquant ensuite — tranché par le BDFL le 2026-08-03. Le basculement se fait quand le registre est vide, et il n’exige alors aucune discussion : c’est le sens du cliquet.

D6 — L’analyse est textuelle, et c’est un choix

Distinguer une méthode de contrôleur (pub fn dans un impl) d’une opération libre (pub fn au niveau module) demande de suivre les blocs. Le gate le fait avec un compteur d’accolades, pas avec un parseur Rust : xtask n’a aucune dépendance externe (ADR-024), et en introduire une pour cela serait payer cher un gain marginal.

Le choix est tenable parce que les erreurs de cette analyse vont dans le sens sûr : elle sur-compte les opérations libres, donc elle alerte — elle n’endort pas.

Conséquences

  • 83 crates déclarent leur rôle : 28 bridge · 18 manager · 16 controleur · 10 vocabulaire · 4 service · 3 user-cli · 3 runtime · 1 syscall.
  • 28 écarts inventoriés, qui deviennent la feuille de route de la mise en conformité ordonnée par l’amendement d’ADR-077 — et la matière de l’ADR de reprise qui les priorisera.
  • Le gate tourne dans la barrière et en CI, en mode consultatif.
  • xtask compile et se teste sur macOS en 4 secondes : ce gate se développe et s’exerce sans exécuteur Linux, contrairement à tout le reste du projet.

Sept bridges qui court-circuitent la porte

La règle du pont trouve 7 des 28 bridges en écart. Deux d’entre eux — air-object-c → air-object et air-value-c → air-value — sont intra-couche 2 : aucune règle inter-couche ne pouvait les voir, et c’est ce qui justifie le rôle bridge mieux que n’importe quel argument de vocabulaire.

Côté syscalls, deux crates seulement sortent de la couche 1 : air-libc-capi (dépendance réelle, sous cfg(target_vendor = "air") — l’advisory qu’encadre ADR-087) et air-libc-spawn (dev-dependency, rien de livré). La discipline est donc déjà presque tenue.

Correction d’un relevé faux. Une première version de cette analyse annonçait « huit coquilles dépendent de la couche 0 » : le relevé comptait des commentaires, dont un qui disait exactement l’inverse (« REMÉDIATION ADR-077 : cette crate ne dépend plus de air-sys-syscall »). Le nettoyage avait déjà eu lieu ; c’est la lecture qui était fautive.

Une crate mal classée, et ce qu’elle révèle

air-notifyd avait été rangée service sur la foi de son suffixe -d — exactement l’inférence que D9 interdit. Elle ne produit aucun binaire : une lib et deux exemples tutoriels, que personne ne consomme. La règle service-consomme l’aurait attrapée seule. Elle reste déclarée service par décision du BDFL (le démon a vocation à exister), mais l’épisode confirme que toute valeur du vocabulaire doit porter une règle, sans quoi elle devient un tiroir où l’on range sans regarder.

La même règle en trouve deux autres, réelles :

CrateLiée parCe que ça dit
air-agent [user-cli]air-sshd (livrée)La crate mélange un outil utilisateur et une bibliothèque de protocole — c’est la seconde qui mérite d’être extraite
air-launchd [service]air-launch-temoin (dev)Le harnais de preuve lie le lanceur ; échafaudage de test, pas architecture livrée

Le rôle ne peut pas dire la vérité sur air-sshd

air-sshd produit deux binaires de natures opposéesair-sshd (démon, donc service) et air-ssh (outil utilisateur, donc user-cli) — plus une lib de 96 opérations libres. Or role se déclare par crate : aucune valeur ne peut être vraie tant que la crate porte les deux.

ADR-095 avait déjà tranché « deux binaires séparés » pour le packaging ; la taxonomie des rôles dit la même chose depuis l’angle de l’architecture. Le découpage en air-sshd (service) + air-ssh (user-cli) + une bibliothèque partagée est matière à l’ADR de reprise, pas au gate. En attendant, la crate est déclarée service — sa nature dominante.

Quatre écarts que l’audit manuel n’avait pas vus

L’amendement d’ADR-077 regardait les managers et les fonctions libres. Il ne regardait pas le graphe de consommation. Le gate y trouve quatre crates de couche 1 déclarées controleur mais consommées depuis la couche 2 :

CrateConsommateurs de couche 2
air-handleair-libc-fileio, air-libc-socketio, air-sshd
air-pollair-libc-socketio, air-sshd
air-randomair-keystore-cli, air-sshd
air-stdioair-libc-stdio

Chacune pose la même question, et elle n’est pas tranchée ici : soit ces crates doivent devenir des managers, soit leurs consommateurs passent par la mauvaise porte. Le cas d’air-stdio est le plus parlant — la coquille air-libc-stdio appelle des fonctions libres, alors que D5 lui impose d’appeler un manager.

Alternatives rejetées

  • Déduire le rôle par heuristique. Rejeté par ADR-149 D9, preuve empirique à l’appui. Le gate aurait mesuré sa propre indulgence.
  • Un gate binaire, bloquant tout de suite. Rejeté : 17 écarts hérités bloqueraient toutes les PR, y compris celles qui n’y touchent pas — exactement le défaut que #635 vient de corriger sur la couverture (un plancher absolu qu’aucune mesure n’atteignait).
  • Attendre la mise en conformité pour écrire le gate. Rejeté, et c’est l’inverse qu’il faut faire : sans cliquet, la conformité se dégraderait au rythme où on la produirait. Le gate d’abord, le nettoyage ensuite.
  • Parser le Rust pour être exact. Rejeté : une dépendance externe dans xtask pour un gain que la sur-approximation rend marginal (cf. D6).

Questions ouvertes

  1. Ratification de D2. Le vocabulaire compte désormais huit valeurs, arrêtées par le BDFL le 2026-08-05.
  2. Le sort des quatre fuites controleur : manager à écrire, ou consommateur à corriger ? À trancher crate par crate, probablement dans le cycle de conformité.
  3. Quand basculer en --bloquant. La réponse par défaut est « quand le registre est vide » ; une réponse plus exigeante serait « dès que le cycle de conformité démarre », les écarts restants étant alors tous inventoriés et en cours de traitement.
  4. air-uring ne manque que du manager (0 opération libre) : c’est le moins cher des dix manager-absent, et le meilleur candidat pour ouvrir le cycle.

Licence du document : MPL 2.0