Outillage cargo xtask — gates qualité gravés
Ce document décrit la crate d’outillage xtask : elle grave en outillage
déterministe et réutilisable les gates qualité qu’on vérifiait à la main —
en particulier ceux qui nous ont mordus (numéros de syscalls par arche,
invariant « couvrable vide », dérive du décompte d’exceptions).
Statut.
xtaskest de l’outillage de développement, pas du code Air livré. Il est membre du workspace (pour quecargo xtask <gate>se résolve via l’alias.cargo/config.toml), mais exclu de la mesure de couverture (--exclude xtask) et non soumis au 100 % des couches 0/1 (carve-out esprit ADR-030). Il reste linté (clippy --workspace -D warnings), testé (cargo test --workspaceexécute ses tests de parseurs) et audité (deny/audit/machete). Il n’introduit aucune dépendance externe (std seulement) : leCargo.lockdu produit et la reproductibilité (ADR-025) restent intacts.
Invocation
cargo xtask barrier [--no-coverage] # toute la barrière, un seul point d'entrée
cargo xtask couvrable-vide [--strict] [--no-root]
cargo xtask check-syscalls
cargo xtask check-layers
cargo xtask audit-exceptions [--strict]
cargo xtask repro [--keep]
Chaque sous-commande accepte --json (sortie machine) et rend un code de
sortie : 0 = vert (ou signalements non bloquants), 1 = violation, 2 =
erreur d’usage / interne. Conçu pour la CI comme pour Claude Code.
Les cinq gates
1. barrier — un seul point d’entrée pour la barrière
Enchaîne et agrège : fmt (stable et nightly), clippy --workspace --all-targets -D warnings, cargo test --workspace, couverture lignes +
branches en root (planchers ADR-031 :
lignes ≥ 96, branches ≥ 78), cargo audit, cargo deny check, cargo machete,
présence d’un // SAFETY: sur chaque bloc unsafe { … } du socle, cohérence
Cargo.toml ↔ DEPENDENCIES.md. Récapitulatif structuré pass/fail + chiffres.
--no-coverage saute les étapes root (boucle de dev rapide sans sudo).
2. couvrable-vide — anti-gaming (le plus important)
Lance llvm-cov (root), parse les lignes non couvertes du code production et
les croise avec COVERAGE-EXCEPTIONS.md.
- Gate DUR (mécaniquement décidable) : le plancher de couverture (lignes ≥ 96, branches ≥ 78). C’est le filet anti-gaming central — la couverture brute ne peut pas chuter en douce.
- Assist (advisory /
--strict) : l’invariant « couvrable VIDE » (toute ligne rouge atteignable ⊆ registre) n’est pas mécaniquement vérifiable — la reachability est une décision humaine (cf. CI.md) et lesfichier:lignedu registre dérivent. La réconciliation se fait donc par symbole (fonction englobante mentionnée au registre) ou par proximité de ligne (±40, pour absorber la dérive). Les lignes non rapprochées sont signalées ;--strictles durcit en violations (et fait échouer aussi sur un fichier production non couvert sans aucune section au registre). Les références du registre au-delà du fichier sont signalées comme orphelines.
--no-root mesure sans sudo (couverture partielle, ebpf plafonné — diagnostic
seulement).
3. check-syscalls — numéros déclarés vs headers uapi (ce qui nous a piégés)
Extrait les numéros de syscalls déclarés dans air-sys-syscall (commentaires
// SAFETY: SYS_<nom> (<arche> = <N>) et constantes d’arche const SYS_… /
const NR_IO_URING_…) et les compare, pour chaque arche, aux headers
uapi :
| Arche | Header (overridable) |
|---|---|
| x86_64 | /usr/include/x86_64-linux-gnu/asm/unistd_64.h (XTASK_UNISTD_X86_64) |
| aarch64 | /usr/include/asm-generic/unistd.h (XTASK_UNISTD_AARCH64) — aarch64 utilise la table générique |
Le header générique est présent sur les deux exécuteurs, donc les deux
arches sont vérifiables depuis n’importe lequel. Tout écart ⇒ VIOLATION
(exit ≠ 0), avec le détail syscall : code_déclaré vs header. Si un header
manque pour une arche, c’est signalé clairement (et ignoré), jamais un échec
silencieux. La résolution des indirections (__NR_mmap → __NR3264_mmap → 222)
est gérée.
4. audit-exceptions — format/taxonomie ADR-035 du registre
Valide chaque entrée de COVERAGE-EXCEPTIONS.md :
- catégorie ∈ {
STRUCTURAL,PRIVILEGE,FEATURE-KERNEL,CHILD-EXIT,DEFERRED-TOOLING} ; aucunTEST-HARNESSau registre production (ADR-035 §2) ; - justification non vide ;
fichier:ligneexistant : tout.rscité doit exister, toute ligne citée ≤ la taille du fichier, et tout symbole entre backticks doit être présent dans le fichier cité (anti-dérive — résout la dette de re-sync notée au sceau) ;- récapitulatif recomputé à partir des rangées et confronté au
récapitulatif affiché — toute divergence est une VIOLATION (anti-dérive du
décompte, le travers dénoncé par l’audit
053).
Les entrées DEFERRED-TOOLING sont signalées (dette à couvrir). --strict
durcit les orphelines fichier:ligne / symbole en violations.
5. repro — double-build bit-pour-bit (ADR-025)
Construit le même arbre deux fois en environnement contrôlé (toolchain
pinné, SOURCE_DATE_EPOCH fixe, --remap-path-prefix sur la source / le
target de chaque build / CARGO_HOME), dans deux target distincts remappés
vers le même préfixe logique, puis diffe bit-pour-bit l’ensemble des
*.rlib du socle. Toute divergence ⇒ exit ≠ 0, avec le détail des fichiers
non déterministes (premier octet divergent). C’est l’attestation de
reproductibilité visée par l’ADR-025 (le sceau couche 0 n’avait qu’un constat
same-host non outillé). --keep conserve target/repro pour inspection.
check-layers — doctrine de couche mécanisée (ADR-052 / ADR-077)
La doctrine de couche — « seule la couche 1 consomme la couche 0 » ; « les toits
C-ABI (air-libc-*, *-capi) bindent la couche 1, jamais la 0 en direct »
(ADR-077) ; air-sys-types transverse
(ADR-052 D6) — était tenue à la main
/ en revue. Ce gate la mécanise : chaque crate air-* déclare sa couche
dans [package.metadata.air] (layer = 0|1|2), zone ignorée par Cargo (même
précédent que [package.metadata.cargo-machete]) donc inerte ; le gate lit ces
déclarations et prouve que le graphe de dépendances les respecte :
- (a) toute crate
air-*déclarelayer(sinon « crate sans couche ») ; - (b) pas de dépendance montante (
M ≤ N) ; - (c) pas de saut (
M ≥ N-1) : un toitlayer=2ne touche pas la couche 0 en direct — il passe par la couche 1.
air-sys-types est exclu de (b)/(c) (transverse). Les arêtes air-*
cible-only ([target.'cfg(…)'.dependencies]) qui violeraient les règles — p.ex.
l’escape hatch syscall de la couche 0 cadré par
ADR-087, absent du graphe hôte livré —
sont signalées en advisory (non bloquant), pas rouge. Branché dans barrier.
Intégration CI
Le job supply-chain du workflow ci appelle cargo xtask check-syscalls et
cargo xtask audit-exceptions (rapides, sans root) en source unique des
gates correspondants. La couverture reste mesurée par les étapes llvm-cov
existantes du job test-coverage (en root, ADR-031),
inchangées hormis l’ajout de --exclude xtask. couvrable-vide --strict,
repro et barrier sont disponibles localement / à la demande (ils
rejouent la couverture en root et ne sont pas câblés en dur pour ne pas
déstabiliser la CI ; ils pourront l’être quand le registre sera ré-ancré par
symbole).
Pourquoi un gate buggé est pire que pas de gate
Un gate qui rougit au hasard (faux positif) ou qui passe à tort (faux négatif)
donne une fausse confiance. C’est pourquoi les parseurs de xtask
(couvrable-vide, check-syscalls, audit-exceptions, et les helpers de
barrier/repro) sont testés sur entrées synthétiques (cargo test --workspace), et pourquoi la frontière entre gate dur (mécaniquement
décidable : planchers, numéros de syscalls, taxonomie, cohérence du décompte) et
assist advisory (reachability, dérive fichier:ligne) est explicite.
Licence : MPL 2.0