@nodefony/devkit 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +318 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
  7. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js +6 -0
  8. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.js +8 -0
  9. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js +9 -0
  10. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js +6 -0
  11. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateParam.js +8 -0
  12. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorate.js +9 -0
  13. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateMetadata.js +6 -0
  14. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateParam.js +8 -0
  15. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  16. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  17. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  18. package/dist/index.js +47 -0
  19. package/dist/nodefony/command/CardCommand.js +70 -0
  20. package/dist/nodefony/config/config.js +200 -0
  21. package/dist/nodefony/config/defineModuleConfig.js +36 -0
  22. package/dist/nodefony/controllers/DevkitController.js +60 -0
  23. package/dist/nodefony/controllers/McpController.js +223 -0
  24. package/dist/nodefony/controllers/OAuthMetadataController.js +89 -0
  25. package/dist/nodefony/interfaces/IDevkitService.js +1 -0
  26. package/dist/nodefony/interfaces/index.js +1 -0
  27. package/dist/nodefony/service/DevkitService.js +198 -0
  28. package/dist/nodefony/src/card.js +2 -0
  29. package/dist/nodefony/src/errors/DevkitError.js +21 -0
  30. package/dist/nodefony/src/mcp/guard.js +51 -0
  31. package/dist/nodefony/src/mcp/protocol.js +127 -0
  32. package/dist/nodefony/src/mcp/server.js +133 -0
  33. package/dist/nodefony/src/mcp/tools.js +163 -0
  34. package/dist/types/index.d.ts +52 -0
  35. package/dist/types/nodefony/command/CardCommand.d.ts +33 -0
  36. package/dist/types/nodefony/config/config.d.ts +25 -0
  37. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  38. package/dist/types/nodefony/controllers/DevkitController.d.ts +37 -0
  39. package/dist/types/nodefony/controllers/McpController.d.ts +70 -0
  40. package/dist/types/nodefony/controllers/OAuthMetadataController.d.ts +39 -0
  41. package/dist/types/nodefony/interfaces/IDevkitService.d.ts +69 -0
  42. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  43. package/dist/types/nodefony/service/DevkitService.d.ts +143 -0
  44. package/dist/types/nodefony/src/card.d.ts +18 -0
  45. package/dist/types/nodefony/src/errors/DevkitError.d.ts +14 -0
  46. package/dist/types/nodefony/src/mcp/guard.d.ts +66 -0
  47. package/dist/types/nodefony/src/mcp/protocol.d.ts +139 -0
  48. package/dist/types/nodefony/src/mcp/server.d.ts +48 -0
  49. package/dist/types/nodefony/src/mcp/tools.d.ts +81 -0
  50. package/docs/index.md +358 -0
  51. package/package.json +77 -0
  52. package/skills/nodefony-add-crud/SKILL.md +199 -0
  53. package/skills/nodefony-add-realtime-channel/SKILL.md +95 -0
  54. package/skills/nodefony-add-service/SKILL.md +90 -0
  55. package/skills/nodefony-browser/SKILL.md +416 -0
  56. package/skills/nodefony-browser/references/socket.md +115 -0
  57. package/skills/nodefony-browser/references/sondes.md +175 -0
  58. package/skills/nodefony-browser/scripts/audit.mjs +169 -0
  59. package/skills/nodefony-browser/scripts/inspect.mjs +903 -0
  60. package/skills/nodefony-browser/scripts/lib/browser.mjs +357 -0
  61. package/skills/nodefony-browser/scripts/lib/probes.mjs +501 -0
  62. package/skills/nodefony-browser/scripts/lib/wcag.mjs +153 -0
  63. package/skills/nodefony-browser/scripts/socket.mjs +354 -0
  64. package/skills/nodefony-browser/scripts/watch.mjs +125 -0
  65. package/skills/nodefony-migrate-schema/SKILL.md +359 -0
  66. package/skills/nodefony-migrate-schema/references/verdicts.md +139 -0
  67. package/skills/nodefony-protect-route/SKILL.md +195 -0
@@ -0,0 +1,359 @@
1
+ ---
2
+ name: nodefony-migrate-schema
3
+ description: >
4
+ Fait évoluer le schéma d'une base Nodefony et le porte en production, par les commandes
5
+ `orm:generate` et `orm:migrate` du framework — jamais par un `ALTER` écrit à la main ni par la
6
+ suppression d'une base. Porte la lecture de l'état (que l'application tourne ou non), le plan
7
+ avant le geste, les codes de refus et le geste que chacun appelle, les trois interdits qui
8
+ cassent un historique, et le patron de déploiement où les migrations passent AVANT les
9
+ exemplaires. À charger AVANT de modifier une entité déjà en base, ou avant de déployer un schéma
10
+ changé.
11
+ Déclencheurs : "j'ai ajouté un champ à une entité", "la colonne n'existe pas en base",
12
+ "modifier une table existante", "migration", "migrer le schéma", "orm:migrate", "orm:generate",
13
+ "la base est en retard", "appliquer les migrations", "déployer un changement de schéma",
14
+ "comment passer ce modèle en production", "ma base ne correspond plus au code",
15
+ "no such column", "column does not exist", "erreur SQL après avoir changé une entité",
16
+ "adopter une base existante", "réparer une migration en échec", "le pod ne devient pas prêt",
17
+ "comment tester ma migration", "éprouver une migration", "vérifier qu'une migration marche",
18
+ "prouver que ma migration s'applique", "essayer sans casser ma base", "base d'essai",
19
+ "rejouer les migrations depuis zéro", "repartir d'une base propre".
20
+ metadata:
21
+ version: 2
22
+ ---
23
+
24
+ # Faire évoluer un schéma, et le porter en production
25
+
26
+ ## 1. La seule chose à savoir avant tout le reste
27
+
28
+ **En développement, il n'y a rien à faire.** La base suit le code : la table naît au démarrage, et
29
+ un champ ajouté **qui accepte le vide** est posé au démarrage suivant.
30
+
31
+ **Deux cas seulement sortent de là**, et ce sont eux qui amènent ici :
32
+
33
+ - un champ **obligatoire** ajouté à une table qui existe déjà — il n'est jamais rattrapé ;
34
+ - **la production**, où le démarrage ne fabrique JAMAIS le schéma.
35
+
36
+ Si l'application est en développement et que le champ ajouté accepte le vide, il suffit de
37
+ redémarrer. Ne produis pas une migration pour ça.
38
+
39
+ ## 2. Lire l'état — deux voies, selon que l'application tourne
40
+
41
+ L'état est **le même objet** dans les deux cas : ne le recompose jamais à partir d'autre chose.
42
+
43
+ ```bash
44
+ npx nodefony orm:migrate:status --json
45
+ ```
46
+
47
+ Codes de sortie, et ils ne changeront pas :
48
+
49
+ | Code | Ce que ça veut dire |
50
+ | ---- | --------------------------------------------------------------------------------- |
51
+ | `0` | à jour |
52
+ | `1` | une action humaine est requise (en attente, dérive, échec, base en écart) |
53
+ | `2` | la commande n'a pas pu travailler (base injoignable, verrou tenu, usage invalide) |
54
+
55
+ Quand l'application **tourne**, le même état se lit par son plan d'administration, sous le rôle
56
+ d'administration : `GET /nodefony/orm/api/migrations?connector=<nom>`. Une porte MCP le catalogue
57
+ sous le domaine `orm`, chemin `migrations` — il n'y a **aucun outil dédié** à chercher.
58
+
59
+ **Lis `verdict` et `nextActions[0].command`, jamais la phrase française.** La phrase est un rendu ;
60
+ le verdict est la source.
61
+
62
+ ## 3. Faire évoluer un schéma — trois gestes, dans cet ordre
63
+
64
+ ```bash
65
+ npx nodefony orm:generate --name ajout_slug
66
+ ```
67
+
68
+ Écrit le fichier de migration qui manque, déduit de la différence entre les entités et la dernière
69
+ migration. Le nom entre dans une identité **immuable une fois publiée** : minuscules et `_`.
70
+
71
+ ```bash
72
+ npx nodefony orm:migrate --dry-run --json
73
+ ```
74
+
75
+ **Le plan AVANT le geste.** Rend ce qui s'appliquerait, dans l'ordre, sans rien écrire. C'est ce
76
+ qu'on montre à un humain avant d'agir, et c'est ce qu'on relit soi-même avant de continuer.
77
+
78
+ ```bash
79
+ npx nodefony orm:migrate
80
+ ```
81
+
82
+ Applique sous verrou, écrit l'historique dans la même transaction que le schéma là où le moteur le
83
+ permet. **Rejouer n'applique rien** et sort `0` : les trois verbes sont idempotents, on peut donc
84
+ reprendre après une coupure sans lire d'état préalable.
85
+
86
+ > Ce que `orm:generate` ne peut pas déduire — une vue, un déclencheur, un remplissage de données —
87
+ > s'écrit dans une migration libre : `npx nodefony orm:generate --custom --name backfill_slug`
88
+ > dépose un fichier vide et son entrée de journal. Le gabarit déposé explique comment séparer les
89
+ > instructions ; suis-le à la lettre.
90
+
91
+ ### 🔴 Un champ OBLIGATOIRE sur une table PEUPLÉE — ton moteur ne fait pas ce que tu crois
92
+
93
+ Ajouter une colonne `NOT NULL` **sans valeur par défaut** à une table qui porte déjà des lignes n'a
94
+ pas le même effet selon le serveur. Mesuré sur les trois, table peuplée :
95
+
96
+ | Moteur | Ce qui se passe |
97
+ | --------------- | ------------------------------------------------------------------------------------------- |
98
+ | sqlite | **refus** — `Cannot add a NOT NULL column with default value NULL` |
99
+ | PostgreSQL | **refus** — `column "x" of relation "y" contains null values` |
100
+ | MySQL / MariaDB | **accepté** — les lignes existantes reçoivent une valeur VIDE (`''`), sans un avertissement |
101
+
102
+ Les deux premiers t'arrêtent parce qu'ils ne peuvent pas inventer la valeur des lignes déjà là. Le
103
+ troisième l'invente : le champ est déclaré obligatoire et ne contient que du vide, ce qui passe tous
104
+ les contrôles et ne se voit qu'au moment où quelqu'un lit ces comptes. **Le mode strict n'y change
105
+ rien** — c'est le comportement de `ALTER TABLE … ADD COLUMN`, pas celui des insertions.
106
+
107
+ Donc, toujours, quel que soit ton moteur : **un champ obligatoire s'ajoute avec une valeur par
108
+ défaut** (`role:string=membre`), ou **se déclare facultatif** (`department:string?`). Si tu as
109
+ besoin des deux — obligatoire, et sans défaut à terme — c'est trois migrations : ajouter avec
110
+ défaut, remplir (`--custom`), puis retirer le défaut.
111
+
112
+ #### 🔴 …et si le champ est UNIQUE, le conseil ci-dessus se retourne contre toi
113
+
114
+ Une valeur par défaut est la **même pour toutes les lignes**. Sur un champ unique, elle ne répare
115
+ donc rien : elle garantit la collision dès la deuxième ligne déjà présente
116
+ (`UNIQUE constraint failed`). Et le générateur écrit l'ajout de colonne **et** son index unique
117
+ dans la MÊME migration — un enchaînement qui ne réussit que sur une table vide.
118
+
119
+ Le geste est en trois temps, et l'ordre ne s'inverse pas :
120
+
121
+ ```bash
122
+ # 1. le champ, FACULTATIF et sans unicité, déclaré dans l'entité — puis :
123
+ npx nodefony orm:generate --name ajout_slug
124
+ npx nodefony orm:migrate
125
+
126
+ # 2. remplir chaque ligne d'une valeur DISTINCTE (SQL libre : le générateur ne
127
+ # peut pas inventer la valeur métier de lignes qu'il ne connaît pas)
128
+ npx nodefony orm:generate --custom --name remplir_slug
129
+ # → écrire l'UPDATE dans le fichier déposé, puis :
130
+ npx nodefony orm:migrate
131
+
132
+ # 3. le champ passe unique (et obligatoire si besoin) dans l'entité — puis :
133
+ npx nodefony orm:generate --name slug_unique
134
+ npx nodefony orm:migrate
135
+ ```
136
+
137
+ `orm:generate` **te le dira** : il relit le SQL qu'il vient d'écrire et signale
138
+ `add-not-null-sans-defaut` et `colonne-neuve-puis-index-unique` sous « À REGARDER avant
139
+ d'appliquer ». Il ne refuse pas — il ne lit pas la base et ignore si ta table porte des lignes —,
140
+ mais s'il le signale et que ta table n'est pas vide, la migration échouera.
141
+
142
+ **À l'étape 3, sur sqlite, attends-toi à un refus `NF_GENERATE_DESTRUCTIVE`** — mesuré sur une
143
+ table de deux lignes. Rendre une colonne obligatoire n'est pas un `ALTER` en sqlite : le moteur
144
+ n'en a pas, alors l'outil RECONSTRUIT la table (`CREATE __new_billets` → `INSERT … SELECT` →
145
+ `DROP TABLE` → `RENAME`). Le `DROP TABLE` est reconnu comme destructeur, et il l'est en général —
146
+ ici il porte sur une table déjà recopiée, une ligne plus haut, dans la même migration. **Relis le
147
+ fichier avant de décider** : si tu y vois l'`INSERT INTO __new_… SELECT … FROM …` juste avant le
148
+ `DROP`, la reconstruction conserve les lignes, et `orm:migrate` l'applique sans broncher (les
149
+ fichiers sont écrits, c'est leur mise en service qui était refusée). Éprouvé de bout en bout :
150
+ deux lignes semées, trois étapes, deux lignes intactes et l'index unique en place.
151
+
152
+ > **Ne jamais** répondre à un échec de migration en refaisant la base. Une migration qui n'est pas
153
+ > passée n'a **rien** changé — sqlite et PostgreSQL l'annulent entière. C'est le fichier qu'il faut
154
+ > découper, pas les données qu'il faut sacrifier. Et si tu dois t'y reprendre à plusieurs fois,
155
+ > `NF_MIGRATE_DATABASE_URL` détourne la commande vers une base d'ESSAI et laisse la tienne intacte.
156
+
157
+ ### La base existait AVANT toute migration — un geste de plus, une seule fois
158
+
159
+ Une application passée du mode développement à la production a ses tables **et** un dossier
160
+ `migrations/` vide. Dans cet état, `orm:generate` refuse — `NF_GENERATE_DATABASE_NOT_ADOPTED` :
161
+ la première migration décrirait la création de tables qui existent déjà, avec leurs données, et
162
+ l'adopter graverait dans l'historique un schéma que la base n'a pas.
163
+
164
+ ```bash
165
+ npx nodefony orm:migrate:baseline --from-database # la référence est LUE sur la base
166
+ npx nodefony orm:generate --name ajout_du_slug # produit un ALTER, plus un CREATE
167
+ npx nodefony orm:migrate
168
+ ```
169
+
170
+ `--from-database` lit le schéma de la base, en écrit la migration de référence et l'inscrit comme
171
+ appliquée. **Aucune instruction n'est exécutée sur la base.** À faire une fois, avant tout le reste.
172
+
173
+ Deux choses qu'il rapporte et qu'il faut lire :
174
+
175
+ - **des tables lues sans être déclarées** — la base est partagée avec autre chose. L'outil de
176
+ lecture ne sait pas restreindre son champ ; ces tables entrent dans la référence, et la
177
+ génération suivante proposera de les SUPPRIMER. Relis le fichier avant de continuer.
178
+ - **un corps resté en commentaire** — la référence ne recréerait rien sur une base neuve.
179
+
180
+ > ⚠️ **Sur MariaDB, `--from-database` ne fonctionne pas**, et il le dit au lieu de mourir. MariaDB
181
+ > écrit le type JSON en `longtext` + `CHECK (json_valid(…))`, que l'outil de lecture ne sait pas
182
+ > relire — et il lit la base ENTIÈRE avant de filtrer, donc les tables du framework suffisent à le
183
+ > bloquer. Le repli, sur ce serveur : relever le schéma (`SHOW CREATE TABLE`), le coller dans un
184
+ > `orm:generate --custom --name base_existante`, puis `orm:migrate:baseline`.
185
+ > Cela ne concerne QUE cette commande de reprise : la création des tables, leur migration et le
186
+ > fonctionnement de l'application sont inchangés sur MariaDB.
187
+
188
+ ## 4. Éprouver une migration — sur une base d'ESSAI, jamais sur la tienne
189
+
190
+ Quand il faut **prouver** qu'une migration fait ce qu'elle annonce, la réponse n'est jamais de
191
+ détruire la base pour repartir de zéro : c'est de migrer **ailleurs**.
192
+
193
+ `NF_MIGRATE_DATABASE_URL` sert exactement à ça. Elle remplace la connexion pour les quatre
194
+ commandes de migration — `orm:migrate`, `orm:migrate:status`, `orm:migrate:baseline`,
195
+ `orm:migrate:repair` — et **pour elles seules** : ni le démarrage de l'application, ni `orm:reset`,
196
+ ni un store ne la lisent. Ta base de développement n'est pas ouverte pendant l'essai ; elle n'est
197
+ même pas touchée.
198
+
199
+ **Deux décors, deux questions différentes. Choisis selon ce que tu dois prouver.**
200
+
201
+ ### a. Une base NEUVE — « la suite s'applique-t-elle depuis zéro ? »
202
+
203
+ C'est le décor d'une installation propre, et celui d'un nouvel environnement.
204
+
205
+ ```bash
206
+ # sqlite : un fichier qui n'existe pas encore suffit — le pilote le crée.
207
+ # PowerShell : $env:NF_MIGRATE_DATABASE_URL = "sqlite:./var/databases/essai.sqlite"
208
+ export NF_MIGRATE_DATABASE_URL="sqlite:./var/databases/essai.sqlite"
209
+
210
+ npx nodefony orm:migrate --dry-run --json # le plan : ce qui s'appliquerait, rien d'écrit
211
+ npx nodefony orm:migrate --json # applique — sur la base d'essai
212
+ npx nodefony orm:migrate:status --json # doit rendre `up-to-date`, code 0
213
+ ```
214
+
215
+ Sur PostgreSQL ou MySQL, la base d'essai se crée à côté (`CREATE DATABASE app_essai;`) et l'URL la
216
+ désigne. Le dialecte doit être le MÊME que celui du connecteur : viser une base d'un autre dialecte
217
+ est refusé — `NF_MIGRATE_URL_MISMATCH`, rien n'est appliqué. Ce refus est une protection, pas un
218
+ obstacle à contourner.
219
+
220
+ ### b. Une COPIE de ta base — « s'applique-t-elle sur mes données ? »
221
+
222
+ C'est le décor qui compte pour une migration qui touche des lignes existantes : un champ
223
+ obligatoire ajouté à une table déjà remplie, un remplissage, une contrainte resserrée. Une base
224
+ neuve ne prouve RIEN de tout ça — elle est vide.
225
+
226
+ ```bash
227
+ # sqlite : une copie du fichier. Le chemin par défaut du connecteur `default` est
228
+ # `var/databases/nodefony-drizzle.db` — `orm:migrate:status --json` le confirme.
229
+ cp var/databases/nodefony-drizzle.db /tmp/essai.sqlite
230
+ # PostgreSQL : CREATE DATABASE app_essai TEMPLATE app; (ou une restauration de sauvegarde)
231
+
232
+ export NF_MIGRATE_DATABASE_URL="sqlite:/tmp/essai.sqlite"
233
+ npx nodefony orm:migrate --json
234
+ ```
235
+
236
+ ### Ce qui fait la PREUVE
237
+
238
+ Trois choses, et elles se montrent :
239
+
240
+ 1. **Le verdict** : `orm:migrate:status --json` rend `up-to-date` et le code `0` sur la base
241
+ d'essai.
242
+ 2. **Ce que la base porte vraiment** — les tables et les colonnes attendues sont là. Un verdict
243
+ `up-to-date` dit que l'historique est complet, pas que le schéma te convient.
244
+ 3. **Que ta base n'a pas bougé.** Montre-le au lieu de l'affirmer : une empreinte avant et après
245
+ (`shasum -a 256 var/databases/nodefony-drizzle.db`) doit être **identique**.
246
+
247
+ > Et si l'essai échoue, il échoue sur la base d'essai. C'est tout l'intérêt : on jette le fichier,
248
+ > on corrige la migration, on recommence. Rien à réparer, rien à réexpliquer.
249
+
250
+ 🔴 **Quand l'essai est fini, RETIRE la variable** — `unset NF_MIGRATE_DATABASE_URL` (PowerShell :
251
+ `Remove-Item Env:NF_MIGRATE_DATABASE_URL`). Oubliée dans le terminal, elle détourne
252
+ silencieusement chaque commande de migration suivante vers la base d'essai : `orm:migrate` rend
253
+ « appliqué » et le code du succès, pendant que ta vraie base ne reçoit rien. Le seul symptôme
254
+ arrive plus tard, quand l'application démarre sur un schéma qui n'a pas bougé.
255
+
256
+ Tu n'as rien à interroger pour savoir où tu tapes : **chaque commande de migration annonce la base
257
+ qu'elle vise**. Quand une variable la détourne, l'en-tête de l'état le dit en toutes lettres —
258
+ « ⚠ NF_MIGRATE_DATABASE_URL détourne ce connecteur vers … » — et la charge utile `--json` porte le
259
+ même fait (`driver.target`). C'est le même chemin pour l'écran et pour la machine : les deux ne
260
+ peuvent pas diverger.
261
+
262
+ ```bash
263
+ nodefony orm:migrate:status
264
+ ```
265
+
266
+ > ⚠️ **`orm:migrate:baseline` n'est pas un outil d'essai.** Il sert à ADOPTER une base qui porte
267
+ > déjà les tables sans historique — une fois, à la reprise d'un existant. S'en servir pour se
268
+ > fabriquer un décor de départ écrit un historique faux dans la base visée : les migrations
269
+ > adoptées y sont marquées appliquées sans l'avoir été. Pour un décor de départ, c'est le §4 —
270
+ > une base d'essai, et rien d'autre.
271
+
272
+ ## 5. Les refus, et le geste que chacun appelle
273
+
274
+ Un refus n'est pas une panne : c'est le produit qui s'arrête devant une décision qui t'appartient.
275
+ Le `code` est stable — **lis-le, il désigne le geste**.
276
+
277
+ | Code | Ce qui s'est passé | Le geste |
278
+ | ---------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
279
+ | `NF_MIGRATE_BASELINE_REQUIRED` | la base porte déjà les tables, sans aucun historique | `orm:migrate:baseline` (l'adopter) |
280
+ | `NF_GENERATE_DATABASE_NOT_ADOPTED` | aucune migration n'existe, et la base porte déjà ces tables | `orm:migrate:baseline --from-database`, PUIS regénérer |
281
+ | `NF_MIGRATE_BASELINE_NOT_EMPTY` | `--from-database` demandé alors que des migrations existent | `orm:migrate:baseline` sans option |
282
+ | `NF_MIGRATE_FAILED_MARKER` | une migration a échoué, ou n'a jamais fini | LIRE l'erreur, puis `orm:migrate:repair` |
283
+ | `NF_MIGRATE_HASH_MISMATCH` | un fichier déjà appliqué a été modifié | RÉTABLIR le fichier (1er geste) ; `--update-hashes` seulement si la modification était sans effet |
284
+ | `NF_MIGRATE_OUT_OF_ORDER` | une migration en attente se range avant la dernière posée | renommer la nouvelle après la dernière appliquée |
285
+ | `NF_MIGRATE_MISSING_FILE` | une migration appliquée n'a plus de fichier | rétablir le fichier — il fait partie de l'historique |
286
+ | `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, et pourtant la base ne porte pas le schéma | l'historique affirme une migration jamais exécutée : `orm:migrate:repair --forget <source>/<tag>` puis `orm:migrate` |
287
+ | `NF_MIGRATE_LOCK_TIMEOUT` | un autre travail de migration tient le verrou | ATTENDRE puis rejouer — ce n'est pas une panne, et le verbe est idempotent |
288
+ | `NF_MIGRATE_DESTRUCTIVE` | les migrations en attente SUPPRIMENT des données | lire le SQL (`--dry-run`), puis assumer avec `--allow-destructive` |
289
+
290
+ Les codes exhaustifs, avec un exemple de charge utile pour chacun :
291
+ [`references/verdicts.md`](references/verdicts.md).
292
+
293
+ ## 6. Les trois interdits
294
+
295
+ Chacun casse l'historique de façon irrattrapable, et aucun ne produit d'erreur au moment où on le
296
+ commet.
297
+
298
+ 1. **Ne jamais modifier un fichier `.sql` déjà appliqué.** Son empreinte est enregistrée : le
299
+ modifier fait basculer le verdict en dérive sur toutes les bases où il est passé. Une correction
300
+ s'écrit dans une migration NEUVE.
301
+ 2. **Ne jamais toucher à la table d'historique à la main.** Elle est le seul témoin de ce qui a été
302
+ appliqué ; une ligne ajoutée ou retirée à la main fait mentir tous les verdicts suivants.
303
+ L'interdit porte sur le client SQL, pas sur le produit : quand l'historique affirme une migration
304
+ que la base n'a jamais reçue, le geste existe et il est borné —
305
+ `orm:migrate:repair --forget <source>/<tag>` désinscrit UNE entrée nommée, pour qu'elle soit
306
+ rejouée. Il ne touche pas la base ; si la migration avait bien été appliquée, son rejeu échouera.
307
+ 3. **Ne jamais renuméroter ni renommer une migration publiée.** L'identité voyage : elle est
308
+ enregistrée dans chaque base où la migration est passée.
309
+
310
+ Et un quatrième, qui n'est pas un interdit d'historique mais de méthode : **ne supprime pas une
311
+ base pour « repartir propre »**, et n'efface pas non plus son dossier de données. La commande qui
312
+ le fait (`orm:reset`) existe, refuse partout sauf en développement, et n'est jamais la réponse à
313
+ une migration qui refuse.
314
+
315
+ **Ce qu'il faut faire à la place** : migrer une base d'ESSAI — c'est le §4, et il couvre les deux
316
+ besoins qui poussent à détruire. « Je veux vérifier que ma migration part d'une base propre » →
317
+ décor (a), une base neuve. « Je veux la voir passer sur des données » → décor (b), une copie. Dans
318
+ les deux cas tu obtiens la même preuve, en gardant ta base ET son historique.
319
+
320
+ ## 7. En production — les migrations passent AVANT les exemplaires
321
+
322
+ Le patron, et il n'a pas d'alternative raisonnable : **un travail dédié applique les migrations, et
323
+ se termine avant que le premier nouvel exemplaire ne démarre**. Les exemplaires, eux, ne fabriquent
324
+ jamais de schéma.
325
+
326
+ Une application générée avec une base SQL porte déjà cette recette dans `deploy/migrate-job.yaml`,
327
+ rendue à son nom. Ne la réécris pas : lis son en-tête, il porte le mode d'emploi.
328
+
329
+ Trois faits qui évitent trois faux diagnostics :
330
+
331
+ - **Un exemplaire dont la base est en retard répond `503` sur `/readyz`** (jamais sur `/livez`) et
332
+ reste hors du répartiteur de charge. Ce n'est pas une panne : c'est la protection. Applique les
333
+ migrations, les exemplaires se mettent en service **seuls**.
334
+ - **Le compte qui migre n'est pas celui qui sert.** `NF_MIGRATE_DATABASE_URL` remplace la connexion
335
+ pour la commande de migration seulement — c'est le véhicule du moindre privilège, et elle doit
336
+ désigner une connexion **directe** (un répartiteur de connexions en mode transaction casse le
337
+ verrou).
338
+ - **Pendant un remplacement progressif, l'ancien et le nouveau code coexistent.** Une migration
339
+ doit donc rester compatible avec la version précédente : on AJOUTE d'abord (colonne facultative,
340
+ table, index), on retire dans une version ULTÉRIEURE.
341
+
342
+ ## 8. Ce que tu n'as pas le droit de faire, et pourquoi ce n'est pas une consigne
343
+
344
+ En production, appliquer des migrations depuis un serveur qui sert le trafic est **refusé par le
345
+ produit**, pas déconseillé : le point d'application du plan d'administration refuse hors
346
+ développement, et le compte de base de données d'un exemplaire n'a pas le droit de modifier un
347
+ schéma. Un refus du moteur est bruyant ; ne cherche pas à le contourner, c'est le travail de
348
+ déploiement qui porte ce droit.
349
+
350
+ En développement, à l'inverse, appliquer est normal — c'est là que le cycle complet se joue.
351
+
352
+ ## 9. Quand passer la main
353
+
354
+ | Le besoin | Où aller |
355
+ | ------------------------------------------------------------- | --------------------------------------------------- |
356
+ | Créer une entité, un service CRUD, un controller de ressource | skill `nodefony-add-crud` |
357
+ | Comprendre la grammaire de champs et les index | skill `nodefony-add-crud` |
358
+ | Le détail des codes de verdict, avec un exemple par code | `references/verdicts.md` |
359
+ | Ce que le module publie sur les migrations | `node_modules/@nodefony/drizzle/docs/migrations.md` |
@@ -0,0 +1,139 @@
1
+ # Les verdicts de migration, en entier
2
+
3
+ > Chargé à la demande. Le `SKILL.md` porte les cinq refus courants et leur geste ; cette page les
4
+ > donne tous, avec la charge utile que la commande rend en `--json`.
5
+
6
+ ## Ce que rend une lecture d'état
7
+
8
+ `orm:migrate:status --json` rend **un seul objet**. Son cœur est NEUTRE — un second moteur de base
9
+ de données remplira la même structure — et tout ce qui est propre au pilote SQL vit sous `driver`.
10
+ N'écris jamais un chemin de lecture qui passe par le nom d'un pilote.
11
+
12
+ ```json
13
+ {
14
+ "formatVersion": 1,
15
+ "connector": "default",
16
+ "verdict": "pending",
17
+ "exitCode": 1,
18
+ "summary": "1 migration en attente sur « default ».",
19
+ "nextActions": [
20
+ {
21
+ "command": "nodefony orm:migrate --dry-run",
22
+ "args": ["orm:migrate", "--dry-run"]
23
+ },
24
+ { "command": "nodefony orm:migrate", "args": ["orm:migrate"] }
25
+ ],
26
+ "sources": [
27
+ {
28
+ "name": "app",
29
+ "applied": 2,
30
+ "pending": 1,
31
+ "failed": 0,
32
+ "pendingTags": ["0003_ajout_slug"],
33
+ "drifted": [],
34
+ "missing": [],
35
+ "entries": [
36
+ {
37
+ "tag": "0001_init",
38
+ "status": "applied",
39
+ "appliedAt": 1756400000000,
40
+ "durationMs": 42,
41
+ "appliedBy": "poste-de-dev",
42
+ "runId": "b0e2…"
43
+ },
44
+ { "tag": "0003_ajout_slug", "status": "pending" }
45
+ ]
46
+ }
47
+ ],
48
+ "driver": {
49
+ "kind": "sql",
50
+ "dialect": "postgres",
51
+ "ddl": "none",
52
+ "historyTable": "nodefony_migrations"
53
+ }
54
+ }
55
+ ```
56
+
57
+ **Les six verdicts**, dans l'ordre de gravité — le premier qui s'applique gagne, et cet ordre dit
58
+ quel geste vient EN PREMIER :
59
+
60
+ | `verdict` | Ce que ça dit | `exitCode` |
61
+ | ------------ | -------------------------------------------------------------------- | ---------- |
62
+ | `failed` | une migration a échoué : rien d'autre ne se discute avant réparation | `1` |
63
+ | `drift` | un fichier appliqué a changé depuis son application | `1` |
64
+ | `adopt` | la base porte les tables sans historique — elle est antérieure | `1` |
65
+ | `divergent` | la base ne correspond pas au schéma déclaré | `0` ou `1` |
66
+ | `pending` | des migrations restent à appliquer | `1` |
67
+ | `up-to-date` | rien à faire | `0` |
68
+
69
+ > `divergent` est le seul dont le code de sortie DÉPEND d'un réglage : selon la conduite choisie,
70
+ > il informe (`0`) ou bloque (`1`). Superviser ne doit pas faire tomber un déploiement par défaut.
71
+
72
+ ## Ce que rend un refus
73
+
74
+ Une sortie qui porte `error` est un ARRÊT ; une sortie qui porte `verdict` est un état lu. Aucune
75
+ n'a jamais les deux — c'est le discriminant à tester.
76
+
77
+ ```json
78
+ {
79
+ "formatVersion": 1,
80
+ "connector": "default",
81
+ "exitCode": 1,
82
+ "error": {
83
+ "code": "NF_MIGRATE_BASELINE_REQUIRED",
84
+ "summary": "Cette base porte déjà les tables du schéma mais n'a aucun historique de migration.",
85
+ "meaning": "",
86
+ "nextActions": [
87
+ {
88
+ "command": "nodefony orm:migrate:baseline --connector default",
89
+ "args": ["orm:migrate:baseline", "--connector", "default"]
90
+ }
91
+ ]
92
+ }
93
+ }
94
+ ```
95
+
96
+ ## Tous les codes
97
+
98
+ ### Refus de l'applicateur — l'état de la base ou des fichiers
99
+
100
+ | Code | Ce qui s'est passé | Le geste |
101
+ | ---------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
102
+ | `NF_MIGRATE_BASELINE_REQUIRED` | tables présentes, historique vide | `orm:migrate:baseline` — adopte explicitement, n'exécute aucun SQL |
103
+ | `NF_MIGRATE_BASELINE_AMBIGUOUS` | la base s'écarte du schéma déclaré : adopter graverait un faux | `--up-to <tag>` pour borner, ou `--from-database` si aucune migration n'existe |
104
+ | `NF_MIGRATE_BASELINE_NOT_EMPTY` | `--from-database` demandé, mais des migrations existent déjà | `orm:migrate:baseline` sans option — l'historique des fichiers fait foi |
105
+ | `NF_GENERATE_DATABASE_NOT_ADOPTED` | aucune migration écrite, et la base porte déjà ces tables | `orm:migrate:baseline --from-database` — elle EST la première migration. Regénérer ensuite SEULEMENT si le refus le propose (couverture partielle) : sinon il n'y a aucun écart à écrire |
106
+ | `NF_MIGRATE_FAILED_MARKER` | une migration a échoué, ou n'a jamais fini | lire l'erreur enregistrée, constater la base, puis `orm:migrate:repair` |
107
+ | `NF_MIGRATE_HASH_MISMATCH` | le fichier d'une migration appliquée a changé | RÉTABLIR le fichier (premier geste proposé) ; `repair --update-hashes` ensuite, et seulement si la modification était sans effet |
108
+ | `NF_MIGRATE_OUT_OF_ORDER` | une migration en attente se range avant la dernière appliquée | renommer la nouvelle pour qu'elle suive la dernière appliquée |
109
+ | `NF_MIGRATE_MISSING_FILE` | une migration appliquée n'a plus de fichier | rétablir le fichier — il fait partie de l'historique |
110
+ | `NF_MIGRATE_UNKNOWN_FORMAT` | un fichier n'est pas au format que cet applicateur lit | vérifier le journal de la source ; ne pas éditer à la main |
111
+ | `NF_MIGRATE_LOCK_TIMEOUT` | le verrou est tenu par un autre travail | attendre, puis rejouer — le verbe est idempotent. Sort en **2** : ce n'est pas une panne, c'est le déploiement d'à côté |
112
+ | `NF_MIGRATE_JOURNAL_MISMATCH` | le journal annonce un fichier que le dossier ne contient pas | rétablir le fichier, ou régénérer la source |
113
+
114
+ ### Refus d'usage — la demande elle-même
115
+
116
+ | Code | Ce qui s'est passé | Le geste |
117
+ | ------------------------------ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
118
+ | `NF_MIGRATE_UNKNOWN_CONNECTOR` | aucun connecteur de ce nom | le message liste ceux que l'application déclare |
119
+ | `NF_MIGRATE_UNKNOWN_TAG` | `--up-to` désigne une migration inconnue | relire `sources[].pendingTags` |
120
+ | `NF_MIGRATE_UNKNOWN_SOURCE` | `--source` n'est pas déclarée par cette application | relire `sources[].name` |
121
+ | `NF_MIGRATE_URL_MISMATCH` | la variable de migration désigne une base d'un AUTRE dialecte | corriger la variable, ou choisir le bon connecteur |
122
+ | `NF_MIGRATE_NOT_CONFIGURED` | connecteur SQL non déclaré dans la configuration | le déclarer pour pouvoir le suivre |
123
+ | `NF_MIGRATE_NO_MIGRATIONS` | ce connecteur est porté par une base qui ne se migre pas ainsi | rien à migrer ici — ce n'est pas une panne |
124
+ | `NF_MIGRATE_NOT_DEVELOPMENT` | geste réservé au développement, demandé ailleurs | passer par le travail de déploiement |
125
+ | `NF_MIGRATE_DESTRUCTIVE` | des migrations en attente SUPPRIMENT des données | relire le fichier produit, puis assumer explicitement |
126
+ | `NF_MIGRATE_UNAVAILABLE` | la commande s'est arrêtée sans pouvoir nommer la cause | constater l'état (`orm:migrate:status`) avant de reprendre |
127
+ | `NF_MIGRATE_CONFIRM_REQUIRED` | `orm:reset` demandé hors terminal, ou en sortie machine | relancer avec `--yes` si l'effacement est voulu |
128
+ | `NF_GENERATE_NAME` | le nom de migration demandé n'est pas utilisable | reprendre la suggestion que le message donne |
129
+ | `NF_GENERATE_MISSING_ENTITY` | une entité est enregistrée sans fichier qui la fournisse | `nodefony inspect entities --json` pour la situer |
130
+ | `NF_GENERATE_FRAMEWORK_TABLE` | une entité de l'application usurpe une table du framework | la renommer, ou écrire une migration libre (`--custom`) |
131
+ | `NF_GENERATE_DESTRUCTIVE` | la migration ÉCRITE supprime des données | relire le fichier, puis annuler (`git checkout`) ou appliquer — la mise en service a sa propre garde |
132
+ | `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, et pourtant la base ne porte pas le schéma | `repair --forget <source>/<tag>` : l'historique affirme une migration jamais exécutée |
133
+ | `NF_GENERATE_TOOL_MISSING` | l'outil de génération n'est pas installé | l'installer en dépendance de développement |
134
+
135
+ ## Un contrat qui ne bougera pas
136
+
137
+ `formatVersion` vaut `1` au premier niveau de chaque sortie. Ajouter un champ est une évolution
138
+ mineure ; en retirer ou en renommer un est interdit sur la série majeure. Les codes de sortie
139
+ `0` / `1` / `2` sont figés : des passes d'intégration continue s'y adossent.