Périmètre V2
{% raw %} Ce document définit le périmètre, les décisions et le plan de travail de Crabster V2. Il prend le relais du plan de travail V1, qui couvre les phases 0 à 10 (V1 API-only) et la phase continue de documentation.
Statut, au 17 septembre 2026 : neuf phases sur dix livrées. Reste la phase 18, dont la definition of done demande un blueprint maintenu par quelqu'un d'extérieur à ce dépôt — elle attend des gens, pas du code.
Ce document a été écrit en planification, et la condition qu'il posait alors — « la V2 ne démarre qu'une fois la V1 livrée et utilisée en conditions réelles » — n'a jamais pu être remplie : rien n'est publié, il n'y a personne à écouter. C'est l'ADR-0003 qui a tranché ce que cela impliquait, en refusant qu'une condition insatisfiable bloque deux phases.
1. Positionnement V1 → V2🔗
| V1 | V2 | |
|---|---|---|
| Promesse | « D'un modèle de domaine à une API Rust de production en 5 minutes » | « Un projet Crabster vit, évolue et se déploie dans la durée » |
| Génération | One-shot + ajout simple d'un enregistrement | Régénération incrémentale, mise à jour de version, fusion assistée |
| Périmètre applicatif | Monolithe API-only, SQL, JWT | Multi-services, NoSQL, OAuth2/OIDC, frontend (option tranchée) |
| Écosystème | Mécanisme de blueprints livré, écosystème vide | Écosystème de blueprints amorcé, blueprints officiels maintenus |
| Migration | Nouveau projet uniquement | Rétro-ingénierie d'une base existante |
La V1 répond à « comment démarrer ». La V2 répond à « comment continuer » — c'est le passage du générateur de squelette à l'outil de cycle de vie, et c'est ce qui décide s'il reste utile après le premier jour.
2. Objectif central de la V2🔗
Si la V2 ne devait livrer qu'une chose, ce serait la mise à jour incrémentale d'un projet généré (Phase 11). C'est le problème le plus difficile du domaine, celui que la V1 esquive délibérément, et celui qui détermine si Crabster reste utile après le premier jour ou devient un outil d'amorçage jetable.
Toutes les autres phases V2 sont subordonnées à ce constat : en cas d'arbitrage de ressources, la Phase 11 passe avant tout le reste.
3. Thèmes de la V2🔗
| Thème | Phases | Motivation |
|---|---|---|
| T1 — Cycle de vie du code généré | 11 | Rendre un projet Crabster maintenable dans la durée |
| T2 — Sécurité de niveau entreprise | 12a, 12b | Lever le blocage OAuth2/OIDC pour les adoptions en entreprise |
| T3 — Architectures distribuées | 13 | Génération multi-services |
| T4 — Persistance étendue | 14 | Sortir du seul SQL (NoSQL, recherche) |
| T5 — Migration & interopérabilité | 15 | Reprendre des bases de données existantes |
| T6 — Frontend | 16 | Lever l'ADR-0001, décision différée depuis la V1 |
| T7 — Déploiement & écosystème | 17, 18 | Kubernetes, cloud, maturité des blueprints communautaires |
| T8 — Expérience de création | 19 | Demander ce dont un projet a besoin, au lieu qu'on le dise en flags |
4. Décisions d'architecture V2🔗
ADR-0002 — Fusion incrémentale par zones protégées + 3-voies🔗
- Statut : implémenté, avec un écart. Les deux mécanismes sont livrés : les zones protégées, et l'instantané dans
.crabster/snapshot/avec la fusion 3-voies qu'il permet, danscrabster upgrade. - Écart : les marqueurs de conflit ne vont pas dans le fichier. La décision disait « des marqueurs de conflit à la
git merge» ; ce que faitcrabster upgrade, c'est laisser le fichier exactement tel que son propriétaire l'a laissé et écrire la fusion marquée dans.crabster/incoming/, sous le même chemin. Git peut se permettre d'écrire des marqueurs dans un arbre de travail parce qu'il est le chemin de retour ; un projet généré n'est pas forcément dans git, et un outil qui écrit dans un fichier qu'il n'a pas écrit doit laisser à son propriétaire de quoi revenir. L'information est la même, à une copie près. - Contexte : la V1 génère en one-shot et refuse d'écraser un fichier modifié à la main. Pour la V2, il faut pouvoir régénérer un projet dont le code a été modifié par l'utilisateur, sans perdre ses modifications.
- Décision : approche hybride en deux mécanismes complémentaires :
- Zones protégées — les fichiers générés portent des marqueurs de commentaire délimitant les régions réservées à l'utilisateur (des marqueurs de zone réservée), préservées à l'identique lors d'une régénération.
- Fusion 3-voies — pour tout le reste, Crabster conserve dans le projet (
.crabster/) un instantané du code généré tel qu'il était à la dernière génération. La mise à jour compare ancien généré / nouveau généré / code actuel de l'utilisateur et produit une fusion, avec des marqueurs de conflit à lagit mergelorsque la résolution automatique est impossible.
- Conséquences : le répertoire
.crabster/(modèle CDL + instantané + version des templates utilisée) devient un artefact versionné du projet généré, à documenter clairement. Les templates doivent être conçus pour minimiser les conflits (séparer le code purement généré du code destiné à être modifié). - Alternative écartée : régénération complète avec
git diffmanuel laissé à l'utilisateur — simple à implémenter, mais reporte tout le coût sur l'utilisateur et ne tient pas à l'échelle d'un projet réel.
ADR-0003 — Frontend : client typé généré, puis blueprints UI🔗
- Statut : accepté (2026-09-13). Cet ADR clôt formellement l'ADR-0001.
- Contexte : l'ADR-0001 a différé le choix frontend. Trois options sont sur la table : (a) générer un frontend JS/TS complet, (b) générer un frontend Rust/WASM (Leptos/Yew), (c) générer seulement un client typé depuis la spec OpenAPI, sans UI.
- Décision : (c), en cœur — générer un client typé (TypeScript et Rust) depuis le contrat OpenAPI stabilisé en V1 — avec (a) et (b) comme blueprints officiels plutôt que comme du cœur.
- Justification : (a) et (b) impliquent de maintenir un ou plusieurs frameworks UI à travers leurs cycles de rupture — le coût de maintenance qui pèse le plus lourd sur ce genre d'outil, et qui se paie dans les termes d'un second écosystème : framework, bundler et lockfile, chacun à son propre rythme de publication. Ce dépôt porte déjà dix modules de templates qui doivent rester en phase à chaque changement de version, et
crabster upgradefusionne du code généré d'une version de templates à la suivante. Ajouter une UI au cœur double cette surface. L'option (c) offre l'essentiel de la valeur — typage bout en bout, plus de client HTTP écrit à la main — pour une fraction du coût, et reste utile quel que soit le framework choisi.
Pourquoi le statut provisoire tombe, ce qui est la vraie décision ici. L'ADR était écrit proposé, provisoire, à reconfirmer avec les retours d'usage réels de la V1 avant implémentation. Cette condition ne peut pas être remplie : la 0.1.0 est balisée et non publiée, le dépôt est privé, et il n'y a personne à écouter. Aucun retour ne peut arriver avant une publication, et la publication n'attend pas ceci — alors que la condition, elle, attend. Elle bloque la phase 16, et à travers elle la moitié « session » de la phase 12b, qui a besoin d'un endroit où poser une session navigateur. Une condition qui ne peut pas être satisfaite et qui bloque deux phases n'est pas de la prudence ; c'est un blocage avec une raison écrite dessus.
La décision est donc prise maintenant et révisable sur preuve, plutôt que différée jusqu'à une preuve que le report lui-même empêche. Ce qui la renverserait : des premiers utilisateurs disant que le client typé ne suffit pas, et qu'une UI imposée qu'ils n'ont pas choisie vaut mieux que rien. C'est un argument d'adoption, et il ne peut se tenir qu'après publication.
- Conséquences :
- Crabster ne promet pas d'application UI clé en main. C'est le coût, et il mérite d'être nommé franchement : c'est le seul endroit où Crabster n'est pas l'équivalent de JHipster, et un lecteur venant de cet écosystème le remarquera dès le premier jour. Il faut que ce soit dit par le projet plutôt que découvert.
- Le wizard de création (ADR-0006) demande le front end, et depuis le 2026-09-17 il propose aucun, React, Vue ou Angular — les trois blueprints livrés dans le binaire. La règle a tenu : l'option est apparue le jour où elle marchait, pas avant.
- « Blueprint officiel » doit vouloir dire quelque chose de vérifiable, sinon c'est un mot. Un blueprint officiel vit dans ce dépôt, est généré et compilé dans le palier lourd, et est tenu par la même barrière que le cœur. Un blueprint qui n'est que référencé est un blueprint communautaire, et doit s'appeler ainsi.
- Le framework visé par le premier blueprint UI officiel n'est pas tranché ici, et n'a pas à l'être : c'est une question sur qui se présente, et le critère est l'écosystème dans lequel les premiers utilisateurs se trouvent déjà.
Amendement du 2026-09-17 — les trois blueprints UI officiels sont livrés dans le binaire. Ce n'est pas un retour sur cette décision, et il faut dire précisément pourquoi. Ce que l'ADR protège, c'est que les gabarits du cœur ne portent aucun framework UI et que l'interface soit remplaçable ; les deux tiennent, et le second est tenu par un test — un --blueprint à soi qui porte un module ui-react remplace celui qui est livré.
Ce qui a changé n'est pas la décision mais la portée. Les trois vivaient sous examples/, qui n'entre pas dans le .crate publié : ui react était donc un réglage qu'aucun binaire installé ne savait honorer, alors que le README promettait trois interfaces. Le premier essai depuis un binaire installé a répondu « ui is not a setting any installed module answers to ». Une promesse que rien ne tient est le défaut qu'on a corrigé, pas la décision.
Ils sont installés d'office parce qu'ils ne font qu'ajouter un module, revendiqué par un réglage que rien d'autre ne réclame : un projet qui n'écrit pas ui … est octet pour octet celui qu'il était avant qu'ils existent. Les blueprints qui remplacent un module du cœur par son nom — Diesel pour record, GitLab pour core — ne peuvent pas l'être, restent sous examples/blueprints/ et se demandent avec --blueprint. Cette règle est tenue par un test, pas par ce paragraphe.
ADR-0004 — Persistance non-SQL hors SeaORM🔗
- Statut : proposé.
- Contexte : SeaORM couvre le SQL. MongoDB (et toute base document) n'entre pas dans son modèle.
- Décision : introduire dans l'IR une abstraction de « backend de persistance » et traiter MongoDB comme une implémentation parallèle (driver
mongodbnatif), et non comme un dialecte de plus derrière SeaORM. Les templates d'enregistrement se spécialisent par backend. - Conséquences : certaines constructions CDL n'ont pas de sens en document store — les références, qui deviennent des clés étrangères, et les migrations de schéma ; le parseur doit rejeter explicitement ces combinaisons avec un message clair, plutôt que générer du code incohérent.
ADR-0005 — Microservices sans équivalent Spring Cloud🔗
- Statut : proposé.
- Contexte : d'autres écosystèmes disposent d'une pile distribuée intégrée (registre, serveur de configuration, passerelle). L'écosystème Rust n'a pas d'équivalent.
- Décision : composer des briques existantes plutôt que reconstruire une pile : Consul pour le service discovery et la configuration centralisée (Consul KV), gateway généré en Axum + tower plutôt qu'un produit tiers.
- Conséquences : moins de fonctionnalités « clé en main » côté microservices, mais aucune dépendance à un framework distribué propriétaire. Le service mesh (Istio/Linkerd) est documenté comme alternative recommandée pour les besoins avancés, hors périmètre de génération.
ADR-0006 — Création interactive, en façade des flags🔗
- Statut : accepté (2026-09-10).
- Contexte : la cible est un assistant de création à la manière de JHipster
— desktop ou web ; si web, monolithique ou microservices ; puis la méthode
d'authentification, le client frontend, la passerelle. Aujourd'hui rien dans
le CLI ne lit un terminal :
crabster newprend des flags,crabster import-cdllit un.cdl, et le blocserviceporte le reste. L'assistant n'est pas une réécriture de tout cela ; la question est ce qu'il a le droit d'être. - Décision : quatre règles, et la première entraîne les autres.
- L'assistant est une façade au-dessus des flags, jamais le seul chemin.
Chaque réponse qu'il recueille a un flag ou un réglage
servicequi dit la même chose, et un appel non interactif le saute entièrement. - Les réponses sont enregistrées là où le cycle de vie regarde déjà —
.crabster/project.tomlet le blocservicedu modèle. Rien de ce que l'assistant demande ne peut n'exister que dans la question. - Aucune question sans réponse générable. Une question dont les options n'existent pas toutes n'est pas ajoutée avant qu'elles existent : proposer un choix que le générateur ne sait pas honorer est pire que ne pas le proposer.
- Chaque phase ajoute sa propre question, dans le commit qui livre la capacité qui est derrière.
- L'assistant est une façade au-dessus des flags, jamais le seul chemin.
Chaque réponse qu'il recueille a un flag ou un réglage
- Conséquences :
- Il peut sortir maintenant, en ne demandant que ce qui a déjà une réponse — la base de données, le port, la méthode d'authentification — et grandir d'une question par phase.
- La suite de tests et la CI continuent de piloter le CLI sans terminal. Ce
n'est pas un détail : quatorze tests bout-en-bout compilent et exécutent des
projets générés entiers, et le corpus de régression pilote
record,applyetupgradepar arguments. Un assistant devenu obligatoire les casserait tous le jour de son arrivée. - Il tranche le mécanisme et non le menu. Deux des questions que nomme la
cible ne sont pas encore celles de Crabster, et sont ouvertes ici plutôt que
décidées :
- Le desktop est hors du produit documenté. Toutes les phases, de 0 à 18, supposent un service HTTP — les enregistrements deviennent une API CRUD, de l'OpenAPI, des sondes de santé, une image, un pipeline. Une application desktop garde le modèle de domaine et pas grand-chose d'autre. C'est une seconde ligne de produit, pas une phase, et il lui faut une décision à elle avant de pouvoir être une question.
- Le client frontend relève de l'ADR-0003, qui garde les frameworks d'interface hors du cœur et les met en blueprints. Les deux se concilient sans le renverser : l'assistant demande, et la réponse sélectionne un blueprint. L'expérience est celle que décrit la cible ; le cœur ne porte toujours aucun framework JS à travers ses cycles de rupture.
- Alternatives écartées :
- L'assistant comme seul chemin. C'est par lui que l'outil se découvrirait,
et c'est aussi par lui qu'il cesserait d'être scriptable. Tous les
générateurs qui l'ont fait ont dû ajouter un
--skip-promptsensuite. - Les réponses dans un fichier à part. Une seconde source de vérité à côté
de
.crabster/, ce que la phase 11 existe précisément pour éviter.
- L'assistant comme seul chemin. C'est par lui que l'outil se découvrirait,
et c'est aussi par lui qu'il cesserait d'être scriptable. Tous les
générateurs qui l'ont fait ont dû ajouter un
5. Plan de travail V2🔗
La numérotation continue celle du plan V1 (phases 0-10). Les phases 12 à 15 sont largement parallélisables entre contributeurs ; la Phase 11 est un préalable structurant pour toutes les autres, car elle change la façon dont les templates sont écrits.
La phase 19 s'ordonne comme la phase C en V1 — pas après les autres, mais à côté d'elles. Elle peut démarrer tout de suite avec les questions qui ont déjà une réponse, et par l'ADR-0006 chaque phase ci-dessous ajoute la sienne en arrivant. Elle est en dernier dans le tableau parce qu'elle est celle qui finit en dernier, pas celle qui commence en dernier.
| Phase | Titre | Dépend de | Priorité |
|---|---|---|---|
| 11 | Cycle de vie : upgrade & fusion incrémentale | V1 complète | Critique |
| 12a | Authentification étendue : resource server OAuth2/OIDC | 11 | Haute |
| 12b | Autorisation déclarative, et la session navigateur | 12a, 16 | Moyenne |
| 13 | Microservices | 11, 12a | Haute |
| 14 | Persistance étendue (NoSQL, recherche) | 11 | Moyenne |
| 15 | Rétro-ingénierie d'un schéma existant | V1 (Phase 3) | Moyenne |
| 16 | Frontend : client typé & blueprints UI | V1 (Phase 6) | Moyenne |
| 17 | Kubernetes & déploiement cloud | 13 | Moyenne |
| 18 | Maturité de l'écosystème blueprints | 11 | Continue |
| 19 | Création interactive d'un projet | — | Continue |
Phase 11 — Cycle de vie : upgrade & fusion incrémentale🔗
Objectif : un projet généré avec Crabster x.y peut être mis à jour vers x.z et voir son modèle CDL évoluer, sans perte des modifications manuelles.
Livrée, hors phase. Les zones protégées ;
crabster upgrade— régénération depuis.crabster/, arbitrage fichier par fichier à l'empreinte ; l'instantané du code généré dans.crabster/snapshot/et la fusion 3-voies qu'il rend possible ; les migrations gelées (write_once) ; etcrabster apply, qui fait d'un modèle modifié des migrations neuves. Une borne@lengthdéplace désormais la colonne avec elle, sur les deux bases qui imposent une largeur, et une référence facultative atteint une table qui existe, en colonne et en clé étrangère, et un type qui ne fait que grandir est réénoncé plutôt que refusé. Le corpus de test de régression est en place, sur ses deux axes :crabster-cli/tests/corpus.rsfait traverser une version de templates à unshopmodifié de façon réaliste et nomme le sort de chaque fichier, etevery_change_a_database_with_rows_can_be_toldapplique chaque forme de changement, dans l'ordre, à une base SQLite qui contient une ligne. Et SQLite est informé de la seule façon dont SQLite peut l'être : un changement sur une table qu'il a déjà écrite devient une reconstruction de cette table, ce qui clôt le dernier des quatre. La phase 11 est terminée. Ce qui reste refusé l'est pour une raison qui n'est pas la limitation d'une base — un changement de type qui n'est pas l'un des trois élargissements sans perte, l'ajout d'une référence obligatoire à des lignes qui ne pointent sur rien, et un@uniqueretiré sur les deux bases qui ont nommé la contrainte elles-mêmes.
Tâches
- Définir le format du répertoire
.crabster/: modèle CDL courant, instantané du code généré, version des templates, empreintes de fichiers. - Implémenter les zones protégées : convention de marqueurs, extraction/réinjection lors de la régénération, tests dédiés.
- Implémenter la fusion 3-voies (ancien généré / nouveau généré / code utilisateur), avec production de marqueurs de conflit lisibles.
- Implémenter
crabster upgrade: détection de la version de templates du projet, régénération, fusion, rapport de synthèse (fichiers inchangés / fusionnés / en conflit). - Étendre
crabster record: gérer la modification et la suppression d'un enregistrement, pas seulement l'ajout, y compris la migration de schéma correspondante. - Revoir les templates V1 pour minimiser la surface de conflit (isoler le code destiné à être édité par l'utilisateur).
- Constituer un corpus de test de régression : projets générés, modifiés de façon réaliste, puis mis à jour — vérification que les modifications survivent.
Definition of Done
- Un projet d'exemple généré en V1, modifié manuellement (logique métier ajoutée dans des handlers et services), puis mis à jour vers les templates V2, conserve 100 % des modifications utilisateur, ou signale explicitement les conflits sans jamais écraser silencieusement.
- Ajouter un champ à un enregistrement existant dans le
.cdlpuis relancer la génération produit la migration correspondante et met à jour le code sans casser le projet.
Phase 12a — Resource server OAuth2/OIDC🔗
Objectif : OAuth2/OIDC, condition fréquente d'adoption en entreprise.
Pourquoi cette phase est coupée en deux. Elle demandait le flux Authorization Code + PKCE, qui est un flux client : redirection navigateur, callback, session. Or un projet généré est une API — ADR-0001 — et une API est un resource server : elle valide le jeton qu'on lui présente, elle ne pilote pas de login. Le flux client n'a de sens que lorsque le backend sert aussi le navigateur, ce qui est le terrain de la phase 16. Le resource server est donc la 12a et tient debout tout seul ; tout ce qui réclame une session est parti en 12b.
Le réglage s'écrit
auth oauth2, et nonauthenticationType oauth2comme le disait ce document avant que le blocserviceexiste. Aucun changement de parseur n'est nécessaire : les réglages arrivent au moteur comme des paires opaques, et un module en revendique une dans son manifeste avec[activation].
Livrée, sauf la question. Le module
auth-oauth2, la découverte et un jeu de clés en cache, la validation contre un algorithme épinglé, l'extracteurAuthenticated, l'extraction de claims configurable, un Keycloak dans ledocker-composegénéré avec son realm importé, un test d'intégration qui demande un jeton à ce Keycloak, et un job de CI qui exécute le tout. Reste la question de l'assistant, qui attend la phase 19, et la hiérarchie de rôles — les rôles composites de Keycloak la couvrent chez le fournisseur, et savoir s'il vaut la peine de la déclarer dans le projet n'est encore tranché par rien que quiconque ait exécuté.Un point corrigé au passage, qui ne relevait d'aucune phase :
Authenticateddocumentait qu'on pouvait le prendre dans un handler généré, et ne le pouvait pas.AppStateaccepte désormais des champs des modules qui en ont besoin.
Tâches
- Le module
auth-oauth2: manifeste et activation, les noms qu'il réserve, et les slots_into/— les huit qu'occupe déjàauth-jwten sont la carte. - Découverte
.well-known, récupération du JWKS, et un cache qui survit à une rotation de clés sans un appel réseau par requête. - Validation du jeton :
iss,aud,exp,nbf, et un algorithme épinglé. - L'extracteur
Authenticated, sous la signature exacte que lui donneauth-jwt, pour qu'un handler écrit à la main survive à un changement de type d'authentification. - Extraction de claims configurable. Keycloak met les rôles dans
realm_access.roles, Entra ID dansroles, Auth0 dans un claim à namespace : sans cela, « deux fournisseurs » est tout simplement impossible. C'est de la configuration, pas du code. - Hiérarchie de rôles, et le choix entre la déclarer dans le projet ou la lire chez le fournisseur.
- Un
docker-composeportant un Keycloak dont le realm est importé depuis un JSON — un realm configuré à la main n'est ni reproductible ni testable. - Les tests d'intégration contre ce Keycloak.
testcontainers-modulesest déjà une dev-dépendance de tout projet généré. - Un fournisseur cloud (Auth0, Entra ID) comme procédure manuelle vérifiée, et non comme un test : il faut des identifiants que la CI ne peut pas détenir, et le dire vaut mieux qu'un test ignoré en silence.
- Le job de CI. Il n'en existe aucun pour l'authentification aujourd'hui —
auth jwtn'est couvert que par le tier#[ignore]. - La question que cette phase ajoute à l'assistant, conformément à
l'ADR-0006 :
authgagne une seconde valeur. - La référence du langage dans les deux langues, où la ligne
authne liste quejwt.
Definition of Done
- Un projet généré avec
auth oauth2s'authentifie de bout en bout contre un Keycloak lancé par ledocker-composegénéré, avec tests d'intégration automatisés. - Le même modèle, généré avec
auth jwtpuis avecauth oauth2, protège un handler écrit à la main avec la même ligne de code.
Phase 12b — Autorisation déclarative, et la session navigateur🔗
Objectif : sortir l'autorisation des handlers écrits à la main pour la mettre dans le modèle, et couvrir les flux qui réclament une session.
Pourquoi ce n'est pas la 12a. Les deux moitiés ont besoin de quelque chose que la 12a n'apporte pas. Déclarer l'autorisation dans le modèle est le premier changement de langage de la V2 — grammaire, IR, validation, templates, et la référence dans deux langues — et c'est orthogonal au module d'authentification qui tourne. La moitié « session » suppose la question du frontend tranchée, ce qui est la phase 16.
La moitié « autorisation » est livrée.
@roles,@readset@writessur un enregistrement,roles { … }déclarant ce qui peut être nommé, et un rôle que rien ne déclare refusé avec la ligne où il est écrit. La garde est générée comme un paramètre du handler plutôt qu'une ligne dans son corps : celui qui ne la prend pas ne compile pas contre la route qui en a besoin. Vérifié en faisant tourner un projet généré — sans jeton c'est401, avec un jeton portant un rôle que l'enregistrement ne nomme pas c'est403, unMANAGERlit ce que@reads(MANAGER, ADMIN)autorise et se voit refuser ce que@writes(ADMIN)ne lui donne pas, etADMINécrit les deux. Le même modèle produit la même garde sousauth jwtet sousauth oauth2, ce qui est vérifié plutôt que supposé.Trois choses sont venues avec. Un modèle gardé dans un projet sans réglage
authest refusé avant que rien ne soit écrit, au lieu de produire un projet qui ne compile pas. Les deux modules d'authentification exposent désormais une seule surface, si bien que le modulerecordest écrit contreAuthenticated,ClaimsetAuthErrorsans savoir lequel tourne. Etauth oauth2devient testable hors ligne : un serveur de ressources ne détient aucune clé de signature, donc jusqu'ici un test généré n'avait aucun moyen d'obtenir un jeton que son propre service accepterait — ce qui aurait laissé toute route gardée sans test sous ce module.Et la moitié « session » y est, ce qui clôt la phase.
session cookiesur un service avecauth oauth2en fait un backend-for-frontend :/auth/loginredirige vers le fournisseur en Authorization Code + PKCE,/auth/callbackéchange le code, et le navigateur reçoit un cookie pendant que le jeton reste ici. Vérifié contre un vrai Keycloak, et tenu par un test qui rejoue tout le flux : la redirection porte un challenge, les identifiants reviennent encode, et une requête ne portant que le cookie franchit une garde déclarée dans le modèle — puis la déconnexion fait répondre 401 à la même requête.Elle n'avait finalement pas besoin d'un blueprint UI, et dire pourquoi importe : la session n'a jamais été au frontend de la tenir. Elle vit dans une table ici, le cookie ne porte qu'un identifiant, et ce qui atteint le reste du projet est l'en-tête qu'il lisait déjà. N'importe quel frontend se place devant, généré ou écrit à la main — c'est ce qui a rendu ceci atteignable dès l'ADR-0003 tranché plutôt qu'à l'apparition d'un blueprint.
Tâches
- Des permissions par ressource déclarées en CDL plutôt qu'appelées à la main.
Aujourd'hui une route se protège en éditant son handler et en y écrivant
claims.require_role("ADMIN"); l'alternative est un attribut sur l'enregistrement. - Les changements de parseur, d'IR et de validation que cela demande, et les refus qui vont avec — un rôle que rien ne déclare est une faute de frappe, et doit être refusé en le nommant.
- Le support session-based, levant l'exclusion V1 : pertinent seulement une fois qu'un blueprint UI existe pour porter la session.
- Le flux Authorization Code + PKCE, pour un backend qui sert aussi le navigateur (BFF).
Definition of Done
- Un enregistrement portant un attribut d'autorisation génère un handler qui l'applique, et un modèle nommant un rôle que rien ne déclare est refusé avec la ligne où il est écrit.
Phase 13 — Microservices🔗
Objectif : générer et faire fonctionner ensemble plusieurs services Crabster (reprend et précise l'ancienne phase 11 du plan V1).
Livrée. Sa forme d'abord : un modèle à plusieurs blocs
servicegénère un projet par service,@servicedistribue les enregistrements, et une référence qui traverserait un service est refusée en le nommant. Chaque service enregistre un modèle de lui-même, donc un service généré parmi d'autres est le même projet que ce service généré seul, octet pour octet — ce qui garderecord,applyetupgradeintacts sur l'un d'eux.Une génération distribuée écrit désormais aussi une racine au-dessus des services : un seul
docker-composequi construit et démarre l'ensemble, chacun sur son port et avec sa base. Vérifié en le lançant — deux services debout, tous deux servant, tous deux sains.La passerelle est générée aussi :
kind gatewaysur un service en fait un proxy inverse devant les autres, routant sur les segments que les enregistrements possèdent, avec sa table enregistrée pour qu'un upgrade la reproduise. Vérifié en lançant le système entier — un enregistrement créé puis relu à travers la passerelle, un second service atteint par la même, et un chemin que personne ne revendique répondu par un 404 de la passerelle elle-même.Consul y est : un service qui dit
discovery consuls'enregistre et se maintient enregistré, et une passerelle demande au catalogue où est un service plutôt que de croire ce qui a été généré. Vérifié en lançant le système puis en le cassant — l'adresse générée deorderspointée sur un hôte qui n'existe pas, la route a quand même répondu, parce que l'adresse venait du catalogue.La corrélation de trace fonctionne et est désormais verrouillée : la passerelle transmet l'identifiant de requête, donc une seule valeur suit un appel à travers le système — vérifié en en envoyant un et en le retrouvant dans les deux spans. Ce que ce n'est pas, c'est une trace, et la distinction est écrite là où un lecteur la rencontre.
Consul KV y est aussi : un service lit des réglages dans le magasin, sous les fichiers qu'il embarque et au-dessus de rien. La passerelle limite également ce qu'un même appelant peut demander et fusionne les documents OpenAPI des services en un seul, pour qu'un système offre une page à lire plutôt qu'une par service.
Et les spans sont réelles, désormais.
telemetry otlpexporte vers un collecteur, un service adopte la trace dans laquelle il arrive, et la passerelle transmet son propre contexte pour que le service situé derrière soit un enfant et non un frère. Vérifié en le faisant tourner face à un collecteur et en lisant ce qui est passé sur le fil : même identifiant de trace, parent différent, décision d'échantillonnage de l'appelant respectée à un taux de 5 %, et une charge utile portant le nom de ce service. La phase est close.Un système s'élève désormais d'un seul tenant.
crabster upgradeà la racine réassemble le modèle depuis ce que chaque service contient et reconstruit la racine et la passerelle à partir de lui — c'est ce qui porte jusqu'à la table de routage un enregistrement ajouté dans un service. Deux défauts sont apparus avec lui, tous deux antérieurs à cette phase : un projet qui recevait son premier enregistrement puis qu'on faisait évoluer perdait toute son API, et la racine d'un système n'était pas relisible du tout. Les deux sont corrigés et tenus par des tests.Deux bogues méritent d'être consignés, car aucun n'était visible à la lecture ni pour
cargo check. Le SDK exporte depuis un fil à lui et termine parfutures_executor::block_on: un client HTTP asynchrone confié à ce fil panique dès le premier lot — et ce que les journaux montrent alors est un avertissement sur le cycle de vie du fournisseur, qui ne désigne pas du tout la cause. Et un échantillonneur qui n'est pas fondé sur le parent jette en silence une trace qu'un appelant avait déjà décidé de garder.
Tâches
- Étendre CDL : un type de service (
monolith/microservice/gateway), description de plusieurs applications dans un même fichier, répartition des enregistrements par service. - Générer un gateway Axum +
tower: routage vers les services, agrégation de la documentation OpenAPI, rate-limiting centralisé, propagation du contexte d'authentification. - Intégrer Consul : enregistrement au démarrage, health-checks, résolution côté gateway, configuration centralisée via Consul KV.
- Propagation du tracing distribué entre services (corrélation via
tracing+ OpenTelemetry, en s'appuyant sur la Phase 9 de la V1). - Exemple de référence multi-services testé en CI (au moins deux services + un gateway).
Definition of Done
- L'exemple multi-services démarre via un unique
docker-compose up, les services s'enregistrent auprès de Consul, et une requête traversant le gateway atteint le bon service avec son contexte d'authentification et un identifiant de trace propagé.
Phase 14 — Persistance étendue🔗
Objectif : sortir du périmètre exclusivement SQL de la V1.
Livrée. Sa première moitié :
database mongodbchoisit un backend et non un quatrième dialecte, ce qui est la forme d'ADR-0004 : il n'y a ni migrations, ni clés étrangères, ni schéma à faire évoluer, donc ce qui diffère est le jeu de gabarits qui tourne et non une branche à l'intérieur d'un.L'API qu'un appelant voit est celle que sert le backend SQL — chaque endpoint, chaque paramètre, chaque refus, chaque document de problème. Un identifiant y est un nombre lui aussi, distribué par une collection
counters: celui de MongoDB est unObjectId, et le laisser passer obligerait les clients typés, l'interface, le document OpenAPI et les routes d'une passerelle à demander où un enregistrement est rangé avant de savoir à quoi ressemble un identifiant.
@uniquetient par un index construit au démarrage,@versionedavec la version dans le filtre de la mise à jour,@filterableavec ses opérateurs traduits en ceux de BSON. Une référence est refusée par le langage, avec sa ligne : un champ qui y ressemble et que rien ne fait respecter n'échoue que lorsque les données sont déjà fausses.Tenu par la suite que chaque projet génère — treize tests, verts contre un MongoDB que la suite démarre elle-même — et par un test de ce dépôt qui génère, compile et l'exécute.
Trois défauts ont été trouvés en l'exécutant et n'auraient pas pu l'être autrement.
price.greaterThan=ne renvoyait rien,rust_decimalse sérialisant en chaîne et une chaîne comparée à unDecimal128ne correspondant à rien.@uniquen'était enforcé par rien du tout. Et un enregistrement@versionedportait une colonne de version que rien ne vérifiait et rien ne déplaçait.Et la recherche y est, ce qui clôt la phase.
search meilisearchdonne à chaque enregistrement un endpoint plein texte qui rend la même page qu'une liste, sur des tables comme sur des documents — ce qui est indexé est le document que l'API renvoie, si bien que le module ne demande jamais où les enregistrements sont rangés. Quinze tests générés verts sur chaque backend.Deux choses écrites ici étaient fausses jusqu'à ce que les exécuter le dise. « Une recherche juste après une écriture la voit » l'était d'environ un dixième de seconde, mesuré et désormais documenté plutôt qu'attendu. Et le test généré passait sur le backend document avant que celui-ci n'ait la moindre indexation : un seul Meilisearch est partagé, et la suite fouillait un index qu'une exécution précédente avait rempli.
Et une suite générée démarre les serveurs qu'elle éprouve, puis les reprend. Elle visait auparavant un Meilisearch à une adresse fixe, ce qui la faisait passer sur la machine où quelqu'un en avait lancé un à la main et échouer partout ailleurs. Quant aux conteneurs, ils n'étaient jamais retirés — une poignée rangée dans un
staticn'est jamais détruite, donc le nettoyage écrit n'avait aucune occasion de s'exécuter, et douze d'entre eux tournaient sur cette machine. Un faucheur tient désormais le bout lecteur d'un tube : le noyau le ferme quoi qu'il advienne,SIGKILLcompris, là où un destructeur ne tourne plus.
Tâches
- Introduire l'abstraction de backend de persistance dans l'IR (cf. ADR-0004) et refactorer les templates d'enregistrement en conséquence.
- Support MongoDB : templates d'enregistrement, repository, absence de migrations (documenter la stratégie d'évolution de schéma applicative).
- Validation CDL : rejet explicite et pédagogique des combinaisons impossibles (relations SQL sur backend document).
- Intégration d'un moteur de recherche — Meilisearch privilégié à Elasticsearch pour son empreinte plus légère et son intégration Rust, à confirmer en début de phase.
- Étendre la matrice de tests CI aux nouveaux backends.
Definition of Done
- Un modèle CDL adapté génère un projet MongoDB fonctionnel, avec CRUD et tests d'intégration verts.
- Un modèle avec
searchactivé expose des endpoints de recherche opérationnels sur l'exemple de référence.
Phase 15 — Rétro-ingénierie d'un schéma existant🔗
Objectif : réduire le coût d'entrée pour les équipes qui ont déjà un modèle (reprend l'ancienne phase 13 du plan V1).
Livrée.
crabster introspect <url>écrit un.cdldepuis une base qui existe déjà, pour les trois. C'est la seule commande de Crabster qui se connecte à une base — tout le reste calcule depuis le modèle, et c'est le projet généré qui exécute une migration.La definition of done demande un
.cdl« équivalent », et comparer deux fichiers comme du texte en est la lecture faible. Ce à quoi elle est tenue, c'est que le modèle qui revient rebâtisse ce que le modèle de départ a bâti : le test fait deux tours — modèle, base, modèle, base — et compare les deux bases, table par table et colonne par colonne.Ce qui ne peut pas revenir est écrit en tête du fichier plutôt que dans une documentation que personne n'ouvre, et un second test exige que l'écart soit exactement cette liste et pas un élément de plus. Un vrai défaut est apparu en le faisant : SQLite enregistre un décimal exact en
real(19, 4), si bien que lire le seul nom du type transformait chaqueDecimalenFloat.
Tâches
- Mode reverse engineering :
crabster introspectgénère un.cdldepuis un schéma SQL existant (base : introspection SeaORM). - Documenter ce que l'introspection sait reprendre, et le processus manuel pour le reste.
Definition of Done
- L'introspection d'une base de l'exemple
shopreproduit un.cdléquivalent à l'original.
Phase 16 — Frontend : client typé & blueprints UI🔗
Objectif : appliquer l'ADR-0003, qui est tranché.
Cette phase n'attend plus rien. L'ADR-0003 a été accepté le 2026-09-13 et son statut provisoire levé : la reconfirmation qu'il demandait attendait des retours d'usage qui ne peuvent pas exister avant une publication, et elle bloquait cette phase ainsi que la moitié « session » de la phase 12b.
Le client TypeScript y est.
client typescriptsur un service génèreclients/typescript/: un paquet publiable sans dépendances, couvrant tous les endpoints que le modèle engendre, avecIf-Matchporté pour un enregistrement versionné et unApiErrortenant le document de problème que renvoie un refus. Vérifié en le compilant en modestrictet en le confrontant au serveur — création, liste, lecture, patch, une version périmée répondue412, un enregistrement absent404, un enregistrement référencé refusé409— et en déplaçant le modèle pour voir les types suivre.Il est généré depuis le modèle et non depuis le document OpenAPI que ce même modèle produit. Les deux s'accordent par construction ; passer par le document mettrait entre eux un parse JSON et un générateur de code, qui peuvent tous deux se tromper.
Et le client Rust avec lui.
client rustgénère son propre crate sousclients/rust/, dont un service voisin dépend par chemin — l'usage qui en fait autre chose que le client TypeScript dans une autre langue, puisqu'un système généré ici est fait de services qui s'appellent. Vérifié contre le serveur de la même façon.client bothdemande les deux, ce qui a exigé que l'activation d'un module réponde à plus d'une valeur.Et le blueprint UI officiel y est, ce qui clôt la phase. React et TypeScript, dans
crates/crabster-codegen/blueprints/react, au-dessus du client typé plutôt qu'à côté. « Officiel » est vérifiable et non affirmé : la barrière le génère, l'installe, le construit —tscsur les pages générées et le client généré ensemble — puis le rend dans un vrai moteur de navigateur, en exigeant que les lignes que l'API détient soient dans le DOM. Seul un arbre React monté ayant appelé le client généré peut les y avoir mises.Le framework était le choix de Samuel, pris sur le critère que l'ADR-0003 nomme autant qu'il pouvait l'être : React est là où se trouve le plus grand public, et ce qu'attend un lecteur venu de JHipster. C'est un blueprint et non du cœur, ce qui est la décision elle-même — un projet qui veut une interface en a une, et un projet qui n'en veut pas est intact quand React change de majeure.
Tâches
- Générer un client TypeScript typé depuis la spec OpenAPI du projet, publiable en package, synchronisé automatiquement à chaque régénération.
- Générer un client Rust typé (utile pour les tests d'intégration et la communication inter-services de la Phase 13).
- Publier un blueprint UI officiel de référence pour valider la voie « blueprint » — choix du framework tranché en début de phase sur le critère que nomme l'ADR-0003 : l'écosystème dans lequel les premiers utilisateurs se trouvent déjà. Officiel signifie généré et compilé dans le palier lourd, comme le cœur.
Definition of Done
- Le client TypeScript généré compile, est typé de bout en bout, et une modification du modèle CDL se propage jusqu'aux types du client après régénération.
- Au moins un blueprint UI fonctionnel est publié et documenté.
Phase 17 — Kubernetes & déploiement cloud🔗
Objectif : couvrir le déploiement au-delà de docker-compose.
Livrée.
deploy kubernetesengendre deux choses qui ne sont pas l'une pour l'autre.k8s/tient des manifestes dont chaque valeur est déjà remplie — le port du modèle, les sondes que le service sert vraiment, l'image que son propreDockerfileconstruit — si bien quekubectl apply -f k8s/est l'instruction entière.chart/tient le même déploiement en chart Helm, pour une équipe qui a déjà une façon de livrer : des valeurs à surcharger par environnement, un nom de release, une révision où revenir.Aucun sous-générateur par fournisseur, ce qui est le choix de sobriété que la phase demandait. Ce qu'EKS, GKE ou AKS réclament de plus qu'un cluster conforme, c'est une classe d'ingress, une classe de stockage et une façon de pousser une image : trois valeurs. Trois générateurs à maintenir coûteraient bien davantage, et pourriraient entre le jour où ils sont écrits et celui où on les lit. Le
k8s/README.mdengendré nomme ces trois-là.Vérifié en le déployant : un projet engendré tourne sur minikube derrière ingress-nginx, et un enregistrement créé à travers l'Ingress y est relu. Chaque manifeste est par ailleurs soumis à un vrai serveur d'API 1.34, et le chart passe
helm lintpuishelm template.Deux choses que seule l'exécution pouvait dire.
La première était de moi : le README affirmait qu'un cluster local partage le démon qui a construit l'image. C'est faux — le magasin d'images d'un cluster est le sien, donc une image fraîchement construite est en
ImagePullBackOffpendant quedocker runla trouve immédiatement, et l'erreur annonce que le dépôt n'existe pas, ce qui se lit comme une faute de frappe dans le tag. Il y a maintenant une ligne par cluster.La seconde n'était pas de moi et est plus grave. Deux répliques qui démarrent ensemble migrent ensemble, et PostgreSQL fait échouer la perdante sur une clé dupliquée dans
pg_type_typname_nsp_index— un index que personne n'a écrit, qui ne nomme rien que le lecteur d'un projet engendré connaisse. Le pod sort en code 1, Kubernetes le relance, l'autre a fini entre-temps : le déploiement « réussit » en plantant à chaque fois. Deux à la fois n'a rien d'exotique — c'est un Deployment à deux répliques, un Compose mis à l'échelle, et toute mise en service progressive. Les migrations passent désormais derrière un verrou que la base elle-même distribue, sur une connexion à elles. Mesuré : le même déploiement, depuis un namespace vide, ne signale plus aucun redémarrage.Le multi-services y est. Un modèle à plusieurs blocs
serviceengendre, au-dessus des projets, unk8s/qui déploie l'ensemble dans un seul namespace — et un seul, ce qui est la décision à énoncer : ces services s'atteignent par leur nom, et un nom se résout dans un namespace sans être qualifié. Les séparer obligerait chaque adresse à devenirservice.namespace.svc.cluster.local, engendrée dans des fichiers de configuration, et fausse la première fois qu'on déploie le système deux fois. Déployer deux fois est justement ce à quoi sert le namespace : on change ce nom-là, on ré-applique, et le second système atteint ses propres services.Consul et Jaeger y sont si des services les demandent, une base par service qui garde quelque chose — jamais partagée, car un service qui lit les tables d'un autre n'est pas un service mais un module avec un saut réseau — et une porte d'entrée : la passerelle s'il y en a une, sinon un hôte par service.
Un piège s'est révélé en écrivant cela : un nom d'objet Kubernetes n'accepte pas l'underscore, alors que la passerelle engendre ses adresses avec le nom brut du modèle. Un service que le modèle appelle
catalog_apirépond ici àcatalog-api, et la table de routage embarquée dans son image pointerait dans le vide — un 502 sur tous les chemins de ce service, sans rien qui l'explique. La racine engendre donc la table de routage en surcharge, montée par-dessus leconfig/prod.tomlde l'image. Par-dessus le seul fichier et non le répertoire, carconfig/default.tomldoit survivre à côté. C'est aussi la seule voie possible : une liste est la seule forme que la configuration ne sait pas recevoir par variable d'environnement, ce que le gabarit de la passerelle disait déjà.Et un système se déploie plus d'une fois.
k8s/porte une base kustomize etoverlays/un exemple travaillé : un second cluster est un répertoire qui dit ce qui diffère, non une copie des manifestes qui divergera. Quatre choses diffèrent d'un cluster à l'autre et rien d'autre dans un système engendré ne le fait — le namespace, la provenance et la version des images, leur nombre, et la porte d'entrée. Vérifié en déployant les deux copies dans un même cluster : elles ne se rencontrent pas, ce qui est la propriété qui rend le second cluster crédible.Ce n'est pas un système réparti entre clusters, avec un service ici qui appelle un service là. Cela demande un maillage — Istio, Linkerd, Cilium, Submariner — et lequel est une décision sur votre réseau plutôt que sur votre modèle. Engendrer pour l'un d'eux serait le sous-générateur par fournisseur que cette phase refuse.
Et ce qui ferme la phase : la definition of done est désormais exécutée plutôt que retenue. Tout ce qui précède avait été vérifié à la main, une fois — et une vérification faite une fois s'arrête le jour où quelqu'un modifie un gabarit. Deux tests la tiennent maintenant.
a_system_deploys_to_a_cluster_and_answers_through_its_ingressfait exactement ce que la DoD demande : il engendre un système à deux services et une passerelle, construit les trois images avec leurs propresDockerfile, les charge dans le magasin du cluster, appliquek8s/avec-k, attend les trois rollouts, puis crée et relit un enregistrement par l'Ingress — pour les deux services, par la même adresse et le même hôte. Rien n'y édite ce qui a été engendré, ce qui est la seconde moitié de la phrase. Quarante secondes quand les images sont en cache, et un garde retire le namespace même si le test explose au milieu.Ce qu'il attrape et que rien n'attrapait : entre un manifeste et une requête servie, il y a tout ce qu'un rendu ne voit pas. Mis à l'épreuve en pointant l'Ingress sur une classe que personne ne sert — tout se déploie, les trois rollouts réussissent, et plus rien ne répond.
kubectl kustomizetrouvait ce YAML parfaitement bien.Et
the_chart_lints_renders_and_takes_the_values_it_offerstient l'autre moitié, le chart :helm lint, le rendu, le port du modèle arrivant aux trois endroits qui doivent s'accorder, un--setqui change vraiment le rendu — sans quoi le chart est devenu une copie des manifestes d'à côté — et l'invariant que personne ne regarde : activer l'autoscaler doit retirerreplicas:du Deployment, faute de quoi les deux possèdent ce nombre et le compte de pods oscille sans que rien ne l'explique.
Tâches
- Génération de manifests Kubernetes (Deployment, Service, ConfigMap, Secret, Ingress, HPA) et d'un chart Helm.
- Support du déploiement multi-services de la Phase 13 (gateway + services + Consul).
- Documenter les cibles cloud courantes sans générer de configuration spécifique par fournisseur (choix de sobriété : les sous-générateurs par fournisseur coûtent cher à maintenir).
Definition of Done
- L'exemple multi-services se déploie sur un cluster local (kind/minikube) à partir des seuls manifests générés, et répond via l'Ingress.
Phase 18 — Maturité de l'écosystème blueprints🔗
Objectif : transformer le mécanisme livré en V1 en écosystème réel.
Entamée par ce qui manquait le plus : savoir ce qu'un blueprint peut casser. Ce contre quoi il est écrit tient en cinq sortes de noms — les modules qu'il requiert, les fichiers qu'ils engendrent, les slots à l'intérieur, les régions protégées, les variables de contexte — et aucun n'était déclaré nulle part comme une promesse. Renommer un slot est une modification d'une ligne dans un module, tous les tests passent encore, et chaque blueprint qui le nommait casse plus tard, sur la machine de quelqu'un d'autre, avec un message sur une contribution qui n'a rien atteint.
blueprint-contract.txtles énumère tous, lus dans les templates eux-mêmes, et un test les compare à ce que les templates disent aujourd'hui en nommant ce qui a disparu. Il ne juge pas le changement — avant la 1.0, une mineure a le droit de tout casser, etCONTRIBUTING.mddit désormais laquelle exige quoi. Il le rend visible à qui le fait, au moment où il le fait.Et l'outillage contributeur :
crabster blueprint newécrit un blueprint qui génère — un squelette qui ne tourne pas est pire que pas de squelette, car l'auteur le modifie, ça échoue, et il ne peut pas savoir si la faute est la sienne.crabster blueprint checkrépond sans rien générer à la question que la génération ne poserait que trop tard : ce blueprint nomme-t-il encore des choses qui existent.crabster blueprint contractimprime ce qu'il y a à nommer.Et le troisième blueprint officiel y est.
examples/blueprints/dieselremplace SeaORM par Diesel, sur SQLite comme sur PostgreSQL : neuf fichiers portent tout le changement, et le document OpenAPI, la validation, le contrat de page et tout ce qu'engendrecoresont hérités sans y toucher. C'est ce qui rend le mécanisme intéressant plutôt que seulement possible — le choix d'un ORM n'était pas quelque chose que Crabster avait à imposer.SQLite, parce que les autres backends de Diesel se lient à libpq ou libmysqlclient, qu'un projet engendré ne peut supposer installées — et surtout pas dans sa propre image
rust:slim. Ce qu'il n'implémente pas, il le refuse :@filterable,@versioned,@audited, les références, les énumérations,DecimaletUuidproduisent uncompile_error!nommant la fonctionnalité et l'enregistrement.Deux défauts trouvés en l'exécutant, invisibles à la lecture. Le
DATABASE_URLengendré est celui que lit sqlx, que Diesel ne sait pas ouvrir. Et{"champ": null}répondait 200 sans rien effacer : Diesel lit unNonedans un changeset comme « ne touche pas à cette colonne ». La suite engendrée passait de bout en bout pendant ce temps, parce que rien ne demandait.Et la vitrine y est, ce qui clôt la phase. Le tableau des blueprints connus les porte tous, avec la version de templates que chacun vise, et un test le compare à ce que le dépôt livre réellement — dans les deux sens : un blueprint ajouté et non listé est un blueprint que personne ne trouve, et une ligne annonçant une version qu'il ne déclare pas envoie le lecteur vers quelque chose que Crabster refusera de charger, en parlant de la version plutôt que de la page qui avait tort.
La convention crates.io existait sur le papier ;
crabster blueprint newl'écrit désormais. Une convention qu'un document se contente de demander est suivie une fois sur deux. Et le tour complet — engendrer, empaqueter, déballer ailleurs, générer — est vérifié plutôt que supposé.Ce qui reste : un blueprint maintenu par un contributeur extérieur au projet. Cela n'attend pas du code. Et la definition of done demande au moins un blueprint maintenu par un contributeur extérieur au projet, ce qu'aucun travail sur ce dépôt ne peut produire : elle attend des gens, pas du code.
Tâches
- Stabiliser et versionner l'API de blueprint (contrat entre le cœur et les templates externes), avec politique de compatibilité explicite.
- Publier les blueprints officiels identifiés : Diesel (alternative ORM), UI (Phase 16), et tout blueprint issu des besoins remontés.
- Outillage contributeur :
crabster blueprint newpour amorcer un blueprint, harnais de test pour blueprints. - Vitrine communautaire (page de blueprints connus, convention de tag crates.io).
Definition of Done
- Au moins trois blueprints fonctionnels existent, dont au moins un maintenu par un contributeur externe au projet.
- L'API de blueprint est versionnée et une rupture y est détectable automatiquement en CI.
Phase 19 — Création interactive d'un projet🔗
Objectif : que crabster new demande, au lieu qu'on le lui dise en flags —
l'expérience dont l'écosystème Java a fixé l'attente.
Régie par l'ADR-0006, qui en est toute la conception : l'assistant est une façade au-dessus des flags, les réponses sont enregistrées dans
.crabster/project.tomlet le blocservice, et aucune question n'est posée à laquelle le générateur ne sait pas répondre. Elle est continue plutôt que séquentielle : elle démarre avec les questions qui ont déjà une réponse, et chaque phase ci-dessus ajoute la sienne en arrivant.
Le mécanisme est livré.
crabster newsans argument demande le nom, la base et le port, et produit le projet que produisent les flags équivalents, octet pour octet. Il ne demande que si l'entrée est un terminal, et--no-inputle coupe même là — un tube, un runner de CI et la suite de ce dépôt ne sont donc pas concernés, ce qui est vérifié plutôt que supposé. À la ligne et numéroté plutôt qu'aux flèches, ce qui est la raison pour laquelle il n'a ajouté aucune dépendance.Et la question de l'architecture y est.
crabster newdemande si vous voulez une application ou plusieurs ; répondez plusieurs et il demande une passerelle, les services, s'ils s'enregistrent dans un catalogue et s'ils exportent des traces, puis il écrit le.cdlet génère le système à partir de lui. Tout ce que la phase 13 a construit est désormais atteignable sans écrire un modèle à la main.Le dessin est ce qui lui donne sa valeur : les questions produisent un modèle, et le modèle emprunte le chemin qu'
import-cdlprenait déjà. Il n'y a pas de second générateur à tenir en phase. Tenu par des tests — le système obtenu en répondant, celui que produisent les flags équivalents et celui importé du.cdlque ces questions ont écrit sont les mêmes fichiers aux mêmes octets — et vérifié en en faisant tourner un : un système issu du wizard démarré sous Compose, un enregistrement ajouté ensuite, créé et relu à travers la passerelle, les deux services passants dans Consul, et une trace où le service est enfant de la passerelle.Deux choses en sont sorties. Un service déclaré avant son premier enregistrement est désormais un projet et non une erreur — c'est ce qu'un système est au départ — et il est généré sans la machinerie des enregistrements, donc il compile sans les avertissements qu'un type de listage que personne n'appelle produit. Et une passerelle route ce que le modèle dit qu'un service possède : celle qui naît avant tout enregistrement ne route rien — ce que l'évolution d'un système d'un seul tenant, en phase 13 ci-dessus, vient refermer.
Et les questions qui manquaient y sont, ce qui clôt la phase.
crabster newdemande désormais le front end — aucun, React, Vue ou Angular — et les comptes, et c'est l'ADR-0006 appliqué tel qu'il est écrit : une question n'est posée que le jour où le générateur sait y répondre. Ce jour-là est arrivé pour deux raisons distinctes, et aucune n'était « on a eu le temps ». Les trois blueprints UI sont livrés dans le binaire, donc la réponse sélectionne quelque chose qui existe chez celui qui a installé la commande. Etnewsait écrire un modèle pour une application seule, comme il le faisait déjà pour un système :authetuiengendrent tous deux contre des enregistrements, donc ils arrivent avec un premier enregistrement — demandé, avec un défaut qu'on peut retaper, jamais inventé en silence. Sans terminal pour le demander et sans--record, c'est un refus qui nomme le drapeau.Ce qui a déclenché tout cela n'est pas une phase mais un essai :
crabster new blocka rendu vingt fichiers à quelqu'un qui attendait une application, et avait raison de l'attendre. Uncrabster newseul donne maintenant une API, une base, des comptes et un écran — et le.cdlreste dans le projet, parce que le modèle est la chose qui grandit.
Tâches
- La couche de questions elle-même, et un
--no-inputqui la saute entièrement. Chaque question a un flag ; c'est le flag donné qui supprime la question. - Les trois premières questions, qui ont déjà une réponse : le nom du projet, la
base de données et le port. Pas la méthode d'authentification :
crabster newcrée un projet sans modèle, et on n'authentifie pas un projet qui n'a rien à protéger. - La détection du non-interactif, pour qu'un tube ou un runner de CI ne se bloque jamais sur une question en attendant un terminal qui n'est pas là.
- Les réponses écrites là où le cycle de vie les lit, pour qu'un projet créé par l'assistant soit indiscernable d'un projet créé par flags — le corpus ne devrait pas pouvoir faire la différence.
Ce que cette phase ne tranche pas
- Les applications desktop. Ce n'est pas une phase : toutes les phases, de 0 à 18, supposent un service HTTP, et une application desktop garde le modèle de domaine et pas grand-chose d'autre. Il lui faut une décision de produit avant de pouvoir être une question de l'assistant.
- La question du client frontend, qui relève de l'ADR-0003 et de la phase 16. Le jour où elle sera posée, la réponse sélectionnera un blueprint — l'assistant offre le choix, le cœur ne porte aucun framework d'interface.
- La question monolithique/microservices, qui ne peut pas être posée avant que la phase 13 sache y répondre.
Definition of Done
crabster newsans aucun flag déroule les questions et produit le même projet que les flags équivalents, octet pour octet.- Toute la suite de tests continue de piloter le CLI sans terminal.
6. Critères de sortie de la V2🔗
La V2 est considérée livrée quand :
- Un projet généré peut être mis à jour vers une nouvelle version de templates sans perte de code (Phase 11) — critère bloquant.
- OAuth2/OIDC est utilisable en production comme resource server (Phase 12a).
- Un déploiement multi-services fonctionne de bout en bout (Phases 13 + 17).
- Une base de données existante peut être reprise (Phase 15).
- Un client typé est généré depuis l'API (Phase 16).
- L'écosystème de blueprints compte au moins un contributeur externe (Phase 18).
crabster newdemande ce dont il a besoin au lieu qu'on le lui dise (Phase 19).
Les phases 14 (persistance étendue) et une partie de la 17 peuvent glisser en V3 sans remettre en cause la livraison V2, si les arbitrages de ressources l'imposent.
7. Explicitement hors périmètre V2 (horizon V3)🔗
- Les applications desktop. Nommées ici parce que l'assistant de création rend la question visible (ADR-0006), et que la réponse n'est pas « plus tard en V2 ». Toutes les phases, de 0 à 18, génèrent un service HTTP ; une application desktop garderait le modèle de domaine et les migrations, et remplacerait tout ce qui est au-dessus. C'est une seconde ligne de produit, et il lui faut une décision à elle, pas une place dans une phase.
- Génération via interface web et éditeur visuel de modèle.
- Sous-générateurs cloud par fournisseur (Heroku, AWS, GCP, Azure) — documentés, non générés (cf. Phase 17).
- Bases NoSQL au-delà de MongoDB (Cassandra, Couchbase, Neo4j).
- Génération d'applications non-Rust — Crabster reste un générateur Rust.
- Support d'un modèle CQRS/event-sourcing — architecture trop spécifique pour un générateur généraliste ; candidat naturel pour un blueprint communautaire.
8. Risques spécifiques à la V2🔗
- La fusion incrémentale est un puits sans fond. C'est le risque numéro un : le problème est intrinsèquement difficile et peut absorber toute la capacité de la V2. Mitigation : viser d'abord la garantie « ne jamais perdre de code utilisateur silencieusement » (un conflit explicite est un succès, pas un échec), et non la fusion parfaite sans intervention.
- Rupture des templates V1. La Phase 11 impose de restructurer les templates existants ; les projets générés en V1 doivent disposer d'un chemin de migration documenté, sinon les premiers utilisateurs sont pénalisés — exactement la population qu'il faut ménager.
- Dispersion sur les thèmes parallèles. Les phases 12 à 16 sont attrayantes et visibles, la Phase 11 est ingrate et invisible. Risque réel de la sacrifier ; l'ordre de priorité du §2 doit être défendu.
- Dépendance à l'écosystème Consul/Meilisearch. Ces choix engagent des dépendances externes hors du contrôle du projet ; les isoler derrière des templates de blueprint pour permettre leur remplacement.
- Pression communautaire sur le frontend. L'ADR-0003 assume l'absence d'UI clé en main dans le cœur. Cette position sera contestée ; elle doit être argumentée publiquement, et révisée sur des données d'usage, pas sur du bruit. {% endraw %}