crabster

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🔗

V1V2
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érationOne-shot + ajout simple d'un enregistrementRégénération incrémentale, mise à jour de version, fusion assistée
Périmètre applicatifMonolithe API-only, SQL, JWTMulti-services, NoSQL, OAuth2/OIDC, frontend (option tranchée)
ÉcosystèmeMécanisme de blueprints livré, écosystème videÉcosystème de blueprints amorcé, blueprints officiels maintenus
MigrationNouveau projet uniquementRé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èmePhasesMotivation
T1 — Cycle de vie du code généré11Rendre un projet Crabster maintenable dans la durée
T2 — Sécurité de niveau entreprise12a, 12bLever le blocage OAuth2/OIDC pour les adoptions en entreprise
T3 — Architectures distribuées13Génération multi-services
T4 — Persistance étendue14Sortir du seul SQL (NoSQL, recherche)
T5 — Migration & interopérabilité15Reprendre des bases de données existantes
T6 — Frontend16Lever l'ADR-0001, décision différée depuis la V1
T7 — Déploiement & écosystème17, 18Kubernetes, cloud, maturité des blueprints communautaires
T8 — Expérience de création19Demander 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🔗

ADR-0003 — Frontend : client typé généré, puis blueprints UI🔗

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.

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🔗

ADR-0005 — Microservices sans équivalent Spring Cloud🔗

ADR-0006 — Création interactive, en façade des flags🔗

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.

PhaseTitreDépend dePriorité
11Cycle de vie : upgrade & fusion incrémentaleV1 complèteCritique
12aAuthentification étendue : resource server OAuth2/OIDC11Haute
12bAutorisation déclarative, et la session navigateur12a, 16Moyenne
13Microservices11, 12aHaute
14Persistance étendue (NoSQL, recherche)11Moyenne
15Rétro-ingénierie d'un schéma existantV1 (Phase 3)Moyenne
16Frontend : client typé & blueprints UIV1 (Phase 6)Moyenne
17Kubernetes & déploiement cloud13Moyenne
18Maturité de l'écosystème blueprints11Continue
19Cré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) ; et crabster apply, qui fait d'un modèle modifié des migrations neuves. Une borne @length dé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.rs fait traverser une version de templates à un shop modifié de façon réaliste et nomme le sort de chaque fichier, et every_change_a_database_with_rows_can_be_told applique 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 @unique retiré sur les deux bases qui ont nommé la contrainte elles-mêmes.

Tâches

Definition of Done


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 non authenticationType oauth2 comme le disait ce document avant que le bloc service existe. 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'extracteur Authenticated, l'extraction de claims configurable, un Keycloak dans le docker-compose gé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 : Authenticated documentait qu'on pouvait le prendre dans un handler généré, et ne le pouvait pas. AppState accepte désormais des champs des modules qui en ont besoin.

Tâches

Definition of Done


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, @reads et @writes sur 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'est 401, avec un jeton portant un rôle que l'enregistrement ne nomme pas c'est 403, un MANAGER lit ce que @reads(MANAGER, ADMIN) autorise et se voit refuser ce que @writes(ADMIN) ne lui donne pas, et ADMIN écrit les deux. Le même modèle produit la même garde sous auth jwt et sous auth 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 auth est 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 module record est écrit contre Authenticated, Claims et AuthError sans savoir lequel tourne. Et auth oauth2 devient 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 cookie sur un service avec auth oauth2 en fait un backend-for-frontend : /auth/login redirige 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 en code, 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

Definition of Done


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 service génère un projet par service, @service distribue 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 garde record, apply et upgrade intacts sur l'un d'eux.

Une génération distribuée écrit désormais aussi une racine au-dessus des services : un seul docker-compose qui 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 gateway sur 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 consul s'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 de orders pointé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 otlp exporte 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 par futures_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

Definition of Done


Phase 14 — Persistance étendue🔗

Objectif : sortir du périmètre exclusivement SQL de la V1.

Livrée. Sa première moitié : database mongodb choisit 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 un ObjectId, 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.

@unique tient par un index construit au démarrage, @versioned avec la version dans le filtre de la mise à jour, @filterable avec 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_decimal se sérialisant en chaîne et une chaîne comparée à un Decimal128 ne correspondant à rien. @unique n'était enforcé par rien du tout. Et un enregistrement @versioned portait 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 meilisearch donne à 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 static n'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, SIGKILL compris, là où un destructeur ne tourne plus.

Tâches

Definition of Done


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 .cdl depuis 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 chaque Decimal en Float.

Tâches

Definition of Done


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 typescript sur un service génère clients/typescript/ : un paquet publiable sans dépendances, couvrant tous les endpoints que le modèle engendre, avec If-Match porté pour un enregistrement versionné et un ApiError tenant le document de problème que renvoie un refus. Vérifié en le compilant en mode strict et en le confrontant au serveur — création, liste, lecture, patch, une version périmée répondue 412, un enregistrement absent 404, 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 rust génère son propre crate sous clients/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 both demande 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 — tsc sur 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

Definition of Done


Phase 17 — Kubernetes & déploiement cloud🔗

Objectif : couvrir le déploiement au-delà de docker-compose.

Livrée. deploy kubernetes engendre 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 propre Dockerfile construit — si bien que kubectl 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.md engendré 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 lint puis helm 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 ImagePullBackOff pendant que docker run la 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 service engendre, au-dessus des projets, un k8s/ 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 à devenir service.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_api ré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 le config/prod.toml de l'image. Par-dessus le seul fichier et non le répertoire, car config/default.toml doit 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 et overlays/ 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_ingress fait exactement ce que la DoD demande : il engendre un système à deux services et une passerelle, construit les trois images avec leurs propres Dockerfile, les charge dans le magasin du cluster, applique k8s/ 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 kustomize trouvait ce YAML parfaitement bien.

Et the_chart_lints_renders_and_takes_the_values_it_offers tient l'autre moitié, le chart : helm lint, le rendu, le port du modèle arrivant aux trois endroits qui doivent s'accorder, un --set qui 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 retirer replicas: 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

Definition of Done


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.txt les é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, et CONTRIBUTING.md dit 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 check ré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 contract imprime ce qu'il y a à nommer.

Et le troisième blueprint officiel y est. examples/blueprints/diesel remplace 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'engendre core sont 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, Decimal et Uuid produisent un compile_error! nommant la fonctionnalité et l'enregistrement.

Deux défauts trouvés en l'exécutant, invisibles à la lecture. Le DATABASE_URL engendré est celui que lit sqlx, que Diesel ne sait pas ouvrir. Et {"champ": null} répondait 200 sans rien effacer : Diesel lit un None dans 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 new l'é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

Definition of Done


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.toml et le bloc service, 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 new sans 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-input le 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 new demande 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 .cdl et 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-cdl prenait 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 .cdl que 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 new demande 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. Et new sait écrire un modèle pour une application seule, comme il le faisait déjà pour un système : auth et ui engendrent 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 block a rendu vingt fichiers à quelqu'un qui attendait une application, et avait raison de l'attendre. Un crabster new seul donne maintenant une API, une base, des comptes et un écran — et le .cdl reste dans le projet, parce que le modèle est la chose qui grandit.

Tâches

Ce que cette phase ne tranche pas

Definition of Done

6. Critères de sortie de la V2🔗

La V2 est considérée livrée quand :

  1. Un projet généré peut être mis à jour vers une nouvelle version de templates sans perte de code (Phase 11) — critère bloquant.
  2. OAuth2/OIDC est utilisable en production comme resource server (Phase 12a).
  3. Un déploiement multi-services fonctionne de bout en bout (Phases 13 + 17).
  4. Une base de données existante peut être reprise (Phase 15).
  5. Un client typé est généré depuis l'API (Phase 16).
  6. L'écosystème de blueprints compte au moins un contributeur externe (Phase 18).
  7. crabster new demande 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)🔗

8. Risques spécifiques à la V2🔗