Brouillon à relire par Paul
Ce texte est un brouillon. Paul Perigault le relira et le réécrira avant publication.
ADR-0006 — Architecture : couches, résultats typés, composants, Rust, sobriété
- Statut : proposé (2026-10-28). Documentation seulement : aucune migration de code par cet ADR.
- Lié : ADR-0005 (TypeScript strict, Rust), D-030, D-035, D-037, AGENTS.md (limites de code), issue #82. Cadre les étapes 3 à 11 et le lot analyse V1 à V5.
Contexte
Le dépôt est un projet personnel qui peut devenir open source. Exigences de Paul : standards
propres, code de niveau senior en briques indépendantes, découpage assez fin pour Sonnet, Rust sans
obligation de gain (D-030) mais sans régression, site statique sur Cloudflare Pages avec du
WebAssembly, crates partageables avec un futur back Rust et Go, personnalisation d’un projet externe
par une variable de couleur et une de typographie. Les paquets d’outillage sont aujourd’hui des
fichiers à plat (packages/guards/src : une trentaine de fichiers et de dossiers au même niveau).
Décision 1 : Clean Architecture et DDD pour les outils
Périmètre : guards, create, mcp, générateur de tokens, web-presence, tools/agents.
Les composants Astro et le CSS n’y sont pas soumis (décision 3).
Structure type d’un paquet :
-
src/domain/: règles pures (types, fonctions, valeurs), aucune E/S. -
src/application/: cas d’usage, qui orchestrent le domaine via des ports ;ports.tsdéclare les interfaces (FS, horloge, process, fetch), c’est l’inversion de dépendance. -
src/infrastructure/: adaptateurs (fs, réseau, CLI avecparseArgs, sortie console). -
src/errors.ts: erreurs typées du paquet, une union fermée ;src/index.ts: API publique. -
test/: miroir desrc/; le domaine se teste sans double. -
Règle de dépendance vers l’intérieur :
infrastructure → application → domain.domainn’importe rien d’autre que lui-même et les types ;applicationn’importe quedomainet ses ports. Seuleinfrastructure(etbin) touchenode:*.guardsrefuse un import qui viole la règle (étape 3), comme il refusera celui qui remontetokens ← css ← astro. -
Ports : interfaces TypeScript (
FileSystem,Clock,Process,Fetcher) déclarées côté application, implémentées en infrastructure, injectées au point d’entrée. C’est déjà la pratique decreate(FS injecté) ; elle devient la règle. -
Erreurs typées centralisées : une union discriminée par paquet dans
errors.ts(champcodelittéral, message, contexte), comme le prévoit ADR-0005 ; aucune erreur anonyme franchit une frontière. -
DTO validés à chaque frontière : tout ce qui entre (JSON, arguments de CLI, fichier, réponse réseau, sortie WebAssembly) passe par une fonction de validation de l’infrastructure qui produit un type du domaine ou une erreur typée. Le domaine ne reçoit jamais de donnée non validée.
-
SRP et limites : une responsabilité par module ; fichier ≤ 120 lignes, fonction ≤ 40, complexité ≤ 8, profondeur ≤ 3, ≤ 4 paramètres. On découpe, on ne relève jamais la limite.
-
DDD, sans cérémonie : le vocabulaire du domaine (jeton, contrôle, violation, composant) nomme les types ; pas d’agrégats ni de dépôts génériques là où une fonction pure suffit.
Décision 2 : Effect-TS non adopté dans le cœur, option facultative plus tard
Effect-TS : non adopté dans le cœur. Prévu plus tard comme OPTION FACULTATIVE, hors cœur : un
paquet séparé (@paulperigault/effect : adaptateurs des ports en Layer, erreurs typées,
validation Schema) et une option de la CLI create (Effect oui/non). Les paquets du cœur n’ont
aucune dépendance à Effect ; les ports restent des interfaces TypeScript et le domaine reste pur,
ce qui rend l’ajout possible sans réécriture. À réaliser après les chantiers en cours ; évaluer
alors le serveur MCP effect-mcp (https://github.com/niklaserik/effect-mcp) pour limiter les erreurs
d’API avec Sonnet.
Le cœur garde un type Result maison dans domain/ de chaque paquet (ou un module commun de
tokens s’il est partagé).
| Critère | Effect-TS | Résultats maison |
|---|---|---|
| Poids d’exécution | dépendance d’exécution de plusieurs centaines de ko (à mesurer) | zéro octet, zéro dépendance |
| Courbe d’apprentissage | forte : fibres, couches, Effect.gen, pipe |
faible : union { ok, … } (ADR-0005) |
| Lisibilité | style fonctionnel propre à la bibliothèque | TypeScript courant |
| Node 24 | compatible | compatible |
| Sécurité, chaîne | grosse surface, mises à jour fréquentes à auditer | rien à auditer |
| Contributeurs, Sonnet | apprentissage requis, erreurs de typage difficiles à lire | schéma que tout modèle écrit sans aide |
Raisons du refus dans le cœur : D-030 et la sobriété (décision 5) imposent « aucune dépendance
d’exécution inutile » ; les paquets publiés guards, create et tokens n’ont aucune dépendance
d’exécution ; les ports injectés (décision 1) couvrent déjà l’injection, les erreurs typées et les
tests sans E/S. L’option reste utile à qui bâtit un site ou une application et veut Effect : elle
se branche aux ports sans toucher au cœur.
Décision 3 : composants Astro
- Sans couches : un composant est un fichier
.astro(≤ 120 lignes) avec son CSS scopé, ses types de propriétés et son test. La séparation domain/application/infrastructure ne s’y applique pas ; la logique non triviale (tri, calcul, mappage de variantes) vit dans un module.tspur testé. - Zéro JS par défaut ; un comportement (
behaviors) est une île optionnelle, activée par attribut, qui garde un repli utilisable sans script. - Accessibles par conception : sémantique native d’abord, nom accessible exigé par le type
quand le rôle l’impose, cibles ≥
--pp-target-min, propriétés logiques, pas destyleen ligne (AGENTS.md). Le test axe fait partie de la définition de fini. - Polymorphisme limité : une propriété
asn’accepte qu’une liste fermée de balises, typée par composant (par exemple'div' | 'section' | 'article'). Pas de balise libre, pas de...restnon filtré. Les variantes passent par des attributsdata-*mappés en CSS. - Modulables : composition par
<slot />et variantes nommées ; aucune valeur brute, tout passe par--pp-*. - Contrat de personnalisation d’un projet externe (à écrire plus tard, décrit ici seulement) :
- une variable pour la couleur d’accent : une teinte (OKLCH) dont dérivent l’accent, son survol,
--pp-color-on-accentet leurs variantes sombres, avec contraste WCAG 2 contrôlé à la dérivation ; - une variable pour la typographie : la pile de la famille de texte, dont dérivent
--pp-font-family-sanset les échelles ; la police est servie par le projet, hors du paquet ; - toute autre valeur garde son défaut ; surcharger un jeton reste possible mais hors contrat ;
- le contrat est stable (versionné, listé dans la documentation et le MCP) ; un changement cassant suit semver. Sa réalisation est un point ouvert (n° 2).
- une variable pour la couleur d’accent : une teinte (OKLCH) dont dérivent l’accent, son survol,
Décision 4 : politique Rust et WebAssembly (D-030)
- Critères d’adoption : parité de comportement prouvée en CI contre la référence TypeScript
(écart ≤ 1e-6 sur les composantes, ratio arrondi identique) ; poids du
.wasmbudgété et contrôlé ; temps de chargement non dégradé (mesuré avant et après) ; nipanicniunwrap(expect, indexation non vérifiée) hors tests ; erreurs typées (thiserrorou enum maison). Aucun gain de performance n’est exigé, aucune régression n’est tolérée. - Espace de travail Cargo :
Cargo.tomlracine ([workspace]),crates/pp-color(OKLCH, sRGB, gamut, contraste WCAG 2 et APCA),crates/pp-data(agrégation, quantiles, sous-échantillonnage). Les crates sont du Rust pur, sans dépendance àwasm-bindgen; la liaison WebAssembly vit dans des crates-wasmminces, de sorte qu’un back Rust ou Go (via une bibliothèque C ou un binaire) réutilise les crates sans le glue JavaScript. - Build : le
.wasmest compilé en CI et publié dans le paquet npm (@paulperigault/color, puis celui depp-data). Le build Cloudflare n’installe nirustupnicargo; il consomme le paquet. Le TypeScript reste la référence tant que la parité n’est pas établie. - Outillage :
rust-toolchain.tomlépinglé ;cargo clippy -- -D warnings -W clippy::pedantic(pedantic en refus) ;cargo fmt --check;cargo audit;cargo deny;cargo test, plusproptestpour les propriétés (aller-retour OKLCH, bornes du gamut). Limites de taille du dépôt appliquées au Rust. - Réexamen d’ADR-0005 (garde-fous) : sa décision reste valable pour le temps d’exécution actuel (0,2 s mesuré, aucun goulot établi) mais le critère « seulement si une mesure le justifie » ne s’applique plus : D-030 n’interdit plus Rust, qui peut y être adopté sans gain mesuré.
Décision 5 : Green IT et FinOps (détail à l’étape 10, #102)
- Un budget de poids par page et par ressource (HTML, CSS, JS, police, image,
.wasm), bloquant en CI. - Une estimation carbone par page, méthode à choisir et à sourcer à l’étape 10.
- Rester dans l’offre gratuite de Cloudflare Pages (builds, bande passante, requêtes) ; surveiller.
- Aucune dépendance d’exécution inutile : chaque ajout est justifié par écrit (ex. Effect-TS, hors cœur).
- Zéro JS par défaut, images au plus juste, polices limitées : la sobriété est une contrainte de conception.
Décision 6 : ordre d’application
| Étape | Issue | Ce que l’ADR impose |
|---|---|---|
| 3 | #83 | guards : structure type (décision 1), contrôle de la règle de dépendance vers l’intérieur et du sens tokens ← css ← astro |
| 4 | #84 | create migré vers la structure type ; ports FS et process conservés ; validation des DTO (arguments) |
| 5 | #85 | mcp et générateur de tokens : structure type, DTO validés à la lecture des données |
| 6 | #86 | tools/agents : structure type ; dist/ pour chaque paquet publié |
| 7a | #88 | crates/pp-color dans un espace de travail Cargo ; clippy pedantic ; sans dépendance wasm-bindgen |
| 7b | #89 | parité TypeScript/Rust en CI aux seuils de la décision 4 ; contraste WCAG 2 et APCA |
| 8 | #90 | .wasm compilé en CI, budget de poids, temps de chargement mesuré ; build Cloudflare sans Rust |
| 9 | #91 | mesure d’images selon les budgets de la décision 5 |
| 10 | #102 | budgets de poids, carbone, offre gratuite (décision 5) ; contrat de personnalisation si retenu |
| 11 | #103 | Dependabot Cargo ; cargo audit et cargo deny en CI ; documentation de l’architecture |
| V1 | #93 | ADR données : palettes dérivées du contrat d’accent, composants selon la décision 3 |
| V2a, V2b | #94, #95 | composants sans couches, polymorphisme fermé, zéro JS |
| V3a, V3b | #96, #97 | idem ; cibles ≥ --pp-target-min |
| V4a | #98 | crates/pp-data : mêmes critères que pp-color |
| V4b | #99 | .wasm de pp-data : parité, budget de poids, build sans Rust côté Cloudflare |
| V5 | #100 | page de démonstration soumise aux budgets de poids de la décision 5 |
Critères d’acceptation à changer (issues non modifiées par cet ADR) :
- #83 : ajouter « le contrôle de dépendance vers l’intérieur (
domainn’importe pasapplicationniinfrastructure) est testé » ; le périmètre est fixé par la décision 1. - #84, #85, #86 : ajouter « structure
domain/application/infrastructure, erreurs danserrors.ts» et « DTO validés aux frontières ». - #88, #89, #98 : remplacer «
cargo clippysans avertissement » par « clippy pedantic en refus,cargo fmt --check,cargo auditetcargo denyverts » ; ajouter « crate sanswasm-bindgen». - #90, #99 : ajouter « temps de chargement mesuré avant et après, non dégradé » et « build Cloudflare sans Rust ».
- #94 à #97, #100 : ajouter « polymorphisme limité à une liste fermée de balises » et « budget de poids de la page ».
- #102 : ajouter « budget bloquant par ressource ; offre gratuite Cloudflare surveillée ».
- #103 : ajouter «
cargo auditetcargo denyen CI ».
Points ouverts et risques
- Méthode d’estimation carbone (étape 10) : à choisir et à sourcer ; aucune n’est retenue ici.
Où et quand réaliser le contrat « une variable pour l’accent, une pour la typographie »: noms fixés le 2026-10-28 (D-039) :--pp-accent-hueet--pp-font-stack(détail : ADR-0007, point ouvert 1). Dérivation livrée par@paulperigault/dataviz(#120) ; branchement dans le build detokenset danscreate: issue #121.- Liaison d’un futur back Go aux crates : bibliothèque C, binaire ou WebAssembly (WASI) ; à trancher à l’arrivée du back.
- Migration des paquets vers la structure type : risque de découpage mécanique qui multiplie les fichiers sans clarté ; vérifié à la revue à chaque étape (une étape, une PR).
- Risque : deux langages à maintenir (TypeScript et Rust) ; parade : la parité en CI est bloquante et le TypeScript reste la référence jusqu’à son établissement.