ddd-ts-kit 0.15.0 → 0.16.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 (144) hide show
  1. package/README.fr.md +1594 -0
  2. package/README.md +426 -6
  3. package/dist/check/catalogue/architecture-rules.d.ts.map +1 -1
  4. package/dist/check/catalogue/architecture-rules.js +2 -0
  5. package/dist/check/catalogue/architecture-rules.js.map +1 -1
  6. package/dist/check/explain/domain-docs.d.ts +3 -0
  7. package/dist/check/explain/domain-docs.d.ts.map +1 -0
  8. package/dist/check/explain/domain-docs.js +30 -0
  9. package/dist/check/explain/domain-docs.js.map +1 -0
  10. package/dist/check/explain/rule-docs.d.ts.map +1 -1
  11. package/dist/check/explain/rule-docs.js +2 -0
  12. package/dist/check/explain/rule-docs.js.map +1 -1
  13. package/dist/check/fixtures/fixtures.d.ts.map +1 -1
  14. package/dist/check/fixtures/fixtures.js +4 -0
  15. package/dist/check/fixtures/fixtures.js.map +1 -1
  16. package/dist/check/fixtures/model-function-fixtures.d.ts +4 -0
  17. package/dist/check/fixtures/model-function-fixtures.d.ts.map +1 -0
  18. package/dist/check/fixtures/model-function-fixtures.js +52 -0
  19. package/dist/check/fixtures/model-function-fixtures.js.map +1 -0
  20. package/dist/check/fixtures/value-object-file-fixtures.d.ts +4 -0
  21. package/dist/check/fixtures/value-object-file-fixtures.d.ts.map +1 -0
  22. package/dist/check/fixtures/value-object-file-fixtures.js +54 -0
  23. package/dist/check/fixtures/value-object-file-fixtures.js.map +1 -0
  24. package/dist/check/fixtures/value-object-files.d.ts +34 -0
  25. package/dist/check/fixtures/value-object-files.d.ts.map +1 -0
  26. package/dist/check/fixtures/value-object-files.js +46 -0
  27. package/dist/check/fixtures/value-object-files.js.map +1 -0
  28. package/dist/check/rules/domain-value-object-class.d.ts +3 -0
  29. package/dist/check/rules/domain-value-object-class.d.ts.map +1 -0
  30. package/dist/check/rules/domain-value-object-class.js +21 -0
  31. package/dist/check/rules/domain-value-object-class.js.map +1 -0
  32. package/dist/check/structure/class-body-brace.d.ts +6 -0
  33. package/dist/check/structure/class-body-brace.d.ts.map +1 -0
  34. package/dist/check/structure/class-body-brace.js +22 -0
  35. package/dist/check/structure/class-body-brace.js.map +1 -0
  36. package/dist/check/structure/read-structure.d.ts.map +1 -1
  37. package/dist/check/structure/read-structure.js +3 -1
  38. package/dist/check/structure/read-structure.js.map +1 -1
  39. package/dist/check/values/answered-literals.d.ts +8 -0
  40. package/dist/check/values/answered-literals.d.ts.map +1 -0
  41. package/dist/check/values/answered-literals.js +20 -0
  42. package/dist/check/values/answered-literals.js.map +1 -0
  43. package/dist/check/values/arrow-body.d.ts +4 -0
  44. package/dist/check/values/arrow-body.d.ts.map +1 -0
  45. package/dist/check/values/arrow-body.js +52 -0
  46. package/dist/check/values/arrow-body.js.map +1 -0
  47. package/dist/check/values/entry-role.d.ts +10 -0
  48. package/dist/check/values/entry-role.d.ts.map +1 -0
  49. package/dist/check/values/entry-role.js +40 -0
  50. package/dist/check/values/entry-role.js.map +1 -0
  51. package/dist/check/values/exported-function-body.d.ts +9 -0
  52. package/dist/check/values/exported-function-body.d.ts.map +1 -0
  53. package/dist/check/values/exported-function-body.js +29 -0
  54. package/dist/check/values/exported-function-body.js.map +1 -0
  55. package/dist/check/values/exported-values.d.ts +9 -0
  56. package/dist/check/values/exported-values.d.ts.map +1 -0
  57. package/dist/check/values/exported-values.js +48 -0
  58. package/dist/check/values/exported-values.js.map +1 -0
  59. package/dist/check/values/extends-value-object.d.ts +9 -0
  60. package/dist/check/values/extends-value-object.d.ts.map +1 -0
  61. package/dist/check/values/extends-value-object.js +25 -0
  62. package/dist/check/values/extends-value-object.js.map +1 -0
  63. package/dist/check/values/function-body.d.ts +10 -0
  64. package/dist/check/values/function-body.d.ts.map +1 -0
  65. package/dist/check/values/function-body.js +2 -0
  66. package/dist/check/values/function-body.js.map +1 -0
  67. package/dist/check/values/generate-hint.d.ts +4 -0
  68. package/dist/check/values/generate-hint.d.ts.map +1 -0
  69. package/dist/check/values/generate-hint.js +8 -0
  70. package/dist/check/values/generate-hint.js.map +1 -0
  71. package/dist/check/values/initializer-of.d.ts +3 -0
  72. package/dist/check/values/initializer-of.d.ts.map +1 -0
  73. package/dist/check/values/initializer-of.js +15 -0
  74. package/dist/check/values/initializer-of.js.map +1 -0
  75. package/dist/check/values/is-model-file.d.ts +8 -0
  76. package/dist/check/values/is-model-file.d.ts.map +1 -0
  77. package/dist/check/values/is-model-file.js +15 -0
  78. package/dist/check/values/is-model-file.js.map +1 -0
  79. package/dist/check/values/is-value-object-file.d.ts +7 -0
  80. package/dist/check/values/is-value-object-file.d.ts.map +1 -0
  81. package/dist/check/values/is-value-object-file.js +14 -0
  82. package/dist/check/values/is-value-object-file.js.map +1 -0
  83. package/dist/check/values/javascript-globals.d.ts +3 -0
  84. package/dist/check/values/javascript-globals.d.ts.map +1 -0
  85. package/dist/check/values/javascript-globals.js +22 -0
  86. package/dist/check/values/javascript-globals.js.map +1 -0
  87. package/dist/check/values/literal-at.d.ts +3 -0
  88. package/dist/check/values/literal-at.d.ts.map +1 -0
  89. package/dist/check/values/literal-at.js +7 -0
  90. package/dist/check/values/literal-at.js.map +1 -0
  91. package/dist/check/values/local-declaration.d.ts +8 -0
  92. package/dist/check/values/local-declaration.d.ts.map +1 -0
  93. package/dist/check/values/local-declaration.js +31 -0
  94. package/dist/check/values/local-declaration.js.map +1 -0
  95. package/dist/check/values/nested-bodies.d.ts +8 -0
  96. package/dist/check/values/nested-bodies.d.ts.map +1 -0
  97. package/dist/check/values/nested-bodies.js +28 -0
  98. package/dist/check/values/nested-bodies.js.map +1 -0
  99. package/dist/check/values/plain-value-functions.d.ts +10 -0
  100. package/dist/check/values/plain-value-functions.d.ts.map +1 -0
  101. package/dist/check/values/plain-value-functions.js +33 -0
  102. package/dist/check/values/plain-value-functions.js.map +1 -0
  103. package/dist/check/values/returns-package-type.d.ts +9 -0
  104. package/dist/check/values/returns-package-type.d.ts.map +1 -0
  105. package/dist/check/values/returns-package-type.js +12 -0
  106. package/dist/check/values/returns-package-type.js.map +1 -0
  107. package/dist/check/values/stands-for-a-value.d.ts +9 -0
  108. package/dist/check/values/stands-for-a-value.d.ts.map +1 -0
  109. package/dist/check/values/stands-for-a-value.js +28 -0
  110. package/dist/check/values/stands-for-a-value.js.map +1 -0
  111. package/dist/check/values/value-object-file-findings.d.ts +5 -0
  112. package/dist/check/values/value-object-file-findings.d.ts.map +1 -0
  113. package/dist/check/values/value-object-file-findings.js +96 -0
  114. package/dist/check/values/value-object-file-findings.js.map +1 -0
  115. package/dist/commands/init.d.ts.map +1 -1
  116. package/dist/commands/init.js +1 -0
  117. package/dist/commands/init.js.map +1 -1
  118. package/dist/config/index.d.ts +1 -1
  119. package/dist/config/index.d.ts.map +1 -1
  120. package/dist/config/index.js +1 -1
  121. package/dist/config/index.js.map +1 -1
  122. package/dist/config/parse-stack.d.ts.map +1 -1
  123. package/dist/config/parse-stack.js +2 -1
  124. package/dist/config/parse-stack.js.map +1 -1
  125. package/dist/config/stack.d.ts +8 -0
  126. package/dist/config/stack.d.ts.map +1 -1
  127. package/dist/config/stack.js +1 -0
  128. package/dist/config/stack.js.map +1 -1
  129. package/dist/help/usage.d.ts +1 -1
  130. package/dist/help/usage.d.ts.map +1 -1
  131. package/dist/help/usage.js +1 -1
  132. package/dist/init/manifest/versions.js +24 -24
  133. package/dist/init/stack/resolve-stack.d.ts.map +1 -1
  134. package/dist/init/stack/resolve-stack.js +3 -1
  135. package/dist/init/stack/resolve-stack.js.map +1 -1
  136. package/dist/init/stack/stack-options.d.ts +2 -0
  137. package/dist/init/stack/stack-options.d.ts.map +1 -1
  138. package/dist/stack/stack-vars.js +2 -1
  139. package/dist/stack/stack-vars.js.map +1 -1
  140. package/dist/version.d.ts +1 -1
  141. package/dist/version.js +1 -1
  142. package/package.json +2 -2
  143. package/templates/init/rules/domain.md.tpl +1 -1
  144. package/templates/pipeline/package-scripts.json +1 -1
package/README.fr.md ADDED
@@ -0,0 +1,1594 @@
1
+ [English](README.md) · **Français**
2
+ <!-- translation-of: README.md sha256:eae59eb228d7b41f -->
3
+
4
+ # ddd-ts-kit
5
+
6
+ Amorcez, générez, documentez et vérifiez des projets en Domain-Driven Design et en Clean
7
+ Architecture, en TypeScript. Un seul binaire : `init` crée le projet, `generate` écrit chaque
8
+ morceau de code répétitif à sa place canonique, `docs` tient à jour la documentation
9
+ d'architecture, `describe`, `status` et `todo` relisent le projet sous forme de faits, `check`
10
+ prouve que le code suit toujours [docs/ARCHITECTURE.fr.md](../../docs/ARCHITECTURE.fr.md).
11
+ `init` installe aussi le *pipeline* de qualité : ESLint avec `@ddd-ts-kit/eslint-plugin`,
12
+ dependency-cruiser, knip, des seuils de couverture, des *hooks* git et la CI de GitHub ou de GitLab.
13
+
14
+ Le générateur et le *checker* partagent une seule définition de l'arbre canonique, et un test
15
+ génère chaque genre de pièce pour chaque *stack* en exigeant zéro violation : ils ne peuvent pas
16
+ être en désaccord.
17
+
18
+ Les principales commandes se rangent en deux familles. La première écrit le projet : `init` une fois,
19
+ `generate` pour chaque pièce, `docs`, `sdk` et `depcruise-config` pour ce qui dérive du code et de
20
+ la configuration, et trois migrations qui font passer un projet d'une forme ancienne à la forme
21
+ actuelle. La seconde relit le projet sans le modifier : `check` le juge, `list` et `describe`
22
+ disent ce qu'il contient, `status` et `todo` disent ce qui reste à faire, `affected` dit ce
23
+ qu'une *pull request* doit exécuter.
24
+
25
+ ![Les commandes de la CLI autour d'un projet : à gauche celles qui l'écrivent (init, generate, docs, sdk et depcruise-config, les trois migrations), au milieu le projet (ddd-ts-kit.config.json, les contextes délimités et la plateforme sous src/, les documents générés), à droite celles qui le relisent (check et check-file, list et describe, status et todo, affected)](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/cli-commands-and-project.fr.svg)
26
+
27
+ Aucune dépendance d'exécution. Node 22.12 ou ultérieur.
28
+
29
+ ```
30
+ npx ddd-ts-kit init
31
+ npx ddd-ts-kit generate context billing --classification core --term "Invoice=A request for payment"
32
+ npx ddd-ts-kit generate aggregate billing Invoice
33
+ npx ddd-ts-kit generate command billing IssueInvoice --aggregate Invoice --permission invoice.issue --term "Issue=To make an invoice final"
34
+ npx ddd-ts-kit generate route billing IssueInvoice --method POST --path /invoices
35
+ npx ddd-ts-kit check
36
+ ```
37
+
38
+ `init` se termine en disant quoi lancer ensuite : `pnpm install`, puis `pnpm run verify`
39
+ (`npm install`, puis `npm run verify` avec `--package-manager npm`), puis les étapes recommandées
40
+ de la méthode, dans l'ordre ([méthodologie](../../docs/guides/methodology.fr.md)) ;
41
+ [quelle commande, quand](../../docs/guides/commands.fr.md) donne la commande de chaque moment d'un
42
+ projet et la référence de chaque option. Un nouveau projet passe toute cette porte avant que quoi
43
+ que ce soit soit généré, sans contexte, avec un et avec plusieurs : la suite de bout en bout
44
+ l'installe pour de vrai et l'exécute, avec les deux gestionnaires de paquets et sur chaque *stack*
45
+ éprouvée. Après l'installation, la CLI est celle que le projet épingle :
46
+ `pnpm exec ddd-ts-kit <command>`, ou `npx ddd-ts-kit <command>`.
47
+
48
+ Chaque commande accepte `--json` (sortie lisible par une machine, erreurs comprises) et
49
+ `--cwd <dir>`. Les commandes autres que `init` cherchent `ddd-ts-kit.config.json` dans le
50
+ répertoire et ses parents. Codes de sortie : `0` succès, `1` violations, documentation périmée
51
+ ou, sous `--strict`, un *commit* au-delà de ses limites, `2` mauvais usage (le message dit quoi
52
+ faire).
53
+
54
+ ## init
55
+
56
+ ```
57
+ ddd-ts-kit init [--http express|fastify|hono|nest|none]
58
+ [--validation zod|class-validator|typebox|standard-schema|none]
59
+ [--di tsyringe|nest|manual] [--tests vitest|jest]
60
+ [--orm drizzle|knex|prisma|typeorm|custom|none]
61
+ [--database postgres|mysql|sqlite|none]
62
+ [--events in-memory|outbox] [--docs-ui swagger|scalar|redoc|none]
63
+ [--ci github|gitlab|both|none] [--ci-extras <extra>,...|none]
64
+ [--authn jwt|keycloak|none] [--authz rbac|abac|rebac|custom]
65
+ [--broker none|rabbitmq|kafka|nats|redis|custom] [--cache none|redis]
66
+ [--http-cache none|varnish] [--storage local|s3|none] [--audit on|off]
67
+ [--errors none|sentry] [--logger stdout|pino|winston]
68
+ [--style declared|explicit] [--assembly barrel|discovery]
69
+ [--dependencies signature|listed] [--refusals factories|classes]
70
+ [--routes controllers|functions] [--value-objects throwing|result]
71
+ [--literals named|free] [--values classes|free] [--tenancy none|shared-table]
72
+ [--queue database|redis|memory]
73
+ [--schemas shared|per-route]
74
+ [--package-manager npm|pnpm]
75
+ [--allow-no-outbox "<reason>"] [--import-alias '$<name>/']
76
+ [--import-extension none|js] [--sdk]
77
+ [--context <name>:<core|supporting|generic>]...
78
+ [--preset identity]...
79
+ [--dry-run] [--force]
80
+ ```
81
+
82
+ Fonctionne dans un répertoire vide comme dans un projet existant. Un fichier existant n'est
83
+ jamais remplacé sans `--force` ; il est signalé comme ignoré. `init` écrit aussi les contrats
84
+ générés tels que les scripts du projet les écrivent pour un projet qui n'a encore ni route ni
85
+ événement d'intégration à lui (`openapi/openapi.yaml`, `docs/integration/asyncapi.yaml` et
86
+ `event-catalog.md` ; en JSON sous `"openapi": { "format": "json" }` de la configuration), afin
87
+ que le premier *commit* les contienne et que `docs:check` passe d'emblée ; un contrat déjà
88
+ présent est laissé tel quel, `--force` compris. Un `package.json` existant est fusionné : les
89
+ scripts et les dépendances qui manquent sont ajoutés, les vôtres sont gardés et mentionnés dans
90
+ les notes. Relancé plus tard, `init` garde la *stack* enregistrée dans la configuration.
91
+
92
+ `init --force` sur un projet déjà initialisé réécrit les fichiers qui appartiennent à `init` et
93
+ garde la configuration : les contextes et leurs amonts, les règles, les limites, les dérogations
94
+ et toute autre clé restent tels qu'ils sont écrits, et la *stack* ne change que là où une option
95
+ nomme un choix (`init --force --logger pino` change le *logger* ; une combinaison que la *stack*
96
+ enregistrée ne peut pas accepter est refusée comme pour un nouveau projet). La racine de
97
+ composition, la preuve que chaque contexte se câble seul et le registre des permissions sont
98
+ réécrits depuis leurs *templates*, puis les contextes déclarés y sont câblés de nouveau ; ce
99
+ qu'un autre générateur avait ajouté à un fichier de la plateforme (la valeur d'une *gateway*, les
100
+ réglages d'un *job*) ne l'est pas, et une note le dit. Une configuration illisible est remplacée.
101
+
102
+ Le *pipeline* n'utilise que des outils gratuits. `--ci-extras` ajoute des *jobs* qui demandent
103
+ une offre payante sur un dépôt privé, ou qui ne remontent dans une *merge request* qu'avec une
104
+ offre payante : `codeql`, `dependency-review` (GitHub), `gitlab-sast`, `gitlab-secret-detection`,
105
+ `gitlab-dependency-scanning` (GitLab). La liste est enregistrée comme `ci.extras` dans la
106
+ configuration ; un nom inconnu, ou un *job* d'une plateforme que `--ci` ne couvre pas, est
107
+ refusé. Le tableau de chaque outil et de ses conditions :
108
+ [pipeline/README.md](../../pipeline/README.md#free-by-default-paid-on-request).
109
+
110
+ Un import d'un fichier du projet s'écrit sans extension
111
+ (`@/contexts/billing/domain/aggregates/invoice.aggregate`) : `init` enregistre
112
+ `"importExtension": "none"`, et `--import-extension js` demande à la place le `.js` du fichier
113
+ émis. Une configuration sans cette clé se lit comme `js`, si bien qu'un projet généré avant
114
+ l'existence de la clé garde son écriture. Voir [docs/CONFIG.fr.md](../../docs/CONFIG.fr.md).
115
+
116
+ Valeurs par défaut : `express`, `zod`, `tsyringe` (`nest` avec `--http nest`, et seulement
117
+ alors), `vitest`, `drizzle` sur `postgres`, `outbox` quand il y a un ORM, `swagger`, `github`,
118
+ `jwt`, `rbac`, aucun *broker*, le cache dans le processus, les fichiers dans un répertoire local
119
+ (`--storage local`), le journal d'audit `on` quand il y a un ORM, les échecs inattendus remontés
120
+ nulle part (`--errors none`), des lignes JSON sur la sortie standard (`--logger stdout`), et le
121
+ style `declared` avec un `barrel` partout où les déclarations sont lues : avec `--di tsyringe`.
122
+
123
+ **Gestionnaire de paquets.** `pnpm` sauf `--package-manager npm` : chaque script, *pipeline*,
124
+ *hook*, le Dockerfile, les consignes et les permissions des agents sont écrits dans le dialecte
125
+ de celui qui est choisi, enregistré sous `stack.packageManager`. pnpm est épinglé dans
126
+ `package.json` et fourni par corepack ; `pnpm-workspace.yaml` dit qu'aucune dépendance n'exécute
127
+ de script de build sans être autorisée par son nom. Les deux sont prouvés par une installation
128
+ réelle d'un projet généré, son build et son `verify`. Le kit est installé depuis le registre dans
129
+ les deux cas.
130
+
131
+ **Pipelines.** Une *pull request* ou une *merge request* exécute les *specs* unitaires, les
132
+ *specs* d'intégration et le lint des contextes que son changement atteint par la carte des
133
+ contextes ([`ddd-ts-kit affected`](#affected)) ; les types, l'architecture, le graphe des
134
+ imports, le code mort, le bout en bout, le build et les documents tournent toujours sur le projet
135
+ entier, et la branche par défaut et l'exécution nocturne planifiée exécutent tout.
136
+
137
+ **Deux styles.** Dans le style `declared`, chaque classe dit ce qu'elle est par un décorateur de
138
+ `@ddd-ts-kit/kernel/declarations` (`@Requires` ou `@Public` sur une commande ou une requête,
139
+ `@CommandHandler`, `@QueryHandler`, `@IntegrationEventHandler`, `@DomainEventHandler`, `@Scheduled`,
140
+ `@Policy`, `@Repository`, `@Gateway`, `@Adapter`), un contexte n'a pas de `<context>.module.ts`, et
141
+ la racine de composition l'assemble en une ligne. Dans le style `explicit`, chaque liaison est
142
+ écrite dans le fichier de module du contexte : `builder.bind(Port).toClass(Adapter, [Ports])`,
143
+ `builder.command(Message).requires(...).handledBy(Handler, [Ports])`, `builder.job(name, schedule)`.
144
+ `--style declared` est refusé avec `--di manual` et `--di nest` : les déclarations sont de simples
145
+ métadonnées, et seul `@ddd-ts-kit/di` (sur tsyringe) les transforme en liaisons.
146
+ `--assembly` dit comment un contexte déclaré rejoint la racine de composition : `barrel` (par
147
+ défaut) est un fichier généré par contexte, `<context>.declarations.ts`, statique, que suivent un
148
+ bundler, knip et dependency-cruiser ; `discovery` écrit à la place `await discoverContext(...)`,
149
+ avec l'entrée knip et l'exception dependency-cruiser dont les fichiers découverts ont besoin
150
+ (*publishers* et *jobs* compris : ils sont découverts comme le reste), et intègre
151
+ `@ddd-ts-kit/di` en ligne (inline) dans les deux configurations vitest, afin que les fichiers
152
+ qu'il importe lui-même soient compilés par l'exécuteur des tests. La découverte est acceptée
153
+ derrière un *broker* : un contexte écoute sur l'abonné que `EventSubscribers.forContext(name)`
154
+ lui remet, dans l'un ou l'autre assemblage. Une configuration écrite avant l'existence de
155
+ `stack.style` est explicite.
156
+
157
+ Le même cas d'usage dans les deux styles. Dans le style déclaré, la commande et son gestionnaire
158
+ (handler) portent leur décorateur, et le constructeur du gestionnaire dit ce dont il a besoin. Le
159
+ corps du gestionnaire est celui qu'écrit
160
+ `generate command billing IssueInvoice --aggregate Invoice --permission invoice.issue`, avant
161
+ que son `TODO(domain)` soit traité ; l'entrée de la commande est remplie :
162
+
163
+ <!-- checked sample -->
164
+ ```ts
165
+ import { definePermissions } from '@ddd-ts-kit/auth';
166
+ import { type Actor, AggregateRoot, AuthorizedCommand, type Clock, type IdGenerator, Repository } from '@ddd-ts-kit/kernel';
167
+ import { CommandHandler, Requires } from '@ddd-ts-kit/kernel/declarations';
168
+
169
+ declare class Invoice extends AggregateRoot {
170
+ static create(id: string, now: Date): Invoice;
171
+ }
172
+
173
+ // domain/repositories/invoice.repository.ts
174
+ export abstract class InvoiceRepository extends Repository<Invoice> {}
175
+
176
+ // application/authorization/billing.permissions.ts
177
+ export const billingPermissions = definePermissions('billing', {
178
+ 'invoice.issue': { description: 'Issue invoice' },
179
+ });
180
+
181
+ // application/commands/issue-invoice/issue-invoice.command.ts
182
+ @Requires(billingPermissions['invoice.issue'])
183
+ export class IssueInvoiceCommand extends AuthorizedCommand<string> {
184
+ constructor(
185
+ actor: Actor,
186
+ readonly customerId: string,
187
+ ) {
188
+ super(actor);
189
+ }
190
+ }
191
+
192
+ // application/commands/issue-invoice/issue-invoice.handler.ts
193
+ @CommandHandler(IssueInvoiceCommand)
194
+ export class IssueInvoiceHandler implements CommandHandler<IssueInvoiceCommand> {
195
+ // chaque paramètre est typé par une classe, qui est aussi son jeton d'injection
196
+ constructor(
197
+ private readonly invoiceRepository: InvoiceRepository,
198
+ private readonly ids: IdGenerator,
199
+ private readonly clock: Clock,
200
+ ) {}
201
+
202
+ async execute(command: IssueInvoiceCommand): Promise<string> {
203
+ const target = Invoice.create(this.ids.next(), this.clock.now());
204
+
205
+ await this.invoiceRepository.save(target);
206
+
207
+ return target.id;
208
+ }
209
+ }
210
+ ```
211
+
212
+ Dans le style explicite, les mêmes classes ne portent aucun décorateur, et le fichier de module
213
+ du contexte lie chacune d'elles, avec sa permission et les ports qu'elle reçoit :
214
+
215
+ <!-- checked sample -->
216
+ ```ts
217
+ import { definePermissions } from '@ddd-ts-kit/auth';
218
+ import { defineContextModule } from '@ddd-ts-kit/di';
219
+ import { AggregateRoot, AuthorizedCommand, Clock, type CommandHandler, IdGenerator, Repository } from '@ddd-ts-kit/kernel';
220
+
221
+ // les classes de l'exemple déclaré, sans leurs décorateurs
222
+ declare class Invoice extends AggregateRoot {}
223
+ declare class IssueInvoiceCommand extends AuthorizedCommand<string> {}
224
+ declare class IssueInvoiceHandler implements CommandHandler<IssueInvoiceCommand> {
225
+ constructor(invoiceRepository: InvoiceRepository, ids: IdGenerator, clock: Clock);
226
+ execute(command: IssueInvoiceCommand): Promise<string>;
227
+ }
228
+ declare class InMemoryInvoiceRepository implements InvoiceRepository {
229
+ findById(id: string): Promise<Invoice | undefined>;
230
+ save(invoice: Invoice): Promise<void>;
231
+ }
232
+
233
+ export abstract class InvoiceRepository extends Repository<Invoice> {}
234
+
235
+ export const billingPermissions = definePermissions('billing', {
236
+ 'invoice.issue': { description: 'Issue invoice' },
237
+ });
238
+
239
+ // billing.module.ts : chaque liaison du contexte, une ligne chacune
240
+ export const billingModule = defineContextModule({
241
+ name: 'billing',
242
+ register(builder) {
243
+ builder.bind(InvoiceRepository).toClass(InMemoryInvoiceRepository).as('repository');
244
+ builder
245
+ .command(IssueInvoiceCommand)
246
+ .requires(billingPermissions['invoice.issue'])
247
+ .handledBy(IssueInvoiceHandler, [InvoiceRepository, IdGenerator, Clock]);
248
+ },
249
+ });
250
+ ```
251
+
252
+ `check` tient les deux formes : `authz/declared` signale une commande qui ne dit ni qui peut
253
+ l'envoyer ni qu'elle est publique, `declarations/role-declared` un fichier de rôle du style
254
+ déclaré sans son décorateur.
255
+
256
+ **Un port est une classe abstraite, et son propre jeton d'injection.**
257
+ `application/ports/<name>.port.ts` et `domain/repositories/<name>.repository.ts` contiennent
258
+ chacun une classe abstraite. Le code généré ne contient aucun `token()`, aucune constante
259
+ d'injection en UPPER_SNAKE, aucun `<context>.ports.ts` et aucun `platform/tokens.ts` : un
260
+ paramètre de constructeur typé par une classe de port nomme ce qui est injecté
261
+ (`private readonly clock: Clock`), les ports du kernel sont importés comme valeurs partout où ils
262
+ sont nommés dans une liste ou une liaison, et comme types là où ils ne font que typer un
263
+ paramètre, un adaptateur `implements` son port.
264
+
265
+ Au lieu d'une interface, d'une constante de jeton et d'une injection par nom :
266
+
267
+ ```ts
268
+ export interface IPaymentGateway {
269
+ charge(invoiceId: string, amountInCents: number): Promise<void>;
270
+ }
271
+
272
+ export const PAYMENT_GATEWAY = Symbol('PaymentGateway'); // ports/no-token-constant
273
+
274
+ constructor(@inject(PAYMENT_GATEWAY) private readonly payments: IPaymentGateway) {}
275
+ ```
276
+
277
+ écrivez une seule classe abstraite, qui est à la fois le contrat et le jeton :
278
+
279
+ <!-- checked sample -->
280
+ ```ts
281
+ import { Adapter } from '@ddd-ts-kit/kernel/declarations';
282
+
283
+ // application/ports/payment-gateway.port.ts
284
+ export abstract class PaymentGateway {
285
+ abstract charge(invoiceId: string, amountInCents: number): Promise<void>;
286
+ }
287
+
288
+ // infrastructure/external/remote-payment-gateway.adapter.ts : implémente le port, ne l'étend jamais
289
+ @Adapter(PaymentGateway, { kind: 'external' })
290
+ export class RemotePaymentGateway implements PaymentGateway {
291
+ async charge(invoiceId: string, amountInCents: number): Promise<void> {
292
+ // TODO(platform): appeler le prestataire de paiement avec invoiceId et amountInCents
293
+ }
294
+ }
295
+
296
+ // un gestionnaire nomme le port par le type de son paramètre : rien d'autre ne dit ce qui est injecté
297
+ export class SettleInvoiceHandler {
298
+ constructor(private readonly payments: PaymentGateway) {}
299
+
300
+ async settle(invoiceId: string, amountInCents: number): Promise<void> {
301
+ await this.payments.charge(invoiceId, amountInCents);
302
+ }
303
+ }
304
+ ```
305
+
306
+ `ports/abstract-class`, `ports/implements-never-extends`, `ports/no-token-constant` et
307
+ `ports/value-import` tiennent cette forme, dans `check` et dans ESLint.
308
+
309
+ **Le constructeur énonce les dépendances.** Dans le style déclaré, une classe générée ne liste
310
+ rien dans son décorateur (`@CommandHandler(IssueInvoiceCommand)`) : chaque paramètre du
311
+ constructeur est typé par une classe, son jeton (`declarations/constructor-injection`).
312
+ `generate`, `docs` et le script `dev` lisent les signatures, avec le scanner sans dépendance du
313
+ *checker* et sans compilateur, et écrivent des instructions `injected(Class, [Tokens])` dans le
314
+ fichier généré de chaque contexte : le barrel `<context>.declarations.ts`, ou
315
+ `<context>.dependencies.ts` sous discovery. `@ddd-ts-kit/di` injecte à partir de là. Rien ne
316
+ repose sur `emitDecoratorMetadata` : il est absent avec les décorateurs standard et sous `tsx`.
317
+ `init` écrit `"stack.dependencies": "signature"` ; une configuration sans la clé vaut `listed`,
318
+ la forme de 0.1 (`@CommandHandler(M, [Ports])`), qui continue de fonctionner sans changement, et
319
+ `--dependencies listed` la demande. Le style explicite reste explicite : son fichier de module
320
+ liste ce que reçoit chaque classe. Chaque fichier généré exporte une classe, une interface ou une
321
+ fonction, et porte son nom.
322
+
323
+ ![Comment un contexte déclaré rejoint la racine de composition : les fichiers de rôle portent leur décorateur et typent les paramètres de leur constructeur par des classes ; generate, docs et le script dev lisent ces signatures et écrivent le barrel, un export par fichier de rôle et une instruction injected par classe qui reçoit quelque chose ; la racine de composition inclut le barrel avec contextModule et @ddd-ts-kit/di injecte à partir de lui ; check et docs --check signalent un barrel qui ne correspond plus aux fichiers de rôle](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/cli-declared-barrel.fr.svg)
324
+
325
+ Un extrait du barrel du contexte `notification` de l'application d'exemple, tel que la CLI
326
+ l'écrit (jamais à la main) :
327
+
328
+ ```ts
329
+ // src/contexts/notification/notification.declarations.ts
330
+ import { injected } from '@ddd-ts-kit/kernel/declarations';
331
+ import { WithdrawReminderHandler } from '@/contexts/notification/application/commands/withdraw-reminder/withdraw-reminder.handler';
332
+ import { ReminderRepository } from '@/contexts/notification/domain/repositories/reminder.repository';
333
+ import { KnexReminderRepository } from '@/contexts/notification/infrastructure/persistence/knex-reminder.repository';
334
+ import { Clock, ExecutionScope } from '@ddd-ts-kit/kernel';
335
+ import { KnexContext } from '@ddd-ts-kit/knex';
336
+
337
+ export * from '@/contexts/notification/application/commands/withdraw-reminder/withdraw-reminder.command';
338
+ export * from '@/contexts/notification/application/commands/withdraw-reminder/withdraw-reminder.handler';
339
+ export * from '@/contexts/notification/infrastructure/persistence/knex-reminder.repository';
340
+
341
+ injected(WithdrawReminderHandler, [ReminderRepository]);
342
+ injected(KnexReminderRepository, [KnexContext, ExecutionScope, Clock]);
343
+ ```
344
+
345
+ et l'unique ligne de la racine de composition qui ajoute tout le contexte :
346
+ `contextModule('notification', notificationDeclarations)` dans `src/platform/compose-application.ts`.
347
+
348
+ **Les erreurs du domaine sont construites par des fabriques (factories).** `init` écrit
349
+ `"stack.refusals": "factories"` : un contexte a une classe d'erreur par concept, qui étend
350
+ `ConceptError` du kernel, et `generate error <context> <Concept> <refusal>` y ajoute une fabrique
351
+ statique. Les règles `errors/*` tiennent cette forme. `--refusals classes`, ou une configuration
352
+ sans la clé, garde une classe par erreur construite avec `new`, comme 0.1 et 0.2 les écrivaient ;
353
+ `error-factories` fait passer un projet de l'une à l'autre.
354
+
355
+ L'ancienne forme, sous `"stack.refusals": "classes"` : une classe par erreur.
356
+
357
+ <!-- checked sample -->
358
+ ```ts
359
+ import { DomainError } from '@ddd-ts-kit/kernel';
360
+
361
+ // domain/errors/slot-taken.error.ts
362
+ export class SlotTakenError extends DomainError {
363
+ readonly code = 'SLOT_TAKEN';
364
+
365
+ constructor(readonly calendarId: string) {
366
+ super(`The slot of calendar ${calendarId} is already held.`);
367
+ }
368
+ }
369
+
370
+ const refusal = new SlotTakenError('calendar-1');
371
+ ```
372
+
373
+ La forme qu'écrit `init` : une classe par concept du glossaire, une fabrique par refus, que
374
+ `generate error scheduling Appointment slotTaken` ajoute.
375
+
376
+ <!-- checked sample -->
377
+ ```ts
378
+ import { ConceptError } from '@ddd-ts-kit/kernel';
379
+
380
+ // domain/errors/appointment.error.ts
381
+ export class AppointmentError extends ConceptError {
382
+ static slotTaken(calendarId: string) {
383
+ return AppointmentError.refusal('SLOT_TAKEN', `The slot of calendar ${calendarId} is already held.`, { calendarId });
384
+ }
385
+ }
386
+
387
+ const refusal = AppointmentError.slotTaken('calendar-1');
388
+
389
+ // ce qui l'attrape reconnaît le refus à sa classe et à son code, et lit ses faits
390
+ if (AppointmentError.is(refusal, 'SLOT_TAKEN')) {
391
+ console.log(refusal.facts.calendarId);
392
+ }
393
+ ```
394
+
395
+ **Une route est une classe contrôleur.** `init` écrit `"stack.routes": "controllers"` : chaque
396
+ route est une classe à une seule méthode, dans un dossier à elle, derrière les gardes de son
397
+ groupe de routes, et un contexte déclaré ne liste aucune route : ses contrôleurs rejoignent
398
+ l'application par son fichier généré. `--routes functions`, ou une configuration sans la clé,
399
+ garde une fonction `defineRoute` par route, listée dans `<context>.routes.ts`, comme 0.1 à 0.3
400
+ les écrivaient ; `controllers` fait passer un projet de l'une à l'autre, un contexte à la fois.
401
+
402
+ **Une forme se déclare une seule fois.** `init` écrit `"stack.schemas": "shared"` : une route de
403
+ commande répond son identifiant avec `acceptedSchema` de `@ddd-ts-kit/http`, un seul composant
404
+ `Accepted` dans le contrat, et `generate schema <context> <Name>` (ou
405
+ `schema shared-kernel <Name>`) écrit une forme qu'utilisent plusieurs routes, que le contrat
406
+ documente une fois et à laquelle chaque route renvoie. Les règles `schema/*` tiennent cette
407
+ forme. `--schemas per-route`, ou une configuration sans la clé, garde un schéma par route,
408
+ réponse d'identifiant comprise, comme 0.1 à 0.5 les écrivaient.
409
+
410
+ **Un objet-valeur lève ses refus.** `init` écrit `"stack.valueObjects": "throwing"` :
411
+ `generate value-object` écrit un `create` qui renvoie l'objet-valeur ou lève le refus de son
412
+ concept, et une *spec* qui le vérifie avec `expectRefusal(() => ..)`. `--value-objects result`,
413
+ ou une configuration sans la clé, garde un `create` qui renvoie un `Result` que l'appelant
414
+ déballe, comme 0.1 à 0.5 l'écrivaient ; `value-objects` fait passer un projet de l'un à l'autre.
415
+
416
+ **Les événements du domaine partent avec la sauvegarde.** Un gestionnaire de commande généré
417
+ construit ou charge son agrégat, appelle une méthode d'intention, `await repository.save(aggregate)`
418
+ et répond : pas de `pullEvents()`, et pas de `DomainEventDispatcher` dans un gestionnaire ni dans
419
+ un adaptateur de *repository*, qui ne fait que persister. Les événements que l'agrégat a
420
+ enregistrés sont distribués une fois la sauvegarde résolue, dans l'unité de travail de la
421
+ commande : par le module du kernel sur tsyringe, par `dispatchingOnSave` autour de chaque
422
+ *repository* dans `platform/create-platform.ts` en câblage manuel
423
+ (`domain-events/dispatched-on-save`).
424
+
425
+ Au lieu de tirer les événements et de les distribuer dans le gestionnaire :
426
+
427
+ ```ts
428
+ async execute(command: IssueInvoiceCommand): Promise<void> {
429
+ const invoice = await this.invoiceRepository.findById(command.invoiceId);
430
+
431
+ invoice.issue(this.clock.now());
432
+ await this.invoiceRepository.save(invoice);
433
+ await this.dispatcher.dispatch(invoice.pullEvents()); // domain-events/dispatched-on-save
434
+ }
435
+ ```
436
+
437
+ écrivez la sauvegarde, et rien après elle :
438
+
439
+ <!-- checked sample -->
440
+ ```ts
441
+ import { type Actor, AggregateRoot, AuthorizedCommand, type Clock, type CommandHandler, ConceptError, Repository } from '@ddd-ts-kit/kernel';
442
+
443
+ declare class Invoice extends AggregateRoot {
444
+ /** Rend la facture définitive et enregistre InvoiceIssued, qui attend dans l'agrégat. */
445
+ issue(now: Date): void;
446
+ }
447
+
448
+ declare class InvoiceError extends ConceptError {
449
+ static notFound(invoiceId: string): InvoiceError;
450
+ }
451
+
452
+ export abstract class InvoiceRepository extends Repository<Invoice> {}
453
+
454
+ export class IssueInvoiceCommand extends AuthorizedCommand<void> {
455
+ constructor(
456
+ actor: Actor,
457
+ readonly invoiceId: string,
458
+ ) {
459
+ super(actor);
460
+ }
461
+ }
462
+
463
+ export class IssueInvoiceHandler implements CommandHandler<IssueInvoiceCommand> {
464
+ constructor(
465
+ private readonly invoiceRepository: InvoiceRepository,
466
+ private readonly clock: Clock,
467
+ ) {}
468
+
469
+ async execute(command: IssueInvoiceCommand): Promise<void> {
470
+ const invoice = await this.invoiceRepository.findById(command.invoiceId);
471
+
472
+ if (invoice === undefined) {
473
+ throw InvoiceError.notFound(command.invoiceId);
474
+ }
475
+
476
+ invoice.issue(this.clock.now());
477
+
478
+ // InvoiceIssued est distribué une fois cette sauvegarde résolue, dans l'unité de travail de la commande
479
+ await this.invoiceRepository.save(invoice);
480
+ }
481
+ }
482
+ ```
483
+
484
+ `--events in-memory` avec un ORM est refusé sauf si `--allow-no-outbox "<reason>"` est donné :
485
+ il enregistre une entrée `deviations` (`stack.events`) et écrit l'enregistrement de décision vers
486
+ lequel elle pointe. La persistance générée des quatre ORM est écrite pour PostgreSQL ; `custom`
487
+ démarre sur des adaptateurs en mémoire tenus aux suites de contrat, à vous de les remplacer. Les
488
+ choix sont enregistrés sous `stack` dans la configuration, `docsUi`, `ci` et `logger` compris.
489
+
490
+ | Écrit | Contenu |
491
+ |---|---|
492
+ | `ddd-ts-kit.config.json` | *stack*, contextes, sévérités des règles ([docs/CONFIG.fr.md](../../docs/CONFIG.fr.md)) |
493
+ | `package.json` | scripts et dépendances de la *stack* (ci-dessous) ; `build` est le compilateur, puis `ddd-ts-kit build-aliases` |
494
+ | `tsconfig.json`, `tsconfig.build.json` | strict, NodeNext, `rootDir` `.` et `outDir` `dist`, et `paths` : l'unique déclaration de l'alias d'import (`@/*` vers `./src/*`, `$test/*`, `$scripts/*`, et `$generated/*` avec Prisma), dérivée de `importAlias` (`--import-alias '$<name>/'`, `@/` par défaut ; `#<name>/` et `@<name>/` sont refusés avec la raison) ; la configuration de build étend l'autre et ne déclare pas de `paths` ; `experimentalDecorators` (et `emitDecoratorMetadata`) seulement pour nest ou class-validator, dont les décorateurs sont de l'ancienne forme (legacy) : les déclarations et les aspects servent les deux modes |
495
+ | `src/main.ts` | racine de composition : le traçage d'abord (la remontée des erreurs en second avec `--errors sentry`), la configuration, la persistance, l'application (`log`, `traceConsumers: config.tracing.consumers`, `resilience`, `jobs: { start: config.runJobs, settings: jobSettings(process.env) }`), le relais de l'outbox, le serveur HTTP ; sur `SIGTERM` ou `SIGINT`, il ferme le serveur, arrête le relais, arrête l'application, ferme la persistance et éteint le traçage |
496
+ | `src/platform/` | un export par fichier, chacun portant son nom, dans un dossier par préoccupation. À sa racine : `tracing.ts`, `compose-application.ts` et `job-settings.ts` (`jobSettings(environment, context?)` : une ligne par *job*, `JOB_<CONTEXT>_<JOB>_ENABLED=false` en désactive un). `config/` (`app-config.ts`, `load-config.ts`, `config-error.ts`, `read-tracing.ts` : échec immédiat sur `DATABASE_URL`, `LOG_LEVEL`, `LOG_FORMAT`, `LOG_SCRUB_MESSAGES`, la politique `OUTBOUND_*`, `RUN_JOBS` et les variables de traçage ci-dessous) ; `observability/` (`create-logger.ts`, voir `--logger` ; `create-metrics.ts` : un `NoopMetrics` derrière un `TODO(platform)`) ; `resilience/` (`resilience-settings.ts`, `default-resilience.ts`, `adapter-aspects.ts` : nouvelle tentative avec temporisation croissante et délai d'expiration pour `external` et `gateway`, `own-policy.ts` pour un adaptateur qui énonce la sienne, `self-governed.ts`) ; `events/`, `database/`, `auth/`, `audit/`, `redis/` (ci-dessous) ; `http/` (`start-http-server.ts`, `with-documentation.ts` : `/docs` et `/openapi.json`, ou `/openapi/<name>.json` par document d'un contrat découpé, désactivés en production sauf `API_DOCS=true` ; `openapi-documents.ts`, le découpage qu'écrit `ddd-ts-kit docs` depuis `openapi.documents`). Avec `--di tsyringe` : `compose-application.ts` (`composeApplication(persistence, options, contexts?)` : `kernelModule({...})`, le module de la plateforme, le module d'autorisation, les contextes ; installe le moteur des aspects), `lay-foundations.ts` (une portée d'exécution, l'horloge, le générateur d'identifiants, le *logger*, les métriques, l'instrumentation et l'audit, construits avant tout module), `platform.module.ts` (ce qui remplace les valeurs par défaut du kernel dans le processus : unité de travail, magasin des événements traités, la classe de stockage, `OutboxStore`, `EventSubscribers` derrière un *broker*, `ErrorMapper`, et `builder.around(['external', 'gateway'], ...)`, la résilience de chaque adaptateur qui sort du processus), `platform-test.module.ts` (ce qu'une *spec* de gestionnaire liste à côté de son contexte : la classe de stockage liée à une fabrique qui lève une erreur), `buses/create-command-bus.ts` et `create-query-bus.ts` (du plus extérieur au plus intérieur : `errorReportingMiddleware`, `loggingMiddleware`, `metricsMiddleware`, la permission, le *span* du cas d'usage, l'entrée d'audit, puis l'unité de travail d'une commande). Avec `--di manual` ou `nest` : `compose-application.ts`, `platform.ts` (`interface Platform`, avec `jobs: JobScheduler` et `adapter`, qui enveloppe ce qu'un fichier de module instancie), `create-platform.ts`, `jobs/configured-job-scheduler.ts` (applique `job-settings.ts` à ce qu'un module enregistre) |
497
+ | traçage | câblé une fois, par la plateforme (`tracing/platform-only`) : pas de *hook* `decorate`, pas de `decorate-bindings.ts`. `tracing.ts` démarre `initTracing` de `@ddd-ts-kit/tracing/node` avant tout autre module, sur ce que valide `config/read-tracing.ts` : `OTEL_EXPORTER_OTLP_ENDPOINT` (une URL ; absente, aucun *span* ne sort du processus), `TRACE_SAMPLE_RATIO` (de 0 à 1) et `TRACE_CONSUMERS` (`continue`, par défaut : un événement consommé reste dans la trace de sa publication ; `link` : chaque livraison démarre une trace liée à elle). Avec `--di tsyringe` : `lay-foundations.ts` construit `traceApplication({ consumers })`, `compose-application.ts` la remet à `kernelModule({ instrumentation })` (un *span* par cas d'usage, appel de *repository*, événement du domaine, publication, consommateur et *job*) et donne au relais de l'outbox `tracedPublisher(bus)`. En câblage manuel : `create-platform.ts` applique lui-même les membres de `traceApplication()` (`useCase` sur les deux bus, `domainEvents`, `publisher`, `binding` autour de chaque adaptateur, `job`, `executionScope`, `errorReporter`), avec `tracedSubscriber` et `tracedPublisher` autour du fournisseur. Avec une *stack* HTTP, dans les deux familles, `auth/http-boundary.ts` pose `around: tracedRoutes()` : un *span* `SERVER` par requête HTTP, nommé d'après sa route |
498
+ | `--logger` | `src/platform/observability/create-logger.ts`, un par choix : `stdout` (par défaut) est le `StdoutLogger` de `@ddd-ts-kit/aspects`, des lignes JSON, identifiants de connexion non masqués ; `pino` est `createPinoLogger` de `@ddd-ts-kit/pino` (dépendances `pino` et `pino-pretty`) ; `winston` est `createWinstonLogger` de `@ddd-ts-kit/winston` ; ces deux-là masquent les clés d'identifiants de connexion du kernel. Chaque entrée porte `traceLogFields(scope)` : l'identifiant de corrélation, l'origine, l'acteur et le *tenant* du travail en cours, et les identifiants de la trace active. `LOG_LEVEL`, `LOG_FORMAT` (`json`, par défaut, ou `pretty` ; avec `stdout`, qui n'a pas de format lisible, `pretty` est refusé au chargement de la configuration) et `LOG_SCRUB_MESSAGES` (`[message removed]` à la place du message d'une erreur journalisée qui n'est pas une `DomainError`) sont validés dans `config/load-config.ts`. L'arrêt de l'application vide le tampon du *reporter* d'erreurs, puis celui du *logger*, en dernier : `container.stop()` le fait sur tsyringe, `compose-application.ts` le fait explicitement en câblage manuel. Les outils et les tests composent l'application sans `log` et n'écrivent rien |
499
+ | `src/platform/auth/` | `create-authenticator.ts` (`--authn jwt` : `JwtAuthenticator` sur `AUTH_ISSUER`, `AUTH_AUDIENCE`, `AUTH_JWKS_URI` ; `--authn keycloak` : `keycloakAuthenticator` sur `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_AUDIENCE` et l'optionnel `KEYCLOAK_CLIENT_ID` ; `--authn none` : `development-header-authenticator.ts`, qui lit `x-development-actor` et lève une erreur en production), `principal-of.ts`, `security-schemes.ts`, `offline-authenticator.ts`, `http-boundary.ts` (`resolvePrincipal` qui répond une `Session`, `executionScope`, `errorMapper` et `onError`, étalés dans les options de montage : chaque requête HTTP s'exécute dans la portée d'exécution, avec son identifiant de corrélation), `create-authorizer.ts` et les fichiers du modèle (`roles.ts`, avec le rôle `system` que détient chaque acteur système ; `attribute-policies.ts` ; `relationship-schema.ts`, `create-tuple-store.ts` et sa *spec* de contrat, `relation-tuples.ts`, `relate-system-actors.ts`, `permission-mapping.ts` et `permissions.ts` ; `custom-authorizer.ts`, sa *spec* `authorizerContract` et `permissions.ts`), `authorization.ts` (`authorizationModule` avec tsyringe) ou `use-case-protection.ts` (la garde des bus câblés à la main), `access-policies.ts`, et avec `--audit off` `log-decisions.ts` (une entrée de journal par décision, identifiants et codes seulement). Sans *stack* HTTP, il n'y a ni authentificateur ni frontière |
500
+ | `--representations negotiated`, `--forms on` | `src/platform/http/presentation-of.ts` (`Representations.standard()` avec `negotiated`, et `fieldDeciderOf` sur l'`Access` de l'application), décomposé dans la frontière HTTP et donné au script du contrat ; avec `--forms on`, `src/platform/http/with-forms.ts` ajoute `formRoutes` aux routes et `scripts/openapi.ts` écrit `openapi/forms/<operationId>.json`. Les deux sont la valeur par défaut avec une *stack* HTTP ; `json` et `off` les laissent de côté ([docs/CONFIG.fr.md](../../docs/CONFIG.fr.md)) |
501
+ | `--audit on` | `src/platform/audit/` : `create-audit.ts` (le journal, le `onDecision` de l'autorisation, les refus des gardes à la frontière remis à la frontière HTTP comme `onGuardRefusal`, le *middleware* avant l'unité de travail, les événements d'intégration abonnés sous le nom de consommateur `audit`), `audited-events.ts` (le registre auquel `generate integration-event` ajoute), `audit.module.ts`, et avec une *stack* HTTP le côté lecture : `audit.permissions.ts` (`audit:trail.read`), `search-audit-entries.query.ts`, son gestionnaire, `search-audit-entries.route.ts` (`GET /audit/entries` : pages par curseur, filtres `actorId`, `action`, `outcome`, `correlationId`). C'est une capacité de la plateforme, pas un contexte délimité : pas de modèle, une requête protégée sur le port `AuditReader`, bornée par le *tenant* de l'acteur (avec `--tenancy shared-table`, par le *tenant* du travail au moyen de `TenantAuditReader` : le personnel de la plateforme ne lit à travers les *tenants* qu'à l'intérieur de `acrossTenants`). Les refus d'un appelant sans identifiant de connexion ne sont pas enregistrés. Un *job* planifié est audité comme une requête HTTP : origine `schedule`, acteur `system:<context>.<job>`. Avec knex, les deux tables et leurs déclencheurs en ajout seul sont créés par la première migration ; avec drizzle, les tables sont dans `schema.ts` et `npm run db:audit-guard` affiche les instructions des déclencheurs pour une migration personnalisée ; avec prisma, typeorm, `custom` ou sans ORM, le journal est un `InMemoryAuditTrail` derrière un `TODO(platform)`. Les bases de test laissent le schéma d'audit hors de `reset()` |
502
+ | `--tenancy shared-table` | enregistré comme `stack.tenancy` ; Knex ou Drizzle seulement. Chaque `generate repository` est cloisonné par *tenant* (comme `--tenant-scoped`), au moyen de `tenantTable` et `ownTenantTable` ou de `tenantCondition` et `ownTenantCondition` ; l'outbox garde le *tenant* de chaque événement (`createOutboxTables(knex, { tenant: true })` et le `tenant: true` de `KnexOutboxStore`, ou `tenantOutboxTable`), si bien que les consommateurs s'exécutent dans ce *tenant* ; `tenancy/scoped-statement` juge les adaptateurs. La sécurité au niveau des lignes, un schéma par *tenant* et une base par *tenant* sont refusés, avec ce qu'il faut câbler à la main ([Multitenance](../../docs/concepts/multitenancy.fr.md)) |
503
+ | `--queue database\|redis\|memory` | enregistré comme `stack.queue`. `database` (par défaut sur Knex ou Drizzle avec tsyringe) : `createJobTable` et `createProcessTable`, ou `jobsTable` et `processesTable`, dans le schéma `messaging`, `Persistence` gagne `jobs` et `processes`, et le module de la plateforme relie `JobStore` et `ProcessStore` à eux, si bien qu'un *job* mis en file par une commande est écrit dans sa transaction ; `redis` (avec `--cache redis`) : `RedisJobStore` sur la connexion du cache, les processus dans la base de données ; `memory` (par défaut sinon) : rien n'est écrit, le module du kernel garde les deux dans le processus. Refusé hors de tsyringe, `database` avec un autre ORM, `redis` sans le cache |
504
+ | `--cache redis` | `src/platform/redis/open-redis.ts` (`RedisServices` : le cache lié au `Cache` du kernel, le limiteur à `RateLimiter`, le verrou à `Lock`, qui réserve un *job* planifié à une seule instance, le signal du relais comme `wakeOn` du relais de l'outbox avec `relayWakeUpMiddleware` sur le bus de commandes), `http/refusal-for.ts` (une fenêtre glissante par adresse d'appelant, 429 `application/problem+json` avec `retry-after` ; `RATE_LIMIT_MAX`, `RATE_LIMIT_WINDOW_MS`), `GET /health/ready` sur le serveur HTTP, `REDIS_URL` validée, un service compose `cache` qui exécute Dragonfly avec un contrôle de santé. Sans serveur (outils, tests sans `REDIS_URL`), les mêmes ports restent dans la mémoire du processus |
505
+ | `--http-cache varnish` | un service compose `varnish` (Varnish 9.1.0, qui joint l'API sur `host.docker.internal`) avec son VCL dans `docker/varnish/default.vcl` (seules les réponses dont le `Cache-Control` les autorise par `max-age` ou `s-maxage` sont gardées, rangées sous leurs étiquettes `xkey`, purgées par `PURGE` depuis la machine de Varnish ou avec `VARNISH_PURGE_KEY`, `x-cache: HIT\|MISS`), `VARNISH_URL` et `VARNISH_PURGE_KEY` validées, `cacheTags: { header: CacheTagHeader.xkey }` dans la frontière HTTP, et le relais de l'outbox de `main.ts` et de chaque point d'entrée de `generate service` qui publie au moyen de `PurgingPublisher` avec `VarnishCachePurger` (l'application de bout en bout enregistre les purges dans `purges`) ; sur `--di manual` et `nest`, la plateforme gagne `cacheInvalidation` ; avec `--tenancy`, les étiquettes des réponses et les purges sont rangées sous le *tenant* (`perTenant`) et le *tenant* d'une requête HTTP est lu dans `x-tenant-id`. Demande un framework HTTP et `--events outbox` : une purge est une ligne de l'outbox |
506
+ | `--errors sentry` | les échecs inattendus remontés à Sentry ou à GlitchTip (un seul protocole, seul le DSN diffère) au moyen de l'`ErrorReporter` du kernel, lié à `@ddd-ts-kit/sentry`. `src/platform/errors.ts` (`initSentry` sur la configuration validée, puis `reportProcessFailures`), le deuxième import de `main.ts`, de `apps/<context>/main.ts` et de `apps/worker/main.ts`, juste après `tracing.ts` ; `errors` remis à `composeApplication`, qui le donne au module du kernel sur tsyringe (`errorReporter`, vidé par `container.stop()` avant le *logger*, `errorReportingMiddleware` en premier sur les deux bus, le planificateur par défaut qui observe avec `reportFailedRuns`) ou à `createPlatform` à la main (le même *middleware*, `onRun: reportFailedRuns(errors)`, `platform.errors`, vidé en deux secondes au plus quand l'application s'arrête) ; `reportHttpErrors` dans `auth/http-boundary.ts` pour chaque requête HTTP qui reçoit une réponse 5xx ; `DeadLetterReports` sur le *hook* `onFailure` d'un *broker* fourni (`createEventProvider(config, errors)`, avec `observing` autour de chaque consommateur) et du relais de l'outbox. `SENTRY_DSN`, `SENTRY_ENVIRONMENT` (par défaut : `NODE_ENV`), `SENTRY_RELEASE` et `SENTRY_SCRUB_MESSAGES` validées dans `config/load-config.ts` comme `errorReporting` : sans DSN, l'application démarre et ne remonte rien ; un DSN qui n'est pas une URL refuse le démarrage. Dépendances `@ddd-ts-kit/sentry` et `@sentry/node`. Un profil compose `errors` qui exécute GlitchTip 6.2.6 avec son propre PostgreSQL et son propre Valkey (`GLITCHTIP_PORT`, sur la boucle locale seulement, amorçage de développement), et une section du README généré qui dit comment lire son DSN. Les outils et les tests composent l'application sans *reporter*. Il va avec tous les autres choix de la *stack* ; avec `--errors none`, rien de tout cela n'est écrit |
507
+ | `--authn keycloak` | un service compose `keycloak` (Keycloak 26.8.0, `start-dev --import-realm`, profil `identity`) et `docker/keycloak/<project>-realm.json` : le client de l'API, un client public `<project>-local` avec l'octroi direct, ses *mappers* d'audience et de `tenant_id`, le rôle de realm `admin`, et les utilisateurs de démonstration `demo-admin` et `demo-user`, pour le développement local seulement |
508
+ | `--storage local\|s3` | enregistré comme `stack.storage` : où vit le contenu des fichiers, jamais dans la base de données. `src/platform/config/read-files.ts` et `src/platform/files/open-files.ts`, un par choix : `local`, un répertoire en développement (`FILES_DIRECTORY`, `.data/files`) et les liens signés servis par l'application sous `/files` (`FILES_PUBLIC_URL` et `FILES_SIGNING_SECRET` requises en production) ; `s3`, un bucket de n'importe quel fournisseur du protocole S3 au moyen de `@ddd-ts-kit/s3` (les variables `S3_*`, MinIO dans la stack compose) ; la mémoire du processus dans les tests et les outils, dans les deux cas. Avec `--di tsyringe` : `files/bind-files.ts`, `files/files.module.ts` et `files/staging-uploads.ts` ; en câblage manuel, `files/create-files.ts` ; avec une *stack* HTTP, `files/file-routes.ts`. Ils lient `FileStorage`, `FileKeys`, `FileScanner` et `FileStaging`, enregistrent la purge horaire des fichiers en attente et mettent en attente le fichier que porte une commande avant sa transaction ; `generate file-upload` en a besoin. `--storage none` n'écrit rien de tout cela ([Stockage de fichiers](../../docs/concepts/file-storage.fr.md)) |
509
+ | `src/platform/events/` | `event-provider.ts` (`bus`, `forConsumer(name)`, `start()`, `close()`), `in-process-events.ts`, et pour `--broker` `create-event-provider.ts` : `RabbitMqEventBus` (`AMQP_URL`), `KafkaEventBus` (`KAFKA_BROKERS`, avec `aggregate-key-of.ts` pour l'ordre par agrégat), `NatsEventBus` (`NATS_URL`) ou `RedisStreamsEventBus` de `@ddd-ts-kit/redis/event-bus` (`REDIS_URL` : Redis, Dragonfly ou Valkey), chacun avec un abonné par contexte consommateur (`forConsumer`), `assertTopology` au démarrage, son adresse validée et un service compose avec un contrôle de santé ; ou `custom-event-bus.ts` avec sa *spec* `eventBusContract`. `start-outbox-relay.ts` avec l'outbox. `--broker redis` avec `--cache redis` fait un seul serveur et un seul service compose, `cache` ; seul, le service s'appelle `streams` |
510
+ | `src/platform/database/` | `persistence.ts` (`interface Persistence` : unité de travail, magasins de l'outbox et des événements traités du *package* de l'ORM, la poignée de stockage), `open-persistence.ts`, `persistence-over.ts`, `inert-persistence.ts`, la connexion (`connect-to-database.ts`, `create-client.ts` ou `create-data-source.ts`), `schema.ts`, `open-test-database.ts` (PostgreSQL dans le processus, ou le serveur nommé par une URL). La classe de stockage qu'un adaptateur de *repository* nomme dans sa liste de dépendances : `database.ts` avec drizzle et prisma (`abstract class Database extends` le type de contexte du *package* de l'ORM), `KnexContext` de `@ddd-ts-kit/knex`, `TypeOrmContext` de `@ddd-ts-kit/typeorm` |
511
+ | outillage des migrations | `drizzle.config.ts`, `knexfile.js` et `migrations/00000000000000_initial.js`, `prisma/schema.prisma` et `prisma.config.ts`, ou `data-source.ts` |
512
+ | `scripts/openapi.ts`, `scripts/events.ts` | écrivent `openapi/openapi.yaml` (un fichier par document de `openapi.documents`), `docs/integration/asyncapi.yaml` et `docs/integration/event-catalog.md`, en JSON sous `openapi.format: json` ; `--check` échoue sur une différence |
513
+ | `docker-compose.yml`, `.env.example`, `Dockerfile`, `.dockerignore` | la base de données avec un contrôle de santé et un volume nommé, `jaegertracing/jaeger:2.22.0` derrière le profil `observability` (son interface et son port OTLP liés à 127.0.0.1), les ports depuis `.env` ; une image multi-étapes, sans root, avec les seules dépendances de production |
514
+ | `vitest.e2e.config.ts`, `test/e2e/support/` | `start-application.ts` (l'application entière sur un port libre, sur une base de données dans le processus sauf si `DATABASE_URL` est définie, aucun *job* lancé par le temps), `create-credentials.ts`, `auth-environment.ts`, `test-environment.ts`, `story-of.ts` (`storyOf(trace)` : une trace sans les appels qu'un adaptateur fait sur lui-même, la forme que vérifie `expectSpanTree`) |
515
+ | `docs/context-map.md`, `docs/integration/README.md` | voir `docs` |
516
+ | `test/architecture/extractable.spec.ts` | écrit avec le premier contexte : démarre chaque contexte seul sur le kernel et la plateforme, ses *jobs* compris |
517
+ | `eslint.config.mjs` | le preset `dddTsKit` de `@ddd-ts-kit/eslint-plugin`, lint typé sur le tsconfig du projet |
518
+ | `.dependency-cruiser.cjs` | les règles du graphe des imports, générées depuis la configuration (voir `depcruise-config`) |
519
+ | `biome.json` | le formateur : Biome avec son linter et son organisateur d'imports désactivés, largeur de ligne 120 ; `npm run format` écrit, `npm run format:check` vérifie, et ce que la CLI génère est déjà formaté |
520
+ | `knip.json` | code mort : les points d'entrée sont `main.ts` et `apps/*/main.ts`, les scripts, les *specs* et les *fakes*, les fichiers de module dans le style explicite, les fichiers de rôle sous discovery (*jobs* et *publishers* compris) |
521
+ | `vitest.config.ts` | projets unitaires et d'intégration, les seuils de couverture lus dans `ddd-ts-kit.config.json` (`governedCoverage`), par contexte et par couche, dans le mode des décorateurs standard `decorators()` de `@ddd-ts-kit/testing/vitest`, qui compile les fichiers contenant un décorateur avec le compilateur du projet (Vitest ne transforme pas les décorateurs standard ; il transforme lui-même les anciens, si bien qu'un projet avec `experimentalDecorators` ne reçoit pas de *plugin*), et sous `--assembly discovery` `server.deps.inline: ['@ddd-ts-kit/di']` |
522
+ | `lefthook.yml`, `commitlint.config.mjs` | *hooks* pre-commit (ESLint, la vérification du format et `check` sur les fichiers indexés, `check --commit-size --staged`, qui ne bloque jamais), pre-push (types, tests unitaires) et commit-msg (`npm run hooks:install`), *commits* conventionnels |
523
+ | `--ci github` | `.github/workflows/ci.yml`, `pr-title.yml`, `.github/dependabot.yml`, `.github/branch-protection.md`, `renovate.json` ; `codeql.yml` avec `--ci-extras codeql` |
524
+ | `--ci gitlab` | `.gitlab-ci.yml` (les constats d'architecture comme rapport Code Quality au moyen de `check --format gitlab`), `.gitlab/merge-request-settings.md`, `renovate.json` |
525
+ | `README.md`, `CLAUDE.md`, `.claude/rules/*.md` | comment le lancer ; un guide léger (commandes, comment lire moins, la carte des règles, fichiers générés, note sur le compilateur, les *skills* du *plugin* : `/ddd-ts-kit:start`, `glossary`, `use-case`, `document`, `quality-gate`) et les invariants dans sept fichiers de règles, `domain.md`, `application.md`, `published-language.md`, `infrastructure.md`, `presentation.md`, `platform.md` et `tests.md`, chacun délimité par un frontmatter `paths` |
526
+ | `.claude/settings.json` | des permissions seulement : les vérifications et les générateurs s'exécutent sans demander, les fichiers `.env` sont illisibles (`.env.example` reste lisible), les trois documents générés ne sont pas modifiés à la main, `git push`, `npm publish` et toute modification des fichiers qui portent les règles et le *pipeline* (`ddd-ts-kit.config.json`, `eslint.config.mjs`, `biome.json`, `.dependency-cruiser.cjs`, `knip.json`, `lefthook.yml`, `.gitlab-ci.yml`, `.github/workflows/**`, `.claude/settings.json`) demandent d'abord |
527
+ | `.gitignore` | `node_modules`, `dist`, `coverage`, `.env`, `*.tsbuildinfo` et `.ddd-ts-kit/`, où écrit `worker-scope` |
528
+
529
+ `--ci both` écrit les deux *pipelines*, `--ci none` aucun (et pas de `renovate.json`). Les
530
+ fichiers de *pipeline* sont des copies des fichiers du répertoire `pipeline/` du dépôt, livrées
531
+ dans `templates/pipeline/` ; un test échoue quand une copie diffère de sa source au-delà de ses
532
+ emplacements à remplacer documentés.
533
+
534
+ Un `.claude/settings.json` existant n'est jamais écrasé : les permissions manquantes sont ajoutées
535
+ à ses listes, tout le reste est gardé, et les notes de la commande listent ce qui a été ajouté. La
536
+ règle sur `.env` repose sur la sémantique gitignore des motifs de permission : `Read(./.env.*)`
537
+ est refusé et `Read(!./.env.example)`, une négation placée après lui dans la même liste, en
538
+ ressort le fichier d'exemple.
539
+
540
+ Chaque fichier `.ts` qu'écrivent `init` et `generate` importe par l'alias (`@/platform/errors`),
541
+ jamais par un chemin relatif ; le barrel et l'index d'un langage publié réexportent par lui aussi.
542
+
543
+ Scripts : `dev` (`tsx watch`, qui résout un alias depuis les `paths` de `tsconfig.json`, avec
544
+ `.env` chargé par `--env-file-if-exists` et le traçage préchargé),
545
+ `build` (le compilateur, puis `ddd-ts-kit build-aliases`), `start` (`node` seul sur `dist/`), `typecheck`, `lint`, `lint:fix`, `test`, `test:e2e`, `check:arch`, `depcruise`,
546
+ `deadcode`, `quality`, `openapi:generate|check`, `events:generate|check`, `docs:generate|check`,
547
+ `db:generate`, `db:migrate`, `infra:up`, `infra:down`, `hooks:install`, `start:<context>` et
548
+ `start:worker` une fois que `generate service` et `generate worker` ont écrit leur point d'entrée,
549
+ les scripts `ci:*` qu'appellent les *pipelines*, `test:coverage` (chaque *spec* une fois, chaque
550
+ contexte et chaque couche tenus aux seuils de `ddd-ts-kit.config.json`) et `verify`, qui enchaîne
551
+ toute la base : typecheck, lint, `format:check`, `test:coverage`, tests de bout en bout,
552
+ `check:arch`, `depcruise`, `deadcode`, `docs:check`.
553
+
554
+ `package.json` porte deux compilateurs : `typescript ~6.0.3`, que les outils chargent comme une
555
+ bibliothèque, et `@typescript/native` (`npm:typescript@~7.0.2`), que `build` et `typecheck`
556
+ appellent par son chemin (`node node_modules/@typescript/native/bin/tsc`). `npx tsc` lancerait
557
+ l'ancien.
558
+
559
+ `init` et `generate context` commencent par le langage : la première entrée de `next` est le
560
+ glossaire du contexte à remplir.
561
+
562
+ ### Presets
563
+
564
+ `--preset identity` (sur `init`, ou sur `generate context` ; une fois par projet) écrit un
565
+ contexte délimité entier au lieu d'un contexte vide : qui sont les personnes de l'application, les
566
+ organisations auxquelles elles appartiennent et avec quel rôle. Les comptes sont provisionnés
567
+ depuis le fournisseur d'identité à la première connexion (aucun mot de passe n'est gardé), les
568
+ organisations (les *tenants* sous `--tenancy shared-table`), les membres avec des rôles d'un
569
+ vocabulaire déclaré, les invitations par jeton avec une expiration, les délégations de droits pour
570
+ une période ; les permissions, les politiques, le langage publié (membre arrivé, rôle changé,
571
+ compte désactivé...), une recherche, les contrôleurs, les *specs* avec *builders* et *fakes*, les
572
+ adaptateurs de l'ORM, et le port vers l'API d'administration du fournisseur d'identité, tenu à
573
+ jour au moins une fois par les consommateurs des faits publiés, avec un adaptateur Keycloak donné
574
+ comme une recette (prouvé contre un serveur qui répond comme Keycloak, pas contre Keycloak dans le
575
+ *pipeline*). Personne ne change son propre rôle ni n'agit au-dessus de lui, quoi que prête une
576
+ délégation ; un compte désactivé quitte ses organisations ; une adresse que le fournisseur n'a pas
577
+ vérifiée ne provisionne aucun compte. Sur `init` sans `--context` de son nom, le contexte
578
+ `identity` est déclaré `generic`. Chaque personne connectée détient les permissions du contexte
579
+ quoi que dise `--authz` : l'attribution `'identity:*'` de `platform/auth/signed-in-grants.ts`,
580
+ à laquelle répond `SignedInGrantsAuthorizer` avant le modèle dans `createAuthorizer`. Avec des
581
+ *tenants*, les tables du contexte n'appartiennent à aucun, sous un enregistrement de décision que
582
+ le *preset* écrit, et avec `--authn keycloak` l'authentificateur lit le *claim* `organization`
583
+ comme les *tenants* de la personne (un autre authentificateur reçoit une étape suivante qui dit
584
+ quoi faire correspondre). Le *preset* est écrit pour `declared` avec les dépendances `signature`,
585
+ `factories`, les objets-valeurs `throwing`, `controllers`, Knex ou Drizzle sur PostgreSQL, Zod ou
586
+ class-validator, Express ou Fastify, et un vérificateur de jeton (`jwt` ou `keycloak`) ; une
587
+ autre *stack* est refusée avec ce qu'il faut changer, de même qu'un second contexte écrit depuis
588
+ le même *preset*. Pourquoi, et quels autres contextes en méritent un :
589
+ [Démarrer avec des contextes bien conçus](../../docs/guides/bootstrap-with-well-designed-contexts.fr.md).
590
+
591
+ ## generate (alias g)
592
+
593
+ ```
594
+ ddd-ts-kit generate <kind> ... [--term "<Term>=<meaning>"]... [--allow-name] [--local-only] [--dry-run] [--force]
595
+ ddd-ts-kit generate --help --json
596
+ ```
597
+
598
+ Un passage de `generate` fait cinq choses, dans cet ordre, et n'écrit rien quand la première
599
+ refuse :
600
+
601
+ ![Un passage de generate command : le nom est vérifié dans le glossaire du contexte (refusé avec le code de sortie 2 et le --term à ajouter quand un mot n'est pas défini), les fichiers sont écrits depuis les templates, les nouvelles pièces sont enregistrées entre des commentaires marqueurs, les documents dérivés sont rafraîchis, et les endroits qui attendent une décision métier sont affichés en fichier:ligne](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/cli-generate-one-run.fr.svg)
602
+
603
+ Dans l'application d'exemple, un cas d'usage nommé avec un mot que son glossaire ne définit pas
604
+ est refusé, et le message dit comment continuer :
605
+
606
+ ```
607
+ $ npx ddd-ts-kit generate command notification ArchiveNotification --permission notification.archive --aggregate Notification
608
+ error: "ArchiveNotification" uses "archive", which the glossary of src/contexts/notification/CONTEXT.md does not define.
609
+ fix: Either use an existing term (Notification, Send, Recipient, Contact, Confirmation, Reminder, Plan, Due, Withdraw, Mailer, Kind, Request, Rescheduling, Cancellation, Read, Search), or add the term in the same command: --term "Archive=<what it means for the business>". The glossary is written before the code (rule language/glossary-term).
610
+ ```
611
+
612
+ Avec le terme donné dans la même commande (et `--dry-run`, qui dit ce qu'il écrirait) :
613
+
614
+ ```
615
+ $ npx ddd-ts-kit generate command notification ArchiveNotification --permission notification.archive --aggregate Notification \
616
+ --term "Archive=To set a sent notification aside, out of the default search" --dry-run
617
+ would create src/contexts/notification/application/commands/archive-notification/archive-notification.command.ts
618
+ would create src/contexts/notification/application/commands/archive-notification/archive-notification.handler.ts
619
+ would create src/contexts/notification/test/unit/archive-notification.handler.spec.ts
620
+ would update src/contexts/notification/CONTEXT.md
621
+ would update src/contexts/notification/application/authorization/notification.permissions.ts
622
+ would update src/contexts/notification/notification.declarations.ts
623
+
624
+ Next, in this order: the language of the context first, then the business decisions only you can make
625
+ src/contexts/notification/application/commands/archive-notification/archive-notification.command.ts:9 declare the input of "archive notification" as a constructor with the actor first, ...
626
+ src/contexts/notification/application/commands/archive-notification/archive-notification.handler.ts:16 decide what "archive notification" does to a Notification: ...
627
+ src/contexts/notification/test/unit/archive-notification.handler.spec.ts:38 replace each entry below by a test named after the business behaviour of "archive notification": ...
628
+ ```
629
+
630
+ | Genre | Arguments | Écrit |
631
+ |---|---|---|
632
+ | `context` | `<name> --classification <c> [--purpose "<two sentences>"] [--term ...]... [--upstream <ctx>[:relationship]]... [--preset identity]` | `CONTEXT.md` avec son glossaire, `published-language/index.ts`, l'entrée de configuration, l'enregistrement dans `platform/compose-application.ts` et dans `test/architecture/extractable.spec.ts` ; sur tsyringe, avec HTTP, `presentation/http/<name>.routes.ts` (une fonction des deux bus, dans les deux styles) ; déclaré : le barrel `<name>.declarations.ts` ; explicite et câblage manuel : `<name>.module.ts`. `application/authorization/<name>.permissions.ts` naît avec la première permission, ou tout de suite quand `platform/auth/permissions.ts` liste les ensembles de permissions (rebac, custom). Avec `--preset`, le contexte est ensuite écrit en entier depuis ce *preset*, sa classification et son objet étant ceux du *preset* sauf si les options disent autre chose ([Presets](#presets)) |
633
+ | `aggregate` | `<context> <Name> [--event-sourced]` | agrégat, *builder*, *spec* sur le modèle étant donné / quand / alors ; avec `--event-sourced`, un agrégat sur `EventSourcedAggregate` dont l'état est le rejeu de ses événements, né avec son événement du domaine `Created`, le codec de ses événements stockés dans `infrastructure/persistence/<name>-events.mapper.ts`, un *builder* qui rejoue un historique et une *spec* à partir d'événements donnés, et une note qui nomme le `EventSourcedRepository` auquel délègue son adaptateur de *repository* |
634
+ | `builder` | `<context> <Aggregate>` | `test/builders/<name>.builder.ts`, une classe qui étend `Builder` |
635
+ | `entity` | `<context> <Name>` | entité, *spec* |
636
+ | `value-object` (`vo`) | `<context> <Name>` | objet-valeur, *spec* |
637
+ | `vocabulary` | `<context> <Name> --value <value> [--value <value>]...` | `domain/vocabularies/<name>.vocabulary.ts` : un objet `as const` des valeurs, indexé par chaque valeur en PascalCase, et le type union du même nom ; relancé, ajoute les valeurs que le fichier ne contient pas |
638
+ | `domain-event` | `<context> <Aggregate> <Fact>` | événement nommé `<context>.<aggregate>.<fact>`, *spec* |
639
+ | `error` (`refusal`) | `<context> <Concept> <refusal> [--code CODE] [--term ...]...` | une fabrique statique `<refusal>` sur `domain/errors/<concept>.error.ts`, la classe des erreurs de ce concept (créée si elle manque, qui étend `ConceptError`), et son cas dans `test/unit/<concept>.error.spec.ts`. La fabrique est ajoutée après la dernière ; relancée, la commande ne change rien. `<Concept>` est un terme du glossaire (`--term` l'ajoute). Le code est le nom du refus en casse de constante, précédé du concept quand le nom est un seul mot ou un code du kit (`Appointment notFound` pose `APPOINTMENT_NOT_FOUND`) ; `--code` prend l'une des formes qu'accepte `errors/factory-name`. Refusés : un nom comme `create` ou `error1`, et un code déjà posé dans le contexte |
640
+ | `domain-error` | `<context> <Name> [--code CODE]` | une classe par erreur avec un code stable, *spec* : la forme d'un projet dont la configuration dit `"stack.refusals": "classes"` ou rien. Refusé sous `factories`, avec la commande `generate error` à lancer à la place |
641
+ | `domain-service` | `<context> <Name>` | service, *spec* |
642
+ | `state-machine` | `<context> <Aggregate> [--value <state>]... [--transition "<intention>:<from>[,<from>]...[><to>]"]...` | le vocabulaire `<Aggregate>Status` (créé depuis `--value`, deux états au moins, ou complété des valeurs qui lui manquent), `domain/state-machines/<aggregate>.state-machine.ts` (`<aggregate>Lifecycle` : chaque valeur un état, la première initiale, les transitions demandées, un `TODO(domain)` pour le refus de l'erreur du concept) et `test/unit/<aggregate>.state-machine.spec.ts` (chaque transition là où elle mène, un refus depuis un état qu'elle ne quitte pas) ; c'est à l'agrégat de l'utiliser |
643
+ | `repository` | `<context> <Aggregate> [--tenant-scoped]` | le port `domain/repositories/<name>.repository.ts` (`abstract class <Name>Repository extends Repository<<Name>>`), l'adaptateur de l'ORM du projet (qui `implements` le port) avec son *mapper* et sa table (ou son modèle), un *fake*, une *spec* `repositoryContract` exécutée sur les deux, l'enregistrement |
644
+ | `command` | `<context> <UseCaseName> (--permission <aggregate>.<action> \| --public) [--aggregate <Name>]` | `<use-case>.command.ts` sur `AuthorizedCommand` (simple `Command` quand elle est publique), `<use-case>.handler.ts` (il construit l'agrégat, le sauvegarde et répond : les événements partent avec la sauvegarde), une *spec* par le bus avec le refus d'un acteur à qui manque la permission, l'enregistrement avec `.requires(...)` ou `.public()` (et le *repository* s'il manque). Une commande générée répond `void` ou l'identifiant de son agrégat, si bien qu'aucun `<use-case>.result.ts` n'est écrit |
645
+ | `query` | `<context> <UseCaseName> (--permission <aggregate>.<action> \| --public) [--search]` | `<use-case>.query.ts` sur `AuthorizedQuery` (simple `Query` quand elle est publique), `<use-case>.result.ts`, `<use-case>.handler.ts`, le port de lecture `application/ports/<use-case>-reader.port.ts` (une classe abstraite), son adaptateur en mémoire, un *fake*, une *spec*, l'enregistrement sur le bus de requêtes. Avec `--search` (demande `--permission`) : une `PageRequest` du kernel dans la requête, l'élément, le tri et `Page<Item>` dans le fichier de résultat, un gestionnaire qui demande `Access.scope` et remet le périmètre au port de lecture, un lecteur en mémoire qui pagine par décalage ou par curseur |
646
+ | `port` | `<context> <Name>` | `application/ports/<name>.port.ts`, une classe abstraite, et son *fake* |
647
+ | `route` | `<context> <UseCaseName> --method <GET\|POST\|PUT\|PATCH\|DELETE> --path <p> [--command\|--query] [--group <Name>] [--mapper] [--serializer] [--cache public\|private\|no-store]` | sous `"stack.routes": "controllers"` : une classe contrôleur dans `presentation/http/<resource>/<use-case>/` (la ressource est le premier segment du chemin), qui dépend de `CommandBus` ou de `QueryBus`, derrière le groupe de routes de sa ressource, écrit avec elle quand il est nouveau (`<resource>.routes.ts` : préfixe, étiquette, `Authenticated`) ; un cas d'usage public ne se tient dans aucun groupe et dit `guards: [Public]` ; avec class-validator, une classe par fichier (`<use-case>.params.ts`, `.body.ts`, `.answer.ts`, `.accepted.ts`, `.line.ts`) ; `--mapper` et `--serializer` ajoutent un *mapper* de requête HTTP et un sérialiseur que reçoit le contrôleur ; une *spec* ; aucun enregistrement dans un projet déclaré, un contrôleur construit à la main dans `<context>.routes.ts` (le fichier de module en câblage manuel) dans un projet explicite. Sous `"functions"` : une fonction `defineRoute`, sa *spec*, son enregistrement dans `<context>.routes.ts`, et des classes `<use-case>-<role>.dto.ts` |
648
+ | `controller` | les mêmes arguments que `route` | la classe contrôleur, quoi que dise `stack.routes` : pour un projet de routes en fonctions qui écrit sa prochaine route comme une classe. `--cache` écrit sa politique de cache : `private` (un `GET` d'un appelant, `max-age=0`, `must-revalidate`, `ETag` fort), `public` (un `GET` d'un cas d'usage public, `max-age` 1m, `s-maxage` 5m, étiqueté avec les ressources de son chemin), `no-store` ; refusé sur une écriture, et `public` derrière un groupe de routes |
649
+ | `file-upload` | `<context> <UseCaseName> --path <p> (--permission <aggregate>.<action> \| --public) [--method POST\|PUT] [--group <Name>]` | un téléversement : un contrôleur qui déclare le fichier qu'il reçoit (sa taille et ses types de média dans le contrat, vérifiés au fil du flux), la commande qui le porte comme un `FileUpload`, mis en attente par le bus de commandes avant la transaction, le gestionnaire qui l'enregistre et le promeut avec la validation (commit), et leurs *specs*. Refusé sans `stack.storage` ou sans *stack* HTTP, avec le `init --force --storage local` à lancer |
650
+ | `route-group` (`group`) | `<context> <Name> [--prefix <path>] [--group <ParentName>] [--public \| --role <role>]` | `presentation/http/<name>.routes.ts` : une classe vide qui porte `@RouteGroup`, derrière `Authenticated` sauf si `--public` ou `--role` dit autre chose (`HasRole(Role.Admin)`, le rôle lu dans le vocabulaire de la plateforme, `<platformDir>/auth/role.ts`, qui le reçoit s'il ne le contient pas) ; avec `--group`, il affine ce groupe et en hérite |
651
+ | `guard` | `<context> <Name>` | `presentation/http/guards/<name>.guard.ts` : une classe de garde déclarée à remplir, et sa *spec* |
652
+ | `representation` | `<context> <UseCase> <xml\|csv\|media/type>[,...]` | ajoute `produces` au `@Controller` du cas d'usage, et `consumes: ['application/xml']` à celui qui reçoit un corps quand XML est demandé ; dit quand CSV a besoin d'une page ou d'une liste en flux, et quand `stack.representations` n'est pas `negotiated` |
653
+ | `form` | `<context> <UseCase>` | ajoute `const form = defineForm<Infer<typeof body>>({ ... })` avant le `@Controller` d'un cas d'usage qui reçoit un corps, et `form` à ses options : les indications de son formulaire à `GET /forms/<operationId>` |
654
+ | `schema` | `<context\|shared-kernel> <Name>` | `presentation/http/shared/<name>.schema.ts` du contexte, ou `presentation/<name>.schema.ts` du noyau partagé : une forme du contrat déclarée une fois (un schéma zod avec `.meta({ id })`, une classe class-validator, ou un schéma enveloppé par `sharedSchema`), à remplir ; les routes l'utilisent en entier, imbriquée ou en partie |
655
+ | `integration-event` | `<context> <Aggregate> <Fact> [--version n]` | la déclaration `defineIntegrationEvent` dans le langage publié (sa charge utile dans `<event>.v<n>.payload.ts` avec class-validator ou un schéma à vous), l'export de l'index, le *publisher* `application/integration/publishers/<event>.publisher.ts`, une *spec*, l'enregistrement, une ligne de `platform/audit/audited-events.ts` |
656
+ | `integration-handler` (`consumer`) | `<context> <upstreamContext> <eventType>` | `application/integration/handlers/<name>.integration-handler.ts`, une classe `<Name>IntegrationHandler` qui dépend de `CommandBus`, sa *spec*, son abonnement idempotent sous le nom de consommateur `<context>.<name>` ; refusé sauf si l'amont est déclaré |
657
+ | `event-handler` | `<context> <Aggregate> <Fact> [--phase before-commit\|after-commit]` | réaction à un événement du domaine du contexte, *spec*, abonnement ; avant la validation (commit) de l'unité de travail par défaut, après elle avec `--phase after-commit` (un effet qu'aucune transaction ne couvre, au plus une fois), et la *spec* exécute la réaction dans cette phase |
658
+ | `projection` (`projector`) | `<context> <Name> <event-type>` | un projecteur dans `application/projections/<name>.projection.ts`, déclaré avec `@Projection('<context>.<name>')` et une méthode `@On(definition)` pour l'événement d'intégration, que publie le contexte lui-même ou un amont déclaré, et sa *spec* ; style déclaré seulement. Il tient un seul modèle de lecture : son port est reçu par le constructeur |
659
+ | `job` | `<context> <Name> (--every <duration> \| --cron "<expression>" [--time-zone <zone>]) --command <UseCaseName>` | `application/scheduling/<name>.job.ts`, une classe `<Name>Job implements ScheduledJob` qui envoie la commande, en tant que `systemActor('<context>.<name>')` quand elle est protégée, avec `execution.scheduledAt` quand la commande le prend ; `test/unit/<name>.job.spec.ts`, qui exécute le *job* (par son nom sur tsyringe, `application.jobs.runNow` ; `job.run(occurrence)` en câblage manuel) et vérifie la commande qu'il a envoyée ; son enregistrement ; son interrupteur dans `platform/job-settings.ts` ; l'autorisation de son acteur système (ci-dessous) |
660
+ | `background-job` | `<context> <Name> --command <UseCaseName>` | `application/background/<name>.background-job.ts`, une classe `<Name>BackgroundJob implements BackgroundJob<<Name>Payload>` nommée par son `jobName` statique `<context>.<name>`, qui envoie la commande en tant que `execution.actor` quand elle est protégée, suivi d'un champ de `<Name>Payload` par autre paramètre du constructeur de la commande (chaînes, nombres, booléens, littéraux et listes de ceux-ci tels qu'écrits, une `Date` sous forme de son texte ISO ; tout le reste est refusé) ; `test/unit/<name>.background-job.spec.ts`, qui le met en file avec une charge utile de chaque champ en tant qu'utilisateur détenant la permission et exécute ce qui est dû ; son enregistrement (`@Background()`, ou `builder.backgroundJob(..)` dans le fichier de module). `--di tsyringe` seulement |
661
+ | `process` (`process-manager`) | `<context> <Name>` | `application/processes/<name>.process.ts` (son état, `processName`, `timeoutJob`, `isState`, une première étape qui arme une échéance, une étape qui y met fin à temps, l'étape de l'échéance), `application/background/<name>-timeout.background-job.ts`, qui livre ses expirations au moyen de `ProcessRunner`, et les *specs* des deux (`givenProcess`, la file d'attente de l'application de test). `--di tsyringe` seulement |
662
+ | `acl` | `<context> <upstreamOrSystemName>` | traducteur dans `infrastructure/anti-corruption/`, *spec* |
663
+ | `e2e` | `<context> <UseCaseName>` | `test/e2e/<use-case>.e2e.spec.ts` qui pilote la route du cas d'usage : 401 sans identifiant de connexion, 403 sans la permission, le statut documenté avec elle ; pour une commande, un test de plus vérifie la trace de la requête HTTP, exacte sur les noms et la structure, avec `installTestTracing()` et `expectSpanTree(storyOf(trace), [...])` de `@ddd-ts-kit/tracing/testing` (`POST /invoices`, puis `command IssueInvoiceCommand`, puis `repository InvoiceRepository.save`) ; demande la route |
664
+ | `policy` | `<context> <Name> --permission <aggregate>.<action>` | `application/authorization/<name>.policy.ts`, *spec* ; déclaré : la classe porte `@Policy(permission, { name })` et rien d'autre n'est écrit ; explicite : l'enregistrement dans `platform/auth/access-policies.ts` |
665
+ | `gateway` | `<context> <upstreamContext> <Name>` | le contrat de lecture dans le langage publié de l'amont (`<name>.contract.ts`, une classe abstraite et le jeton de la valeur de l'amont), le port `application/ports/<name>-gateway.port.ts`, un adaptateur dans le processus dans `infrastructure/external/` lié `.as('gateway')`, un *fake*, une suite de contrat ; refusé sauf si l'amont est déclaré |
666
+ | `adapter` | `<context> <Port> [--kind external\|gateway] [--decorators]` | `infrastructure/external/remote-<port>.adapter.ts` pour un port existant, sa *spec*, et sur tsyringe sa liaison `builder.bind(<Port>).toClass(Remote<Port>).as(<kind>).with(ownPolicy(retry(...), timeout(...)))` ; avec `--decorators`, sur toute *stack*, la classe porte `@Retry` et `@Timeout` de `@ddd-ts-kit/aspects/decorators` sur son unique méthode sortante et est liée `.with(ownPolicy())`. Explicite : un port déjà lié, ou le câblage manuel, reçoit une note au lieu d'une liaison. Déclaré : la classe porte `@Adapter(<Port>, { kind, aspects })`, ou `@Gateway(<Port>, { aspects })` dans `remote-<port>.gateway.ts` avec `--kind gateway`, et un port qui a déjà un adaptateur déclaré est refusé |
667
+ | `service` | `<context>` | `src/apps/<context>/main.ts` (kernel, plateforme, ce contexte seul, avec ses *jobs* : `jobSettings(process.env, '<context>')`), le script `start:<context>`, un service compose derrière le profil `services`, l'entrée knip, `deployable: true` dans la configuration. Le Dockerfile prend le point d'entrée comme argument de build `ENTRY` |
668
+ | `worker` | aucun | `src/apps/worker/main.ts` : les modules de `main.ts`, pas de serveur HTTP, pas de relais de l'outbox, les *jobs* démarrés quoi que dise `RUN_JOBS`, un arrêt propre ; le script `start:worker`, un service compose `worker` derrière le profil `services`, l'entrée knip, `"worker": true` dans la configuration, et `RUN_JOBS` qui vaut `false` par défaut à partir de là, si bien que l'API cesse d'exécuter les *jobs* qu'exécute le *worker*. Avec plusieurs instances, le verrou doit être un vrai verrou : `--cache redis` lie le verrou Redis, et sans lui le README généré dit la limite. Un contexte nommé `worker` est refusé |
669
+ | `adr` | `"<title>" [--deviation <what>] [--reason "<why>"]` | `docs/adr/NNNN-<slug>.md` ; avec `--deviation`, l'entrée `deviations` de la configuration qui pointe vers lui |
670
+
671
+ - **Le style déclaré, genre par genre.** Aucun jeton nulle part : une liste nomme des classes de
672
+ port. `command` et `query` mettent `@Requires(<permissions>[...])` ou `@Public()` sur le
673
+ message et `@CommandHandler(Message)` ou
674
+ `@QueryHandler(Message, [<UseCase>Reader])` sur le gestionnaire ; `repository` écrit
675
+ `@Repository(InvoiceRepository, [<storage>, ExecutionScope, Clock])`, le stockage étant
676
+ `Database` de la plateforme (drizzle, prisma), `KnexContext` ou `TypeOrmContext` ; l'adaptateur
677
+ en mémoire d'un port de lecture est `in-memory-<name>-reader.repository.ts`, un fichier de rôle
678
+ avec `@Repository(<UseCase>Reader)` ; `integration-handler` écrit
679
+ `@IntegrationEventHandler(definition, { consumer })` et est refusé quand l'amont ne déclare pas
680
+ l'événement ; `event-handler` et le *publisher* de `integration-event` écrivent
681
+ `@DomainEventHandler(Event, { name })` (le *publisher* avec
682
+ `[IntegrationEventPublisher, IdGenerator]`), et tous deux sont trouvés comme n'importe quel
683
+ fichier de rôle : la racine de composition ne liste aucun *publisher* ; `job` écrit
684
+ `@Scheduled('<context>.<name>', schedule)` ; `policy` écrit une classe avec
685
+ `@Policy(permission, { name })` et n'enregistre rien ; `gateway` écrit
686
+ `@Gateway(<Name>Gateway, [<Name>Contract])` et lie la valeur de l'amont dans les régions
687
+ marquées de `platform/platform.module.ts` et de `platform/platform-test.module.ts` : une valeur
688
+ ou une fabrique, c'est ce qu'un décorateur ne peut pas dire ; `route` ajoute sa fonction à
689
+ `presentation/http/<context>.routes.ts`. Une *spec* de gestionnaire construit son application
690
+ de test à partir de `platformTestModule`, de `contextModule('<context>', <context>Declarations)`,
691
+ d'un module d'autorisation et de *fakes* (`fake(InvoiceRepository, new FakeInvoiceRepository())`).
692
+ - **Le style explicite, genre par genre.** Les mêmes fichiers sans décorateurs, et une ligne du
693
+ fichier de module chacun : `builder.bind(InvoiceRepository).toClass(DrizzleInvoiceRepository, [Database, ExecutionScope, Clock]).as('repository')`,
694
+ `builder.command(IssueInvoiceCommand).requires(...).handledBy(IssueInvoiceHandler, [InvoiceRepository, IdGenerator, Clock])`,
695
+ `builder.on(definition, { consumer }).handledBy(Handler, [CommandBus])`,
696
+ `builder.job('<context>.<name>', schedule).handledBy(<Name>Job, [CommandBus])` ; un *publisher*
697
+ et un gestionnaire d'événement du domaine sont liés sous eux-mêmes et abonnés dans la région
698
+ `start`. En câblage manuel (`--di manual`, `--di nest`), le fichier de module est une fonction
699
+ de la `Platform` :
700
+ `platform.adapter(new DrizzleInvoiceRepository(...), { kind: 'repository', context, token })`,
701
+ qui donne au *repository* son *span* et la distribution de ce qu'il sauvegarde,
702
+ `platform.commands.register(Message, new Handler(...), { context })`,
703
+ `platform.jobs.register({ name, schedule, run })`.
704
+ - **Les *jobs*.** Le planning est validé quand le *job* est généré, par les règles du planificateur
705
+ (`parseJobSchedule` du kernel, fixé par un test) : une durée (`15m`, `2s`, des chiffres seuls
706
+ sont des millisecondes) ou une expression cron à cinq champs, avec un fuseau horaire IANA ;
707
+ `--time-zone` sans `--cron` est refusé, de même qu'un *job* sans `--command`, une commande qui
708
+ n'existe pas (avec la commande qui la crée) et une commande dont le constructeur demande plus
709
+ que l'acteur et l'instant pour lequel le travail est fait : un *job* dit seulement que le moment
710
+ est venu, le gestionnaire trouve sur quoi agir. Les paramètres optionnels ou qui ont une valeur
711
+ par défaut sont laissés hors de l'appel.
712
+ - **L'autorisation d'un *job*.** Un *job* dont la commande est protégée l'envoie en tant que
713
+ `system:<context>.<name>`, qui n'a aucun droit tant que le modèle ne lui en donne pas.
714
+ `generate job` écrit l'autorisation là où le modèle en fait une ligne, dans une région marquée
715
+ `system-grants`, et `next` pointe vers cette ligne : `--authz rbac`, l'identifiant de la
716
+ permission sous le rôle `system` de `platform/auth/roles.ts`, le rôle que `createAuthorizer`
717
+ donne à chaque acteur système (`systemRole`) ; `--authz abac`, une règle
718
+ `permit('system-<job>', [permission], ({ actor }) => actor.id === 'system:<job>')` dans
719
+ `platform/auth/attribute-policies.ts` ; `--authz rebac`, le tuple
720
+ `context:<context>#operator@system:system:<job>` dans `platform/auth/relate-system-actors.ts`,
721
+ écrit dans le magasin de tuples au démarrage de l'application, tant que le schéma laisse encore
722
+ un sujet `system` opérer un contexte. Avec `--authz custom`, une région supprimée, un schéma de
723
+ relations remodelé ou une permission que le générateur ne sait pas lire, rien n'est écrit et le
724
+ fichier du *job* porte le `TODO(domain)` qui nomme l'acteur et la permission.
725
+ - **Le barrel est dérivé.** `<context>.declarations.ts` contient un `export *` par fichier de
726
+ rôle, trié par chemin, puis un `injected(Class, [Tokens])` par classe dont le constructeur
727
+ reçoit quelque chose, lu depuis sa signature, avec les imports dont ils ont besoin. Chaque
728
+ `generate`, et `docs`, le réécrivent ; `docs --check` et `check`
729
+ (`declarations/barrel-current`) signalent un barrel qu'un fichier de rôle écrit à la main a
730
+ laissé en arrière ou dont un constructeur a changé depuis, en nommant la classe
731
+ (`stale src/contexts/billing/billing.declarations.ts: the dependency list of IssueInvoiceHandler is stale: ...`).
732
+ Le script `dev` charge `ddd-ts-kit/dist/refresh-declarations.js` avant l'application, à chaque
733
+ démarrage et à chaque rechargement de `tsx watch` : un port ajouté à un constructeur est injecté
734
+ au rechargement qui suit l'enregistrement du fichier. Le même module est le binaire
735
+ `ddd-ts-kit-refresh-declarations`. Il est mis en page comme Biome le met en page à une largeur
736
+ de ligne de 120 ; un projet qui formate à une autre largeur laisse `**/*.declarations.ts` et
737
+ `**/*.dependencies.ts` hors de son formateur.
738
+ - **Refus par défaut.** `command` et `query` sont refusés sans `--permission` ou `--public`. Une
739
+ nouvelle permission rejoint `<context>.permissions.ts` ; ses mots sont vérifiés dans le
740
+ glossaire comme tout autre nom. Avec `--authz rebac`, l'exigence nomme le contexte comme sa
741
+ ressource, un point de départ à remplacer par l'élément sur lequel porte le droit.
742
+ - `route` remet `actorOf(context.principal)` à un cas d'usage protégé et pose `security` (le
743
+ schéma de `--authn`, ou `false` pour un cas d'usage public). `integration-handler` porte
744
+ `systemActor('<context>.<name>')` pour la commande qu'il construit ; derrière un *broker*, il
745
+ écoute sur l'abonné de son contexte (`EventSubscribers.forContext`), sa marque et son travail
746
+ dans une seule unité de travail.
747
+ - Un adaptateur de *repository* généré ne fait que persister : il prend `ExecutionScope` et
748
+ `Clock`, jamais `ActorContext` ni un distributeur, et estampille `created_at`, `created_by`,
749
+ `updated_at`, `updated_by` avec `stampCreation` et `stampUpdate` ; la table utilise les
750
+ `auditColumns` du *package* de l'ORM (les cinq champs sont écrits en entier pour Prisma). Le
751
+ fait d'exemple d'un agrégat est `registeredAt`, puisque `created_at` est la colonne d'audit.
752
+ `repository --tenant-scoped` (drizzle et knex), et chaque *repository* d'un projet dont
753
+ `stack.tenancy` vaut `shared-table`, ajoute `tenantColumn`, nomme le *tenant* à l'insertion et
754
+ atteint la table par les fonctions utilitaires du *package* de l'ORM (`tenantTable` et
755
+ `ownTenantTable` sur Knex, `tenantCondition` et `ownTenantCondition` sur Drizzle), avec une
756
+ *spec* qui vérifie qu'un *tenant* ne voit pas les lignes d'un autre. Avec un autre ORM,
757
+ l'adaptateur ordinaire est écrit et une note dit quoi ajouter.
758
+ - `adapter --decorators` est accepté sur toute *stack* : les décorateurs des aspects servent le
759
+ mode standard et l'ancien mode (legacy) qu'impose un `tsconfig.json` avec
760
+ `experimentalDecorators`.
761
+ - `route` d'une requête `--search` lit la page avec `pageQuerySchema` et répond avec
762
+ `pageSchemaOf` et `pageResponse`. Un abonnement d'un gestionnaire d'événement d'intégration qui
763
+ n'est pas un `builder.on` est écrit dans la région `start` avec `idempotent(...)` autour de
764
+ `inEventScope(...)`.
765
+ - **Le langage d'abord.** Le nom d'un agrégat, d'une entité, d'un objet-valeur, d'un service du
766
+ domaine, d'un événement du domaine, d'une commande ou d'une requête est refusé quand l'un de ses
767
+ mots n'est couvert par aucun terme du tableau `## Ubiquitous language` du contexte (les formes
768
+ du pluriel, du participe passé et en -ing sont reconnues). `--term "<Term>=<meaning>"`,
769
+ répétable, ajoute les lignes dans la même commande. Les mots techniques (`Manager`, `Helper`,
770
+ `Data`, `Impl`, `Model`...) et un fait d'événement qui ne se lit pas au passé sont refusés eux
771
+ aussi. `--allow-name` passe outre les trois vérifications ; `check` signalera tout de même ce
772
+ que le glossaire ne définit pas.
773
+ - Le code généré compile tel quel et passe `check`, le preset ESLint, dependency-cruiser et knip
774
+ aux sévérités par défaut : un export par fichier, des fichiers bien en dessous de 350 lignes,
775
+ des membres `private`, la disposition des lignes vides. Chaque endroit qui demande une
776
+ connaissance métier porte un commentaire `// TODO(domain): ...` qui dit quelle décision est
777
+ attendue, et un endroit où un adaptateur est à remplacer un commentaire `// TODO(platform): ...` ;
778
+ les seuls autres commentaires disent ce que le code ne peut pas dire.
779
+ - Les *specs* contiennent un vrai test qui passe là où il peut s'écrire de façon générique, et
780
+ des entrées `it.todo` qui nomment les comportements à couvrir. Aucun *mock* : des *fakes*, des
781
+ *builders* et l'application de test. Sur tsyringe, une *spec* de gestionnaire passe par
782
+ `createTestApplication` de `@ddd-ts-kit/di` et observe les faits annoncés avec la sauvegarde
783
+ par `application.domainEvents.on('<name>', ...)` ; en câblage manuel, elle passe par
784
+ `createTestBuses()` de `@ddd-ts-kit/testing`, dont `dispatching(fake)` fait que la sauvegarde
785
+ d'un *fake* annonce ses événements à `domainEvents`.
786
+ - `--json` affiche `{ dryRun, created, updated, skipped, next, todos, notes }`. `next` liste les
787
+ endroits `TODO(domain)` en `file:line`, le glossaire d'abord, puis les lignes qu'une commande a
788
+ écrites pour que l'équipe les vérifie (l'autorisation d'un *job*) ; `todos` ajoute leur texte.
789
+ - Un genre qui a besoin d'autre chose pour compiler soit le crée (un *repository* ou un événement
790
+ du domaine manquant), soit s'arrête avec la commande qui le crée (un agrégat, un cas d'usage ou
791
+ une route manquant).
792
+ - L'enregistrement se fait entre des commentaires marqueurs (`// ddd-ts-kit:<region>` ...
793
+ `// /ddd-ts-kit:<region>`, des commentaires HTML en Markdown). Retirez un marqueur, et le
794
+ générateur affiche ce qu'il faut ajouter à la main.
795
+ - Chaque `generate` rafraîchit la documentation générée et `.dependency-cruiser.cjs` quand ils
796
+ existent ; générer deux fois ne change rien.
797
+ - `--local-only` refuse, en nommant les fichiers et sans rien écrire, quand le passage écrirait
798
+ hors de `<contextsDir>/<context>/` et de `test/e2e/*.e2e.spec.ts`. `generate --help --json`
799
+ affiche chaque genre avec son `usage`, ses `aliases`, son `summary` et son `writesShared`, plus
800
+ `shared` et `sharedWhen` : ce qu'il écrit hors de son contexte, et quand. Les deux servent un
801
+ build réparti entre des *workers* (voir `worker-scope`).
802
+ - Un *fake* est un point d'entrée pour knip et peut n'avoir aucun importateur pour
803
+ dependency-cruiser : un port et son *fake* générés avant qu'un cas d'usage en ait besoin ne font
804
+ échouer ni l'un ni l'autre analyseur. Un service du domaine est gardé en vie par la *spec*
805
+ générée avec lui.
806
+
807
+ Les *templates* sont des fichiers ordinaires dans `templates/<kind>/`, avec des emplacements
808
+ `{{name.pascal}}`, `{{name.camel}}`, `{{name.kebab}}`, `{{name.constant}}`, `{{context.kebab}}`
809
+ et des blocs `{{#if x}}` / `{{#unless x}}`. Un emplacement inconnu est une erreur.
810
+
811
+ ## docs
812
+
813
+ ```
814
+ ddd-ts-kit docs [--check] [--stored-events]
815
+ ```
816
+
817
+ Régénère ce qui dérive de la configuration et du code, et rien d'autre :
818
+
819
+ - `docs/context-map.md`, entre des marqueurs `<!-- ddd-ts-kit:context-map -->` : un diagramme
820
+ Mermaid (un nœud par contexte, stylé selon sa classification, une flèche par relation, de
821
+ l'amont vers l'aval, étiquetée du patron) et le tableau des contextes avec la première phrase
822
+ de leur raison d'être, leurs amonts, leurs avals, les événements qu'ils publient et s'ils sont
823
+ déployables seuls. Le texte hors des marqueurs est gardé ; à la première création, les quatre
824
+ sections écrites sont posées avec des indications.
825
+ - les listes générées de chaque `CONTEXT.md` (agrégats, événements publiés, permissions, *jobs*
826
+ planifiés en tableau de nom, planning et commande, relations) ;
827
+ - le fichier généré de chaque contexte déclaré : le barrel `<context>.declarations.ts`, avec ce
828
+ que reçoit chaque constructeur, lu depuis sa signature, ou sous discovery
829
+ `<context>.dependencies.ts`, qui ne contient que cela ;
830
+ - la référence de chaque `CONTEXT.md` : les faits de `describe`, en tableaux sous
831
+ `## Reference`, entre des marqueurs `ddd-ts-kit:reference` ; un `CONTEXT.md` qui n'a pas cette
832
+ section la reçoit après sa dernière liste générée ;
833
+ - `docs/integration/README.md`, créé une fois et jamais réécrit ; sa section Provider commence par
834
+ la stratégie du *broker* choisi.
835
+
836
+ `--stored-events` enregistre aussi dans `stored-events.lock.json` la forme de chaque version d'un
837
+ événement stocké déclarée dans les codecs de `infrastructure/persistence/`, sans jamais réécrire
838
+ une version enregistrée : lancez-le une fois que la forme d'une version est définitive.
839
+
840
+ `--check` n'écrit rien, nomme chaque fichier périmé et chaque version d'événement stocké déclarée
841
+ qui n'est pas enregistrée dans `stored-events.lock.json`, et sort avec le code 1. Le catalogue
842
+ des événements et le document AsyncAPI ont besoin d'exécuter le code du projet, c'est pourquoi
843
+ ils sont produits par `scripts/events.ts` (`npm run events:generate`, qui fait partie de
844
+ `npm run docs:generate`).
845
+
846
+ ## sdk
847
+
848
+ ```
849
+ ddd-ts-kit sdk [--check] [--dry-run]
850
+ ```
851
+
852
+ Génère le client typé de chaque document du contrat OpenAPI, lu depuis les `openapi/*.yaml` (ou
853
+ `.json`) commités : un *package* du *workspace* sous `clients/<document>` (`clients/api` pour le
854
+ contrat entier), sans dépendance d'exécution. Chaque opération est une méthode de
855
+ `createClient()` et une fonction à elle ; son résultat est typé par statut, un refus par les
856
+ codes d'erreur que le contrat liste pour elle (`x-error-codes`) ; un identifiant de connexion est
857
+ donné par schéma de sécurité. Le premier passage enregistre `"sdk"` dans la configuration
858
+ (`init --sdk` le fait pour un nouveau projet) et câble le projet : le *workspace*, l'entrée
859
+ `paths` de chaque client dans `tsconfig.json`, les exclusions de Biome et de knip, les scripts
860
+ `sdk:generate` et `sdk:build`, et `docs:generate`, `docs:check`, `ci:docs`, `ci:build` étendus. Le
861
+ package.json d'un client garde sa version et les clés qu'une équipe ajoute. Sous npm, tant que
862
+ `package-lock.json` ne liste pas encore un client, il dit de lancer `npm install` et de commiter
863
+ le lockfile, ce que `npm ci` refuserait. `--check` n'écrit rien, nomme chaque fichier périmé (et
864
+ un client que le lockfile ne connaît pas) et sort avec le code 1. La comparaison avec
865
+ openapi-typescript, hey-api et orval, et comment publier un client :
866
+ [docs/tools/client-sdk.fr.md](../../docs/tools/client-sdk.fr.md).
867
+
868
+ ## depcruise-config
869
+
870
+ ```
871
+ ddd-ts-kit depcruise-config [--check | --write]
872
+ ```
873
+
874
+ Affiche la configuration de dependency-cruiser dérivée de `ddd-ts-kit.config.json` : couches,
875
+ langages publiés, amonts déclarés, *packages* autorisés, cycles et orphelins. `--write` la
876
+ sauvegarde sous `.dependency-cruiser.cjs` ; `--check` sort avec le code 1 quand le fichier
877
+ manque ou est périmé, ce que `npm run depcruise` exécute en premier. Modifiez la configuration,
878
+ pas le fichier.
879
+
880
+ ## value-objects
881
+
882
+ ```sh
883
+ ddd-ts-kit value-objects [--dry-run]
884
+ ```
885
+
886
+ Fait passer les objets-valeurs d'un projet d'un `Result` à des fabriques qui renvoient la valeur
887
+ ou lèvent une erreur (`"stack.valueObjects": "throwing"`). Ce qu'il réécrit, et ce qu'il laisse à
888
+ une personne, est dans [CONFIG.fr.md](../../docs/CONFIG.fr.md#stack).
889
+
890
+ ## controllers
891
+
892
+ ```sh
893
+ ddd-ts-kit controllers [<context>] [--dry-run]
894
+ ```
895
+
896
+ Fait passer les routes en fonctions d'un projet, ou d'un contexte, à des classes contrôleurs
897
+ (`"stack.routes": "controllers"`). Pour chaque `presentation/http/<name>.route.ts` :
898
+
899
+ - le contrôleur est écrit dans `presentation/http/<resource>/<name>/<name>.controller.ts`, la
900
+ ressource étant le premier segment de son chemin : la déclaration garde la méthode, le résumé,
901
+ les schémas, l'unique réponse avec sa description et les codes d'erreur ; le gestionnaire de la route devient
902
+ `handle`, avec l'acteur lu dans la requête HTTP et la réponse renvoyée sans son statut ;
903
+ - un groupe de routes par ressource est écrit (`<resource>.routes.ts` : son préfixe, les
904
+ étiquettes de ses routes, `Authenticated`, et `FORBIDDEN` quand chaque route le déclarait) ;
905
+ une route avec `security: false` ne se tient dans aucun groupe et dit `guards: [Public]` ;
906
+ - les fichiers de schéma nommés d'après la route la suivent (`<name>-body.dto.ts` devient
907
+ `<name>.body.ts`), les autres vont dans `shared/` sous leur nom, et chaque import d'un fichier
908
+ déplacé est réécrit ;
909
+ - la *spec* devient `<name>.controller.spec.ts` et exécute le contrôleur sans ses gardes ;
910
+ - dans un projet déclaré, la liste `<context>.routes.ts`, sa *spec* et sa ligne dans la racine
911
+ de composition sont supprimées ; dans un projet explicite, la liste construit chaque contrôleur
912
+ à la main ;
913
+ - la configuration dit `controllers` une fois qu'il ne reste plus aucune route en fonction dans
914
+ les contextes.
915
+
916
+ Un contexte est converti en entier ou pas du tout. Une route qui n'est pas écrite comme
917
+ `export function <name>Route(<bus>): Route { return defineRoute(spec, async (input) => { .. }); }`,
918
+ qui déclare plusieurs réponses, ou dont le gestionnaire répond autre chose que `{ status, body }`,
919
+ est nommée avec la raison, et son contexte est laissé tel quel : les routes en fonctions
920
+ continuent de tourner à côté des contrôleurs. La commande se termine par ce qui reste à une
921
+ personne : lancer le formateur et `ddd-ts-kit docs`, déplacer les fonctions utilitaires qu'un
922
+ contrôleur a gardées vers un *mapper* de requête HTTP ou un sérialiseur, donner leurs gardes aux
923
+ groupes, tenir les gardes dans les *specs*.
924
+
925
+ ## error-factories
926
+
927
+ ```
928
+ ddd-ts-kit error-factories --plan
929
+ ddd-ts-kit error-factories --apply [--dry-run]
930
+ ```
931
+
932
+ Fait passer un projet d'une classe par erreur (`new SlotTakenError(..)`) à des fabriques
933
+ statiques sur une classe d'erreur par concept (`AppointmentError.slotTaken(..)`), en deux étapes,
934
+ parce que le concept sur lequel porte un refus est une décision du modèle.
935
+
936
+ `--plan` écrit `error-factories.plan.json` : par contexte, une entrée par classe de
937
+ `domain/errors/` qui étend `DomainError`, avec son code, le concept proposé pour elle (l'agrégat
938
+ par lequel son nom commence ou finit, vide quand aucun ne convient) et le nom de fabrique qui dit
939
+ son code (`APPOINTMENT_NOT_FOUND` sur `Appointment` donne `notFound`). Modifiez-le : chaque
940
+ entrée a besoin d'un `concept`, un terme du glossaire.
941
+
942
+ `--apply` suit le plan et refuse une entrée sans concept, un nom de fabrique vague ou un nom qui
943
+ ne dit pas le code. Il :
944
+
945
+ - écrit `domain/errors/<concept>.error.ts`, une classe qui étend `ConceptError` avec une fabrique
946
+ par classe déplacée là : les paramètres du constructeur, le message qu'il donnait à `super`, et
947
+ comme faits les paramètres qui sont de simples données (texte, nombres, booléens, dates) ;
948
+ - supprime chaque classe et sa *spec*, dont les cas vont sous `describe('<factory>')` dans
949
+ `test/unit/<concept>.error.spec.ts` ;
950
+ - réécrit chaque fichier sous la racine des sources et sous `test/` : `new X(..)` devient
951
+ `<Concept>Error.<factory>(..)`, `thenError(X)` et `thenError(X, code)` deviennent
952
+ `thenError('<CODE>')`, `toThrow(X)` et `expect.any(X)` deviennent une assertion sur le code,
953
+ `toBeInstanceOf(X)` devient `toMatchObject({ code })`, `e instanceof X` devient
954
+ `<Concept>Error.is(e, '<CODE>')`, un import de la classe devient l'import de sa classe de
955
+ concept, et un type nomme la classe de concept. Une classe du même nom dans deux contextes va
956
+ au concept du sien. Les commentaires et les chaînes sont laissés tels quels ;
957
+ - pose `"stack.refusals": "factories"` dans la configuration.
958
+
959
+ Il laisse, et le dit : une classe qui a des membres à elle, lit `this` dans son constructeur ou
960
+ donne des options à `super` (écrite à la main) ; un endroit où une classe tenait lieu de valeur
961
+ qu'il ne sait pas traduire ; une propriété de l'erreur devenue un fait (`error.personId` est
962
+ désormais `error.facts.personId`) ; une fabrique de plus de trois paramètres. Et toujours : quels
963
+ faits un client peut lire (aucun n'est exposé), les fabriques qui prennent encore une phrase
964
+ comme seul paramètre, et les faits à vérifier dans les *specs*. Lancez le formateur ensuite.
965
+ Relancé, il ne trouve plus rien à déplacer. `--json` répond
966
+ `{ dryRun, moved, written, removed, manual }`.
967
+
968
+ ## build-aliases
969
+
970
+ ```
971
+ ddd-ts-kit build-aliases [--project <tsconfig>]
972
+ ```
973
+
974
+ La seconde étape de `npm run build`, après le compilateur. Le compilateur émet un alias d'import
975
+ tel qu'il a été écrit (`@/platform/errors`), et Node ne résout aucun alias et ne complète aucune
976
+ extension : cette commande réécrit, dans chaque `.js`, `.mjs`, `.cjs`, `.d.ts`, `.d.mts` et
977
+ `.d.cts` sous le répertoire de sortie du tsconfig (`tsconfig.build.json` par défaut), chaque
978
+ spécificateur d'alias en chemin relatif vers le fichier émis qu'il nomme, avec son extension
979
+ `.js` (`../../platform/errors.js`), afin que `node dist/src/main.js` s'exécute sans chargeur ni
980
+ option.
981
+
982
+ ```js
983
+ // dist/src/contexts/billing/domain/aggregates/invoice.aggregate.js, tel que le compilateur l'émet
984
+ import { InvoiceError } from '@/contexts/billing/domain/errors/invoice.error';
985
+
986
+ // la même ligne après ddd-ts-kit build-aliases
987
+ import { InvoiceError } from '../errors/invoice.error.js';
988
+ ```
989
+
990
+ Elle lit `importAlias` et `importExtension` de la configuration : `@/platform/errors` nomme
991
+ `platform/errors.js` sous `none`, `@/platform/errors.js` le nomme sous `js`, et aucune autre
992
+ écriture n'est devinée.
993
+
994
+ - Réécrits : le spécificateur de `import ... from`, `export ... from`, un `import` nu,
995
+ `import('...')` et `require('...')` avec une chaîne littérale, `import.meta.resolve('...')`, et
996
+ dans les déclarations `import type`, `typeof import('...')` et `declare module '...'`.
997
+ - Exacte : le fichier est analysé, pas fouillé ; une chaîne qui ressemble seulement à un import,
998
+ dans un autre code, un commentaire, un template literal ou une expression régulière, est laissée
999
+ telle quelle. Les lignes ne bougent jamais et les source maps ne sont pas touchées.
1000
+ - Idempotente : un second passage ne réécrit rien.
1001
+ - Elle sort avec le code 2 et n'écrit rien quand un alias ne nomme aucun fichier émis, quand il
1002
+ nomme un répertoire (un alias nomme un fichier : `.../published-language/index`), quand il est
1003
+ écrit dans une forme que la configuration ne demande pas, quand `import()` reçoit un alias
1004
+ complété à l'exécution (un template literal avec une interpolation, une concaténation), ou quand
1005
+ le répertoire de sortie n'existe pas. Chaque refus dit le fichier, la ligne et quoi écrire à la
1006
+ place.
1007
+ - Un *package* n'est jamais touché, quelle que soit la fin de son sous-chemin
1008
+ (`zod/v4/core.js`, `@scope/package/file.js`, `node:fs`).
1009
+
1010
+ `--json` répond `{ outDir, files, changed, specifiers }`. `ddd-ts-kit check` signale un script
1011
+ qui compile sans cette étape (`imports/alias-declared`). Tout le mécanisme :
1012
+ [docs/tools/import-aliases.fr.md](../../docs/tools/import-aliases.fr.md).
1013
+
1014
+ ## list
1015
+
1016
+ ```
1017
+ ddd-ts-kit list [--context <name>]
1018
+ ```
1019
+
1020
+ `--context` restreint la réponse à un contexte. Les contextes avec leur classification, la
1021
+ taille de leur glossaire, leurs amonts, agrégats, commandes, requêtes, événements du domaine,
1022
+ événements publiés, gestionnaires d'événements d'intégration, *jobs* planifiés (nom, planning, la
1023
+ commande que chacun envoie ; `jobs` en `--json`), *builders*, permissions, politiques, *gateways*
1024
+ et s'ils sont déployables seuls, puis les *specs* de bout en bout. Dans le style déclaré, les cas
1025
+ d'usage, les gestionnaires, les politiques, les *gateways* et les adaptateurs sont lus dans les
1026
+ décorateurs : une commande ou une requête montre ce qu'elle exige (ou `public`), un gestionnaire
1027
+ son nom de consommateur et son événement, une politique sa permission, un adaptateur son port ;
1028
+ `--json` les ajoute sous `declarations`. Dans le style explicite, les *jobs* sont lus dans les
1029
+ lignes `builder.job(...)` et `platform.jobs.register(...)` du fichier de module.
1030
+
1031
+ ## describe
1032
+
1033
+ ```
1034
+ ddd-ts-kit describe <context>
1035
+ ```
1036
+
1037
+ Un contexte sous forme de faits lus dans la configuration et le texte des sources, dans les deux
1038
+ styles : les cas d'usage avec leur permission (ou `public`), les routes (méthode, chemin,
1039
+ identifiant d'opération, cas d'usage, noms de schémas, codes d'erreur, les types de média dans
1040
+ lesquels elles répondent et qu'elles lisent en plus de JSON, leur formulaire), les événements consommés
1041
+ et publiés, les ports avec leurs adaptateurs, les politiques, les *gateways* avec leur amont, les
1042
+ *jobs* planifiés, les amonts et les avals. Les listes de champs ne sont pas devinées : elles sont
1043
+ dans `openapi/openapi.yaml` et `docs/integration/event-catalog.md`. `docs` écrit les mêmes faits
1044
+ dans chaque `CONTEXT.md`.
1045
+
1046
+ `--json` affiche un seul objet :
1047
+
1048
+ | Clé | Valeur |
1049
+ |---|---|
1050
+ | `name`, `classification`, `purpose`, `deployable`, `doc` | le contexte, sa raison d'être (vide tant qu'elle n'est pas écrite) et le chemin de son `CONTEXT.md` |
1051
+ | `glossary`, `aggregates`, `domainEvents`, `permissions` | des noms |
1052
+ | `upstreams`, `downstreams` | `{ context, relationship }` |
1053
+ | `useCases` | `{ kind, name, permission, file }`, `kind` valant `command` ou `query` |
1054
+ | `routes` | `{ method, path, operationId, useCase, permission, request: [{ part, schema }], responses: [{ status, schema }], errors, file }`, et `produces`, `consumes` (types de média en plus de JSON) et `form` (`GET /forms/<operationId>`) quand la route les déclare |
1055
+ | `consumedEvents` | `{ type, version, from, handler, consumer, file }` |
1056
+ | `publishedEvents` | `{ type, versions, publishers }` |
1057
+ | `ports` | `{ name, kind, adapters, file }`, `kind` étant celui de sa liaison (`repository`, `external`, `gateway`) |
1058
+ | `policies` | `{ name, permission, file }` |
1059
+ | `gateways` | `{ name, upstream, adapters, file }` |
1060
+ | `jobs` | `{ name, schedule, command }` |
1061
+ | `contracts` | `{ openapi, eventCatalog }` : où sont les listes de champs |
1062
+
1063
+ ## status, todo
1064
+
1065
+ ```
1066
+ ddd-ts-kit status
1067
+ ddd-ts-kit todo [<context>]
1068
+ ```
1069
+
1070
+ `status` affiche la ligne `Next:` de `docs/architecture/plan.md`, les lignes de
1071
+ `docs/architecture/slices.md` par statut et par responsable, et par contexte les `TODO(domain)`,
1072
+ `TODO(platform)` et `it.todo` restants, puis ceux hors des contextes. Un fichier manquant n'est
1073
+ pas une erreur. `--json` affiche `{ next, slices, remaining }` : `next` et `slices` valent `null`
1074
+ quand leur fichier manque, `slices` contient `{ file, rows, byStatus, byOwner }`, `remaining` un
1075
+ `{ context, domain, platform, test }` par contexte, et un avec un `context` vide pour ce qui reste
1076
+ hors d'eux.
1077
+
1078
+ `todo` liste ces marqueurs en `file:line` avec leur genre et leur texte, dans le projet ou dans
1079
+ un contexte ; `--json` affiche `{ count, remaining: [{ kind, context, file, line, text }] }`.
1080
+
1081
+ ## worker-scope
1082
+
1083
+ ```
1084
+ ddd-ts-kit worker-scope <context> [--worktree <path>] [--branch <name>] [--dry-run]
1085
+ ```
1086
+
1087
+ Prépare la place d'un *worker* d'un build réparti par contexte : crée un *worktree* git du projet
1088
+ (`../<project>-<context>` par défaut) sur la branche `build/<context>`, y écrit
1089
+ `.ddd-ts-kit/worker.json` (`{ "context": "<context>" }` ; `init` ignore `.ddd-ts-kit/`), et
1090
+ affiche la consigne du *worker* : son périmètre, les générateurs qu'il peut lancer, ses tranches
1091
+ encore à faire d'après `docs/architecture/slices.md`, les langages publiés qu'il consomme. Un
1092
+ *worktree* existant est gardé et seul le fichier est réécrit ; `--dry-run` ne crée et n'écrit
1093
+ rien. Il faut un dépôt git avec au moins un *commit*. Le *worker* génère ensuite avec
1094
+ `--local-only`.
1095
+
1096
+ ## affected
1097
+
1098
+ ```
1099
+ ddd-ts-kit affected [--base <git-ref>] [--head <git-ref>] [--format text|json|github|env]
1100
+ ```
1101
+
1102
+ Ce qu'une *pull request* ou une *merge request* doit exécuter, lu dans la carte des contextes et
1103
+ dans les chemins qui diffèrent entre `--base` et `--head` (`HEAD` par défaut ; la base de fusion
1104
+ est utilisée quand le clone l'a). Chaque chemin modifié atteint quelque chose :
1105
+
1106
+ - un chemin sous un contexte déclaré atteint ce contexte et chaque contexte en aval de lui,
1107
+ puisque leur code peut lire son langage publié ; un chemin sous son seul `test/` atteint ce
1108
+ contexte seul, puisqu'aucun autre contexte ne peut importer ses tests ;
1109
+ - un document n'atteint rien qui s'exécute : un fichier Markdown à la racine, tout ce qui est
1110
+ sous `docs/`, `openapi/` ou `.claude/` (la porte des documents tourne toujours) ;
1111
+ - tout le reste atteint tout : la plateforme, le noyau partagé, `main.ts`, la configuration, le
1112
+ lockfile, le *pipeline*, un répertoire de `src/contexts/` que la configuration ne déclare pas.
1113
+
1114
+ Sans `--base`, ou quand git ne sait pas dire ce qui a changé (une référence inconnue, un clone
1115
+ superficiel sans la base), tout est retenu : c'est ce qu'obtiennent la branche par défaut et
1116
+ l'exécution nocturne. La sélection n'est jamais plus étroite que les relations déclarées, parce
1117
+ que `context/declared-relationship` garde chaque import entre contextes à l'intérieur d'elles, et
1118
+ que cette règle tourne sur le projet entier dans chaque *pipeline*.
1119
+
1120
+ ![Un changement dans le contexte scheduling de l'application d'exemple : scheduling est retenu, et avec lui notification et analytics, qui sont en aval de lui dans la carte des contextes ; identity, en amont, est laissé à la branche par défaut](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/cli-affected-selection.fr.svg)
1121
+
1122
+ Dans l'application d'exemple, une branche qui modifie un agrégat de `scheduling` :
1123
+
1124
+ ```
1125
+ $ npx ddd-ts-kit affected --base main
1126
+ Affected contexts: scheduling, notification, analytics (the changed contexts and the contexts downstream of them in the context map).
1127
+ Specs: src/contexts/scheduling/**/*.spec.ts src/contexts/notification/**/*.spec.ts src/contexts/analytics/**/*.spec.ts !src/contexts/**
1128
+ Left to the default branch: src/contexts/identity/
1129
+
1130
+ $ npx ddd-ts-kit affected --base main --format github
1131
+ scope=contexts
1132
+ contexts=scheduling,notification,analytics
1133
+ args=--passWithNoTests
1134
+ lint=--ignore-pattern src/contexts/identity/
1135
+ ```
1136
+
1137
+ `--format github` écrit des lignes `key=value` pour `$GITHUB_OUTPUT`, `--format env` les mêmes
1138
+ clés en majuscules derrière `AFFECTED_` pour un rapport `dotenv` de GitLab, `--json` la sélection
1139
+ sous forme de données (`{ scope, reason, contexts, specs, leftOut }`). `contexts` est ce que
1140
+ l'exécuteur des tests lit comme `DDD_TS_KIT_AFFECTED`, `args` ce qu'une exécution partielle y
1141
+ ajoute, `lint` ce qu'elle ajoute à ESLint ; les *pipelines* générés câblent les trois.
1142
+
1143
+ ## check
1144
+
1145
+ ```
1146
+ ddd-ts-kit check [paths...] [--rule <id>]... [--format text|json|github|gitlab]
1147
+ [--changed-since <git-ref>] [--self]
1148
+ ddd-ts-kit check --clean-only <dir>... [--rule <id>]... [--format text|json|github]
1149
+ ddd-ts-kit check --commit-size [--staged] [--strict]
1150
+ ```
1151
+
1152
+ Chaque violation a un identifiant de règle, une sévérité, un fichier, une ligne, un message et un
1153
+ `fix` : une phrase qui dit quoi faire à la place. Code de sortie 1 quand il y a au moins une
1154
+ erreur. Supposons que l'agrégat `Reminder` de l'application d'exemple lise l'horloge système dans
1155
+ son domaine : un `isDue()` en ligne 63 qui appelle `Date.now()`, là où le vrai reçoit l'instant,
1156
+ `isDue(asOf: Date)`. `check` signalerait :
1157
+
1158
+ ```
1159
+ $ npx ddd-ts-kit check src/contexts/notification/domain
1160
+ src/contexts/notification/domain/aggregates/reminder.aggregate.ts:64 error purity/no-system-clock
1161
+ Date.now() is used in the domain layer.
1162
+ fix: Receive the instant as a parameter in the domain, and take it from the Clock port (`this.clock.now()`) in a handler, so that a test fixes time with FixedClock.
1163
+
1164
+ 1 error, 0 warnings, 0 suppressions in 772 files.
1165
+ ```
1166
+
1167
+ Le `fix` est le changement à faire. Au lieu de cette méthode hypothétique :
1168
+
1169
+ ```ts
1170
+ isDue(): boolean {
1171
+ return this.state.status === ReminderStatus.Planned && this.state.dueAt.getTime() <= Date.now();
1172
+ }
1173
+ ```
1174
+
1175
+ écrivez :
1176
+
1177
+ <!-- checked sample -->
1178
+ ```ts
1179
+ import { Clock, FixedClock } from '@ddd-ts-kit/kernel';
1180
+
1181
+ // domaine : l'instant est un paramètre, comme dans l'agrégat Reminder de l'exemple
1182
+ export class Reminder {
1183
+ constructor(private readonly dueAt: Date) {}
1184
+
1185
+ isDue(asOf: Date): boolean {
1186
+ return this.dueAt.getTime() <= asOf.getTime();
1187
+ }
1188
+ }
1189
+
1190
+ // application : le gestionnaire prend l'instant du port Clock
1191
+ export class SendDueRemindersHandler {
1192
+ constructor(private readonly clock: Clock) {}
1193
+
1194
+ isDueNow(reminder: Reminder): boolean {
1195
+ return reminder.isDue(this.clock.now());
1196
+ }
1197
+ }
1198
+
1199
+ // un test fixe le temps
1200
+ const handler = new SendDueRemindersHandler(new FixedClock(new Date('2026-03-02T09:00:00Z')));
1201
+ const due = handler.isDueNow(new Reminder(new Date('2026-03-01T09:00:00Z'))); // true
1202
+ ```
1203
+
1204
+ `ddd-ts-kit explain <rule-id>` affiche le même genre de mauvais et de bon exemple pour chaque
1205
+ règle.
1206
+
1207
+ - `paths` et `--changed-since` limitent ce qui est signalé, pas ce qui est analysé.
1208
+ `--changed-since` lance `git diff --name-only --relative <ref>...` dans le projet.
1209
+ - `--format github` affiche des annotations `::error file=...,line=...::`.
1210
+ - `--format gitlab` affiche un rapport Code Quality de GitLab : un problème par violation,
1211
+ `major` pour une erreur et `minor` pour un avertissement, avec une empreinte qui ne dépend pas
1212
+ de la ligne.
1213
+ - Les sévérités viennent de `rules` dans la configuration (`error`, `warn`, `off`). Par défaut
1214
+ `error`, sauf `test/no-mocks`, `test/builder-required`, `clean/function-size`,
1215
+ `clean/no-noise-comment`, `clean/no-flag-argument`, `standard/no-parallel-interface` et, tenue
1216
+ par ESLint, `application/command-query-separation` (`warn`). `any` dans un fichier de test est
1217
+ toujours un avertissement.
1218
+ - `limits: { "maxParameters": 3, "maxFunctionLines": 30 }` dans la configuration règle
1219
+ `clean/function-size`. `maxFunctionLines` va de 1 à 100 : une valeur au-dessus est lue comme 100
1220
+ et signalée par `config/valid`, et une fonction de plus de 100 lignes est une `error` quelle que
1221
+ soit la sévérité de la règle (seul `off` la fait taire). `clean/file-size` (350 lignes) n'est
1222
+ pas configurable.
1223
+ - `onePerFile: { "allow": [{ "file": "<path or glob>", "reason": "<why these exports are one thing>" }] }`
1224
+ exempte des fichiers du décompte de `clean/one-export-per-file`, jamais de sa vérification du
1225
+ nom de fichier ; `*` correspond à l'intérieur d'un segment de chemin, `**` à travers les
1226
+ segments ; une entrée sans raison est ignorée et signalée par `config/valid`.
1227
+ - `domainAllowedPackages`, `applicationAllowedPackages` et `publishedLanguageAllowedPackages`
1228
+ étendent ce que les couches intérieures peuvent importer. `@ddd-ts-kit/aspects`,
1229
+ `@ddd-ts-kit/audit`, `@ddd-ts-kit/redis`, `@ddd-ts-kit/scheduling`, `@ddd-ts-kit/sentry` et
1230
+ `@ddd-ts-kit/tracing` restent refusés dans `domain/` et `application/` même quand une liste les
1231
+ nomme.
1232
+ - `standardInterfaces: { "<DeclaredName>": "<kernel interface to use instead>" }` dans la
1233
+ configuration ajoute des noms à `standard/no-parallel-interface` en plus de ses noms par défaut ;
1234
+ la valeur `false` retire un nom par défaut.
1235
+ - Désactivez une ligne avec `// ddd-ts-kit-disable-next-line <rule-id> -- ADR-0007: <reason>`.
1236
+ Sans raison, le commentaire est lui-même une violation, tout comme une raison qui ne nomme aucun
1237
+ enregistrement de décision existant. Sur la première ligne d'un fichier, il couvre aussi les
1238
+ violations au niveau du fichier.
1239
+ - `--clean-only` exécute les règles qui jugent un fichier seul (`clean/stepdown`,
1240
+ `clean/function-size`, `clean/no-commented-out-code`, `clean/no-noise-comment`,
1241
+ `clean/no-escape-hatch`, `clean/no-hash-private`, `clean/blank-lines`, `clean/braces`,
1242
+ `clean/one-export-per-file`, `clean/file-size`) sur n'importe quels répertoires, sans projet ni
1243
+ configuration.
1244
+ - `--self` rejoue les *fixtures* de règles livrées avec la CLI et sort avec le code 0 quand chaque
1245
+ règle se comporte comme attendu.
1246
+
1247
+ | Règle | Détecte |
1248
+ |---|---|
1249
+ | `layer/domain-purity` | dans `domain/` : un *package* hors de `domainAllowedPackages`, tout module natif de Node, une autre couche, un autre contexte ; toute entrée de `@ddd-ts-kit/aspects`, `@ddd-ts-kit/audit`, `@ddd-ts-kit/redis`, `@ddd-ts-kit/scheduling`, `@ddd-ts-kit/sentry` ou `@ddd-ts-kit/tracing`, autorisée ou non ; `@ddd-ts-kit/kernel/declarations`, bien que le kernel soit autorisé : le domaine ne déclare rien |
1250
+ | `layer/application-inward` | dans `application/` : `infrastructure/`, `presentation/`, la plateforme, un *package* hors de la liste autorisée, un module natif de Node ; toute entrée de `@ddd-ts-kit/aspects`, `@ddd-ts-kit/audit`, `@ddd-ts-kit/redis`, `@ddd-ts-kit/scheduling`, `@ddd-ts-kit/sentry` ou `@ddd-ts-kit/tracing`, autorisée ou non, avec l'endroit où la préoccupation a sa place (la racine de composition, un *middleware* de bus, un adaptateur, un port) |
1251
+ | `layer/presentation-no-infrastructure`, `layer/infrastructure-no-presentation` | l'import interdit, à l'intérieur d'un contexte |
1252
+ | `context/published-language-only` | un import d'un autre contexte hors de son `published-language/` |
1253
+ | `context/declared-relationship` | un import d'un langage publié dont le contexte n'est pas un amont déclaré |
1254
+ | `context/published-language-is-a-contract` | `published-language/` qui importe autre chose que sa liste autorisée et d'autres langages publiés |
1255
+ | `context/no-cycle` | un cycle dans les amonts déclarés, avec son chemin |
1256
+ | `tree/closed` | un répertoire hors de l'arbre canonique et de `extraDirectories` ; un fichier source hors d'un répertoire de rôle ; dans le style déclaré, `<context>.declarations.ts` est accepté à la racine d'un contexte |
1257
+ | `tree/context-doc` | un `CONTEXT.md` manquant, une section manquante, une classification qui contredit la configuration |
1258
+ | `naming/suffix` | un fichier sans le suffixe de son rôle, ou pas en kebab-case. `application/scheduling/` contient des `*.job.ts` ; un dossier de cas d'usage accepte un `*.result.ts` à côté de son message et de son gestionnaire ; `presentation/http/<context>.routes.ts`, nommé d'après son contexte au lieu d'un rôle, est accepté dans les deux styles |
1259
+ | `naming/one-concept-per-file` | plusieurs classes exportées dans un fichier d'agrégat, d'entité, d'objet-valeur, d'événement, de commande, de requête, de gestionnaire, de gestionnaire d'événement, de gestionnaire d'événement d'intégration ou de *job* ; dans le style déclaré, plusieurs classes portant un décorateur de déclaration dans un même fichier de rôle |
1260
+ | `naming/search-not-list` | dans un contexte, une méthode nommée `list` ou `list<Something>` dans un port (`domain/repositories/*.repository.ts`, `application/ports/*.port.ts`), dans une classe de `infrastructure/` ou dans un *fake* de `test/fakes/` ; sous `application/queries/`, un dossier ou un fichier nommé `list`, `list-...` ou `list....`, ou une classe nommée `List` ou `List<Something>`. Les propriétés et `listAccessible` (exigée par le port `Authorizer`) ne sont pas jugées. Nommez-les `search...` : `generate query <context> Search<Things> --search` |
1261
+ | `test/spec-required` | un agrégat, un objet-valeur, un service du domaine, un gestionnaire, un gestionnaire d'événement du domaine, un gestionnaire d'événement d'intégration ou un *job* sans *spec* (un `*.result.ts` n'en a pas besoin) |
1262
+ | `test/no-mocks` | `vi.mock`, `vi.fn`, `vi.spyOn`, leurs jumeaux `jest`, ou une bibliothèque de *mocks* dans les tests d'un contexte |
1263
+ | `test/builder-required` | un agrégat sans `test/builders/<name>.builder.ts` |
1264
+ | `events/name-format` | un nom d'événement qui n'est pas `<context>.<aggregate>.<fact>` en kebab-case avec le bon contexte |
1265
+ | `events/defined-in-published-language` | `defineIntegrationEvent` hors d'un langage publié, sans charge utile, deux fois pour une même version ; un événement décrit à la main |
1266
+ | `language/glossary-term` | une classe du modèle nommée avec un mot que le glossaire de son contexte ne définit pas ; un glossaire vide ; sous `"stack.refusals": "factories"`, une classe d'erreur de `domain/errors/` aussi |
1267
+ | `language/declared-vocabulary` | une chaîne littérale dont la valeur est une valeur d'un vocabulaire fermé que le projet déclare (un objet `as const` exporté avec le type union de son nom, un ensemble de permissions de `definePermissions`), hors du fichier qui le déclare, à l'endroit qui parle son langage (son contexte ; la plateforme et les groupes de routes pour la plateforme ; partout pour un identifiant de permission ou le noyau partagé) ; les tests, les langages publiés et le schéma de relations ne sont pas jugés ; sous `"stack.literals": "named"` |
1268
+ | `language/no-technical-name` | `Manager`, `Helper`, `Util`, `Utils`, `Data`, `Info`, `Processor`, `Wrapper`, `Impl`, `Base`, `Abstract`, `Dto`, `Entity`, `Model` dans un type déclaré sous `domain/` ou dans un dossier de cas d'usage |
1269
+ | `purity/no-system-clock` | `new Date()` sans argument, `Date.now()`, `performance.now()` dans `domain/` ou `application/` |
1270
+ | `purity/no-ambient-randomness` | `Math.random`, la source aléatoire de `crypto`, une bibliothèque d'identifiants |
1271
+ | `purity/no-environment` | `process.env` et apparentés, `console`, `fetch`, les modules de système de fichiers et de réseau |
1272
+ | `clean/stepdown` | une méthode privée avant la dernière méthode publique ou au-dessus de son premier appelant ; une fonction non exportée au-dessus de sa première utilisation |
1273
+ | `clean/function-size` | plus de paramètres ou de lignes de corps que les limites (les constructeurs sont exemptés de la limite de paramètres, les fichiers de module et les callbacks de `describe` de la limite de lignes) ; toute fonction de plus de 100 lignes, celles-là comprises, signalée comme une `error` |
1274
+ | `clean/no-commented-out-code` | des lignes de commentaire qui ont la forme d'instructions |
1275
+ | `clean/no-noise-comment` | les bannières, les étiquettes d'accolades fermantes, les auteurs et les dates, les `@param` et `@returns` vides, un commentaire qui ne fait que répéter le nom déclaré, un `TODO` sans responsable ni référence |
1276
+ | `clean/no-escape-hatch` | `any`, une assertion de non-nullité, `@ts-ignore`, `@ts-nocheck`, `@ts-expect-error` ou `eslint-disable` sans raison |
1277
+ | `clean/no-flag-argument` | un paramètre `boolean` sur une méthode publique d'une classe du domaine ou sur `execute` |
1278
+ | `clean/no-hash-private` | un membre de classe `#name` : champ, méthode, accesseur, statique |
1279
+ | `clean/blank-lines` | une approximation de la disposition que tiennent ESLint et le formateur : un `return` dont la ligne précédente n'est ni vide, ni une accolade ouvrante, ni une étiquette `case` ; une ligne vide juste après une accolade ouvrante ou juste avant une accolade fermante |
1280
+ | `clean/braces` | un `if`, `else`, `for`, `while` ou `do` dont le corps n'a pas d'accolades ou partage une ligne avec elles, un `else` sur une ligne à lui ; une approximation de ce que tiennent ESLint (`curly`) et le formateur |
1281
+ | `clean/one-export-per-file` | sous la racine des sources, tests à part : plus d'une classe, interface ou fonction exportée que les tolérances n'expliquent pas (les types de la signature de l'export principal, un type et une valeur qui partagent un nom, un type avec ses constructeurs, les accesseurs d'un même état privé, `onePerFile.allow`) ; un fichier qui ne porte pas le nom de son export principal en kebab-case (`invoice.aggregate.ts` pour `Invoice`) ; un `index.ts` ou un `<context>.declarations.ts` qui contient autre chose que des réexports |
1282
+ | `clean/file-size` | un fichier de plus de 350 lignes, sources et tests ; un chemin avec un segment `generated` et les fichiers de déclaration sont exemptés |
1283
+ | `composition/only-in-module` | `new X`, ou `bind(..).toClass(X, ..)` et `rebind(..).toClass(X, ..)`, d'une classe importée de `infrastructure/`, hors du fichier de module, de `main.ts`, de la plateforme et des tests ; dans un fichier qui importe `@ddd-ts-kit/di`, un appel de `contextModule`, `discoverContext` ou `.include(` hors de ces endroits et de `apps/<context>/main.ts` |
1284
+ | `imports/alias-only` | sous la racine des sources, `test/` et `scripts/` : un `import`, `export ... from` ou `import()` avec un spécificateur relatif, même répertoire compris (la correction nomme l'alias à écrire ; `eslint --fix` l'écrit) ; un spécificateur d'alias avec un segment `.` ou `..` |
1285
+ | `imports/extension` | sous la racine des sources, `test/` et `scripts/` : avec `importExtension` `none`, un spécificateur d'alias qui se termine par `.js`, `.mjs`, `.cjs`, `.ts`, `.mts` ou `.cts`, ou qui nomme un répertoire contenant un `index.ts` ; avec `js` (ou sans clé), un spécificateur d'alias qui ne se termine pas par `.js` (la correction nomme le spécificateur à écrire ; `eslint --fix` l'écrit). Un sous-chemin de *package* n'est jamais jugé |
1286
+ | `imports/alias-declared` | un `importAlias` qui n'est pas `@/` ou `$<name>/`, ou qui est `$test/`, `$scripts/`, `$generated/` ; une entrée `paths` de `tsconfig.json` manquante ou différente de ce que donne `importAlias` (la correction donne l'entrée) ; une autre entrée `paths` sous un alias ; un `baseUrl` ; un `tsconfig.build.json` dont les `paths` diffèrent ; des `paths` de `knip.json` qui associent un alias ; un script qui compile (`tsc` sans `--noEmit`) et ne lance pas `ddd-ts-kit build-aliases` ensuite ; un script qui lance `tsx --tsconfig` sur un autre fichier ; une configuration `vitest` à la racine sans `tsconfigPaths: true`, une configuration `jest` sans les entrées `moduleNameMapper` dérivées, ou `.dependency-cruiser.cjs` sans `tsConfig` ; un `module` et un `moduleResolution` de `tsconfig.json` (ou d'un `tsconfig.build.json` qui pose les siens) qui ne sont pas ce que donne `importExtension` : `ESNext`, `Bundler` et `customConditions` contenant `node` sous `none`, et sous `js` une valeur écrite qui n'est pas une résolution Node ; une association jest dont le motif ne se termine pas comme les imports. Un projet sans `package.json` n'est pas jugé |
1287
+ | `config/valid` | un amont, une classification, une relation, un identifiant de règle, un mode de `guard` ou un `importExtension` inconnu ; un contexte absent du disque ou de la configuration ; une limite de `commit` qui n'est pas un entier positif |
1288
+ | `config/suppression-needs-reason` | une désactivation sans identifiant de règle ou sans raison (gardée comme alias à côté de `governance/recorded-deviation`, qui juge la raison) |
1289
+ | `authz/declared` | dans un fichier de module, `builder.command(X)` ou `builder.query(X)` sans `.requires(` ni `.public(` dans sa chaîne, sauf si la classe du message porte `@Requires` ou `@Public` de `@ddd-ts-kit/kernel/declarations` ; en câblage manuel ou nest, une classe de commande ou de requête absente de tous les `requires(X, ...)` et `publicUseCase(X)` du fichier de module et de `application/authorization/` ; un `defineRoute` sans `security` (le type de la route le refuse déjà, et une liste vide aussi : c'est le filet au niveau du texte pour une définition construite de façon lâche) ; dans le style déclaré, la classe exportée d'un fichier `*.command.ts` ou `*.query.ts` qui ne porte ni `@Requires` ni `@Public` |
1290
+ | `authz/in-application` | `presentation/` : un test sur les rôles, `isAdmin`, l'import d'un autorisateur ou de `Access` ; `infrastructure/` : `authorize(` ou `check(` sur un autorisateur ou un accès, `ForbiddenError` ; `domain/` : tout import de `@ddd-ts-kit/auth`, des types d'acteur du kernel (`Actor`, `ActorContext`, `Session`, `ExecutionContext`, les messages autorisés), une lecture de `.roles` ou `.permissions` sur un acteur. La glu de requête HTTP de `@ddd-ts-kit/auth` (`actorOf`, `sessionOf`, `resolveSession`, `SessionAuthenticator`) est libre dans `presentation/`. `presentation/` et `infrastructure/` : l'import de `Requires`, `Public` ou `Policy` depuis `@ddd-ts-kit/kernel/declarations`, ou des ports `Access` et `PolicyAccess` depuis `@ddd-ts-kit/auth` ; ils sont libres dans `application/` |
1291
+ | `authz/actor-explicit` | dans `domain/` et `application/` : `ActorContext`, `ExecutionScope` (comme type ou comme valeur : la classe est le jeton d'une liste de dépendances), `AsyncLocalStorageExecutionScope`, `ScopedActorContext`, `AsyncLocalStorage` ; `.current()` ou `.require()` sur un contexte d'acteur ou sur quelque chose nommé `scope`, `scopes` ou `...executionScope` ; un import de `stampCreation`, `stampUpdate` ou `tenantScopeOf` |
1292
+ | `authz/fields-by-schema` | sous `application/queries/` : `roles.includes(`, `.some(`, `.every(`, `.find(`, `.indexOf(`, une comparaison avec un `role`, `isAdmin` |
1293
+ | `aspects/outside-in` | un import de `@ddd-ts-kit/aspects/decorators` hors de `infrastructure/` d'un contexte et du répertoire de la plateforme : `presentation/`, le fichier de module, `published-language/`, le noyau partagé, `main.ts`, toute autre source (`domain/` et `application/` sont signalés par les deux règles `layer/*` à la place) |
1294
+ | `tracing/platform-only` | un import de `@ddd-ts-kit/tracing` ou d'un *package* `@opentelemetry/*` hors du répertoire de la plateforme, de `main.ts` et de `apps/`, tests compris |
1295
+ | `tenancy/scoped-statement` | avec `stack.tenancy` autre que `none`, dans `infrastructure/` d'un contexte, tests mis à part : sur Knex, l'exécuteur utilisé autrement que comme argument (`executor()(`, `executor().from(`, `executor().raw(`, `const db = ...executor();`) et tout `.raw(` ; sur Drizzle, instruction par instruction, une lecture, une mise à jour ou une suppression sur l'exécuteur dont le where n'a ni `tenantCondition(` ni `ownTenantCondition(` (en ligne ou dans la variable locale qu'il reçoit), une insertion dont la ligne n'a pas de `tenantScopeOf(`, une requête relationnelle (`executor().query`), un `execute(` brut, ou l'exécuteur gardé dans une variable ; sur les deux, un magasin de modèle de lecture construit sans son option `tenant`. Un where construit par une autre fonction n'est pas suivi |
1296
+ | `standard/no-parallel-interface` | une classe, une interface ou un alias de type déclaré comme `ILogger`, `AppLogger`, `LoggerService`, `LoggerInterface`, `IMetrics`, `MetricsService`, `ICache`, `CacheService`, `UserContext`, `CurrentUser`, `AuthContext`, `RequestContext` (hors de `presentation/` et de la plateforme), `UserSession`, `AuthSession`, `SessionData`, `PaginatedResult`, `PageResult`, `PaginatedResponse`, `Paginated`, `PaginationParams`, `PageParams`, `AuditLog`, `AuditLogEntry`, `AuditRecord`, `IJobScheduler`, `JobSchedulerService`, `CronScheduler`, `TaskScheduler`, `IScheduledJob`, `CronJob`, `ScheduledTask`, `BackgroundJob`, `IErrorReporter`, `ErrorReporterService`, `ErrorTracker`, `ExceptionReporter`, `CrashReporter`, `CacheInvalidator`, `CacheInvalidationService`, `CachePurgeService`, `CdnPurger`, `I` suivi d'un nom standard (`IClock`, `IIdGenerator`, `IUnitOfWork`, `ISession`, ...) ou un nom de `standardInterfaces` ; une classe qui implémente ou étend l'interface standard est un adaptateur et n'est pas signalée |
1297
+ | `events/outbox-required` | `stack.events` autre que `outbox` avec un ORM et sans dérogation `stack.events` ; dans `domain/` et `application/`, l'import d'un client de *broker* ou d'une implémentation de bus ou de relais ; un *publisher* qui dépend de plus que `IntegrationEventPublisher` ; un module de la plateforme qui distribue un *publisher* et ne nomme jamais `OutboxPublisher` |
1298
+ | `events/consumer-idempotent` | une classe de gestionnaire d'événement d'intégration (`application/integration/handlers/*.integration-handler.ts`) qui ne porte pas `@IntegrationEventHandler` et n'est pas non plus enregistrée dans son fichier de module sous un nom de consommateur ; un `@IntegrationEventHandler` dont le `consumer` n'est pas une chaîne littérale non vide ; un `on(` ou un `consume(` libre sans `processed` ; un nom de consommateur utilisé deux fois, dans les déclarations comme dans les enregistrements |
1299
+ | `domain-events/dispatched-on-save` | un appel de `pullEvents()` dans `application/` ou `infrastructure/` d'un contexte ; `DomainEventDispatcher` importé, listé comme dépendance ou reçu par un constructeur dans `application/commands/`, `application/queries/` ou un adaptateur de *repository*. Le fichier de module, les tests, la plateforme et `main.ts` sont laissés tranquilles |
1300
+ | `domain-events/transactional-before-commit` | un gestionnaire d'événement du domaine (une classe de `application/event-handlers/*.event-handler.ts` ou de `application/integration/publishers/*.publisher.ts`, ou toute classe portant `@DomainEventHandler`) qui s'exécute avant la validation (commit) et dépend de `Cache`, `CachePurger`, `Lock`, `RateLimiter`, `JobScheduler` ou `EventBus` du kernel, d'un port de `application/ports/` qui n'est pas un port de persistance (aucun adaptateur sous `infrastructure/persistence/`, ou un adaptateur ailleurs sous `infrastructure/`), de quoi que ce soit importé de `infrastructure/`, d'un autre contexte, de la plateforme ou d'un *package* autre que `@ddd-ts-kit/kernel` et `@ddd-ts-kit/auth` ; un gestionnaire après la validation (`phase: 'after-commit'` dans son décorateur, ou dans le troisième argument de chaque `.on(` du fichier de module qui le mentionne) qui dépend de `IntegrationEventPublisher`. `check-file` laisse les adaptateurs d'un port à la vérification complète |
1301
+ | `event-sourcing/state-in-apply` | dans une classe de `domain/aggregates/` qui étend `EventSourcedAggregate`, une écriture sur quelque chose de `this` (affectation, incrément, appel qui modifie) hors du constructeur, des méthodes statiques, des méthodes `apply<Fact>` et de `restore` ; `this.record(..)` dans une méthode apply |
1302
+ | `event-sourcing/past-tense` | dans un fichier de `domain/aggregates/` qui contient un agrégat en *event sourcing*, un nom importé de `domain/events/` dont le fait (les mots après le nom de l'agrégat) ne contient aucun participe passé, ou seulement un participe qui s'écrit comme son impératif en tête (`SetPrice`) |
1303
+ | `event-sourcing/store-behind-repository` | `EventStore`, `SnapshotStore`, `CheckpointStore`, `EventSourcedRepository`, `EventSerializer`, `CatchUpSubscription` ou `InMemoryEventStore` importé dans `domain/` ou `application/` |
1304
+ | `event-sourcing/projection-never-appends` | dans `application/projections/` ou un fichier qui déclare `@Projection`, `EventStore` ou `EventSourcedRepository` importé ; dans une projection déclarée `{ feed: 'event-store' }`, tout ce qui vient de `domain/repositories/` aussi |
1305
+ | `event-sourcing/upcaster-required` | un `defineStoredEvent` dont `encode` ne nomme aucun type, ou un type non déclaré dans le fichier, ou un type qu'il atteint et que le fichier ne déclare pas ; une version au-dessus de 1 sans *upcaster* depuis chaque version inférieure ; une version en dessous de la dernière enregistrée dans `stored-events.lock.json`, ou une version enregistrée dont la forme a changé. `ddd-ts-kit docs --stored-events` enregistre les nouvelles versions et n'en réécrit jamais une ; `docs --check` échoue tant qu'une version déclarée n'est pas enregistrée |
1306
+ | `context/extractable` | un fichier de module qui importe un autre contexte hors de son langage publié ; une persistance qui importe l'infrastructure d'un autre contexte ou nomme son schéma ; un gestionnaire qui importe le *repository* d'un autre contexte ; `export let` ou un `new Map`, `Set`, `WeakMap` exporté au niveau du module ; `deployable: true` sans `apps/<context>/main.ts` ; dans le style déclaré, un fichier de rôle qui importe quoi que ce soit d'un fichier de port d'un autre contexte |
1307
+ | `governance/recorded-deviation` | un commentaire `ddd-ts-kit-disable` ou `eslint-disable` dont la raison ne nomme aucun `ADR-<number>` avec un fichier `docs/adr/<number>-*.md` ; une règle mise à `off` (ou à `warn` en dessous de sa valeur par défaut), un répertoire supplémentaire, un *package* autorisé supplémentaire, un `guard` autre que `enforce` ou `stack.events` hors de l'outbox, sans entrée `deviations` `{ what, adr, reason }` qui pointe vers un enregistrement existant |
1308
+ | `declarations/role-declared` | style déclaré seulement : un fichier de rôle (autre que `*.command.ts` et `*.query.ts`, tenus par `authz/declared` ; `*.job.ts` sous `application/scheduling/` avec `@Scheduled` et `*.publisher.ts` avec `@DomainEventHandler` sont des rôles) qui n'exporte aucune classe portant le décorateur de son suffixe et de son dossier, ou une classe qui en porte une autre ; une classe déclarée dans un fichier qui n'est pas un fichier de rôle ; une option `consumer` ou `name` manquante, vide, une variable, une concaténation ou un template literal avec une substitution ; un nom de `@DomainEventHandler` qui diffère du littéral `name` de la classe d'événement importée ; tout décorateur de déclaration sur une classe sous `domain/` |
1309
+ | `declarations/barrel-current` | style déclaré seulement : avec `stack.assembly: barrel`, un `<context>.declarations.ts` manquant ou qui diffère d'un `export *` par fichier de rôle trié par chemin, suivi d'un `injected(Class, [Tokens])` par classe dont le constructeur reçoit quelque chose (lu depuis les signatures, avec leurs imports) ; avec `discovery`, un barrel qui existe encore, et un `<context>.dependencies.ts` qui diffère de ces instructions `injected` (son absence est acceptée tant qu'il n'en contiendrait aucune). Le message nomme le fichier de rôle manquant ou la classe dont la liste est périmée. Elle a besoin de l'arbre, c'est pourquoi `check-file` ne l'exécute pas |
1310
+ | `declarations/constructor-injection` | style déclaré seulement, dans un fichier de rôle : sur une classe dont le décorateur ne liste rien, un paramètre de constructeur optionnel, qui a une valeur par défaut, est un paramètre du reste ou est déstructuré, n'a pas de type, ou est typé par autre chose qu'un nom de classe (arguments de type mis à part) : une union, une intersection, un tableau, un nom qualifié, un type du langage, une interface, un alias de type, une enum ou une classe non exportée du fichier ou du fichier du projet d'où elle est importée ; une classe qui hérite son constructeur d'une classe que le projet ne sait pas lire. Sous `stack.dependencies: signature`, un décorateur qui porte une liste alors que le constructeur peut être lu : une liste qui le répète, une qui en diffère |
1311
+ | `ports/abstract-class` | dans un fichier de port (`domain/repositories/*.repository.ts`, `application/ports/*.port.ts`) : une classe qui n'est pas `abstract` ; un membre qui n'est pas une méthode ou une propriété abstraite (`static readonly tokenKey = '<text>'` mis à part) : un constructeur, un corps, un initialiseur, un membre `private`, `protected` ou `#` ; une `interface` exportée (un alias `type`, une forme de données, n'est pas signalé) |
1312
+ | `ports/implements-never-extends` | une classe portant `@Repository(P)`, `@Gateway(P)` ou `@Adapter(P)`, ou donnée à `bind(P).toClass(C)` ou `rebind(P).toClass(C)`, sans `implements P` ou avec `extends P` ; sous `infrastructure/`, une classe qui étend une classe importée d'un fichier de port |
1313
+ | `ports/no-token-constant` | un appel de `token` importé d'un *package* `@ddd-ts-kit/*` ; `Symbol(..)`, `Symbol.for(..)` ou une chaîne littérale comme premier argument de `bind`, `rebind`, `resolve`, `fake` ou `override` dans un fichier qui importe `@ddd-ts-kit/di` ; un nom en UPPER_SNAKE_CASE dans une liste de dépendances ; un fichier nommé `*.ports.ts` ou `tokens.ts` |
1314
+ | `ports/no-concrete-dependency` | une liste de dépendances qui nomme une classe importée de `infrastructure/` ou de `test/fakes/` d'un contexte ; dans le style déclaré, un paramètre de constructeur d'une classe déclarée qui ne liste rien, typé par une telle classe |
1315
+ | `ports/value-import` | une classe nommée dans une liste de dépendances, ou donnée à `bind` ou `rebind`, importée avec `import type` ou `import { type X }` |
1316
+ | `scheduling/job-triggers-use-case` | dans `application/scheduling/*.job.ts` : une classe exportée qui n'implémente pas `ScheduledJob` ; un `run` qui n'envoie aucune commande (pas d'appel `.execute(`) ; un *repository* dans la liste de dépendances ou le constructeur ; un import de `domain/aggregates/`, `domain/entities/`, `domain/services/` ou `infrastructure/`. Un enregistrement `builder.job(..)` d'une classe qui vit ailleurs |
1317
+ | `scheduling/job-name` | le premier argument de `@Scheduled(..)` ou de `.job(..)` qui n'est pas une chaîne littérale `<context>.<job-name>` en kebab-case minuscule avec le contexte de son fichier ; un `systemActor(..)` d'un fichier de *job* qui passe un autre nom ; un même nom de *job* utilisé deux fois dans le projet |
1318
+ | `errors/code-unique` | sous `"stack.refusals": "factories"`, dans le `domain/errors/` d'un contexte : un code posé une seconde fois, par une fabrique (`refusal('CODE', ..)`) ou par une classe (`readonly code = 'CODE'`), avec le fichier et la ligne du premier. Le même code dans deux contextes n'est pas signalé |
1319
+ | `schema/presentation-only` | sous `"stack.schemas": "shared"` : dans le noyau partagé hors de `presentation/`, un import d'une bibliothèque de validation ; dans `domain/`, `application/`, `infrastructure/`, `published-language/` et le noyau partagé hors de `presentation/`, un import d'un fichier de `presentation/` du noyau partagé |
1320
+ | `schema/no-duplicate-shape` | sous `"stack.schemas": "shared"`, dans la couche présentation d'un contexte, ou dans celle-ci et `presentation/` du noyau partagé : une classe DTO ou un schéma objet (`z.object`, `v.object`, `Type.Object`) qui déclare la forme d'un autre, avec l'endroit de l'original (le noyau partagé, puis `presentation/http/shared/`) ; deux contextes ne sont pas comparés, et les paramètres de requête HTTP et les formes qui portent encore un `TODO(domain)` du générateur ne sont pas jugés |
1321
+ | `errors/known-code` | sous `"stack.refusals": "factories"`, dans un `*.route.ts` : un code de la liste `errors` qu'aucun refus du contexte de la route ne pose, qu'aucune classe hors des contextes ne pose, et qui n'est pas un code du kit |
1322
+ | `domain/value-object-class` | sous `"stack.values": "classes"` : dans `domain/value-objects/` d'un contexte ou un `*.vo.ts` du noyau partagé hors de `presentation/`, aucune classe exportée ou une seconde, une classe qui n'étend ni `ValueObject` ni un autre objet-valeur du projet, un constructeur public, une classe concrète sans méthode statique, une fonction, une constante fléchée ou un objet littéral exporté (en ligne, par une liste ou par défaut) ; dans `domain/` et dans le noyau partagé hors de `presentation/`, une fonction exportée qui répond un enregistrement de données : des entrées de données, aucune décomposition, rien qu'un appel du modèle a construit |
1323
+
1324
+ D'autres identifiants appartiennent à `@ddd-ts-kit/eslint-plugin`, qui lit l'arbre syntaxique là
1325
+ où ce *checker* lit du texte : `domain/aggregate-encapsulation`,
1326
+ `domain/value-object-immutability`, `domain/no-generic-error`, `application/handler-shape`,
1327
+ `application/command-query-separation` et `clean/no-magic-number` parmi eux (`ddd-ts-kit rules`
1328
+ les liste tous). `rules` et `explain` les montrent comme « checked by @ddd-ts-kit/eslint-plugin »,
1329
+ la configuration les accepte sous `rules`, et `check --rule` sur l'un d'eux répond avec
1330
+ `npm run lint`. Un identifiant, `clean/formatted`, appartient au formateur : `rules` et `explain`
1331
+ le montrent comme « checked by Biome », et `check --rule` sur lui répond avec
1332
+ `npm run format:check`.
1333
+
1334
+ `guard` dans la configuration (`enforce`, `advise` ou `off`) dit au *hook* d'éditeur du *plugin*
1335
+ comment réagir à une violation après une écriture : la refuser, seulement la signaler, ou rester
1336
+ silencieux. `check` l'ignore, en dehors de la validation de sa valeur.
1337
+
1338
+ ### Taille des commits
1339
+
1340
+ `check --commit-size [--staged] [--strict]` compte les fichiers et les lignes sur le point d'être
1341
+ commités (les changements suivis depuis HEAD et les fichiers non suivis ; l'index seulement avec
1342
+ `--staged`) par rapport à `commit` dans la configuration (`{ "maxFiles": 20, "maxLines": 600 }`
1343
+ par défaut). Les documents générés, les lockfiles, `.dependency-cruiser.cjs`,
1344
+ `docs/context-map.md`, les barrels de déclarations et `.ddd-ts-kit/worker.json` ne sont pas
1345
+ comptés ; un fichier non suivi de plus de 1 Mo ne compte aucune ligne. Il nomme les tranches
1346
+ auxquelles appartiennent les changements : un cas d'usage, une couche d'un contexte, la
1347
+ plateforme. Indicatif : code de sortie 0, sauf avec `--strict` au-delà des limites. Hors d'un
1348
+ dépôt git, il le dit et sort avec le code 0. Le `lefthook.yml` généré l'exécute sur l'index à
1349
+ chaque *commit*, sans bloquer.
1350
+
1351
+ ### Limites
1352
+
1353
+ Le *checker* lit les sources avec un petit analyseur lexical et une lecture superficielle des
1354
+ déclarations, pas avec un compilateur. Il suit les spécificateurs relatifs, les alias du projet
1355
+ (`importAlias`, `$test/`, `$scripts/`, `$generated/`) et les *packages* nus, et classe les
1356
+ fichiers d'après leur chemin : un import par alias est jugé par chaque règle de frontière
1357
+ exactement comme le chemin relatif vers le même fichier.
1358
+
1359
+ - Les commentaires, les chaînes, les template literals et les expressions régulières sont
1360
+ sautés ; une expression régulière dans une position inhabituelle peut masquer le reste de sa
1361
+ ligne. La syntaxe `.tsx` n'est pas comprise.
1362
+ - Les réexports, les barrels, les `paths` de `tsconfig` autres que l'alias et les spécificateurs
1363
+ calculés ne sont pas résolus, si bien qu'un type qui atteint une couche par le barrel d'une
1364
+ couche autorisée n'est pas vu.
1365
+ - Déclarations : les déclarations `function` de premier niveau et les méthodes de classe sont
1366
+ lues ; les fonctions fléchées affectées à des constantes ou à des propriétés, les méthodes de
1367
+ littéraux d'objet et les fonctions imbriquées ne le sont pas, si bien que `clean/stepdown`,
1368
+ `clean/function-size` et `clean/no-flag-argument` ne les jugent pas.
1369
+ - `clean/stepdown` reconnaît les utilisations par leur nom : une fonction utilitaire masquée par
1370
+ une variable locale du même nom, ou une méthode privée appelée par un autre receveur que
1371
+ `this`, est mal jugée. Les méthodes protégées sont neutres.
1372
+ - `clean/no-commented-out-code` et `clean/no-noise-comment` sont des heuristiques sur la forme
1373
+ d'une ligne : de la prose qui se termine comme une instruction peut être signalée, du code
1374
+ écrit comme une phrase passe inaperçu.
1375
+ - `language/glossary-term` compare mot à mot avec une flexion anglaise grossière ; elle ne
1376
+ connaît pas les synonymes, et un terme de plusieurs mots couvre chacun de ses mots séparément.
1377
+ - `purity/*` cherche les jetons et les modules listés ; une enveloppe autour d'eux passe
1378
+ inaperçue.
1379
+ - `clean/no-escape-hatch` ne voit pas un `any` produit par inférence, ni une assertion de type
1380
+ vers un autre type que `any`.
1381
+ - `composition/only-in-module` voit `new` et `toClass` sur des noms importés ; une fonction
1382
+ fabrique ou un adaptateur réexporté lui échappe.
1383
+ - `events/name-format` vérifie la forme et le préfixe du contexte, pas la grammaire du fait.
1384
+ - `test/spec-required` et `test/builder-required` vérifient que le fichier existe, pas son
1385
+ contenu.
1386
+ - `authz/*`, `events/*` et `context/extractable` reconnaissent des noms et des formes : un import
1387
+ renommé, une définition de route construite à partir d'une variable ou d'un étalement, un test
1388
+ de propriété (`invoice.ownerId === actor.id`), un nom de schéma dans du SQL brut, une
1389
+ transaction entre contextes et un tableau ou un objet exporté modifiable passent inaperçus ; le
1390
+ contenu de `apps/<context>/main.ts` n'est pas jugé.
1391
+ - `governance/recorded-deviation` vérifie que l'enregistrement de décision existe, pas ce qu'il
1392
+ dit ; une entrée de dérogation qui ne justifie plus rien n'est pas signalée, et `limits` et
1393
+ `standardInterfaces` ne sont pas gouvernés.
1394
+ - `authz/actor-explicit` ne connaît aucun type : `.current()` ou `.require()` sans argument sur
1395
+ un concept du domaine nommé `scope` est signalé, une portée d'exécution gardée sous un autre nom
1396
+ ne l'est pas, et `stampCreation`, `stampUpdate` et `tenantScopeOf` ne sont vus que lorsqu'ils
1397
+ sont importés sous leur propre nom.
1398
+ - `aspects/outside-in` et les *packages* extérieurs des règles `layer/*` sont reconnus sur le
1399
+ spécificateur d'import : un décorateur ou un client réexporté par un fichier d'un endroit
1400
+ autorisé passe inaperçu.
1401
+ - `standard/no-parallel-interface` compare des noms déclarés entiers avec un tableau et ne lit
1402
+ pas le glossaire : un concept du domaine légitimement appelé `AuditLog` ou `CurrentUser` est
1403
+ signalé (retirez le nom dans `standardInterfaces` ou enregistrez une désactivation), une copie
1404
+ sous n'importe quel autre nom ne l'est pas, et l'exemption d'une classe lit les noms après
1405
+ `implements` et `extends`, pas ce vers quoi ils se résolvent.
1406
+ - Un décorateur de déclaration est vu quand il se trouve sur une classe de premier niveau et que
1407
+ son propre nom est importé de `@ddd-ts-kit/kernel/declarations` dans le même fichier. Un import
1408
+ renommé (`Requires as Needs`), un import d'espace de noms, un décorateur réexporté par un autre
1409
+ module, appliqué par une fonction utilitaire ou appelé comme une fonction, et un décorateur sur
1410
+ une expression de classe ou une classe imbriquée passent inaperçus (ESLint suit les renommages
1411
+ et les espaces de noms). Les options `consumer` et `name` ne sont lues que dans un littéral
1412
+ d'objet écrit dans l'appel du décorateur : gardées dans une variable, étalées ou construites par
1413
+ une fonction, elles sont signalées comme n'étant pas une chaîne littérale.
1414
+ - `declarations/role-declared` compare un nom de `@DomainEventHandler` avec `name = '...'` dans
1415
+ le corps de la classe d'événement, trouvée par un import direct relatif ou par alias : un nom
1416
+ posé dans un constructeur, un accesseur ou une classe de base, ou un événement atteint par un
1417
+ barrel, n'est pas comparé. `authz/declared` suit un import direct du message depuis un fichier
1418
+ de module.
1419
+ - `context/extractable` reconnaît un port à son fichier (`domain/repositories/*.repository.ts`,
1420
+ `application/ports/*.port.ts`) : un port d'un autre contexte atteint par un réexport n'est pas
1421
+ vu.
1422
+ - `composition/only-in-module` signale chaque `.include(` d'un fichier qui importe
1423
+ `@ddd-ts-kit/di` hors des endroits de composition, quel que soit le receveur.
1424
+ - `declarations/barrel-current` compare le texte exactement (fins de ligne mises à part) : un
1425
+ barrel reformaté est périmé. Elle ne vérifie pas que la racine de composition importe le
1426
+ barrel.
1427
+ - `clean/blank-lines` tient la partie bon marché de la disposition, le `return` et le bloc
1428
+ rembourré ; la ligne vide après un groupe de déclarations, autour de `if`, `for`, `try` et
1429
+ d'une instruction sur plusieurs lignes et entre les membres d'une classe est tenue par ESLint
1430
+ (`@stylistic`), qui fait autorité et la corrige.
1431
+ - `clean/braces` lit les instructions de contrôle comme du texte : une méthode ou une propriété
1432
+ nommée `if`, `for`, `while`, `else` ou `do` est distinguée par ce qui la suit, pas par un arbre
1433
+ syntaxique. ESLint (`curly`) fait autorité pour les accolades et le formateur pour leur
1434
+ emplacement.
1435
+ - `clean/formatted` est listée et expliquée, jamais jugée : le formateur du projet, Biome, en
1436
+ décide (`npm run format:check`).
1437
+ - `clean/one-export-per-file` lit les déclarations comme du texte : les types d'une signature
1438
+ sont les interfaces et les types exportés dont le nom apparaît dans le texte de l'export
1439
+ principal, une famille fermée est reconnue aux annotations de type de retour et aux noms des
1440
+ liaisons de niveau module. Un fichier que les deux analyseurs jugent différemment revient à
1441
+ ESLint ; ce que ni l'un ni l'autre n'explique va dans `onePerFile.allow`.
1442
+ - Le plafond de 100 lignes de `clean/function-size` est jugé sur les fonctions que lit ce
1443
+ *checker* : une fonction fléchée et le callback d'un `describe` sont tenus par ESLint
1444
+ (`clean-function-ceiling`).
1445
+ - Les règles `ports/*` lisent une liste de dépendances là où elle est écrite comme un littéral de
1446
+ tableau : le dernier argument d'un décorateur de déclaration, le second de `.toClass(..)` et de
1447
+ `.handledBy(..)`. Une liste gardée dans une constante ou construite par une fonction n'est pas
1448
+ lue. `bind`, `rebind`, `resolve`, `fake` et `override` ne sont des appels de liaison que dans un
1449
+ fichier qui importe `@ddd-ts-kit/di`. `ports/implements-never-extends` lit la classe liée par
1450
+ un import direct relatif ou par alias ; une classe d'un *package* ou derrière un barrel n'est
1451
+ pas jugée.
1452
+ - Le constructeur d'une classe déclarée est lu comme du texte, par le scanner du *checker* : le
1453
+ constructeur de la classe exportée qui porte le décorateur de son fichier de rôle, le sien ou le
1454
+ plus proche dont elle hérite par des imports du projet. Un type importé d'un *package* est
1455
+ présumé être une classe (le compilateur juge le fichier généré, qui l'importe comme une
1456
+ valeur) ; un type atteint par un barrel est présumé l'être aussi. Un constructeur surchargé par
1457
+ des signatures sans corps, ou construit par un mixin, n'est pas lu : écrivez un constructeur
1458
+ simple. `declarations/barrel-current` compare le texte exactement, si bien que le fichier
1459
+ généré n'est jamais reformaté à la main.
1460
+ - `scheduling/job-triggers-use-case` prend n'importe quel appel `.execute(` dans `run` pour une
1461
+ commande envoyée ; un *job* qui exécute autre chose, ou qui envoie sa commande depuis une
1462
+ fonction utilitaire, est mal jugé. `scheduling/job-name` compare des noms littéraux : un *job*
1463
+ enregistré sous un nom calculé est signalé pour son nom et échappe à la vérification
1464
+ d'unicité.
1465
+ - `domain-events/transactional-before-commit` lit les dépendances là où elles sont écrites : un
1466
+ littéral de tableau (le décorateur, `.toClass(Handler, [..])` dans le fichier de la classe ou
1467
+ dans le fichier de module) et le type d'un paramètre de constructeur, résolu par les imports de
1468
+ ce fichier. Un nom qui n'est pas importé, un port du kernel importé sous un autre nom et un
1469
+ effet atteint sans dépendance (un client de niveau module) ne sont pas vus. Un gestionnaire du
1470
+ style explicite est associé à ses enregistrements `.on(` par son nom de classe ou par une
1471
+ variable affectée depuis `new <Class>(` ; un adaptateur est une classe qui `implements` le port
1472
+ sous le nom qu'exporte le fichier du port.
1473
+ - Rien ne contraint ce qu'importent `shared-kernel/`, `platform/` et `main.ts`.
1474
+
1475
+ ## check-file
1476
+
1477
+ ```
1478
+ ddd-ts-kit check-file <path>
1479
+ ```
1480
+
1481
+ Exécute les règles par fichier (couches, contextes, nommage, événements, langage, pureté, code
1482
+ propre, *mocks*, composition, désactivations) sur un fichier sans parcourir le projet : conçu pour
1483
+ un *hook* d'éditeur ou d'agent après chaque écriture. Un fichier qui n'est pas une source du
1484
+ projet est ignoré.
1485
+
1486
+ ## rules, explain
1487
+
1488
+ ```
1489
+ ddd-ts-kit rules
1490
+ ddd-ts-kit explain <rule-id>
1491
+ ```
1492
+
1493
+ `rules` liste les identifiants, les descriptions et les sévérités actuelles, et dit lesquels
1494
+ ESLint vérifie. `explain` affiche la règle, pourquoi elle existe, un mauvais et un bon exemple,
1495
+ et la syntaxe de désactivation.
1496
+
1497
+ ## Développement
1498
+
1499
+ ```
1500
+ node node_modules/@typescript/native/bin/tsc -b packages/cli
1501
+ npx vitest run packages/cli # la suite rapide
1502
+ npm run build && npm run ci:test:e2e # la suite lente : DDD_TS_KIT_E2E=1, un fichier après l'autre
1503
+ node packages/cli/dist/bin.js check --self
1504
+ npm run clean:self --workspace ddd-ts-kit # les règles clean/* sur ce package
1505
+ npm run clean:packages --workspace ddd-ts-kit # les mêmes règles sur les sources de chaque autre package
1506
+ ```
1507
+
1508
+ La suite lente vit dans `test/e2e/`, dans ses fichiers de *spec* et leurs fichiers de support ;
1509
+ hormis pour les *specs* `first-minute`, elle génère chaque projet avec chaque genre, un *job* sur
1510
+ un intervalle et un sur une expression cron avec un fuseau horaire, `adapter --decorators`,
1511
+ `generate service` et `generate worker` compris. Elle s'exécute dans le processus, sans
1512
+ conteneur :
1513
+
1514
+ - `zero-violation.spec.ts` (fait aussi partie de la suite rapide) : les 39 *stacks* de `STACKS`
1515
+ dans `test/e2e/stacks.ts`, parmi lesquelles les trois *loggers* et les deux familles de câblage
1516
+ avec `--errors sentry`, passent `check` et `docs --check` telles que générées.
1517
+ - `typecheck.spec.ts` : les 36 d'entre elles que le *workspace* sait compiler (jest, prisma et
1518
+ typeorm à part) passent la vérification de types contre les sources des *packages* du
1519
+ *workspace*.
1520
+ - `proven-stacks.spec.ts` : les huit projets de `PROVEN` sont installés comme le serait un kit
1521
+ publié et tenus à tout ce qu'exécute leur propre *pipeline* : `check` avec zéro violation et
1522
+ zéro avertissement, ESLint avec le preset (le test lit d'abord dans `eslint --print-config` que
1523
+ les règles des ports, de la planification, de la forme des fichiers et de la disposition sont
1524
+ actives), dependency-cruiser avec la configuration générée, knip, leurs *specs* (chaque *job*
1525
+ exécuté, par `runNow` sur tsyringe, et tenu à la commande qu'il a envoyée, un adaptateur
1526
+ décoré, l'extraction de chaque contexte), leurs *specs* de bout en bout (401 sans jeton, 403
1527
+ sans la permission, 2xx avec elle, avec des jetons signés dans le test, et la trace de la
1528
+ requête HTTP en un seul arbre), leur build (le *worker* et le service extrait compris) et leurs
1529
+ documents générés. Ce sont la *stack* par défaut, qui est déclarée ; déclarée avec
1530
+ `--authz rebac --broker rabbitmq --ci gitlab` ; déclarée avec
1531
+ `--orm knex --assembly discovery`, seule et derrière `--broker nats` avec `--errors sentry`
1532
+ (aucun DSN n'est configuré : rien n'est remonté, le câblage est linté, construit et exécuté) ;
1533
+ le style explicite sur la *stack* par défaut et sur
1534
+ `--authz rebac --broker rabbitmq --ci gitlab` ; et le câblage manuel (`--di manual`) sur
1535
+ `--authz abac --orm knex` et sur
1536
+ `--http fastify --orm knex --broker rabbitmq --cache redis --errors sentry --logger winston`.
1537
+ - `first-minute.spec.ts`, `first-minute-proven.spec.ts`, `first-minute-no-context.spec.ts` et
1538
+ `first-minute-two-contexts.spec.ts` : un projet est vert dès sa première minute, avant que quoi
1539
+ que ce soit soit généré. La première exécute le démarrage rapide du README racine ligne à ligne
1540
+ tel qu'il est écrit, sur pnpm et, dans les termes que le README donne pour lui, sur npm :
1541
+ `init` seul, l'installation et le `verify` que nomme la note de `init` (tenus aussi au README et
1542
+ au `CLAUDE.md` du projet), puis un contexte, un second en aval de lui, un cas d'usage avec sa
1543
+ route, `docs:generate`, avec un `verify` à chaque étape ; et la séquence avec laquelle le défaut
1544
+ de 0.1.0 a été signalé, des contextes demandés à `init`. La deuxième fait de même sur les sept
1545
+ autres *stacks* éprouvées, un gestionnaire de paquets chacune à tour de rôle : aucun contexte,
1546
+ un, deux avec un amont, un premier cas d'usage. Les deux installent pour de vrai depuis le kit
1547
+ empaqueté et tournent dans les *jobs* de gestionnaires de paquets du *pipeline*
1548
+ (`ci:test:managers`). Les deux dernières prennent chaque *stack* que le *workspace* sait
1549
+ exécuter, installée comme le serait un kit publié, sans contexte et avec deux : aucun constat
1550
+ des quatre analyseurs, des *specs* qui passent, un build, et les contrats écrits par `init`
1551
+ égaux à ce que génèrent les scripts du projet.
1552
+ - `package-managers.spec.ts` : un projet par gestionnaire de paquets sur la *stack* par défaut et
1553
+ sur NestJS avec Prisma, chaque genre généré, installé pour de vrai depuis le kit empaqueté :
1554
+ l'installation figée d'un *pipeline*, le build, `verify`, et `affected` depuis la CLI installée.
1555
+ - `first-class.spec.ts` : la *stack* de première classe, `--validation class-validator --orm knex --authn
1556
+ keycloak --authz rebac --cache redis --http-cache varnish --audit on --errors sentry --logger pino --style declared`
1557
+ (mode des anciens décorateurs), trois fois, avec `--broker redis` (un seul serveur pour le cache
1558
+ et le bus d'événements), `--broker nats` et `--broker kafka` : les mêmes analyseurs avant et
1559
+ après les décisions métier de `test/first-class-proof.ts`, un *job* en échec et un événement
1560
+ envoyé en file de rejet remontés au *reporter* en mémoire de l'application de test, puis une
1561
+ liste restreinte par les relations, le journal d'audit lu par sa route, l'identifiant de
1562
+ corrélation d'une requête HTTP suivi jusqu'à l'événement publié, cet événement porté d'un
1563
+ contexte jusqu'au gestionnaire d'un autre, et le tout vérifié comme une seule trace avec
1564
+ `expectSpanTree`.
1565
+ - `error-reporting.spec.ts` : `--errors sentry` dans les deux familles de câblage (déclaré sur
1566
+ tsyringe derrière RabbitMQ et l'outbox, à la main sans base de données), installé comme le
1567
+ serait un kit publié : les mêmes analyseurs sans constat, les trois points d'entrée construits
1568
+ avec le *reporter* comme deuxième import, puis une *spec* écrite dans le projet
1569
+ (`fixtures/reported-failures.spec.ts.txt`) qui fait échouer une requête derrière sa route : une
1570
+ remontée sur un `InMemoryErrorReporter`, nommée d'après la requête, et un vidage quand
1571
+ l'application s'arrête ; un `NoopErrorReporter` et une application qui fonctionne sans
1572
+ `SENTRY_DSN` ; et, avec un DSN qui nomme un point d'accès de l'interface de boucle locale
1573
+ ouvert par la *spec*, exactement un événement envoyé par le vrai SDK, qui porte
1574
+ l'environnement, la version publiée et l'identifiant de l'acteur, et ni l'identifiant de
1575
+ connexion ni la chaîne de requête de la requête HTTP. Aucun serveur Sentry ou GlitchTip n'est
1576
+ impliqué.
1577
+ - `violations.spec.ts` et `new-rule-violations.spec.ts` : dans un projet de la *stack* par
1578
+ défaut, une violation délibérée à la fois doit être attrapée par l'analyseur qui tient la
1579
+ règle : une erreur générique dans le domaine (ESLint), un domaine qui importe son infrastructure
1580
+ (dependency-cruiser), un export inutilisé (knip), un barrel périmé (le *checker* et
1581
+ `docs --check`), et par le *checker* comme par ESLint une commande qui dit que personne ne peut
1582
+ l'envoyer, un gestionnaire qui ne se déclare plus, un gestionnaire d'événement nommé d'après un
1583
+ autre événement, puis `ports/abstract-class`, `ports/implements-never-extends`,
1584
+ `ports/no-token-constant`, `ports/no-concrete-dependency`, `ports/value-import`,
1585
+ `clean/no-hash-private`, `clean/blank-lines` (un `return` collé et un bloc rembourré),
1586
+ `clean/braces`, `clean/one-export-per-file`, `clean/file-size`, le plafond de 100 lignes de
1587
+ `clean/function-size`, `scheduling/job-triggers-use-case` et `scheduling/job-name` ; deux
1588
+ *jobs* sous un même nom sont attrapés par le *checker*.
1589
+
1590
+ Les événements, le cache et la base de données restent dans le processus sauf si
1591
+ `DDD_TS_KIT_TEST_REDIS_URL`, `DDD_TS_KIT_TEST_NATS_URL`, `DDD_TS_KIT_TEST_KAFKA_BROKERS`,
1592
+ `DDD_TS_KIT_TEST_AMQP_URL` et `DDD_TS_KIT_TEST_DATABASE_URL` (*stack* de première classe
1593
+ seulement, migrée par le test) nomment de vrais services : l'événement traverse alors vraiment le
1594
+ *broker*.