@nodefony/devkit 10.0.0-beta.4 → 10.0.0-beta.6

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 (28) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
  3. package/dist/index.js +3 -3
  4. package/dist/nodefony/controllers/DevkitController.js +1 -1
  5. package/dist/nodefony/controllers/McpController.js +2 -2
  6. package/dist/nodefony/controllers/ProfileSessionController.js +1 -1
  7. package/dist/nodefony/mcp/profileReport.js +1 -1
  8. package/dist/nodefony/mcp/profileRoute.js +28 -9
  9. package/dist/nodefony/service/DevkitService.js +4 -4
  10. package/docs/index.md +1 -1
  11. package/package.json +12 -12
  12. package/skills/nodefony-add-crud/SKILL.md +43 -24
  13. package/skills/nodefony-add-realtime-channel/SKILL.md +376 -111
  14. package/skills/nodefony-add-realtime-channel/references/binaire.md +191 -0
  15. package/skills/nodefony-add-realtime-channel/references/production.md +121 -0
  16. package/skills/nodefony-browser/SKILL.md +27 -2
  17. package/skills/nodefony-browser/references/sondes.md +13 -1
  18. package/skills/nodefony-browser/scripts/audit.mjs +12 -0
  19. package/skills/nodefony-browser/scripts/inspect.mjs +94 -88
  20. package/skills/nodefony-browser/scripts/lib/actions.mjs +113 -0
  21. package/skills/nodefony-browser/scripts/lib/browser.mjs +50 -11
  22. package/skills/nodefony-browser/scripts/lib/probes.mjs +56 -0
  23. package/skills/nodefony-browser/scripts/watch.mjs +27 -11
  24. package/skills/nodefony-dev/SKILL.md +11 -0
  25. package/skills/nodefony-dev/references/rapport-de-build.md +19 -0
  26. package/skills/nodefony-devops/SKILL.md +28 -21
  27. package/skills/nodefony-migrate-schema/SKILL.md +110 -20
  28. package/skills/nodefony-migrate-schema/references/verdicts.md +22 -20
@@ -1,4 +1,4 @@
1
- // \0@oxc-project+runtime@0.155.0/helpers/esm/decorate.js
1
+ // \0@oxc-project+runtime@0.157.0/helpers/esm/decorate.js
2
2
  function __decorate(decorators, target, key, desc) {
3
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1,4 +1,4 @@
1
- // \0@oxc-project+runtime@0.155.0/helpers/esm/decorateParam.js
1
+ // \0@oxc-project+runtime@0.157.0/helpers/esm/decorateParam.js
2
2
  function __decorateParam(paramIndex, decorator) {
3
3
  return function(target, key) {
4
4
  decorator(target, key, paramIndex);
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
- import { BUILTIN_MCP_TOOL_KEYS, builtinMcpTools, selectBuiltinMcpTools } from "./nodefony/mcp/tools.js";
2
1
  import defaults, { devkitConfigSchema } from "./nodefony/config/config.js";
3
2
  import { defineDevkitConfig } from "./nodefony/config/defineModuleConfig.js";
4
- import { buildCard } from "./nodefony/src/card.js";
5
- import __decorate from "./_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
6
3
  import DevkitService_default from "./nodefony/service/DevkitService.js";
7
4
  import DevkitController_default from "./nodefony/controllers/DevkitController.js";
8
5
  import McpController_default from "./nodefony/controllers/McpController.js";
9
6
  import ProfileSessionController_default from "./nodefony/controllers/ProfileSessionController.js";
7
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
8
+ import { BUILTIN_MCP_TOOL_KEYS, builtinMcpTools, selectBuiltinMcpTools } from "./nodefony/mcp/tools.js";
9
+ import { buildCard } from "./nodefony/src/card.js";
10
10
  import { Module, services } from "nodefony";
11
11
  import { controllers } from "@nodefony/framework";
12
12
  // index.ts
@@ -1,4 +1,4 @@
1
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
1
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
2
2
  import { Controller, controller, route } from "@nodefony/framework";
3
3
  // nodefony/controllers/DevkitController.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
2
- import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorateParam.js";
1
+ import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorateParam.js";
2
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
3
3
  import { Body, Controller, controller, route } from "@nodefony/framework";
4
4
  // nodefony/controllers/McpController.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
1
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
2
2
  import { Controller, controller, route } from "@nodefony/framework";
3
3
  // nodefony/controllers/ProfileSessionController.ts
4
4
  /**
@@ -109,7 +109,7 @@ function gaps(report, ctx) {
109
109
  if (report.requests === 0 && shot !== void 0) out.push("aucune réponse profilée" + (shot.client.failure === null ? "" : ` : ${shot.client.failure}`));
110
110
  else if (report.requests === 0) out.push("aucune requête observée : l'application n'a reçu aucun trafic pendant la fenêtre (cet outil n'en génère pas — lancer la page, les tests ou une charge pendant l'observation)");
111
111
  if (ctx.lost > 0) out.push(`${ctx.lost} profil(s) perdu(s) avant lecture (tampon du profileur débordé) : les chiffres portent sur un échantillon`);
112
- if (report.untimed > 0) out.push(`${report.untimed} requête(s) sans durée (mesure des phases désactivée, ou requête interrompue)`);
112
+ if (report.untimed > 0) out.push(`${report.untimed} requête(s) sans durée (chronométrage coupé par timing.enabled: false)`);
113
113
  if (ctx.interrupted) out.push(`${shot === void 0 ? "observation interrompue" : "tir interrompu"} après ${ms(ctx.elapsedMs / 1e3)} s : le serveur s'arrête (rechargement après une modification de fichier ?) — rappeler ` + (shot === void 0 ? "nodefony_observe" : "nodefony_profile_route") + " une fois le serveur redémarré, sans modifier de fichier pendant la mesure");
114
114
  if (ctx.cancelled) out.push(shot === void 0 ? `observation abandonnée après ${ms(ctx.elapsedMs / 1e3)} s sur ${ms(ctx.windowMs / 1e3)} s` : `tir abandonné après ${ms(ctx.elapsedMs / 1e3)} s : ${shot.sent} requête(s) envoyée(s) sur ${shot.requested}`);
115
115
  return out;
@@ -1,7 +1,7 @@
1
- import { renderProfileReport } from "./profileReport.js";
2
- import { watchToolStop } from "./shutdown.js";
3
1
  import { NOT_SERVING_MESSAGE } from "./observe.js";
2
+ import { renderProfileReport } from "./profileReport.js";
4
3
  import { SHOT_REQUEST_TIMEOUT_MS, createShotClient } from "./shotClient.js";
4
+ import { watchToolStop } from "./shutdown.js";
5
5
  import { PROFILE_SESSION_PATH } from "../src/profileSession.js";
6
6
  import { ADMIN_SCOPE_WRITE, EVENT_LOOP_RESOLUTION_MS, ProcessProbe } from "nodefony";
7
7
  import { MCP_RESERVED_PREFIX, SESSION_TICKET_HEADER, mcpText, readTicketSessionVerdict } from "@nodefony/ai";
@@ -171,26 +171,42 @@ async function openShotSession(client, issueTicket, account, signal) {
171
171
  } catch (error) {
172
172
  return {
173
173
  ok: false,
174
+ aborted: false,
174
175
  message: error instanceof Error ? error.message : String(error)
175
176
  };
176
177
  }
177
- const res = await client.send({
178
- method: "POST",
179
- path: PROFILE_SESSION_PATH,
180
- headers: { [SESSION_TICKET_HEADER]: ticket },
181
- readBody: true
182
- }, signal);
178
+ let res;
179
+ try {
180
+ res = await client.send({
181
+ method: "POST",
182
+ path: PROFILE_SESSION_PATH,
183
+ headers: { [SESSION_TICKET_HEADER]: ticket },
184
+ readBody: true
185
+ }, signal);
186
+ } catch (error) {
187
+ if (error instanceof Error && error.name === "AbortError") return {
188
+ ok: false,
189
+ aborted: true
190
+ };
191
+ return {
192
+ ok: false,
193
+ aborted: false,
194
+ message: `session de profilage non ouverte pour \`${account}\` (${error instanceof Error ? error.message : String(error)}). L'application écoute-t-elle sur son adresse ?`
195
+ };
196
+ }
183
197
  const read = readTicketSessionVerdict(res.body);
184
198
  if (res.status !== 200 || "message" in read) {
185
199
  const why = "message" in read ? read.message : `statut ${res.status}`;
186
200
  return {
187
201
  ok: false,
202
+ aborted: false,
188
203
  message: `session de profilage non ouverte pour \`${account}\` (${res.status} — ${why}). ` + (res.status === 403 ? "Compte inconnu, inactif ou verrouillé ?" : res.status === 503 ? "" : "La route de connexion du profilage répond-elle ?")
189
204
  };
190
205
  }
191
206
  const cookie = res.setCookie.map((c) => c.split(";", 1)[0] ?? "").filter((c) => c.length > 0).join("; ");
192
207
  if (cookie.length === 0) return {
193
208
  ok: false,
209
+ aborted: false,
194
210
  message: `session ouverte pour \`${account}\`, mais aucun cookie reçu`
195
211
  };
196
212
  return {
@@ -417,7 +433,10 @@ async function profileRoute(profiler, target, deps, opts) {
417
433
  if (opts.account !== null) {
418
434
  if (deps.issueTicket === void 0) return mcpText("Profilage authentifié indisponible : aucun émetteur de ticket.", true);
419
435
  const opened = await openShotSession(client, deps.issueTicket, opts.account, stop.signal);
420
- if (!opened.ok) return mcpText(opened.message, true);
436
+ if (!opened.ok) {
437
+ if (!opened.aborted) return mcpText(opened.message, true);
438
+ return mcpText(stop.interrupted ? "tir interrompu avant le premier envoi : le serveur s'arrête pendant l'ouverture de la session de profilage." : "tir abandonné avant le premier envoi : la session de profilage n'était pas encore ouverte.");
439
+ }
421
440
  session = opened.session;
422
441
  headers.cookie = session.cookie;
423
442
  identity = {
@@ -1,10 +1,10 @@
1
+ import { selectBuiltinMcpTools } from "../mcp/tools.js";
1
2
  import { asProfilerReader } from "../mcp/observe.js";
2
- import { readShotTarget } from "../mcp/shotClient.js";
3
3
  import { PROFILE_SESSION_FACTOR } from "../src/profileSession.js";
4
- import { selectBuiltinMcpTools } from "../mcp/tools.js";
5
- import defaults from "../config/config.js";
4
+ import { readShotTarget } from "../mcp/shotClient.js";
6
5
  import { buildCard } from "../src/card.js";
7
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
6
+ import defaults from "../config/config.js";
7
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
8
8
  import { ADMIN_DEFAULT_ROLE, Nodefony, ROLES_WITHIN, Service, extend, injectable } from "nodefony";
9
9
  import { defineMcpGate, defineTicketSessionGate, mcpDeclaredScopes } from "@nodefony/ai";
10
10
  // nodefony/service/DevkitService.ts
package/docs/index.md CHANGED
@@ -477,7 +477,7 @@ agent invente : faute d'une marche à suivre, il écrit un CRUD à la main, un
477
477
  service à méthodes `static` que le conteneur ne voit pas, un contrôle de droits
478
478
  dans le corps de l'action.
479
479
 
480
- Le paquet livre donc onze **skills** au format [Agent Skills](https://agentskills.io) :
480
+ Le paquet livre donc douze **skills** au format [Agent Skills](https://agentskills.io) :
481
481
  un qui porte la conduite commune de toute tâche, un qui part du symptôme d'une panne,
482
482
  et un par geste où l'invention coûte cher :
483
483
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/devkit",
3
- "version": "10.0.0-beta.4",
3
+ "version": "10.0.0-beta.6",
4
4
  "type": "module",
5
5
  "description": "Outillage de développement d'une application Nodefony : carte de visite du projet et portes de découverte pour un agent de développement",
6
6
  "author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
@@ -27,24 +27,24 @@
27
27
  "test:integration": "vitest run --config vitest.integration.config.ts"
28
28
  },
29
29
  "peerDependencies": {
30
- "@nodefony/ai": "^10.0.0-beta.4",
31
- "@nodefony/framework": "^10.0.0-beta.4",
32
- "@nodefony/http": "^10.0.0-beta.4",
33
- "lighthouse": "^13.0.0",
34
- "nodefony": "^10.0.0-beta.4",
30
+ "@nodefony/ai": "^10.0.0-beta.6",
31
+ "@nodefony/framework": "^10.0.0-beta.6",
32
+ "@nodefony/http": "^10.0.0-beta.6",
33
+ "lighthouse": "^13.6.0",
34
+ "nodefony": "^10.0.0-beta.6",
35
35
  "playwright": "^1.50.0",
36
36
  "zod": "^4.6.7"
37
37
  },
38
38
  "devDependencies": {
39
- "@modelcontextprotocol/sdk": "1.32.1",
40
- "@nodefony/ai": "^10.0.0-beta.4",
41
- "@nodefony/framework": "^10.0.0-beta.4",
42
- "@nodefony/http": "^10.0.0-beta.4",
43
- "nodefony": "^10.0.0-beta.4"
39
+ "@modelcontextprotocol/sdk": "1.32.2",
40
+ "@nodefony/ai": "^10.0.0-beta.6",
41
+ "@nodefony/framework": "^10.0.0-beta.6",
42
+ "@nodefony/http": "^10.0.0-beta.6",
43
+ "nodefony": "^10.0.0-beta.6"
44
44
  },
45
45
  "dependencies": {
46
46
  "axe-core": "4.14.0",
47
- "tslib": "2.8.1"
47
+ "tslib": "2.9.1"
48
48
  },
49
49
  "peerDependenciesMeta": {
50
50
  "playwright": {
@@ -32,7 +32,7 @@ description: >
32
32
  ## Le geste
33
33
 
34
34
  ```bash
35
- npx nodefony create entity Article title:string body:text? published:bool
35
+ npx nodefony create entity Article title:string "body:text?" published:bool
36
36
  ```
37
37
 
38
38
  Une seule commande produit la chaîne entière : la table, son interface de ligne, les schémas de
@@ -57,24 +57,25 @@ fixe un défaut LITTÉRAL, `:unique` pose une contrainte d'unicité, `:index` un
57
57
  camelCase (`publishedAt`). Une faute de casse ou un type d'un autre outil (`boolean`, `integer`)
58
58
  est refusé avec la forme juste proposée.
59
59
 
60
- | Type | Ce que ça produit | SQLite | PostgreSQL | MySQL / MariaDB | MongoDB |
61
- | -------------- | ----------------------------- | --------------- | ---------------- | --------------- | ---------------------- |
62
- | `string(n)` | texte court, borné (255) | `text` ¹ | `varchar(n)` | `varchar(n)` | `String` + `maxlength` |
63
- | `text` | texte long | `text` | `text` | `text` | `String` |
64
- | `int` | entier | `integer` | `integer` | `int` | `Number` |
65
- | `float` | nombre à virgule | `real` | `double` | `double` | `Number` |
66
- | `decimal(p,s)` | décimal EXACT (voyage chaîne) | `numeric` | `numeric(p,s)` | `decimal(p,s)` | `String` |
67
- | `bool` | booléen | `integer` (0/1) | `boolean` | `boolean` | `Boolean` |
68
- | `json` | document libre | `text` (JSON) | `jsonb` | `json` | `Mixed` |
69
- | `date` | horodatage (ms) | `integer` (ms) | `timestamptz(3)` | `datetime(3)` | `Date` |
70
- | `uuid` | identifiant | `text` | `uuid` | `varchar(36)` | `String` |
71
- | `char(n)` | longueur EXACTE | `text` ¹ | `char(n)` | `char(n)` | `String`, `n` exact |
72
- | `enum(a,b)` | valeurs admises ² | `text` | `varchar(255)` | `varchar(255)` | `String` + `enum` |
73
- | `ref:<Entité>` | relation ³ | clé étrangère | clé étrangère | clé étrangère | `ObjectId` + `ref` |
74
-
75
- ¹ SQLite n'applique aucune longueur : c'est le schéma d'entrée (Zod) qui borne, sur TOUS les
76
- transports. ² Même colonne partout, sans type SQL nommé (qui exigerait une migration) : le type
77
- TypeScript et le schéma Zod bornent les valeurs. ³ En SQL, la colonne prend le type de la clé
60
+ | Type | Ce que ça produit | SQLite | PostgreSQL | MySQL / MariaDB | MongoDB |
61
+ | -------------- | ----------------------------- | ------------------- | ------------------ | --------------------- | ---------------------- |
62
+ | `string(n)` | texte court, borné (255) | `text(n)` + CHECK ¹ | `varchar(n)` | `varchar(n)` | `String` + `maxlength` |
63
+ | `text` | texte long | `text` | `text` | `text` | `String` |
64
+ | `int` | entier | `integer` | `integer` | `int` | `Number` |
65
+ | `float` | nombre à virgule | `real` | `double precision` | `double` | `Number` |
66
+ | `decimal(p,s)` | décimal EXACT (voyage chaîne) | `text` | `numeric(p,s)` | `decimal(p,s)` | `String` |
67
+ | `bool` | booléen | `integer` (0/1) | `boolean` | `boolean` | `Boolean` |
68
+ | `json` | document libre | `text` (JSON) | `jsonb` | `json` | `Mixed` |
69
+ | `date` | horodatage (ms) | `integer` (ms) | `timestamptz(3)` | `datetime(3)` | `Date` |
70
+ | `uuid` | identifiant | `text` | `uuid` | `varchar(36)` | `String` |
71
+ | `char(n)` | longueur EXACTE | `text` + CHECK ¹ | `char(n)` + CHECK | `char(n)` + CHECK | `String`, `n` exact |
72
+ | `enum(a,b)` | valeurs admises ² | `text` + CHECK | `text` + CHECK | `varchar(64)` + CHECK | `String` + `enum` |
73
+ | `ref:<Entité>` | relation ³ | clé étrangère | clé étrangère | clé étrangère | `ObjectId` + `ref` |
74
+
75
+ ¹ SQLite ne borne pas un `text` : une contrainte `CHECK` sur la longueur le fait en base, et le
76
+ schéma d'entrée (Zod) avant elle, sur TOUS les transports. ² Aucun type `ENUM` SQL : une colonne
77
+ texte et une contrainte `CHECK` qui liste les valeurs, chacune de 64 caractères au plus ; le type
78
+ TypeScript et le schéma Zod bornent avant la base. ³ En SQL, la colonne prend le type de la clé
78
79
  visée (`uuid`, entier pour `--id serial`, texte pour `User`) ; en MongoDB, aucune clé étrangère.
79
80
 
80
81
  La table exacte, par moteur, telle que CETTE version l'écrit :
@@ -108,7 +109,7 @@ tests — sans table ni migration (la collection naît à la première écriture
108
109
  les options qui ont un sens ici :
109
110
 
110
111
  ```bash
111
- npx nodefony create entity Post title:string(120) body:text? views:int=0 status:enum(draft,published)=draft slug:string:unique tags:json? author:ref:User --soft-delete --route /api/posts --dry-run
112
+ npx nodefony create entity Post "title:string(120)" "body:text?" views:int=0 "status:enum(draft,published)=draft" slug:string:unique "tags:json?" author:ref:User --soft-delete --route /api/posts --dry-run
112
113
  ```
113
114
 
114
115
  - La clé est l'`_id` natif, servie en `id` ; `ref:<Entité>` est un `ObjectId` indexé, chargé
@@ -121,6 +122,17 @@ npx nodefony create entity Post title:string(120) body:text? views:int=0 status:
121
122
  `--id`, `--index`, `--unique`. Un index composite s'écrit à la main dans le schéma.
122
123
  - `User` ne se régénère pas : il s'étend dans `nodefony/entity/User.ts` (son TSDoc donne le geste).
123
124
 
125
+ ## Le graphe d'entités de la console
126
+
127
+ Les liens du graphe viennent des `ref:` eux-mêmes : la clé étrangère que la base porte devient
128
+ une arête, sans rien répéter ailleurs. Le regroupement et la couleur suivent le **domaine** de
129
+ l'entité, sinon son module — et une application de centaines de tables tient souvent dans UN
130
+ module :
131
+
132
+ ```bash
133
+ npx nodefony create entity Invoice "number:string(30):unique" "total:decimal(24,8)" customer:ref:Customer --domain facturation --dry-run
134
+ ```
135
+
124
136
  ## Épouser une table qui existe déjà
125
137
 
126
138
  Trois réglages, et ils ne touchent **que** le SQL — la propriété TypeScript reste `id`, `siteId` :
@@ -195,9 +207,16 @@ refus tombe tout seul.
195
207
 
196
208
  ## Les trois vérités à savoir avant de livrer (SQL)
197
209
 
198
- 1. **La table naît au prochain démarrage en développement** (`CREATE TABLE IF NOT EXISTS`).
199
- 2. **La modifier n'altère rien** — aucun `ALTER` n'est émis. Une colonne ajoutée à une entité déjà
200
- créée n'apparaîtra pas dans une base existante.
210
+ 1. **La table existe dès la commande quand l'application versionne ses migrations** (toute
211
+ application `complete`) : `create entity` écrit ET applique la migration de la table neuve —
212
+ `--no-migrate` s'en dispense ; si des entités modifiées attendaient, la migration est écrite mais
213
+ PAS appliquée, et la sortie nomme les tables en trop. Sinon, la table naît au prochain démarrage
214
+ en développement (`CREATE TABLE IF NOT EXISTS`).
215
+ 2. **La modifier passe par une migration** dès que l'application versionne ses migrations :
216
+ `npx nodefony orm:generate --name <nom> --apply` (skill **`nodefony-migrate-schema`**). Sans
217
+ migrations versionnées, le démarrage en développement rattrape seul une colonne ajoutée qui
218
+ **accepte le vide** (`ADD COLUMN`, annoncé) ; une colonne **obligatoire** ne l'est jamais — le
219
+ démarrage nomme l'écart au lieu d'inventer la valeur des lignes existantes.
201
220
  3. **La production ne fabrique JAMAIS le schéma.** Elle l'applique par des migrations, écrites par
202
221
  `npx nodefony orm:generate` et posées par `npx nodefony orm:migrate` — c'est un geste à part,
203
222
  avec ses refus et ses interdits : skill **`nodefony-migrate-schema`**.
@@ -214,7 +233,7 @@ geste :
214
233
  - **nom réservé par un module du framework** (`session`, `access_token`, `audit_event`…) — un
215
234
  homonyme dépossède le module, et l'application ne démarre plus sur un message parlant d'une
216
235
  colonne inconnue. **`User` fait exception** : l'identité appartient à l'application, et
217
- `create entity User firstName:string(100)?` écrit l'entité avec les colonnes du contrat plus
236
+ `create entity User "firstName:string(100)?"` écrit l'entité avec les colonnes du contrat plus
218
237
  les tiennes. Trois refus s'y appliquent alors — renommer la table ou changer la casse des
219
238
  colonnes (des requêtes les écrivent en dur), la poser ailleurs que dans l'application racine
220
239
  (l'ordre de chargement n'y est pas garanti), et déclarer un champ obligatoire **sans valeur par