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 :
| Avant | Après | Pourquoi |
|---|---|---|
interne | controleur | Le 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-c | bridge | Ce 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 :
| Valeur | Le contrat qu’elle porte | Effectif |
|---|---|---|
bridge | Le 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érations | 28 |
syscall | Le 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égitime | 1 |
service | Aucun — un démon système Air est consommé par son protocole | 4 |
user-cli | Aucun — un outil en ligne de commande est consommé par un humain | 3 |
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ègle | Ce qu’elle refuse | Origine |
|---|---|---|
| déclaration | role absent ou hors vocabulaire | D1 ci-dessus |
manager-absent | déclare manager sans porter de Air*Manager | ADR-149 D6 |
vocabulaire-impur | déclare vocabulaire en exposant des opérations publiques | ADR-149 D1 |
manager-non-declare | porte un Air*Manager en déclarant autre chose | ⚠️ voir ci-dessous |
controleur-fuite | déclarée controleur mais consommée depuis la couche N+1 | ADR-149 D9 |
bridge-vers-controleur | un bridge atteint un contrôleur (ou les syscalls) | D5, rendue vérifiable (BDFL 2026-08-05) |
syscall-hors-couche-1 | une crate hors couche 1 consomme les syscalls | BDFL 2026-08-05 |
service-consomme | un service ou un user-cli est lié par une autre crate | BDFL 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· 18manager· 16controleur· 10vocabulaire· 4service· 3user-cli· 3runtime· 1syscall. - 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.
xtaskcompile 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 :
| Crate | Liée par | Ce 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ées — air-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 :
| Crate | Consommateurs de couche 2 |
|---|---|
air-handle | air-libc-fileio, air-libc-socketio, air-sshd |
air-poll | air-libc-socketio, air-sshd |
air-random | air-keystore-cli, air-sshd |
air-stdio | air-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
xtaskpour un gain que la sur-approximation rend marginal (cf. D6).
Questions ouvertes
- Ratification de D2. Le vocabulaire compte désormais huit valeurs, arrêtées par le BDFL le 2026-08-05.
- Le sort des quatre fuites
controleur: manager à écrire, ou consommateur à corriger ? À trancher crate par crate, probablement dans le cycle de conformité. - 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. air-uringne manque que du manager (0 opération libre) : c’est le moins cher des dixmanager-absent, et le meilleur candidat pour ouvrir le cycle.
Licence du document : MPL 2.0