ddd-ts-kit 0.18.0 → 0.19.0

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 (37) hide show
  1. package/CHANGELOG.md +1273 -0
  2. package/README.fr.md +2 -1
  3. package/README.md +1 -0
  4. package/dist/check/catalogue/architecture-rules.d.ts.map +1 -1
  5. package/dist/check/catalogue/architecture-rules.js +2 -0
  6. package/dist/check/catalogue/architecture-rules.js.map +1 -1
  7. package/dist/check/explain/events-docs.d.ts.map +1 -1
  8. package/dist/check/explain/events-docs.js +13 -2
  9. package/dist/check/explain/events-docs.js.map +1 -1
  10. package/dist/check/fixtures/event-sourcing-fixtures.js +3 -3
  11. package/dist/check/fixtures/event-sourcing-fixtures.js.map +1 -1
  12. package/dist/check/fixtures/events-fixtures.d.ts.map +1 -1
  13. package/dist/check/fixtures/events-fixtures.js +30 -1
  14. package/dist/check/fixtures/events-fixtures.js.map +1 -1
  15. package/dist/check/fixtures/language-fixtures.d.ts.map +1 -1
  16. package/dist/check/fixtures/language-fixtures.js +1 -1
  17. package/dist/check/fixtures/language-fixtures.js.map +1 -1
  18. package/dist/check/rules/events-name-declared-once.d.ts +3 -0
  19. package/dist/check/rules/events-name-declared-once.d.ts.map +1 -0
  20. package/dist/check/rules/events-name-declared-once.js +51 -0
  21. package/dist/check/rules/events-name-declared-once.js.map +1 -0
  22. package/dist/init/manifest/versions.js +24 -24
  23. package/dist/progress/format-progress.js +4 -0
  24. package/dist/progress/format-progress.js.map +1 -1
  25. package/dist/progress/progress.d.ts +2 -0
  26. package/dist/progress/progress.d.ts.map +1 -1
  27. package/dist/progress/read-progress.js +1 -0
  28. package/dist/progress/read-progress.js.map +1 -1
  29. package/dist/progress/read-slices.d.ts +5 -1
  30. package/dist/progress/read-slices.d.ts.map +1 -1
  31. package/dist/progress/read-slices.js +17 -7
  32. package/dist/progress/read-slices.js.map +1 -1
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/package.json +5 -3
  36. package/templates/init/rules/domain.md.tpl +1 -1
  37. package/templates/pipeline/package-scripts.json +1 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,1273 @@
1
+ # Changelog
2
+
3
+ What each version changes for someone who uses the kit. The packages of `packages/` are released
4
+ together, under one version; the notes of each [GitHub release](https://github.com/gbourgeat/ddd-ts-kit/releases)
5
+ list the pull requests behind it.
6
+
7
+ ## 0.19.0
8
+
9
+ **`status` and `worker-scope` read every slices table.** `docs/architecture/slices.md` may plan
10
+ the work in several tables, one per milestone or story, under the same header: only the first was
11
+ read, so `status` counted a part of the plan and a worker started on a later table got an empty
12
+ briefing. Every table is now read, in the order of the file, and `status` names a row number that
13
+ more than one row carries (`duplicates` in `--json`).
14
+
15
+ **The notes travel with the CLI, and a version gap is said.** The `ddd-ts-kit` package ships this
16
+ file, so an agent finds the steps of a version in `node_modules/ddd-ts-kit/CHANGELOG.md`, offline.
17
+ The plugin states the version of the packages its skills describe: when the installed
18
+ `ddd-ts-kit` is another one (below 1.0.0, another minor version), the session start says so and
19
+ what to do, upgrade the packages together or update the plugin. It never blocks.
20
+
21
+ **Every domain event declares its name once, consumed or not.** `events/name-declared-once`
22
+ (`ddd-ts-kit check`) reports a class of `domain/events/` without `static readonly eventName`, and
23
+ one whose `readonly name` writes the name again instead of reading it. Until now an event that no
24
+ handler or projection reacted to could keep the form 0.5 wrote, `readonly name = '..'` alone, and
25
+ the first reaction attached to it failed to compile. To move, give each reported class
26
+ `static readonly eventName = '<context>.<aggregate>.<fact>'` and `readonly name = <Event>.eventName`.
27
+
28
+ ## 0.18.0
29
+
30
+ **The tenant check of `RoleBasedAuthorizer` follows memberships and the tenant of the work.**
31
+ With `tenantOf`, an actor of several tenants (its `tenants`, from a list claim or the
32
+ `organization` claim of Keycloak) was refused in each of its own tenants and listed nothing, and
33
+ a system actor of a per-tenant job was refused in the tenant it ran for. The guard of the bus and
34
+ `PolicyAccess` now give the authorizer the tenant of the work (`workTenantId` on `CheckRequest`
35
+ and `ListRequest`), read from the execution scope that `authorizationModule` wires. A request is
36
+ refused when the actor does not belong to the tenant it targets, when that target is not a tenant
37
+ id, and when it is another tenant than the one the work runs in; a system actor belongs to the
38
+ tenant its work runs in. A list is filtered on the tenant of the work, and is empty for an actor
39
+ of several tenants outside of a tenant's work. `RoleBasedAuthorizer` now refuses at construction
40
+ a cross-tenant role or a system role that no role declares, and `crossTenantRoles` without
41
+ `tenantOf`: each silently did nothing. To move, declare the role named by `systemRole` or
42
+ `crossTenantRoles` in `roles`, and give `tenantOf: tenantOfContext` where `crossTenantRoles` is
43
+ set. `authorizationMiddleware` and `PolicyAccess` built by hand take the `scope` option to do the
44
+ same.
45
+
46
+ **A policy is asked, or the use case fails.** The bus decides the permission of a use case before
47
+ its handler runs, with the identifier of the element at most; a policy, the part of the right
48
+ that needs the element, applies only when the handler asks `Access` once the element is loaded.
49
+ Forgetting to ask is now caught twice. `authz/policy-asked` (`ddd-ts-kit check`) reports the
50
+ handler of a command or query whose permission a policy refines (`@Policy` or `definePolicy`)
51
+ when it never asks `Access` about that permission. At run time, `authorizationModule` refuses the
52
+ result of such a handler with `PolicyNotAskedError`, inside the unit of work, so that its change
53
+ is rolled back: the guard gains a step that wraps the handler alone, `UseCaseGuard.within`, which
54
+ the buses of `kernelModule()` run with `aroundHandler`, a new method of the in-process buses that
55
+ places a middleware right around the handler, inside every other one: a middleware that answers
56
+ in place of the handler, as a spec that records the commands a job sends, is not judged. Without
57
+ the container, `policiesAskedMiddleware({ map, refined })` is the same check. The generators
58
+ follow: a command or a plain query generated once a policy refines its permission receives
59
+ `Access` and asks it, about the message until the element is loaded, with a `TODO(domain)` to
60
+ ask about that element instead; `generate policy` writes a use case of its permission again that
61
+ way when its files are still as generated, and otherwise leaves a `TODO(domain)` where the policy
62
+ is asked, which the rule accepts and `ddd-ts-kit todo` lists.
63
+
64
+ To move: a handler whose permission a policy refines calls `await this.access.authorize(actor,
65
+ permission, element)` before its change; `npx ddd-ts-kit check --rule authz/policy-asked` lists
66
+ the ones that do not. A project that binds its own `Access` instead of `PolicyAccess` passes
67
+ `policiesAsked: false` to `authorizationModule`, since only `PolicyAccess` records the questions.
68
+ A bus of your own, given through `kernelModule({ commandBus })`, runs `UseCaseGuard.within` with
69
+ `aroundHandler`.
70
+
71
+ **A domain event handler names its event once.** `@DomainEventHandler` and `@On` no longer take
72
+ the name of the event: they read the one its class declares, `static readonly eventName`. A
73
+ handler is now `@DomainEventHandler(InvoiceIssued)`, `@DomainEventHandler(InvoiceIssued, { phase:
74
+ 'after-commit' })` or, with a dependency list, `@DomainEventHandler(InvoiceIssued, [Mailer])`; a
75
+ method of a projection applies a domain event with `@On(AppointmentBooked)`, as it already did an
76
+ integration event. The compiler refuses an event class without a `static readonly eventName`, or
77
+ one that declares it empty, and the decorator refuses at declaration a name that is empty when it
78
+ is read. `NamedDomainEventType` (`@ddd-ts-kit/kernel/declarations`) is the type of such a class.
79
+ Since a handler can no longer be written under another name than the one its event carries,
80
+ `declarations/role-declared` (in `ddd-ts-kit check` and `@ddd-ts-kit/eslint-plugin`) no longer
81
+ judges that name. In the declared style, `generate event-handler` and `generate integration-event`
82
+ refuse an event class that does not declare its name, and say the two lines to write.
83
+ `@IntegrationEventHandler(definition, { consumer })` is unchanged: the consumer name is the
84
+ identity of the handler, not of the event.
85
+
86
+ To move:
87
+
88
+ 1. give every domain event class that declares only `readonly name = '..'` its
89
+ `static readonly eventName = '..'` and `readonly name = <Event>.eventName` (the classes
90
+ generated since 0.7 already have them);
91
+ 2. remove `name: ..` from the options of `@DomainEventHandler` and `@On`, and the options object
92
+ when nothing is left in it: `@DomainEventHandler(X, { name: X.eventName })` becomes
93
+ `@DomainEventHandler(X)`, `@DomainEventHandler(X, { name: X.eventName }, [Mailer])` becomes
94
+ `@DomainEventHandler(X, [Mailer])`;
95
+ 3. `npx tsc --noEmit` lists what is left.
96
+
97
+ **A quieter, more legible documentation website.** The site takes the pine green of the domain
98
+ layer of its diagrams as its accent instead of the default blue, cool slate greys, and IBM Plex
99
+ Sans and Plex Mono served with the site (no request to a font service). Headings, links, tables,
100
+ inline code, the current page of the sidebar and the cards of the home page are restyled; keyboard
101
+ focus stays visible and reduced motion is respected.
102
+
103
+ **Codex and GitHub Copilot, beside Claude Code.** `init` writes the guide of the project in
104
+ `AGENTS.md`, which Codex, Copilot and most coding agents read, and `CLAUDE.md` imports it; a
105
+ project initialised before keeps its `CLAUDE.md`, and `ddd-ts-kit sdk` writes in whichever of the
106
+ two it has. The rules by layer of `.claude/rules/` are also written for Copilot in
107
+ `.github/instructions/<layer>.instructions.md`, scoped by `applyTo`;
108
+ `.github/copilot/settings.json` declares the plugin of the kit, which the Copilot cloud agent
109
+ installs and VS Code recommends; with `--ci github`, `copilot-setup-steps.yml` installs the
110
+ project before the cloud agent starts. The plugin installs in Codex
111
+ (`codex plugin marketplace add gbourgeat/ddd-ts-kit`, from `.agents/plugins/marketplace.json`)
112
+ and in the Copilot CLI (`copilot plugin marketplace add gbourgeat/ddd-ts-kit`). Its guard reads
113
+ the patches of Codex and the tool calls of Copilot: a hand-written file of a generated role is
114
+ refused there as in Claude Code, and a stop with architecture errors is refused once per session
115
+ on a host that does not say it already was. Codex cannot ask the user, so a suppression or a
116
+ change of the rules is announced there instead of put to the project owner. The agent
117
+ configuration of Copilot and of Codex joins the files whose change the guard puts to the owner.
118
+
119
+ ## 0.17.0
120
+
121
+ **Names of the business, never `target`.** The command handler templates named the aggregate
122
+ they load or build `target`, and every project generated from them inherited the word. A
123
+ generated handler now names it after its concept, `const invoice = Invoice.create(..)`, and a
124
+ generated search names its lines after what it searches (`invoice`, `invoices`, never `item`).
125
+ A new rule, `naming/business-name`, held by `ddd-ts-kit check` and by `@ddd-ts-kit/eslint-plugin`
126
+ (`ddd-ts-kit/naming-business-name`), reports a variable, a loop variable, a destructured binding,
127
+ a parameter or a property of `domain/`, `application/` and the shared kernel named `target`,
128
+ `targets`, `data`, `item`, `items`, `obj`, `object`, `thing`, `things`, `stuff`, `info`,
129
+ `element`, `el`, `tmp`, `temp`, `foo`, `bar` or `baz`. ESLint fixes what it can name: a variable
130
+ built by `X.create(..)` or `new X(..)`, read from a repository (by the type of the call when the
131
+ parser has types, as the presets give it, by `xRepository.findById(..)` otherwise), or both
132
+ (`found ?? X.create(..)`) is renamed with every use to the camel name of the concept, unless that
133
+ name is taken; a loop variable is reported with the singular of its collection. The list is
134
+ `businessNames` of the config (`{ "words": [..], "allow": [..] }`), which the `words` and `allow`
135
+ options of the ESLint rule come over. `ddd-ts-kit explain naming/business-name` gives the reason
136
+ and both forms; the clean code and ubiquitous language pages say why `target`, `data` and `item`
137
+ hide the language.
138
+
139
+ The rule is on in every project, new or existing: a config without `businessNames` reads the
140
+ default list. To move:
141
+
142
+ 1. `npx eslint . --fix` renames the bindings whose initializer names the concept (in the example
143
+ application, 23 of 27);
144
+ 2. `npx eslint .` and `npx ddd-ts-kit check` list what is left, each message with the name to
145
+ write when the code says it (a loop variable, the result of a domain service): rename them;
146
+ 3. a word that is the term of your business (`item` of a catalogue, `object` of a relationship
147
+ model) goes in `businessNames.allow` of the config.
148
+
149
+ **The documentation is a website.** Every page of the documentation, the README of every package,
150
+ the page of every ESLint rule and the documents of the example are published at
151
+ <https://gbourgeat.github.io/ddd-ts-kit/>, in English and in French under `/fr/`, with search,
152
+ a sidebar that follows the reading path of the concepts, a dark theme and the diagrams of the
153
+ pages. The Markdown files of the repository stay the only source: `site/` assembles them at each
154
+ build (a private workspace, never published), `npm run site:dev` previews it, and the site is
155
+ deployed to GitHub Pages from `main`. A link or an anchor that leads nowhere fails its build,
156
+ which the CI runs on every pull request that touches the documentation.
157
+
158
+ ## 0.16.1
159
+
160
+ **The documentation is in French too.** Every reader-facing page of the repository (the root
161
+ pages, `docs/`, the README of every package, the page of every ESLint rule and the docs of the
162
+ example) has a French translation next to it, `X.fr.md`, reached by a switcher on its first
163
+ line; start at `README.fr.md` or `docs/README.fr.md`. A French page records the version of the
164
+ English page it translates, so a change to the English page without its translation is
165
+ reported. Release notes, agent instructions (`CLAUDE.md`, `.claude/`, `plugin/`), generated
166
+ files and the partly generated `CONTEXT.md` stay in English only.
167
+
168
+ **The pages teach with diagrams and samples that compile.** The concepts, guides, tools pages,
169
+ package READMEs and the example docs are enriched with worked examples and hand-written SVG
170
+ diagrams (one English and one French drawing each, readable in dark mode and on npm). A
171
+ TypeScript block marked `<!-- checked sample -->` is compiled against the packages of the
172
+ workspace, in both languages, so a sample cannot drift from the API; several samples that no
173
+ longer matched the API were corrected on the way.
174
+
175
+ **Two new checks.** `npm run docs:translations` (in `ci:docs`) reports a page without its
176
+ switcher, a missing or stale French page, a link or anchor that leads nowhere, a French page
177
+ that links to an English page whose French sibling exists, and a diagram without its French
178
+ drawing or malformed. `npm run docs:samples` (in `ci:typecheck`) compiles every checked sample.
179
+ [docs/translation.md](docs/translation.md) says what is translated, the shape of a French page,
180
+ the glossary, the style of the diagrams and how to keep it all current.
181
+
182
+ ## 0.16.0
183
+
184
+ **A new project writes its values as classes, and the kit checks it.** `init` writes
185
+ `"stack.values": "classes"` (`--values classes|free`), under which a new rule,
186
+ `domain/value-object-class`, held by `ddd-ts-kit check` and by `@ddd-ts-kit/eslint-plugin`
187
+ (`ddd-ts-kit/domain-value-object-class`), judges the model. A file of `domain/value-objects/` (or
188
+ a `*.vo.ts` of the shared kernel) exports exactly one class extending `ValueObject`, directly or
189
+ through another value object of the project, with a private or protected constructor and static
190
+ factories, and no function, arrow constant or object literal, inline, by an export list or by
191
+ default; the types of its props may sit next to it. In `domain/` and in the shared kernel outside
192
+ `presentation/`, an exported function that answers a record of data,
193
+ `export const money = (amount, currency) => ({ amount, currency })`, is reported with the command
194
+ that writes the class (`npx ddd-ts-kit generate value-object`): an object is a record of data when
195
+ it holds data (a literal, a parameter, a call of the language), no spread and nothing a call or
196
+ `new` of the model built. A domain service, a state machine, a specification answering a boolean
197
+ or functions only, a record of value objects the function builds, a reducer that spreads its state
198
+ and a function declaring a return type of a package are not concerned; a decision answered as a
199
+ plain record is. `ddd-ts-kit explain domain/value-object-class` gives the reason and both forms.
200
+
201
+ A config without the key reads as `free`: an existing project upgrades without a change, and
202
+ nothing is reported. To move: set `"stack.values": "classes"`, run
203
+ `npx ddd-ts-kit check --rule domain/value-object-class` and ESLint, write each reported value as a
204
+ class with `generate value-object` (its checks in the static factory, its helpers as methods, its
205
+ callers on `Money.of(..)` and `equals`), make each reported constructor private and add the
206
+ factory the rule names. The steps are in `docs/CONFIG.md`; until they are done, the rule can stay
207
+ at `warn` under `rules`, recorded in `deviations`.
208
+
209
+ **The text checker reads a class whose heritage holds an object type.** `class Money extends
210
+ ValueObject<{ readonly cents: number }> {` was read as a class whose body was the object type:
211
+ its methods and constructor are now found, for every rule that reads them.
212
+
213
+ **Why a value object is a class.** `docs/concepts/entities-and-value-objects.md` explains it with
214
+ three diagrams (the two forms, what the rule reads, how an answered object is read entry by
215
+ entry) and both forms side by side: equality without identity, invariants checked once in
216
+ the factory, behaviour next to the data, refusals from static factories, immutability and
217
+ `instanceof` in the boundary mappers. The domain rules `init` writes and the `scaffold` and
218
+ `clean-code` skills say the same.
219
+
220
+ **The example models its values as classes.** In the analytics context, the outcome the occupancy
221
+ measure reads and the figures it answers are value objects, `MeasuredOutcome` and
222
+ `OccupancyFigures`, each with its spec. Each checks its rules in its factory: an outcome never
223
+ ends before it starts, figures are whole numbers, none below zero, with at least as many
224
+ appointments held as completed and missed ones, refused through `OccupancyError`.
225
+ `CalendarDesk` and `FreedSlot` stay parameter objects: they group the arguments of one factory,
226
+ and `FreedSlot` holds a `Calendar`, an entity no equality by value can compare.
227
+
228
+ ## 0.15.0
229
+
230
+ **A context written whole: the identity preset** ([Bootstrap with well-designed contexts](docs/guides/bootstrap-with-well-designed-contexts.md)).
231
+
232
+ - `init --preset identity` and `generate context <name> --preset identity` write a generic
233
+ context instead of an empty one: accounts provisioned from the identity provider at the first
234
+ sign-in (no password kept), organisations (the tenants under `--tenancy shared-table`, their
235
+ tables then under a decision record), members with roles of a declared vocabulary, invitations
236
+ by token with an expiry, delegations of rights for ninety days at most; seven policies, the
237
+ published language (`member.joined`, `member.role-changed`, `account.deactivated`...), a
238
+ search, controllers, specs with builders and fakes, the Knex or Drizzle adapters, and the port
239
+ `IdentityProviderAdmin`, kept in line at least once by consumers of the published facts, with a
240
+ Keycloak 26 adapter given as a recipe (run once by hand against Keycloak 26.8.0, not in the
241
+ pipeline). Nobody changes their own role or acts above it, whatever
242
+ a delegation lends, and a change decided under a delegation names it (`delegationId`); every
243
+ change of a member saves its organisation, so two owners who remove each other at once cannot
244
+ both commit; a deactivated account leaves its organisations; an address the provider did not
245
+ verify (`email_verified`) provisions no account. With tenants and Keycloak, the authenticator
246
+ reads the `organization` claim as the tenants of the person. Written for the declared style on
247
+ Knex or Drizzle on PostgreSQL, Zod or class-validator, Express or Fastify, with a token
248
+ verifier, and for both `stack.schemas`: shared, its identifier answers are `acceptedSchema` and
249
+ the token of an invitation one shared schema; per route, each route keeps its own; another stack is refused with the flag to change, and so is a second context of the
250
+ same preset. Nothing is recorded in the config. Which other contexts deserve one:
251
+ [Context presets](docs/proposals/context-presets.md).
252
+ - `@ddd-ts-kit/auth`: `SignedInGrantsAuthorizer({ grants, otherwise })` grants every signed-in
253
+ person (an actor of type `user`) the same permissions before the model of the project decides
254
+ the rest, whatever that model is. The preset wraps `createAuthorizer` in it, its grants in
255
+ `platform/auth/signed-in-grants.ts`.
256
+ - `@ddd-ts-kit/auth`: the default mappings of `JwtAuthenticator` and `keycloakAuthenticator` read
257
+ a tenant claim that lists several tenants (the `organization` claim of a member of several
258
+ organizations) as the `tenants` attribute of the actor, among which a request names one, where
259
+ they gave no tenant; and both give the attributes `username`, `email` and `emailVerified`
260
+ (`email_verified`), where `JwtAuthenticator` gave none.
261
+ - The barrels and dependency files the CLI writes set a call that takes several lines apart by
262
+ blank lines, as `@stylistic/padding-line-between-statements` asks.
263
+ - Plugin 0.22.0: the skill `bootstrap` proposes the preset for a generic identity context.
264
+
265
+ **The example serves several networks of practices, one tenant each** (its ADR-0003, accepted).
266
+
267
+ - `@ddd-ts-kit/knex`: `addTenantColumn(knex, { schema, table, tenantOfExistingRows })`, the
268
+ migration of a table of a project that becomes multi-tenant: the rows already there go to the
269
+ tenant named (refused when there are rows and none), then the column is required and indexed; a
270
+ table that has the column is left alone. `derivedTable(context, query, alias)` selects from a
271
+ query confined by `tenantTable`, for a measure grouped then paged by its output columns.
272
+ - `@ddd-ts-kit/knex`: `addOutboxTenantColumn(knex, { schema, tenantOfUnpublished })` gives the
273
+ events not published yet (pending or dead) the tenant the database served, so that their
274
+ consumers run in it.
275
+ - `@ddd-ts-kit/http`: `servedTenantOf(resolver, served)` answers 404 to a tenant the deployment
276
+ does not serve, whoever names it, and reads the headers its resolver reads, so that a cached
277
+ answer still varies on them.
278
+ - `@ddd-ts-kit/knex`: `createProcessTable(knex, { tenant: true })` adds `tenant_id` to the
279
+ instances of the process managers, and `KnexProcessStore(context, { tenant: scope })` keeps each
280
+ one in the tenant of the work that saves it: an instance of another tenant is neither found nor
281
+ changed, and work that names no tenant is refused (`NoCurrentTenantError`).
282
+ - `@ddd-ts-kit/di`: `kernelModule({ tenants })` binds a `TenantDirectory` and gives it to the
283
+ default scheduler, so that a job declared `tenancy: 'per-tenant'` can be registered; the test
284
+ application takes `tenants` too, by default the tenant of its execution context, and composes
285
+ with none named (such a job then runs for no tenant), where it refused the job before.
286
+ - `tenancy/scoped-statement` reads the literal text of a raw call: a statement of the schema with
287
+ no word of rows, and an expression that holds no word reading or writing rows or starting a
288
+ statement (`table`, `with`, `values`, `call`, `do`...) and calls only known expression
289
+ functions (aggregates, casts, arithmetic, text), are no longer reported; raw SQL whose text is
290
+ not a literal, or a template that interpolates, still is. Bind a list as one value
291
+ (`state = any(?)`) instead of interpolating its placeholders.
292
+ - Plugin 0.22.0: the guard lets `test/e2e/journey-*.e2e.spec.ts` be written by hand, since a
293
+ journey crosses several routes and no generator writes it.
294
+ - Example: `"stack.tenancy": "shared-table"`, a tenant column on its twelve tables and the
295
+ migration that adds it to an existing database, `tenantIsolationContract` on every Knex
296
+ repository, the jobs run per network (`NETWORKS`), the audit trail read by platform staff
297
+ through a recorded elevation, `x-tenant-id` refused at the edge for another network than the
298
+ caller's, and a network missing from `NETWORKS` not found. Its HTTP contract changed: `POST
299
+ /practices` asks the role `network-admin`, `GET /audit/entries` takes `network`, every request
300
+ that reaches data needs the `tenant_id` claim, a token or header naming a network not served is
301
+ answered 404, and a person registered in one network who registers in another is refused
302
+ (422, `CONCURRENCY_CONFLICT`). What 0.11 to 0.14 added to it is confined the same way: the
303
+ instances of the confirmation deadline keep their network, the migration gives the network to
304
+ the instances and the unfinished background jobs already there, a clearance is read in the
305
+ network of its job, and the public opening hours are tagged and purged per network
306
+ (`cacheTags.perTenant`, `PublishedCacheInvalidation` with `perTenant`), named by `x-tenant-id`
307
+ for an anonymous caller and varying on it; their tags read `<network>/calendar:<id>`.
308
+
309
+ ## 0.14.0
310
+
311
+ **A route answers in JSON, XML or CSV, as the request asks.** `Representations.standard()` of
312
+ `@ddd-ts-kit/http`, given to the adapters (`representations` of the mount options) and to
313
+ `generateOpenApi`, chooses per request the media type the `Accept` header prefers among those
314
+ the route declares in `produces` (on `@Controller` or `defineRoute`), JSON first; none
315
+ acceptable is `406 NOT_ACCEPTABLE`, before the handler runs. A refusal is written as
316
+ `application/problem+xml` when XML was negotiated. A route that declares `consumes:
317
+ ['application/xml']` reads an XML body into the value the JSON path produces, validated by the
318
+ same schema; another media type is `415 UNSUPPORTED_MEDIA_TYPE`. XML follows one convention,
319
+ the default of the XML object of OpenAPI, with a parser of the package's own that refuses any
320
+ document type declaration (no new dependency). CSV is written for a page or a list of flat rows,
321
+ and a handler that answers an `AsyncIterable` under `streamSchemaOf(item)` is streamed in every
322
+ representation; one that fails once its status is sent is reported to `onError` with
323
+ `interrupted: true`. `everyPage(readPage)` streams a search page after page, each page a query
324
+ sent through the bus, so each one runs under the middlewares of the bus (transaction, tenant
325
+ session, timeout, tracing). A route that declares no body schema (a file, a redirect, no
326
+ content) is never refused 406 and its answer, a `Readable` included, goes out as the handler
327
+ wrote it. The contract follows the representations it is given, which must be those the
328
+ server is mounted with: every media type of each route, `406 NOT_ACCEPTABLE` on every route a
329
+ representation writes, `415 UNSUPPORTED_MEDIA_TYPE` on every route that reads a body; without
330
+ them, JSON only and neither status. Mounting a route that declares `produces` or `consumes`
331
+ without representations is refused. Both codes join the default error mapping and the codes
332
+ of the kit. Without the option nothing changes: every answer stays JSON. `money` and
333
+ `decimalText` write money and decimals as strings, the rule every representation follows.
334
+ The documentation page of a generated project (`withDocumentation`) is written with the error
335
+ mapper and the representations of the boundary; the contract script looks for stale files in
336
+ `openapi/` itself and, with forms, in `openapi/forms/` only.
337
+
338
+ **A generated project negotiates its representations and serves its forms.** Two keys of
339
+ `stack`, chosen at `init`: `representations` (`negotiated`, `json`) and `forms` (`on`, `off`),
340
+ both on by default with an HTTP stack. `negotiated` writes `src/platform/http/presentation-of.ts`,
341
+ which gives the HTTP boundary and the contract script `Representations.standard()` and a field
342
+ decider over the `Access` of the application; `forms: on` writes `with-forms.ts`, which serves
343
+ `GET /forms/<operationId>` for each route that declares `form`, and `npm run docs:generate`
344
+ writes the forms to `openapi/forms/`. A config without the keys is read as `json` and `off`:
345
+ nothing changes until `init --force --representations negotiated --forms on`. `generate
346
+ representation <context> <UseCase> xml,csv` adds `produces` (and `consumes` of XML for a
347
+ route with a body) to a controller, `generate form <context> <UseCase>` its `defineForm`
348
+ hints, and `describe` lists the media types and the form of each route.
349
+
350
+ **A route describes its body as a form.** A route that declares `form` (on `@Controller` or
351
+ `defineRoute`, `defineForm<Body>(hints)` next to the schemas of the controller) is served by `formRoutes(routes,
352
+ { fieldAccess })` at `GET /forms/<operationId>`: JSON Forms by default (schema, UI schema,
353
+ data), or a neutral tree of groups and fields with `?format=blocks`. Fields, constraints and
354
+ options come from the request schema and its closed vocabularies, layout, widgets, lookups and
355
+ conditions (`visibleWhen`, `requiredWhen`) from the hints, labels are translation keys, and
356
+ values given in the query prefill it. The form respects the caller: given only to who may run
357
+ its use case when it declares `requires` (the permission asked as `@Requires` asks it, from the
358
+ values of the query; 403 otherwise), a field they may not see is absent (and not echoed in
359
+ `data`), one they may not set is read-only, an object with every field it holds; the server
360
+ still validates and the use case still decides. `formDocumentsOf(routes)` writes every form whole for the documentation.
361
+
362
+ **A field of an answer can be reserved to some callers.** `withFieldAccess(schema, { field: {
363
+ read, write, subject } })` of `@ddd-ts-kit/http` marks a field with a permission or a policy:
364
+ the pipeline leaves it out of every representation (JSON, XML, CSV, streamed lists) for an actor
365
+ who may not read it, refuses it with 403 in a request body from one who may not set it, and the
366
+ contract marks it with `x-field-access` and no longer lists it as required. `fieldDeciderOf({
367
+ access, clock })` decides through the `Access` of `@ddd-ts-kit/auth`, once per element and per
368
+ request, remembering the last 256 elements per requirement; without a decider a restricted
369
+ field is refused to everybody. A field shown or hidden is decided without being recorded, a
370
+ field set in a body is recorded: `Access.decideOn({ actor, permission, subject, context,
371
+ recorded })`, new and optional on `Access`, implemented by `PolicyAccess`. The new rule
372
+ `authz/fields-by-schema` (checker and ESLint) refuses a test on the roles of the actor under
373
+ `application/queries/`, the hand-written way of shaping an answer.
374
+
375
+ **The JSON Schema of a class-validator class holds what it inherits.** A class that extends
376
+ another one, `class SearchPracticeAgendaLine extends SearchAppointmentsLine {}`, was described as
377
+ an empty object: the contract said nothing of its fields. `classSchema` now gives it the
378
+ properties and the required names of its ancestors. A project with such classes sees its
379
+ contract grow on the next `npm run docs:generate`. The end-to-end client of
380
+ `@ddd-ts-kit/testing` sends a string body as it is when the call names another content type
381
+ (`{ headers: { 'content-type': 'application/xml' } }`), as JSON otherwise.
382
+
383
+ The concepts and the tools are explained in two new pages, `docs/concepts/representations-and-forms.md`
384
+ and `docs/tools/representations-and-forms.md` (JSON Forms renderers for React, Angular and Vue,
385
+ the alternatives, why no XML or CSV library), and parts 2 and 3 of `docs/proposals/read-side.md`
386
+ are marked built.
387
+
388
+ **Representations, caching and the four adapters together.** `mountHono` serves what
389
+ `mountExpress` and `mountFastify` serve: a body in a media type the route consumes is read as
390
+ text under `bodyLimit`, a streamed list is streamed, and the mount refuses `produces` or
391
+ `consumes` without representations. A cached answer written in XML or CSV keeps the validators
392
+ and tags of its policy: the version, the instant and the tags are read from what the handler
393
+ answered, a strong `ETag` digests the bytes of the media type sent (none on a streamed list),
394
+ and `Vary` holds the headers of the policy and `accept`, in the answer and in the contract. A
395
+ public answer chosen by `Accept` is refused at the mount behind a cache that ignores `Vary`, and
396
+ so is a public answer with fields reserved to some callers whose `vary` names neither
397
+ `Authorization` nor `Cookie`, even on a route anybody may call: a shared cache would hand the
398
+ fields one caller may read to the next. A shared schema keeps its restricted fields whichever
399
+ wraps the other (`withFieldAccess(sharedSchema(...))` or `sharedSchema(name,
400
+ withFieldAccess(...))`, through `schemaBehind` of `@ddd-ts-kit/validation`), in the definition it
401
+ refers to and in the answer, and the page of a shared item keeps the restricted fields of the item.
402
+
403
+ ## 0.13.0
404
+
405
+ **A schema shared by several routes is documented once.** Declared once, a schema used whole or
406
+ inside other schemas by several routes lands once in `components.schemas` of the OpenAPI
407
+ contract and every route points at it with `$ref`: a zod schema with `.meta({ id })`,
408
+ `classSchema(Dto, { shared: true })`, or any schema through `sharedSchema(name, schema)` of
409
+ `@ddd-ts-kit/validation`. class-validator gains `pickClass`, `omitClass` and `partialClass` to
410
+ reuse a DTO class in part, and the Standard Schema adapter documents a schema its library
411
+ converts itself (Standard JSON Schema, verified with zod 4) when no JSON Schema is given. The
412
+ platform shares its own: `acceptedSchema` (`Accepted`, the `{ id }` a command route answers),
413
+ the page of a shared item (`<Item>Page`) and the problem document. A shared `params`, `query`
414
+ or `headers` schema still gives one parameter per property.
415
+
416
+ **The contract of a project on the Standard Schema adapter can change.** `standardSchema(schema)`
417
+ without a JSON Schema documented `{}` (any value); it now documents what the library converts
418
+ itself, so a project on `"stack.validation": "standard-schema"` whose schemas implement Standard
419
+ JSON Schema (zod 4 does) sees its OpenAPI contract and client SDK grow the real shapes, and
420
+ `openapi --check` or `sdk --check` report drift until they are regenerated. To move: run
421
+ `npm run docs:generate` and commit the contract and the client; to keep a schema documented as
422
+ any value, pass `{}` as its second argument. A schema the library cannot convert (a date, a
423
+ custom check) is still documented `{}`, never a failure.
424
+
425
+ **A new project declares a shape once.** `init` writes `"stack.schemas": "shared"`: a generated
426
+ command route answers its identifier with `acceptedSchema` instead of a schema of its own,
427
+ `generate schema <context> <Name>` writes a shape several routes use in
428
+ `presentation/http/shared/<name>.schema.ts` (`generate schema shared-kernel <Name>` in
429
+ `presentation/` of the shared kernel, for what several contexts answer with), and two rules hold
430
+ it: `schema/no-duplicate-shape` reports a DTO class or an object schema that declares by hand
431
+ the shape of another one of its context or of the shared kernel (two contexts may say the same
432
+ thing in their own words; a placeholder the generator left to fill is not judged), `schema/presentation-only` keeps every schema in a presentation layer
433
+ (the second in ESLint too). A config without the key reads as `per-route`: nothing changes and
434
+ nothing is reported; the steps to move are in `docs/CONFIG.md`. The example now shares its
435
+ schemas: its twenty identifier answers are the one `Accepted` component, its two agenda
436
+ searches answer `AgendaLinePage` pages of one `AgendaLine`, and one `TimeSpan` is the body of
437
+ the time off route whole and the start of a booking or a rescheduling through
438
+ `pickClass(TimeSpan, ['startsAt'])`; the bytes on the wire are unchanged.
439
+
440
+ **Hono and TypeBox.** `@ddd-ts-kit/http/hono` mounts the routes on Hono 4 (`mountHono`), through
441
+ the same pipeline and the same behaviour suite as Express, Fastify and NestJS, uploads and signed
442
+ file links included. The adapter reads a JSON body chunk by chunk up to `bodyLimit` (1 MiB by
443
+ default, as Fastify): a larger one is answered 413 `BODY_TOO_LARGE`, one that is not JSON 400
444
+ `MALFORMED_BODY`, both as problems. `@ddd-ts-kit/validation/typebox` validates with TypeBox 1
445
+ (`typeboxSchema`, compiled once, defaults filled, undeclared properties dropped, the failed
446
+ keyword as the issue code and a missing property at its own path, `{ coerce: true }` for query
447
+ strings) and documents the schema as it is, a nested schema with an `$id` shared once in the
448
+ contract (`Type.Intersect` extends it and keeps the reference). Both libraries are optional peers.
449
+
450
+ **Four contract pairs a project starts from.** `init --http hono` serves the routes with Hono on
451
+ Node (`@hono/node-server`, the limiter and the readiness probe in front when the cache is
452
+ shared), and `init --validation typebox` writes every schema of a route (the line of a search
453
+ included), a function route and a shared schema with TypeBox. A new end-to-end spec,
454
+ `contract-pairs.spec.ts`, generates one project per pair (Express and zod, Fastify and TypeBox,
455
+ Hono and zod, NestJS and class-validator), shares one shape between two routes, adds a search,
456
+ and holds each to zero finding, its specs and end-to-end specs (401, 403, 201 and the trace of
457
+ the request), a current contract that documents the shape once, and a client generated from it
458
+ that names `Money` and `Accepted` once and compiles. NestJS with class-validator, generated before but never run end to end, is now.
459
+
460
+ **Which pair for which team.** `docs/tools/contract-pairs.md` compares the four pairs (type
461
+ inference, composition of shapes, contract fidelity, issue codes, ecosystem, maintenance) with
462
+ numbers measured by `pipeline/monorepo/measure-contract-pairs.mjs`: the pipeline of the kit costs
463
+ about a microsecond per request before validation, zod parses in about half a microsecond,
464
+ class-validator and TypeBox (through the adapter) in five to ten. oRPC was run and is documented
465
+ as not fitting: its procedures, error format and second contract generator would sit next to the
466
+ kit's, and the typed client it offers comes here from the generated contract.
467
+
468
+ ## 0.12.0
469
+
470
+ **Cache policies on routes, and `no-store` for authenticated answers.** A controller, a route
471
+ group or a function route declares `cache: { scope, maxAge, sMaxAge, staleWhileRevalidate,
472
+ staleIfError, mustRevalidate, vary }` of `@ddd-ts-kit/http`: the pipeline answers its
473
+ `Cache-Control` and `Vary`, the contract documents them, and a policy that would let a cache keep
474
+ what it must not is refused when the route is declared, above all the `public` answer of a route
475
+ that knows its caller without the request headers it varies on. When the routes are mounted, a
476
+ public answer is held to the options of the application: on a route that knows its caller, `vary`
477
+ names `Authorization`, `Cookie` or the header its tenant is read from; behind a cache that ignores
478
+ `Vary` (Cloudflare, Bunny; `cacheTags.honoursVary` to say otherwise) a varying answer is refused;
479
+ and every kept answer varies on the headers `resolveTenant` reads (`TenantResolver.headers`, set by
480
+ `tenantFromHeader` and `firstTenantOf`). The policy of a route group reaches its reads only: a
481
+ write of the group keeps the default of a write. A route that is not public and
482
+ declares nothing now answers `Cache-Control: no-store`, refusals included (before, nothing): a
483
+ generated contract gains that header on every authenticated operation, regenerate it with
484
+ `npm run docs:generate`. The ESLint rule `presentation/shared-cache-varies` reports in the editor a
485
+ public policy, without `vary`, on a declaration that writes guards or security schemes. See
486
+ `docs/concepts/http-caching.md`.
487
+
488
+ **Conditional requests.** A cache policy names the version of an answer, `etag: 'representation'`
489
+ (a strong tag hashed from the bytes) or `etag: { version: 'version' }` (a weak tag from a field of
490
+ the body), and `lastModified: 'changedAt'`: the pipeline answers `ETag` and `Last-Modified`, and
491
+ `304 Not Modified` to `If-None-Match` and `If-Modified-Since`. A write declares
492
+ `ifMatch: 'optional' | 'required'`: the version named in `If-Match` reaches the handler as
493
+ `context.expectedVersion`, `expectVersion(aggregate, expected)` of the kernel refuses a stale one
494
+ with `PreconditionFailedError` (`PRECONDITION_FAILED`, 412), and a required one that is missing
495
+ answers `PRECONDITION_REQUIRED` (428). Both codes join the default error mapping and the codes a
496
+ route may declare without a class of its own (`errors/known-code`). The contract documents the
497
+ parameters, headers and answers.
498
+
499
+ **Cache tags, purged after the commit.** A public policy names the resources its answer shows
500
+ (`tags: [{ resource: 'calendar', param: 'calendarId' }]`), sent in the header the shared cache
501
+ reads (`cacheTags: { header: CacheTagHeader.xkey }` of the options; `surrogate-key`, `cache-tag`,
502
+ `cdn-tag`), filed under the tenant of the request when the application says so (`perTenant`, on
503
+ the answers and on the purges). The kernel gains two ports: `CacheInvalidation`,
504
+ which a `before-commit` handler asks and which `kernelModule()` binds to purge requests published
505
+ on the outbox (a rollback purges nothing, a commit purges even across a crash), and `CachePurger`,
506
+ which the relay calls through `new PurgingPublisher(purger, broker)` and which
507
+ `domain-events/transactional-before-commit` now refuses before the commit. `@ddd-ts-kit/http/purgers`
508
+ holds `VarnishCachePurger` (xkey or ban, with the purge `key` of the VCL), `FastlyCachePurger` and
509
+ `CloudflareCachePurger`, all held to `cachePurgerContract` of `@ddd-ts-kit/testing/contracts` (the
510
+ CDN ones on a fake of the purge API each documents); the test application exposes
511
+ `cacheInvalidation`. `RepeatedCachePurger` purges once more a little later, for a read that reached
512
+ the cache after the purge. Filed per tenant, a tagged answer to a request naming no tenant is
513
+ `no-store`, a tagged public route anybody may call needs `resolveTenant`, and an invalidation asked
514
+ without a tenant throws `NoCurrentTenantError`. `CacheInvalidation` and `CachePurger` join the
515
+ standard interfaces (`standard/no-parallel-interface`: `CacheInvalidator`, `CdnPurger` and the
516
+ like). Varnish 9.1.0 runs in the integration job. See `docs/tools/http-caches.md`.
517
+
518
+ **Varnish in front of a generated project.** `init --http-cache varnish` (recorded as
519
+ `stack.httpCache`; a config without it is `none`) adds the Varnish 9.1.0 service to the compose
520
+ file with its VCL in `docker/varnish/default.vcl` (it keeps only what an answer allows by
521
+ `max-age` or `s-maxage`, and lets a purge in from its machine or with `VARNISH_PURGE_KEY`),
522
+ validates `VARNISH_URL` and `VARNISH_PURGE_KEY`, tags the public answers in `xkey` from the HTTP
523
+ boundary, and has the outbox relay of `main.ts` and of every `generate service` entry point purge
524
+ through `PurgingPublisher` and `VarnishCachePurger`; the end-to-end application records the purges
525
+ (`purges.purged`). It needs an HTTP framework and `--events outbox`. On `--di manual` and `nest`
526
+ the platform gains `cacheInvalidation`. With `--tenancy`, the answers and the purges are tagged
527
+ per tenant and the HTTP boundary reads the tenant of a request from `x-tenant-id`.
528
+ `generate controller --cache public|private|no-store` writes the cache policy of a controller.
529
+ The generated presentation rules say when to cache, purge and condition a write.
530
+
531
+ ## 0.11.0
532
+
533
+ **File import and export, designed.** `docs/concepts/file-import-export.md` says how a large
534
+ file is to be processed on the job queue and the file storage: an import read as a stream and
535
+ sent as commands batch by batch, with its checkpoint, its progress and a rejection report; an
536
+ export written in parts; chunks followed by a process manager beyond one attempt. Nothing of it
537
+ is built in this version; the page lists what would be added.
538
+
539
+ **A long background job in the example.** A practitioner going away clears a period of their
540
+ calendar: `POST /calendars/:calendarId/clearances` (202, the identifier of the clearance) enqueues
541
+ `ClearCalendarBackgroundJob` in the unit of work of the command, which cancels the appointments
542
+ of the period one by one as the person who asked, with a checkpoint and its progress after each,
543
+ and `GET /calendars/:calendarId/clearances/:clearanceId` answers its status (an enum of
544
+ `QueuedJobStatus`) and `{ done, total }`, for a clearance of that calendar only (the subject of
545
+ its job);
546
+ then the time off is accepted. Both routes are new, with the permission `scheduling:calendar.clear`
547
+ and the error code `UNKNOWN_CLEARANCE` (404); no existing route of the contract changes. The
548
+ end-to-end support of the example can start the worker (`startApplication({ jobs: true })`).
549
+
550
+ **Generators for background work.** `ddd-ts-kit generate background-job <context> <Name>
551
+ --command <UseCase>` writes a background job that sends its command as the actor that enqueued
552
+ it (`execution.actor`, new on `BackgroundJobExecution`), with one field of its payload per other
553
+ input of the command (a `Date` as its ISO text), registered by `@Background()` or on the builder
554
+ of the module file, and a spec that enqueues a payload of every field and runs what is due. `generate
555
+ state-machine <context> <Aggregate> --value <state>... --transition "<intention>:<from>>[<to>]"`
556
+ writes the status vocabulary (or adds the values it misses), the machine and its spec; a machine
557
+ without `refused` throws the new `TransitionError.forbidden` (code `FORBIDDEN_TRANSITION`), and
558
+ `lifecycle/status-through-machine` asks an aggregate to import its machine once it writes a
559
+ status. `generate process <context> <Name>` writes a process manager, the background job of its
560
+ timeouts and their specs. The two that run on the queue need `--di tsyringe`. `init --queue
561
+ database|redis|memory` chooses where jobs and process instances are kept (`stack.queue`):
562
+ `database` by default on Knex or Drizzle with tsyringe, the jobs and processes tables created by
563
+ the first migration and bound by the platform module; `redis` puts the jobs on the server of the
564
+ cache, a write lost after the commit logged and reported (`reportLostJobs`); a config without
565
+ the key is read as `memory`, as before. A queue kept in the memory of a process is consumed by
566
+ that process even with its jobs off, so the API of a project with a worker still runs the jobs
567
+ it enqueues. Under standard decorators,
568
+ `@Background` records the job once the class is defined, when its static `jobName` exists.
569
+
570
+ **Process managers.** A long-running process that reacts to events, keeps its own state and
571
+ sends commands is a class of `application/processes/` whose steps decide (`ProcessDecision`: the
572
+ next state, the commands, the timeouts armed or disarmed, the end). The new `ProcessRunner`
573
+ port (runner in `@ddd-ts-kit/scheduling`) runs a step in one unit of work: it loads the instance
574
+ an event correlates, saves its state with optimistic concurrency through `ProcessStore`
575
+ (`InMemoryProcessStore` in the kernel, `KnexProcessStore` and `createProcessTable`,
576
+ `DrizzleProcessStore` and `processesTable`, held by `processStoreContract`), arms its timeouts
577
+ as delayed background jobs and sends the commands. `givenProcess` of `@ddd-ts-kit/testing`
578
+ tests a step without infrastructure. The new rule `process/sends-commands-only` (checker and
579
+ ESLint) refuses a process that imports an aggregate, a repository, a port or infrastructure;
580
+ `CONTEXT.md` gains a `Process managers` section and `describe` lists them. In the example, the
581
+ confirmation deadline of a requested appointment is a process manager; the booking trace gains
582
+ one handler span, the HTTP contract is unchanged.
583
+
584
+ **State machines.** `defineStateMachine` (kernel) declares the lifecycle of an aggregate once,
585
+ in `domain/state-machines/<aggregate>.state-machine.ts`: its states, the states it starts in,
586
+ transitions named after the intention methods, guards, and the refusal of a forbidden transition
587
+ from a static factory of the concept error. The aggregate asks `machine.next(status, 'confirm',
588
+ context)`; the new rule `lifecycle/status-through-machine` (checker and ESLint) refuses a status
589
+ written by hand in an aggregate whose machine exists, and `ddd-ts-kit docs` draws each machine in
590
+ a new `State machines` section of `CONTEXT.md` (and lists the background jobs in a
591
+ `Background jobs` section); `describe` lists both. A CONTEXT.md written before gains the
592
+ sections by hand: copy them from a context generated by this version. In the example, the
593
+ appointment lifecycle is a state machine; its HTTP contract is unchanged.
594
+
595
+ **Background jobs.** A use case hands work to a worker with the new `JobQueue` port of the
596
+ kernel: `jobs.enqueue(CompileStatementsBackgroundJob, { month }, { delay, runAt, idempotencyKey, subject })`, the
597
+ job a class with a static `jobName` declared with `@Background({ maxAttempts, backoff, timeout,
598
+ lease })`, its payload typed by its `run`. With a database the job is a row of the jobs table of
599
+ its context, written by the transaction of the command (`KnexJobStore` and `createJobTable` in
600
+ `@ddd-ts-kit/knex`, `DrizzleJobStore` and `jobsTable` in `@ddd-ts-kit/drizzle`), so a command
601
+ that rolls back leaves no job and one that commits loses none; `RedisJobStore` and
602
+ `InMemoryJobStore` write it, and a cancellation or a revival, once the unit of work committed
603
+ (`whenCommitted`, new in the kernel). An idempotency key is scoped by the tenant of the job, and
604
+ work done for a tenant finds and cancels the jobs of that tenant only; `find` answers the tenant
605
+ and the `subject` of a job, never its payload, and its status is the closed vocabulary
606
+ `QueuedJobStatus`. The `QueueWorker` of `@ddd-ts-kit/scheduling` claims the jobs of the queues of its
607
+ contexts with a lease it extends by heartbeats, at least once, with retries and an exponential
608
+ backoff, dead jobs, cancellation, progress, checkpoints for long jobs, a concurrency per queue
609
+ and a graceful stop; each attempt runs as the actor, in the correlation, the tenant and the
610
+ trace of the work that enqueued it, is counted in `background_job_runs_total` and
611
+ `background_job_duration_ms`, and reports an unexpected failure to the error reporter.
612
+ `@ddd-ts-kit/di` registers the declared jobs and runs the worker in the processes that run the
613
+ jobs; the test application gains `jobQueue` and `backgroundJobs.runDue()`. One contract,
614
+ `jobStoreContract`, holds every store; the Knex and Drizzle stores are proven on PostgreSQL in
615
+ CI behind a pool of several connections, the Redis store on Dragonfly. Prisma and TypeORM get no
616
+ store yet. `givenProcess(..).thenState` fails a state that JSON would change, as the runner
617
+ stores it. See [Background jobs](docs/concepts/background-jobs.md).
618
+
619
+ ## 0.10.0
620
+
621
+ **Event sourcing, chosen per aggregate.** `EventSourcedAggregate` in the kernel: a command
622
+ checks its invariants and records an event, the state changes only in the apply method of each
623
+ event, and the aggregate is rebuilt by replaying its stream, from a snapshot when one was
624
+ taken. An `EventStore` port (append at an expected version, a concurrency conflict otherwise,
625
+ each stream in order, the whole store by position), `InMemoryEventStore`, stored events
626
+ declared with `defineStoredEvent` and read back through upcasters (a stored event is never
627
+ rewritten), `EventSourcedRepository` for the adapters, snapshots per aggregate, and
628
+ crypto-shredding of personal data (`personalData`, `PersonalDataCipher`, `SubjectKeyStore`).
629
+ `@ddd-ts-kit/testing` adds `eventStoreContract`, `eventStoreUnitOfWorkContract` (nothing is
630
+ published for an append rolled back), `givenEvents` (given events, when a command, then events,
631
+ without a database) and `expectSnapshotEquivalence`. The other aggregates of a context keep
632
+ their state in tables: nothing changes for a project that does not opt in.
633
+
634
+ **An event store on PostgreSQL through every ORM adapter.** One implementation in the kernel
635
+ (`PostgresEventStore`, with the snapshot, checkpoint and data-subject key stores), run by
636
+ `createKnexEventSourcing`, `createDrizzleEventSourcing`, `createPrismaEventSourcing` and
637
+ `createTypeOrmEventSourcing`, each held to `eventStoreContract` and to
638
+ `eventStoreUnitOfWorkContract`. The tables come with the migrations of the project:
639
+ `createEventStoreTables` (knex), `eventStoreTables` (drizzle-kit), the models of
640
+ `prisma/event-store.postgresql.prisma`, `eventStoreEntities` (TypeORM), or the statements of
641
+ `eventStoreTablesSql`. An append locks the head of the store until its transaction ends, so
642
+ positions follow the commits without gaps; two appends never run side by side.
643
+
644
+ **Projections fed by the event store.** `@Projection(name, { feed: 'event-store' })` is applied
645
+ by a `CatchUpSubscription` with a stored checkpoint (one batch and its checkpoint per unit of
646
+ work), never by the dispatcher, and rebuilt from position zero by
647
+ `EventStoreProjectionRebuild` into a copy that takes the place of the table.
648
+
649
+ **Five rules for event-sourced aggregates**, in `ddd-ts-kit check` and in the ESLint plugin:
650
+ `event-sourcing/state-in-apply` (the state changes only in apply methods, which record nothing),
651
+ `event-sourcing/past-tense`, `event-sourcing/store-behind-repository` (the domain and the use
652
+ cases never read the store), `event-sourcing/projection-never-appends` (no projection names the
653
+ event store; one declared `{ feed: 'event-store' }` names no repository either) and
654
+ `event-sourcing/upcaster-required`: the data of a stored event is a named type declared in its
655
+ codec file with every named type it reaches, a version above 1 has an upcaster from each
656
+ version below, and the shape of each version is recorded by `ddd-ts-kit docs --stored-events`
657
+ in `stored-events.lock.json`, so a shape changed in place is refused. `ddd-ts-kit docs --check`
658
+ (`npm run docs:check` in the generated pipeline) fails while a declared version is not
659
+ recorded. The rules judge event-sourced code, the kernel's event store and projections declared
660
+ `{ feed: 'event-store' }`, all new in this release: a project that has none sees no new finding,
661
+ and a projection fed by the dispatcher may still name a repository port.
662
+
663
+ **`ddd-ts-kit generate aggregate <context> <Name> --event-sourced`** writes the aggregate with
664
+ its birth event (`<Name>Created`) and its apply method, the codec of its stored events
665
+ (`infrastructure/persistence/<name>-events.mapper.ts`), a builder that replays a history and a
666
+ given-events spec; `generate domain-event` on such an aggregate adds the event to its union, to
667
+ `appliers()` with an empty apply method, and to the codec. `CONTEXT.md`, `describe` and `list`
668
+ mark the event-sourced aggregates. `ddd-ts-kit docs --stored-events` records the stored shapes.
669
+ Like every reaction since 0.7.0, `appliers()` and the codec read the name an event declares
670
+ once, `[LedgerCredited.eventName]: ...`; the checker and `docs --stored-events` resolve that key.
671
+
672
+ **Documentation.** A concept page, [Event sourcing, per aggregate](docs/concepts/event-sourcing.md):
673
+ when to choose it and when not, what it costs, crypto-shredding, projections fed by the store,
674
+ and how to move an aggregate to it and back. The example keeps the attendance record of a
675
+ patient as its events (scheduling), with a history rebuilt from the store and the erasure of a
676
+ patient (`npm run erase:patient`: the key destroyed, the projections fed by the store rebuilt);
677
+ its only change of
678
+ HTTP contract is the additive route `GET /practices/{practiceId}/patients/{patientId}/attendance`.
679
+ Plugin 0.13.0: the persistence, testing and scaffold skills say how.
680
+
681
+ ## 0.9.0
682
+
683
+ **Files live in a storage, never in the database.** The kernel gains the ports `FileStorage`
684
+ (streams in and out, `stat`, `delete`, `copy`, `move`, `list`, short signed download and upload
685
+ links), `FileKeys` (keys generated on the server, under the tenant when there is one) and
686
+ `FileScanner` (an antivirus or content check before a file is accepted), the reference an
687
+ aggregate keeps, `StoredFile` (key, size, media type, checksum), and `FileStaging`, which stages a
688
+ file, moves it to its key as the last step of the transaction that records it
689
+ (`promoteOnCommit`, on the new `atCommit` step of the unit of work, undone on a rollback) and
690
+ purges the staged files, every one an orphan since no committed row points into `staging/`. A
691
+ command carries a client's file as a `FileUpload`, which `fileUploadsMiddleware` stages after
692
+ the permission and before the unit of work, so no transaction waits on an upload. A direct
693
+ upload is confirmed from the `PendingUpload` grant the server kept (`uploadUrl` answers the link
694
+ and the grant, `confirm(pending)` checks the size cap it carries), and the upload links of the
695
+ storages the application serves enforce that cap. Media types are checked on the bytes.
696
+ `InMemoryFileStorage` and `LocalFileStorage` pass the new `fileStorageContract` of
697
+ `@ddd-ts-kit/testing/contracts`; a signed download link is an attachment on every connector.
698
+
699
+ **`@ddd-ts-kit/s3`, the connector of a deployment.** `S3FileStorage` holds files in a bucket of
700
+ any provider of the S3 protocol (AWS, MinIO, OVHcloud, Scaleway, Cloudflare R2): multipart
701
+ uploads streamed part by part, presigned links whose upload signature covers the media type,
702
+ copy and move by the service. The AWS SDK v3 packages are optional peers. The contract runs on
703
+ MinIO in the integration job (`pgsty/minio`, pinned by digest: the upstream project no longer
704
+ publishes images).
705
+
706
+ **A controller declares the file it receives.** `upload: { field, maxBytes, mediaTypes }` on
707
+ `@Controller` puts the multipart form, the size and the media types in the OpenAPI contract
708
+ (with the 413, 415 and 422 refusals) and streams the file to `handle` as `upload`, checked on
709
+ its way: `multipartUploads()` of the new `@ddd-ts-kit/http/uploads` entry reads it (busboy, an
710
+ optional peer); a client that drops the connection part-way fails the file stream, so nothing
711
+ waits on it. `FILE_TOO_LARGE` answers 413, `UNSUPPORTED_MEDIA_TYPE` 415 and `FILE_NOT_FOUND`
712
+ 404 by default. `signedFileRoutes(storage)` answers the signed links of the local and in-memory
713
+ storages, and a handler may answer a stream.
714
+
715
+ **A generated project keeps files, from its first day.** `init --storage local|s3|none` (new key
716
+ `stack.storage`, `local` in what `init` writes, `none` when a config does not say it, so an
717
+ existing project changes nothing) writes `platform/files/` and `platform/config/read-files.ts`
718
+ (with `local`, `FILES_PUBLIC_URL` and `FILES_SIGNING_SECRET` are required in production, even
719
+ before the first file arrives):
720
+ a local directory in development, a bucket of the S3 protocol with a MinIO service in
721
+ `docker-compose.yml` for `s3`, the memory in the tests; the ports bound, the hourly purge of
722
+ staged files, the routes that answer the links of a served storage and the upload reader.
723
+ `generate file-upload <context> <UseCase> --path ...` writes the controller that declares the
724
+ file, the command that carries it as a `FileUpload`, the handler that records and promotes it,
725
+ and their specs.
726
+ Two rules: `persistence/no-file-content` (no binary or base64 column in a schema or a
727
+ migration) and `files/port-not-connector` (the model and the use cases name the port, never a
728
+ connector); `@ddd-ts-kit/s3` is refused in `domain/` and `application/`. To add files to an
729
+ existing project: `npx ddd-ts-kit init --force --storage local`.
730
+ `FileStaging.deleteAfterCommit` deletes, once committed, a file an aggregate no longer
731
+ references, and the refusals of a file are codes of the kit for `errors/known-code`.
732
+
733
+ **The example keeps the documents of appointments.** A practice attaches a document (`POST
734
+ /appointments/{appointmentId}/document`, multipart, at most 10 MiB of PDF, PNG or JPEG checked on
735
+ its bytes), reads it through a link of five minutes (`GET`, every read recorded in the audit
736
+ trail) and removes it (`DELETE`); the contract only gains these three operations. The local
737
+ directory serves development and the memory the tests; MinIO waits behind the `files` profile
738
+ of the compose stack.
739
+
740
+ ## 0.8.0
741
+
742
+ **The tenant travels with every unit of work** ([Multitenancy](docs/concepts/multitenancy.md)).
743
+
744
+ - `IntegrationEvent.tenantId`: `scopedPublisher` stamps the tenant of the work on every event
745
+ published without one, `executionContextOfEvent` runs the consumer in it, and the four brokers
746
+ carry it in the envelope and in a header (`x-tenant-id`; `tenant-id` for Redis Streams). The
747
+ outbox keeps it where asked: `createOutboxTables(knex, { tenant: true })` or
748
+ `addOutboxTenantColumn` with `KnexOutboxStore`'s `tenant: true`, `tenantOutboxTable()` for
749
+ Drizzle. Without them nothing changes: the tenant is not stored, as before.
750
+ - Jobs state `tenancy: 'global'` (the default, as before) or `'per-tenant'`: the in-process
751
+ scheduler, given a `TenantDirectory` (`tenants`), runs a per-tenant job once per tenant, each
752
+ run in that tenant's scope, and `JobExecution.tenantId` says which.
753
+ - One grammar for a tenant id, `isTenantId` (letters, digits, `_`, `-`, at most 63): the edge
754
+ answers 404 to any other, a broker treats an envelope carrying one as poison, and
755
+ `executionContext` throws `InvalidTenantIdError`. A project whose tenant ids hold other
756
+ characters (a `.`, a `:`) must map them before upgrading.
757
+ - Kernel: `tenantReachOf`, `tenantsOf`, `isTenantMember`, `tenantKey`, `tenantCacheKey`,
758
+ `TenantDirectory` and `InMemoryTenantDirectory`, and `acrossTenants`, the explicit elevation of
759
+ platform staff over tenants, refused without a reason and written to the audit trail before
760
+ the work runs (`ExecutionContext.elevation`, logged as `elevation`).
761
+ - `@ddd-ts-kit/testing`: `eventBusContract` checks that the tenant of an envelope arrives intact;
762
+ `outboxStoreContract` does with `carriesTenant: true`.
763
+
764
+ **Tenant isolation, four strategies, proven by one contract.**
765
+
766
+ - Shared tables: `tenantTable`, `ownTenantTable` and `whereTenant` (Knex), `tenantCondition` and
767
+ `ownTenantCondition` (Drizzle): reads reach the tenant of the work or the tenants of an
768
+ elevation, writes the one tenant of the work. The read model stores of both take `tenant`, and
769
+ `TenantAuditReader` (`@ddd-ts-kit/audit`) confines every search of the trail to the tenant.
770
+ - Row-level security on PostgreSQL, the second line: `enableTenantIsolation` and
771
+ `tenantIsolationStatements` (Knex), `tenantPolicies` and `forceRowLevelSecurity` (Drizzle), and
772
+ `tenantSession(scope)`, the new `begin` option of `createKnexUnitOfWork` and
773
+ `createDrizzleUnitOfWork`, which sets the tenant of each transaction only. One policy per
774
+ command: an elevated update or delete still touches only the tenant of the work.
775
+ - Schema per tenant (Knex): `tenantSchemaOf`, `tenantSchemaTable`, `migrateTenantSchema` with
776
+ `tenantOfMigration`, `tenantSearchPath`. Database per tenant (Knex): `TenantDatabases` (one
777
+ bounded pool per tenant held by a lease, at most `maxTenants` instances closing ones included,
778
+ an idle one closed first and work waiting rather than exceeding the bound),
779
+ `createTenantRoutedUnitOfWork` (statements inside the unit of work only, one tenant per
780
+ transaction), `migrateEveryTenant`.
781
+ - `@ddd-ts-kit/testing`: `tenantIsolationContract`, run against every strategy above on PGlite
782
+ and PostgreSQL; `asTenant(scope, tenantId, work)` and `anExecutionContext().elevatedOver(...)`.
783
+ - Prisma and TypeORM get the `tenant_id` column and the kernel helpers, applied by hand; no
784
+ helper, no row-level security, no contract run yet.
785
+
786
+ **The tenant of a request is resolved at the edge** (`@ddd-ts-kit/http`): `resolveTenant` with
787
+ `tenantFromSubdomain`, `tenantFromHeader`, `tenantFromPath`, `tenantFromClaim` or `firstTenantOf`;
788
+ the pipeline answers 403 to an authenticated caller that does not belong to the tenant named,
789
+ before any guard or handler. The guard `TenantMember` keeps routes for the members of the tenant
790
+ of the request. Without `resolveTenant` the tenant is the actor's, as before.
791
+
792
+ **`init --tenancy shared-table`** (Knex or Drizzle) records `"stack.tenancy"` and generates every
793
+ repository confined to the tenant through the helpers above, the outbox keeping the tenant of each
794
+ event, the search of the audit trail confined to the tenant of the work by `TenantAuditReader`; the new rule `tenancy/scoped-statement` reports a statement of a context that reaches a table
795
+ straight from the executor. `init` refuses the strategy with Prisma, TypeORM, `custom` or no ORM,
796
+ and refuses row-level security, schema per tenant and database per tenant, which are wired by hand
797
+ for now. `generate repository --tenant-scoped` writes the same helpers. A config without the key is
798
+ `none`: nothing changes for an existing project.
799
+
800
+ **Access control: a decision guide, scopes, and the refusals at the edge in the audit trail.**
801
+
802
+ - [Choosing access control](docs/guides/choosing-access-control.md): five questions that pick
803
+ roles, attribute policies or relationships (Zanzibar), a worked preset for each (typed
804
+ permissions and roles, a policy on one element, a scoped search), the combined model of the
805
+ example, and where relationships are evaluated.
806
+ - `Session.scopes`, filled by the JWT and Keycloak authenticators from `scope` (or `scp`);
807
+ `hasScope`, and `SessionScopes` to declare the vocabulary of a project; the guard `HasScope`
808
+ (`@ddd-ts-kit/http`) refuses a request whose session lacks a scope named.
809
+ - A request refused by a guard (`HasRole`, `HasScope`, `TenantMember`, a tenant the caller does
810
+ not belong to) is reported to the new `onGuardRefusal` of the HTTP pipeline, and
811
+ `auditGuardRefusals` (`@ddd-ts-kit/audit`) records it as a denied decision, except for a caller
812
+ without a credential, so that unauthenticated traffic cannot write to the trail without bound.
813
+ It is part of `--audit on` (`stack.audit`), with no key of its own. A generated project
814
+ with the audit trail wires it (`create-audit.ts`, `http-boundary.ts`, `compose-application.ts`);
815
+ a project initialised before adds `refusals: auditGuardRefusals(trail, { clock, ids })` to the
816
+ call of `httpBoundary` and `onGuardRefusal: refusals` to what it answers, as `init --force` writes.
817
+
818
+ **`@ddd-ts-kit/openfga`, a new package: relationships on OpenFGA.** `OpenFgaRelationTupleStore`
819
+ and `OpenFgaAuthorizer` answer the `RelationTupleStore` and `Authorizer` ports of
820
+ `@ddd-ts-kit/auth` on an OpenFGA server (OpenFGA 1.10 or later), with the mapping and the denial
821
+ codes of `RelationBasedAuthorizer`. The authorization model is generated from the relationship
822
+ schema (`authorizationModelOf`), written when it differs (`ensureAuthorizationModel`) and
823
+ compared (`modelDrift`): the schema stays the one source. `@openfga/sdk` 0.9.7 is an optional
824
+ peer, imported by the `/sdk` entry (`openFgaClient`). Both adapters pass the contract suites of
825
+ the in-process ones, in memory and against `openfga/openfga` v1.22.0 in the integration job. A
826
+ `ListObjects` answer as long as the server's cap (`listObjectsLimit`, 1000 by default) is refused
827
+ with `ReverseExpansionLimitError`, since the server cuts a longer list without an error.
828
+ [Authorization engines](docs/tools/authorization-engines.md) says when to move from the
829
+ PostgreSQL tuple store to OpenFGA.
830
+
831
+ ## 0.7.0
832
+
833
+ **A value object throws its refusals.** `init` writes `"stack.valueObjects": "throwing"`:
834
+ `generate value-object` writes a `create` that returns the value object or throws the refusal
835
+ built by the static factory of its concept (`throw PersonError.invalidEmail(raw)`), as the rest
836
+ of the domain refuses, and a spec that asserts it with `expectRefusal(() => Email.create(raw),
837
+ code)`; a caller no longer unwraps anything. `Result`, `ok`, `err` and `unwrap` stay in the
838
+ kernel: `--value-objects result`, or a config without the key, keeps the form of 0.5, so an
839
+ existing project upgrades without a change. `ddd-ts-kit value-objects` moves a project from one
840
+ to the other: it rewrites the factories, removes every `unwrap(..)` of them, turns the assertions
841
+ on a `Result` into assertions on the factory, tidies the imports, sets the key, and prints what
842
+ is left to a person.
843
+
844
+ **Closed vocabularies are declared once.** A status, a kind or a role is an object `as const`
845
+ and the union type of the same name (`AppointmentStatus.NoShow`), not an `enum`: Node's type
846
+ stripping and `erasableSyntaxOnly` refuse an `enum`, measured on Node 24. `generate vocabulary
847
+ <context> <Name> --value <value>...` writes one under `domain/vocabularies/`, a new canonical
848
+ role, and adds the values a file does not hold yet. `init` writes the roles a credential carries
849
+ as the vocabulary `Role` of the platform (`<platformDir>/auth/role.ts`), read by the grants
850
+ (`[Role.Admin]: ['*']`, `systemRole: Role.System`) and the attribute policies;
851
+ `generate route-group --role <role>` writes `HasRole(Role.<Key>)` and adds the role to it; the
852
+ grant `generate job` gives a system actor names the permission through its set
853
+ (`billingPermissions['invoice.cancel']`) instead of its id.
854
+
855
+ **A domain event names itself once.** `generate domain-event` writes `static readonly eventName =
856
+ 'billing.invoice.issued'` and `readonly name = InvoiceIssued.eventName`; the reactions
857
+ `generate event-handler` and `generate integration-event` write read it,
858
+ `@DomainEventHandler(InvoiceIssued, { name: InvoiceIssued.eventName })` and
859
+ `domainEvents.on(InvoiceIssued.eventName, ..)` in a module file, so a renamed event cannot leave a
860
+ handler on the old name. An integration event handler reads the type it handles from the
861
+ declaration of its upstream, `static readonly eventType = invoiceIssuedV1.type`, which the event
862
+ catalog script resolves. `events/name-format` judges `eventName`, and `declarations/role-declared`
863
+ accepts `{ name: <Event>.eventName }` when `<Event>` is the class the decorator is given. An event
864
+ written before, `readonly name = '..'`, and a handler that writes the name out are still read and
865
+ compared; a reaction generated for such an event writes the name out, so it compiles.
866
+
867
+ **Two rules against magic values.** `language/declared-vocabulary` (checker and ESLint) reports a
868
+ string literal whose value a vocabulary of the project declares, an object `as const` with the
869
+ union type of its name or a permission set of `definePermissions`, and names the key to write
870
+ instead: a vocabulary of a context in that context, the one of the platform in the platform and
871
+ in the route groups, a permission id and a vocabulary of the shared kernel everywhere; tests,
872
+ published languages and the relationship schema are not judged. A vocabulary is parsed, so a
873
+ documented entry, double quotes and `as const satisfies ..` are read. `clean/no-magic-number`
874
+ (ESLint) reports a number in `domain/` and `application/` other than 0, 1, -1, an index, a type or
875
+ the initializer of a module constant or a `static readonly`; the `allow` option changes the list.
876
+ Both rules hold under `"stack.literals": "named"`, which `init` writes (`--literals free` leaves
877
+ them out). A config without the key reads as `free`: an existing project upgrades without a
878
+ change, including the grants `generate job` wrote by their permission id, which a job generated
879
+ again recognises instead of granting twice. To turn the rules on, set the key and follow the fix
880
+ each finding gives ([Configuration](docs/CONFIG.md#stack)). The example declares 8 vocabularies and
881
+ replaced 227 raw values and 8 numbers, its OpenAPI contract unchanged.
882
+
883
+ **Every package carries the MIT notice.** Each published package now holds the `LICENSE` of the
884
+ repository next to its `dist/`, and `npm run package:check` refuses a tarball without it, or with
885
+ another text. Earlier versions were published without it.
886
+
887
+ ## 0.6.0
888
+
889
+ **A typed client of the contract, for the callers of the API.** `npx ddd-ts-kit sdk` (or
890
+ `init --sdk`) generates one client per document of the OpenAPI contract, a workspace package
891
+ under `clients/<document>` with no runtime dependency: each operation answers its outcome typed
892
+ by status, a refusal typed by the error codes its contract declares (`problem.code` is
893
+ `'UNKNOWN_TIME_ZONE' | 'VALIDATION_FAILED'`, not `string`), credentials are given per security
894
+ scheme, and `unwrap` with `isProblem` serve the code that prefers exceptions. A status the
895
+ contract does not declare throws `UnexpectedResponseError`, which reads the problem document of
896
+ a fault of the server: its `code` (`INTERNAL_ERROR`) and its `requestId`. An operation id that
897
+ is a reserved word or a name of the client is renamed (`deleteOperation`, `send2`). The command
898
+ records `"sdk"` in the config (`sdk.name`, `sdk.access`, see `docs/CONFIG.md`), tells the agents
899
+ in CLAUDE.md never to edit a client, and wires the project: `docs:generate` regenerates the
900
+ clients, `docs:check` fails while one differs from the contract, `ci:build` builds them, so the
901
+ GitHub and GitLab pipelines check them; publishing stays a manual step. Under npm, while
902
+ `package-lock.json` does not list a client, the command says to run `npm install` and
903
+ `sdk --check` fails, before `npm ci` refuses the lock in the pipeline. openapi-typescript with
904
+ openapi-fetch, hey-api and orval were measured on the example contract first: none reads the
905
+ error codes, and the first refuses TypeScript 6 (`docs/tools/client-sdk.md`). A project without
906
+ the key is unchanged: only with it does the ESLint preset leave out `clients/*/src/**`, and the
907
+ command add `!**/clients/*/src` to Biome.
908
+
909
+ ## 0.5.0
910
+
911
+ **Migrations are named by a timestamp and what they do.** The generated `drizzle.config.ts`
912
+ sets `migrations: { prefix: 'timestamp' }`, and every `db:generate` requires a name:
913
+ `npm run db:generate add-invoices` writes `migrations/20261012093000_add-invoices.sql` (Drizzle),
914
+ `migrations/20261012093000_add-invoices.js` (Knex, from its ES module stub now set in
915
+ `knexfile.js`; the former default stub wrote CommonJS, which an ES module project cannot load),
916
+ `prisma/migrations/20261012093000_add_invoices/` (Prisma); TypeORM takes the path
917
+ (`npm run db:generate migrations/add-invoices`) and fixes its own form,
918
+ `<milliseconds>-add-invoices.ts`. The first Knex migration of a new project is
919
+ `00000000000000_initial.js`. Timestamps keep two branches from writing the same file, but
920
+ drizzle-kit's journal still conflicts, and its migrator applies only the entries newer than the
921
+ last one a database recorded: a migration merged after a later one was deployed is skipped
922
+ without an error (measured). The new rule `persistence/migration-history` reports a migration
923
+ without its number, two sharing one, a journal that does not match the files, entries out of
924
+ the order they were generated in, and two snapshots generated from the same parent; the merge
925
+ procedure is in `docs/tools/persistence.md`. Projects initialised before keep their
926
+ `0001_initial.js`: an applied migration is never renamed.
927
+
928
+ **The generated pipelines use free tools only.** CodeQL and dependency review (free on public
929
+ GitHub repositories, paid on private ones) and GitLab's SAST, Secret Detection (findings in a
930
+ merge request on Ultimate only) and Dependency Scanning (Ultimate) are no longer written by
931
+ default: `init --ci-extras codeql,dependency-review,gitlab-sast,gitlab-secret-detection,gitlab-dependency-scanning`
932
+ adds the ones a team wants, recorded as `ci.extras` in the config. `npm audit` and gitleaks
933
+ still block a vulnerable dependency or a committed secret on every plan. The table of every
934
+ tool, its class and its source is in `pipeline/README.md`. To keep a removed job in a project
935
+ initialised before: `init --force --ci-extras <names>`.
936
+
937
+ **The contracts are YAML by default, and the OpenAPI contract can be split.**
938
+ `npm run docs:generate` writes `openapi/openapi.yaml` and `docs/integration/asyncapi.yaml`
939
+ (`"openapi": { "format": "json" }` keeps JSON), with a deterministic YAML writer of the CLI
940
+ (`ddd-ts-kit/dist/contracts.js`) that the scripts of the project import. `openapi.documents`
941
+ splits the contract by route groups, contexts and tags into self-contained documents,
942
+ `openapi/<name>.yaml`; a route in no document stops the generation; `/docs` offers a selector
943
+ between them. `@ddd-ts-kit/http` adds `routesByDocument`, `openApiDocumentsRoutes` and the
944
+ `groups` of a controller route. A project initialised before: compare `scripts/openapi.ts`,
945
+ `scripts/events.ts` and `src/platform/http/with-documentation.ts` with what `init --force`
946
+ writes, run `npx ddd-ts-kit docs` (it writes `src/platform/http/openapi-documents.ts`), then
947
+ `npm run docs:generate`, which replaces the JSON contracts.
948
+
949
+ **Onboarding.** The root README starts with the recommended steps; `init` ends with the next
950
+ step of the method, the README and `CLAUDE.md` it writes list the steps, `status` points at
951
+ step 1 when there is no plan, and the `start` and `bootstrap` skills say plainly when a step is
952
+ skipped. A new guide, `docs/guides/commands.md`, gives the command or skill for each moment of a
953
+ project and the reference of every command and flag, generated from the help of the CLI and
954
+ held to it by a test.
955
+
956
+ ## 0.4.1
957
+
958
+ - A project generated on a stack that tests against PGlite (knex, prisma, typeorm, audit) failed
959
+ `npm install` with `ERESOLVE`: `@electric-sql/pglite-socket` 0.2.12 requires exactly
960
+ `@electric-sql/pglite` 0.5.9, and both were written as caret ranges. The pair is now pinned to
961
+ exact, matching versions, and Dependabot and Renovate move it as one group. The workspace root
962
+ declares the pair as well: drizzle-orm and the copy of the example used by the conformance suite
963
+ resolve them from there.
964
+
965
+ ## 0.4.0
966
+
967
+ **A route is a controller class with one method.** One class per route; its decorator is the
968
+ single statement of the route, and the compiler holds `handle` to it:
969
+
970
+ ```ts
971
+ @RouteGroup({ prefix: '/appointments', tags: ['scheduling'], guards: [Authenticated], errors: ['FORBIDDEN'] })
972
+ export class AppointmentsRoutes {}
973
+
974
+ @Controller({
975
+ group: AppointmentsRoutes,
976
+ method: 'POST',
977
+ path: '/',
978
+ summary: 'Book appointment',
979
+ body,
980
+ response: { status: 201, body: accepted },
981
+ errors: ['SLOT_TAKEN'],
982
+ })
983
+ export class BookAppointmentController {
984
+ constructor(
985
+ private readonly commands: CommandBus,
986
+ private readonly mapper: BookAppointmentMapper,
987
+ ) {}
988
+
989
+ async handle(request: HttpRequest<{ body: BookAppointmentBody }>): Promise<BookAppointmentAccepted> {
990
+ return { id: await this.commands.execute(this.mapper.map(request)) };
991
+ }
992
+ }
993
+ ```
994
+
995
+ - `@ddd-ts-kit/http`: `Controller`, `RouteGroup`, `Guard`, `Authenticated`, `Public`, `HasRole`,
996
+ `RequestMapper`, `Serializer`, `HttpRequest`, `controllerRoutes`, `controllerRoute`. A
997
+ controller becomes a `Route`: the adapters, validation, problem documents, the error mapper
998
+ and tracing are unchanged. `defineRoute` stays supported, and both forms run in one project.
999
+ - Route groups carry a prefix, tags, guards and error codes, and a child group inherits. A
1000
+ guard is a control at the edge; permissions stay on the use cases (`@Requires`). A controller
1001
+ with no group and no guard is refused: `guards: [Public]` says a route is open.
1002
+ - `"stack.routes": "controllers" | "functions"` in the config, `init --routes`. A new project
1003
+ writes controllers; a config without the key is read as `functions` and nothing changes for
1004
+ it. `generate route` follows the key (`--group`, `--mapper`, `--serializer`), `generate
1005
+ controller` always writes a class, `generate route-group` and `generate guard` are new, and
1006
+ `ddd-ts-kit controllers [<context>] [--dry-run]` turns the function routes of a context into
1007
+ controllers, files moved and imports rewritten.
1008
+ - `presentation/http/` has one folder per route, `<resource>/<use-case>/`, with the controller,
1009
+ its schemas (`*.params.ts`, `*.body.ts`, `*.accepted.ts`, `*.answer.ts`, `*.line.ts`), its
1010
+ `*.mapper.ts` and `*.serializer.ts`; groups are `<name>.routes.ts`, guards
1011
+ `guards/*.guard.ts`, shared schemas `shared/`.
1012
+ - Controllers, guards, mappers and serializers are declared classes: `Edge` in
1013
+ `@ddd-ts-kit/kernel/declarations`, `DeclaredEdges` in `@ddd-ts-kit/di`, and discovery reads
1014
+ `presentation/`. The generated file of a context registers what each one is given, and a
1015
+ declared context lists no route.
1016
+ - Rules: `presentation/controller-shape`, `presentation/controller-delegates`,
1017
+ `presentation/guard-at-the-edge`; `authz/declared` and `errors/known-code` read controllers.
1018
+ - `describe` and the reference tables of `CONTEXT.md` list each route with its group and its
1019
+ guards, next to its use case and its permission.
1020
+
1021
+ **To do when upgrading.** Nothing for a project that keeps its function routes. A project
1022
+ initialised before this version that writes its first controller adds one line to
1023
+ `compose-application.ts`, which `ddd-ts-kit controllers` prints.
1024
+
1025
+ **The `detail` of a problem no longer quotes a private fact.** The message of a refusal built
1026
+ by a factory is answered as `detail` unless it quotes a fact the factory did not expose; then
1027
+ the client reads a sentence made from the code (`INVALID_EMAIL` gives `Invalid email.`), and the
1028
+ message stays in logs and error reports. A client that parsed such a `detail` reads the `code`
1029
+ and the exposed `facts` instead. `RequestContext` gains an optional `rawHeaders`.
1030
+
1031
+ ## 0.3.0
1032
+
1033
+ **A domain error is built by a static factory named in the words of the business.** One error
1034
+ class per concept of the language, one factory per refusal, instead of one class per error
1035
+ built with `new`:
1036
+
1037
+ ```ts
1038
+ // domain/errors/appointment.error.ts
1039
+ export class AppointmentError extends ConceptError {
1040
+ static slotTaken(calendarId: string, startsAt: Date) {
1041
+ return AppointmentError.refusal(
1042
+ 'SLOT_TAKEN',
1043
+ `The slot starting at ${startsAt.toISOString()} on calendar ${calendarId} is already held.`,
1044
+ { calendarId, startsAt },
1045
+ ).exposing('startsAt');
1046
+ }
1047
+ }
1048
+
1049
+ throw AppointmentError.slotTaken(calendar.id, slot.startsAt);
1050
+ ```
1051
+
1052
+ - `ConceptError` is a new export of `@ddd-ts-kit/kernel`, a `DomainError`. Its constructor is
1053
+ protected and a subclass declares none: a refusal exists only through a factory, which calls
1054
+ the protected `refusal(code, message, facts?)`. The codes of a class are a closed vocabulary
1055
+ the compiler derives from its factories: `CodeOf<typeof AppointmentError>`,
1056
+ `RefusalOf<typeof AppointmentError, 'SLOT_TAKEN'>`, and `AppointmentError.is(error, code?)`
1057
+ narrows to a union a `switch` on `error.code` exhausts, each case with its facts.
1058
+ `isRefusal(error, code?)` knows a refusal by its code whatever its class.
1059
+ - The facts of a refusal travel with it, as data: `error.facts`. They are private. A factory
1060
+ names the ones a client may read, `.exposing('startsAt')`, and `@ddd-ts-kit/http` writes
1061
+ those, and no other, under `facts` in the problem document; the `Problem` schema of the
1062
+ OpenAPI document gains that optional member, which is the one change of a regenerated
1063
+ contract. `DomainError.toJSON()` answers `{ name, code, message }` and the exposed facts,
1064
+ never the stack or the cause. The audit trail, the spans and the error reports are unchanged:
1065
+ they key on the code.
1066
+ - `@ddd-ts-kit/testing`: `expectRefusal(subject, code, facts?)` on an action, a `Result` or a
1067
+ caught error; `thenError(code, facts)` in a scenario; `expectProblem(response, { status,
1068
+ code, facts })`.
1069
+ - `ddd-ts-kit generate error <context> <Concept> <refusal>` creates the error class of the
1070
+ concept when it is absent and adds a factory when it is there, with its case in the spec;
1071
+ run again, it changes nothing. The concept is a term of the glossary. It refuses a name such
1072
+ as `create`, a code that says something else than the name, and a code already set in the
1073
+ context.
1074
+ - Five rules, under `"stack.refusals": "factories"` only. ESLint:
1075
+ `errors/through-factory` (no `new` of a class of `domain/errors/`),
1076
+ `errors/constructor-not-public`, `errors/factory-name` (a business phrase, and a code that
1077
+ says the same words: `slotTaken` setting `BOOKING_FAILED` is reported). `ddd-ts-kit check`:
1078
+ `errors/code-unique` (one refusal per code in a context) and `errors/known-code` (a route
1079
+ declares in `errors` only codes that exist). `language/glossary-term` now holds the name of
1080
+ an error class too.
1081
+ - `CONTEXT.md` and `ddd-ts-kit describe` list the errors of a context: class, refusal, code
1082
+ and exposed facts.
1083
+
1084
+ **Nothing to do to upgrade.** `class SlotTakenError extends DomainError { readonly code =
1085
+ 'SLOT_TAKEN' }` and `new SlotTakenError(..)` keep working: `DomainError` is unchanged but for
1086
+ `toJSON`. A config without `stack.refusals` is read as `classes`: `generate domain-error` still
1087
+ writes one class per error there, and no `errors/*` rule reports. `init` writes
1088
+ `"stack.refusals": "factories"` in a new project (`--refusals classes` for the other form),
1089
+ where `generate domain-error` points to `generate error`.
1090
+
1091
+ **To move a project:** `npx ddd-ts-kit error-factories --plan` writes
1092
+ `error-factories.plan.json` with the concept it proposes for each class (the aggregate its name
1093
+ starts or ends with); give every entry its concept, then `npx ddd-ts-kit error-factories
1094
+ --apply` writes the classes of the concepts with a factory per error (same parameters, same
1095
+ message, the plain parameters as facts), merges the specs, rewrites every `new`, `thenError`,
1096
+ `toThrow`, `expect.any`, `toBeInstanceOf`, `instanceof`, import and type, and sets the key. Then
1097
+ `npm run format`. Left to a person, and listed by the command: which facts to expose, a
1098
+ property of an error that became a fact (`error.personId` is `error.facts.personId`), a class
1099
+ with members of its own.
1100
+
1101
+ Also:
1102
+
1103
+ - The `jest.config.js` that `init` writes compiled legacy decorators in every stack. It now
1104
+ follows the stack: `decoratorVersion: '2022-03'` where nothing needs the legacy ones,
1105
+ `legacyDecorator` and `decoratorMetadata` with class-validator or NestJS.
1106
+ - The example application imports with `import type` every port and kernel type it only uses
1107
+ as a type, as the templates do; the generated declarations import the values.
1108
+
1109
+ ## 0.2.0
1110
+
1111
+ **A class states its dependencies once, in its constructor.** In the declared style a
1112
+ decorator no longer repeats what the constructor takes:
1113
+
1114
+ ```ts
1115
+ @CommandHandler(RegisterPersonCommand)
1116
+ export class RegisterPersonHandler implements CommandHandler<RegisterPersonCommand> {
1117
+ constructor(
1118
+ private readonly personRepository: PersonRepository,
1119
+ private readonly clock: Clock,
1120
+ ) {}
1121
+ }
1122
+ ```
1123
+
1124
+ - The constructor signature is the single statement of what a declared class depends on, for
1125
+ every declaring decorator: `@CommandHandler`, `@QueryHandler`, `@DomainEventHandler`,
1126
+ `@IntegrationEventHandler`, `@Scheduled`, `@Projection`, `@Repository`, `@Gateway`,
1127
+ `@Adapter`, `@Policy`. Each parameter is typed by one class, which is its injection token.
1128
+ - No compiler metadata. `emitDecoratorMetadata` exists only with legacy decorators compiled by
1129
+ `tsc` or swc: it is absent with standard decorators and under `tsx` and esbuild, and it is
1130
+ `Object` for a port imported with `import type`. `ddd-ts-kit docs`, every `generate` and
1131
+ every start of `npm run dev` read the signatures from the sources and write
1132
+ `injected(Class, [Tokens])` statements in the generated file of each context:
1133
+ `<context>.declarations.ts`, after its exports, or `<context>.dependencies.ts` under
1134
+ discovery, which `discoverContext` loads with the role files. `@ddd-ts-kit/di` injects from
1135
+ there, in both decorator modes, whoever compiles. `injected` and `injectedDependencies` are
1136
+ new exports of `@ddd-ts-kit/kernel/declarations`, `declaredTokens` of `@ddd-ts-kit/di`.
1137
+ - Three guards against a stale generated list. `declarations/barrel-current` and
1138
+ `ddd-ts-kit docs --check` fail when a constructor changed without regeneration, and now name
1139
+ the class. The `dev` script loads `ddd-ts-kit/dist/refresh-declarations.js` before the
1140
+ application, on every start and reload: a port added to a constructor is injected on the
1141
+ reload that follows the save. And `createApplication` refuses, by name, a class that takes
1142
+ parameters and has no list, or a list of another length: new `WiringError` codes
1143
+ `MISSING_DEPENDENCIES` and `DEPENDENCY_ARITY`. Nothing is injected as `undefined`.
1144
+ - New rule `declarations/constructor-injection`, in `ddd-ts-kit check` and in the ESLint
1145
+ plugin: a constructor parameter of a declared class names exactly one class. An interface, a
1146
+ type alias, a union, a primitive, an optional or defaulted parameter, a rest parameter and a
1147
+ constructor inherited from a class of a package are refused, each with its fix; a value is
1148
+ carried, as before, by an abstract class bound in the platform.
1149
+ `ports/no-concrete-dependency` now also holds the types of constructor parameters.
1150
+ - **A 0.1 project keeps working untouched.** A list written in a decorator,
1151
+ `@CommandHandler(Message, [Ports])`, is still read, still checked by the compiler and wins
1152
+ over the signature. New config key `stack.dependencies`: `signature`, which `init` writes
1153
+ (`init --dependencies listed` for the other), or `listed`, which is what a config without the
1154
+ key means and under which lists are never reported. To migrate: set
1155
+ `"dependencies": "signature"`, run `npx eslint . --fix` (the rule removes every list that
1156
+ repeats its constructor and reports the ones that differ), run `npx ddd-ts-kit docs`, and add
1157
+ `--import ddd-ts-kit/dist/refresh-declarations.js` to the `dev` script. Under discovery, add
1158
+ `src/contexts/*/*.dependencies.ts` to the entries of knip (inside the pattern of the role
1159
+ files, as `init` writes it) and to the orphan exceptions of dependency-cruiser (`ddd-ts-kit depcruise-config --write` does the second).
1160
+ - In the types, the dependency list of every decorator is optional, and a decorator without a
1161
+ list accepts any constructor: the compiler no longer reports, at the decorator, a list that
1162
+ was forgotten. The list is derived instead, and the generated `injected(...)` is checked
1163
+ against the constructor as a written list was. A declaration read with `declarationOf`
1164
+ carries `dependencies: undefined` when its decorator lists nothing, where it carried `[]`.
1165
+ - The explicit style stays explicit: `.toClass(Adapter, [Ports])` and
1166
+ `.handledBy(Handler, [Ports])` list what a class is given. NestJS and manual wiring are not
1167
+ concerned.
1168
+ - A test assembles a context from its generated file, as the composition root does; to build
1169
+ one class alone, take it from that file
1170
+ (`contextModule('billing', { IssueInvoiceHandler: billing.IssueInvoiceHandler })`).
1171
+
1172
+ **Projections are a role of the CLI.** `application/projections/*.projection.ts` joins the
1173
+ canonical tree (`tree/closed`, `naming/suffix`, `naming/one-concept-per-file`,
1174
+ `test/spec-required`), the generated barrel exports a projector and registers what its
1175
+ constructor takes, discovery, knip and dependency-cruiser know the suffix, and
1176
+ `ddd-ts-kit describe` and the reference of `CONTEXT.md` list each projection with the events it
1177
+ applies. New generator `ddd-ts-kit generate projection <context> <Name> <event-type>`
1178
+ (declared style): a projector of an integration event of the context itself or of a declared
1179
+ upstream, and its spec. A project that declared `application/projections` in
1180
+ `extraDirectories` removes the entry and its deviation, runs `ddd-ts-kit docs` (its barrels
1181
+ gain one line per projection) and gives each projection a spec, which `test/spec-required`
1182
+ now asks for; a composition root or a test that
1183
+ added projections to a context by name removes them, the barrel now brings them, and a handler
1184
+ spec gives a fake to the read model a projection of its context writes.
1185
+
1186
+ **Imports without the `.js` extension, by default.** A new project imports its own files by
1187
+ their alias and nothing else:
1188
+
1189
+ ```ts
1190
+ import { PersonRepository } from '@/contexts/identity/domain/repositories/person.repository';
1191
+ ```
1192
+
1193
+ - `importExtension` in `ddd-ts-kit.config.json` says how an import ends: `none`, which `init`
1194
+ now writes, or `js`, the previous spelling (`init --import-extension js`). Everything `init`
1195
+ and `generate` write follows it: every template, the declarations barrels, the published
1196
+ language indexes, the wiring of the composition root.
1197
+ - With `none`, the generated `tsconfig.json` resolves as a bundler does (`"module": "ESNext"`,
1198
+ `"moduleResolution": "Bundler"`, `"customConditions": ["node"]`); a file that does not exist
1199
+ is still a compile error. Node needs the extension at run time, so `ddd-ts-kit build-aliases`,
1200
+ the second step of `npm run build`, now writes the full relative path with `.js` whichever
1201
+ spelling the sources use, and the output runs under plain `node` as before.
1202
+ - New rule `imports/extension`, in `ddd-ts-kit check` and in the ESLint plugin, with a fix: an
1203
+ import of a file of the project ends as the config says. Under `none` it also refuses an
1204
+ import that names a directory: write `.../published-language/index`.
1205
+ - `imports/alias-declared` holds what derives from the key: the two resolution options of
1206
+ `tsconfig.json`, the pattern of the jest mapping, and the build step.
1207
+
1208
+ **Nothing breaks.** A config file without `importExtension`, which is every project generated by
1209
+ 0.1.x, is read as `js`: its imports, its `tsconfig.json`, its jest mapping and its build are
1210
+ judged and rewritten exactly as before, and `init` run again keeps that spelling. To move such a
1211
+ project: set the key to `none`, write the two compiler options `ddd-ts-kit check` then asks
1212
+ for, run `eslint --fix` and the formatter ([docs/CONFIG.md](docs/CONFIG.md)).
1213
+
1214
+ One behaviour of `build-aliases` is stricter in both spellings: a specifier names one emitted
1215
+ file, by the path the config's spelling gives. It used to fall back to `<specifier>.js` and to
1216
+ `<specifier>/index.js`, which no generated project relied on, since the compiler refused both
1217
+ under `NodeNext`. It now says what to write instead.
1218
+
1219
+ Also:
1220
+
1221
+ - **`init --force` keeps the config.** It used to write the config anew: the declared contexts,
1222
+ the rules, the limits and the deviations were dropped, and the composition root no longer
1223
+ assembled anything. It now changes in the config the stack choices its flags name and nothing
1224
+ else, and wires the declared contexts again into the files it rewrites.
1225
+ - `init` run again on a pnpm project no longer adds every agent permission a second time to
1226
+ `.claude/settings.json`.
1227
+ - The checker and the ESLint plugin read an import written without its extension as the same
1228
+ source file as with it, whatever dots its name holds (`invoice.aggregate`): the layer and
1229
+ context rules judge both spellings alike.
1230
+
1231
+ ## 0.1.1
1232
+
1233
+ **A project is green from its first minute.** In 0.1.0, `npx ddd-ts-kit init`, the install and
1234
+ `verify`, which is what `init` tells you to run, failed on a project nothing had been generated
1235
+ in. Fixed in the generator, and held by an end-to-end spec that runs that exact sequence on both
1236
+ package managers and on every proven stack, with no context, one, several, and a first use case:
1237
+
1238
+ - `init` writes the generated contracts as the scripts of the project write them for a project
1239
+ without a route or an integration event of its own (`openapi/openapi.json`,
1240
+ `docs/integration/asyncapi.json`, `docs/integration/event-catalog.md`): `docs:check` passes at
1241
+ once and the first commit holds the contract. A contract that is already there is left alone.
1242
+ - The dead-code gate passes on an empty project. The helper `ownPolicy`
1243
+ (`platform/resilience/own-policy.ts`) is written with the first adapter that uses it
1244
+ (`generate adapter`) instead of by `init`, where nothing imported it on tsyringe; knip is told
1245
+ what it enters a context by with the first context, and reads the fakes as entries of the test
1246
+ runner, so that it has no remark about its own configuration either.
1247
+ - A project without a context has a spec to run and passes its linter: the proof that the
1248
+ platform wires alone is written by `init` (`test/architecture/extractable.spec.ts`), which each
1249
+ context then joins, and the composition root imports `contextModule` or `discoverContext` with
1250
+ the first context instead of before it.
1251
+ - The integration run of the pipeline passes while the project has no integration spec yet.
1252
+ - Under `--assembly discovery`, a context that was just declared and has no role file yet is
1253
+ discovered as an empty context (`@ddd-ts-kit/di`): it used to stop the application, the specs
1254
+ and the documents with `DISCOVERY_FAILED`.
1255
+
1256
+ In a project created with 0.1.0, nothing has to change: these fixes are about what `init` writes.
1257
+ `generate adapter` now counts as writing a shared file for the first adapter of a project wired on
1258
+ tsyringe (`--local-only` refuses it once, naming the file).
1259
+
1260
+ Also since 0.1.0:
1261
+
1262
+ - **pnpm by default.** `init` writes a project for pnpm, pinned in `package.json` and provided by
1263
+ corepack, unless `--package-manager npm`: every script, pipeline, hook, the Dockerfile, the
1264
+ guidance and the agent permissions are said in the words of the manager chosen.
1265
+ - **`ddd-ts-kit affected`.** A pull or merge request runs the unit specs, the integration specs
1266
+ and the lint of the contexts its change reaches through the context map; the generated
1267
+ pipelines call it, and the default branch and the nightly schedule run everything.
1268
+ - The release is published through npm trusted publishing (OIDC), with provenance and without a
1269
+ stored token.
1270
+
1271
+ ## 0.1.0
1272
+
1273
+ First publication to the npm registry: the CLI `ddd-ts-kit` and the 23 packages `@ddd-ts-kit/*`.