@alveolus/arch 0.1.0 → 0.3.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 (60) hide show
  1. package/README.md +8 -1
  2. package/dist/bin.mjs +4 -2
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/{cli-CwPCGjDg.mjs → docs-DsQHpTtV.mjs} +287 -38
  5. package/dist/docs-DsQHpTtV.mjs.map +1 -0
  6. package/dist/index.d.mts +90 -36
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +2 -2
  9. package/docs/core/application/command-handlers.md +617 -0
  10. package/docs/core/application/event-publishers.md +234 -0
  11. package/docs/core/application/event-translators.md +329 -0
  12. package/docs/core/application/index.md +99 -0
  13. package/docs/core/application/integration-events.md +277 -0
  14. package/docs/core/application/outbox.md +416 -0
  15. package/docs/core/application/query-handlers.md +292 -0
  16. package/docs/core/application/unit-of-work.md +352 -0
  17. package/docs/core/domain/aggregates.md +822 -0
  18. package/docs/core/domain/domain-errors.md +251 -0
  19. package/docs/core/domain/domain-events.md +292 -0
  20. package/docs/core/domain/domain-services.md +249 -0
  21. package/docs/core/domain/entities.md +431 -0
  22. package/docs/core/domain/index.md +93 -0
  23. package/docs/core/domain/ports.md +284 -0
  24. package/docs/core/domain/repositories.md +335 -0
  25. package/docs/core/domain/value-objects.md +425 -0
  26. package/docs/core/domain/views.md +265 -0
  27. package/docs/core/index.md +108 -0
  28. package/docs/core/strategic/anti-corruption-layers.md +349 -0
  29. package/docs/core/strategic/index.md +83 -0
  30. package/docs/core/strategic/open-host-services.md +287 -0
  31. package/docs/core/strategic/published-language.md +265 -0
  32. package/docs/core/utilities/result.md +413 -0
  33. package/docs/guide/agents.md +68 -0
  34. package/docs/guide/existing-project.md +105 -0
  35. package/docs/guide/getting-started.md +275 -0
  36. package/docs/guide/learning-path.md +123 -0
  37. package/docs/guide/project-layout.md +324 -0
  38. package/docs/guide/versioning.md +42 -0
  39. package/docs/integrations/index.md +112 -0
  40. package/docs/integrations/nestjs.md +169 -0
  41. package/docs/rules/index.md +183 -0
  42. package/docs/rules/layers/no-driving-shortcut.md +119 -0
  43. package/docs/rules/layers/no-impure-domain.md +189 -0
  44. package/docs/rules/layers/no-outward-import.md +184 -0
  45. package/docs/rules/layers/no-portless-adapter.md +123 -0
  46. package/docs/rules/strategic/no-cross-context-import.md +140 -0
  47. package/docs/rules/strategic/no-fat-shared-kernel.md +81 -0
  48. package/docs/rules/strategic/no-leaky-host-service.md +107 -0
  49. package/docs/rules/strategic/no-unmapped-context.md +111 -0
  50. package/docs/rules/tactical/no-aggregate-reference.md +139 -0
  51. package/docs/rules/tactical/no-foreign-command-dependency.md +119 -0
  52. package/docs/rules/tactical/no-foreign-query-dependency.md +106 -0
  53. package/docs/rules/tactical/no-loose-code.md +171 -0
  54. package/docs/rules/tactical/no-misplaced-class.md +146 -0
  55. package/docs/rules/tactical/no-public-field.md +113 -0
  56. package/docs/rules/tactical/no-stateful-service.md +102 -0
  57. package/docs/rules/tactical/no-thrown-failure.md +162 -0
  58. package/docs/rules/tooling/no-loose-disable.md +98 -0
  59. package/package.json +4 -3
  60. package/dist/cli-CwPCGjDg.mjs.map +0 -1
@@ -0,0 +1,413 @@
1
+ ---
2
+ description: "The Result type in TypeScript: the outcome of an operation that can fail for a business reason, ok with a value or err with a domain error."
3
+ ---
4
+
5
+ # Result
6
+
7
+ A `Result` is the outcome of an operation that can fail for a business reason: `ok` with a value,
8
+ or `err` with a [domain error](../domain/domain-errors.md).
9
+
10
+ <dl class="al-glance">
11
+ <dt>Layer</dt><dd>Every layer</dd>
12
+ <dt>Type</dt><dd><a href="#api"><code>Result&lt;T, E&gt; = Ok&lt;T&gt; | Err&lt;E&gt;</code></a></dd>
13
+ <dt>Returned by</dt><dd><a href="/core/domain/aggregates">Aggregates</a>, <a href="/core/domain/entities">entities</a>, <a href="/core/domain/value-objects">value object</a> factories, <a href="/core/domain/domain-services">domain services</a>, <a href="/core/application/command-handlers">command handlers</a></dd>
14
+ <dt>Checked by</dt><dd><a href="/rules/tactical/no-thrown-failure"><code>tactical/no-thrown-failure</code></a></dd>
15
+ </dl>
16
+
17
+ ## Why
18
+
19
+ `order.place()` can fail: the order is already placed, or it has no line. If it throws, nothing in
20
+ its signature says so. The handler forgets the `try`, the controller answers 500 instead of 422,
21
+ and the client never learns why.
22
+
23
+ ::: tip The fix
24
+ `place` returns `Result<void, OrderAlreadyPlaced | EmptyOrder>`. The failures are part of the type:
25
+ TypeScript makes every caller look at them, and each one passes them on or turns them into a
26
+ response.
27
+ :::
28
+
29
+ ## How it works
30
+
31
+ A `Result` is a plain object, not a class. Two functions build it, and its `ok` flag tells them
32
+ apart.
33
+
34
+ <div class="al-cards">
35
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span>Build it</span><code>ok(value)</code> for a success, <code>ok()</code> when there is no value, <code>err(error)</code> for a failure.</div>
36
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span>Check it</span>After <code>if (!result.ok)</code>, TypeScript knows <code>result.error</code>. After it, <code>result.value</code>.</div>
37
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span>Pass it on</span>Return the failure as is: its type joins the error union of the caller.</div>
38
+ </div>
39
+
40
+ ```ts
41
+ place(
42
+ eventId: string,
43
+ now: Date,
44
+ ): Result<void, OrderAlreadyPlaced | EmptyOrder> {
45
+ if (this.isPlaced) {
46
+ return err(new OrderAlreadyPlaced());
47
+ }
48
+ if (this.lines.length === 0) {
49
+ return err(new EmptyOrder());
50
+ }
51
+ // … change the state and record OrderPlaced
52
+ return ok();
53
+ }
54
+ ```
55
+
56
+ ## Where it fits
57
+
58
+ A failure travels as a value from the aggregate to the edge, where a driving adapter turns it into
59
+ a response.
60
+
61
+ <div class="al-diagram">
62
+ <svg viewBox="0 0 680 372" role="img" aria-label="Order.place returns err(EmptyOrder). The PlaceOrderHandler returns the same err. The OrdersController maps it to an HTTP 422 response that names the error.">
63
+ <defs>
64
+ <marker id="result-flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
65
+ <path class="arrow" d="M 0 0 L 10 5 L 0 10 z" />
66
+ </marker>
67
+ </defs>
68
+ <rect class="box" x="40" y="16" width="260" height="52" rx="8" />
69
+ <text class="label" x="170" y="38" text-anchor="middle">order.place(…)</text>
70
+ <text class="note" x="170" y="56" text-anchor="middle">aggregate: checks the rules</text>
71
+ <path class="link" d="M 170 68 L 170 110" marker-end="url(#result-flow-arrow)" />
72
+ <rect class="boundary" x="196" y="76" width="200" height="28" rx="8" />
73
+ <text class="note" x="296" y="95" text-anchor="middle">err(EmptyOrder)</text>
74
+ <rect class="box" x="40" y="112" width="260" height="52" rx="8" />
75
+ <text class="label" x="170" y="134" text-anchor="middle">PlaceOrderHandler</text>
76
+ <text class="note" x="170" y="152" text-anchor="middle">returns the failure as is</text>
77
+ <path class="link" d="M 170 164 L 170 206" marker-end="url(#result-flow-arrow)" />
78
+ <rect class="boundary" x="196" y="172" width="200" height="28" rx="8" />
79
+ <text class="note" x="296" y="191" text-anchor="middle">the same err</text>
80
+ <rect class="box" x="40" y="208" width="260" height="52" rx="8" />
81
+ <text class="label" x="170" y="230" text-anchor="middle">OrdersController</text>
82
+ <text class="note" x="170" y="248" text-anchor="middle">driving adapter: maps it</text>
83
+ <path class="link" d="M 170 260 L 170 302" marker-end="url(#result-flow-arrow)" />
84
+ <text class="note" x="196" y="287">422 { error: "EmptyOrder" }</text>
85
+ <rect class="box" x="40" y="304" width="260" height="52" rx="8" />
86
+ <text class="label" x="170" y="326" text-anchor="middle">HTTP response</text>
87
+ <text class="note" x="170" y="344" text-anchor="middle">the client sees why</text>
88
+ </svg>
89
+ </div>
90
+
91
+ ::: tip
92
+ Nobody in between wraps, logs or rethrows the failure. Only the edge decides what it becomes.
93
+ :::
94
+
95
+ ## API
96
+
97
+ ```ts
98
+ import {
99
+ andThen,
100
+ combine,
101
+ err,
102
+ map,
103
+ mapErr,
104
+ ok,
105
+ type Result,
106
+ } from "@alveolus/core";
107
+ // or: import { … } from "@alveolus/core/result";
108
+ ```
109
+
110
+ ### Type parameters
111
+
112
+ ```ts
113
+ type Result<T, E> = Ok<T> | Err<E>;
114
+ ```
115
+
116
+ | Parameter | What it is |
117
+ | --- | --- |
118
+ | `T` | The value of a success. `void` when there is none. |
119
+ | `E` | The error of a failure: usually a union of domain errors. |
120
+
121
+ ### `ok()` <Badge type="tip" text="returned on a success" />
122
+
123
+ ```ts
124
+ function ok(): Ok<void>;
125
+ function ok<T>(value: T): Ok<T>;
126
+ ```
127
+
128
+ Builds a success. Without an argument, it carries no value: `ok()` is a `Result<void, E>`.
129
+
130
+ ### `err(error)` <Badge type="tip" text="returned on a failure" />
131
+
132
+ ```ts
133
+ function err<E>(error: E): Err<E>;
134
+ ```
135
+
136
+ Builds a failure that carries `error`, usually a [domain error](../domain/domain-errors.md).
137
+
138
+ ### `result.ok` <Badge type="info" text="readonly" /> <Badge type="tip" text="read by the caller" />
139
+
140
+ ```ts
141
+ readonly ok: true; // on Ok<T>
142
+ readonly ok: false; // on Err<E>
143
+ ```
144
+
145
+ `true` on a success, `false` on a failure. Checking it narrows the type to `Ok<T>` or `Err<E>`.
146
+
147
+ ### `result.value` <Badge type="info" text="readonly" /> <Badge type="tip" text="read by the caller" />
148
+
149
+ ```ts
150
+ readonly value: T; // on Ok<T>
151
+ ```
152
+
153
+ The value of a success. It exists only after checking `result.ok`.
154
+
155
+ ### `result.error` <Badge type="info" text="readonly" /> <Badge type="tip" text="read by the caller" />
156
+
157
+ ```ts
158
+ readonly error: E; // on Err<E>
159
+ ```
160
+
161
+ The error of a failure. It exists only after checking `!result.ok`.
162
+
163
+ ### `map(result, transform)` <Badge type="tip" text="to combine results" />
164
+
165
+ ```ts
166
+ function map<T, E, U>(
167
+ result: Result<T, E>,
168
+ transform: (value: T) => U,
169
+ ): Result<U, E>;
170
+ ```
171
+
172
+ Transforms the value of a success. A failure passes through unchanged.
173
+
174
+ ### `mapErr(result, transform)` <Badge type="tip" text="to combine results" />
175
+
176
+ ```ts
177
+ function mapErr<T, E, F>(
178
+ result: Result<T, E>,
179
+ transform: (error: E) => F,
180
+ ): Result<T, F>;
181
+ ```
182
+
183
+ Transforms the error of a failure. A success passes through unchanged.
184
+
185
+ ### `andThen(result, next)` <Badge type="tip" text="to combine results" />
186
+
187
+ ```ts
188
+ function andThen<T, E, U, F>(
189
+ result: Result<T, E>,
190
+ next: (value: T) => Result<U, F>,
191
+ ): Result<U, E | F>;
192
+ ```
193
+
194
+ Runs `next` on the value of a success and returns its result. A failure passes through, and the
195
+ errors of `next` add to the union.
196
+
197
+ ### `combine(results)` <Badge type="tip" text="to combine results" />
198
+
199
+ ```ts
200
+ function combine<
201
+ const Results extends
202
+ | readonly AnyResult[]
203
+ | Readonly<Record<string, AnyResult>>,
204
+ >(
205
+ results: Results,
206
+ ): Result<CombinedValues<Results>, CombinedErrors<Results>>;
207
+ ```
208
+
209
+ Takes an array or an object of results. Returns all the values in the same shape, or the first
210
+ failure in order. The error type is the union of the errors of every result.
211
+
212
+ ::: warning Caveats
213
+ - `Result` is a discriminated union, not a class: there are no methods, and narrowing on `ok`
214
+ works as for any union.
215
+ - `combine` stops at the first failure in order: it does not collect every error.
216
+ - A `Result` carries expected failures. Technical failures, such as a lost connection, are thrown
217
+ by adapters and handled like any other exception; the domain and the application never throw.
218
+ :::
219
+
220
+ ## Usage
221
+
222
+ Build the command handler that places an order, one idea at a time. Each step shows the whole file:
223
+ added lines are highlighted, replaced lines are struck out.
224
+
225
+ <div class="al-cards">
226
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">1</span><a href="#_1-start-from-a-method-that-can-fail">Start from a method that can fail</a></span>The domain already returns a Result.</div>
227
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">2</span><a href="#_2-return-success-and-failure">Return success and failure</a></span>ok for success, err for failure.</div>
228
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">3</span><a href="#_3-pass-a-failure-on-as-is">Pass a failure on as is</a></span>Narrow on ok, return the rest.</div>
229
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">4</span><a href="#_4-combine-several-results">Combine several results</a></span>One check for many values.</div>
230
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">5</span><a href="#_5-turn-it-into-a-response">Turn it into a response</a></span>Only the edge knows the transport.</div>
231
+ <div class="al-card"><span class="al-card-title"><span class="al-card-step">6</span><a href="#_6-check-it">Check it</a></span>Let the rules keep it that way.</div>
232
+ </div>
233
+
234
+ ### 1. Start from a method that can fail
235
+
236
+ `Order.place` returns `Result<void, OrderAlreadyPlaced | EmptyOrder>`: see the
237
+ [aggregate](../domain/aggregates.md). The handler that calls it builds on the same type.
238
+
239
+ ### 2. Return success and failure
240
+
241
+ So that the caller sees in the signature what can go wrong, the handler declares its error union
242
+ and returns a `Result`: `err` when the order does not exist, `ok()` when all went well. Nothing
243
+ is thrown.
244
+
245
+ ```ts [src/ordering/application/commands/place-order.command.ts]
246
+ import {
247
+ CommandHandler,
248
+ err,
249
+ ok,
250
+ type Result,
251
+ } from "@alveolus/core";
252
+
253
+ import { OrderNotFound } from
254
+ "../../domain/errors/order-not-found.error";
255
+ import { Orders } from "../../domain/repositories/orders.repository";
256
+ import { OrderId } from
257
+ "../../domain/value-objects/order-id.identifier";
258
+
259
+ export interface PlaceOrder {
260
+ readonly orderId: string;
261
+ }
262
+
263
+ export type PlaceOrderError = OrderNotFound;
264
+
265
+ export class PlaceOrderHandler extends CommandHandler<
266
+ PlaceOrder,
267
+ void,
268
+ PlaceOrderError
269
+ > {
270
+ constructor(private readonly orders: Orders) {
271
+ super();
272
+ }
273
+
274
+ async handle({
275
+ orderId,
276
+ }: PlaceOrder): Promise<Result<void, PlaceOrderError>> {
277
+ const order = await this.orders.findById(new OrderId(orderId));
278
+ if (order === undefined) {
279
+ return err(new OrderNotFound({ orderId }));
280
+ }
281
+ return ok();
282
+ }
283
+ }
284
+ ```
285
+
286
+ ### 3. Pass a failure on as is
287
+
288
+ When `place` fails, the handler has nothing to add: it returns the failed `Result` as is, after
289
+ checking `ok`. Its errors join the union of the handler, so the caller still sees every one.
290
+
291
+ ```ts [src/ordering/application/commands/place-order.command.ts]
292
+ import {
293
+ Clock, // [!code ++]
294
+ CommandHandler,
295
+ err,
296
+ IdGenerator, // [!code ++]
297
+ ok,
298
+ type Result,
299
+ } from "@alveolus/core";
300
+
301
+ import { EmptyOrder } from "../../domain/errors/empty-order.error"; // [!code ++]
302
+ import { OrderAlreadyPlaced } from // [!code ++]
303
+ "../../domain/errors/order-already-placed.error"; // [!code ++]
304
+ import { OrderNotFound } from
305
+ "../../domain/errors/order-not-found.error";
306
+ import { Orders } from "../../domain/repositories/orders.repository";
307
+ import { OrderId } from
308
+ "../../domain/value-objects/order-id.identifier";
309
+
310
+ export interface PlaceOrder {
311
+ readonly orderId: string;
312
+ }
313
+
314
+ export type PlaceOrderError = OrderNotFound; // [!code --]
315
+ export type PlaceOrderError = // [!code ++]
316
+ | OrderNotFound // [!code ++]
317
+ | OrderAlreadyPlaced // [!code ++]
318
+ | EmptyOrder; // [!code ++]
319
+
320
+ export class PlaceOrderHandler extends CommandHandler<
321
+ PlaceOrder,
322
+ void,
323
+ PlaceOrderError
324
+ > {
325
+ constructor(private readonly orders: Orders) { // [!code --]
326
+ constructor( // [!code ++]
327
+ private readonly orders: Orders, // [!code ++]
328
+ private readonly clock: Clock, // [!code ++]
329
+ private readonly ids: IdGenerator, // [!code ++]
330
+ ) { // [!code ++]
331
+ super();
332
+ }
333
+
334
+ async handle({
335
+ orderId,
336
+ }: PlaceOrder): Promise<Result<void, PlaceOrderError>> {
337
+ const order = await this.orders.findById(new OrderId(orderId));
338
+ if (order === undefined) {
339
+ return err(new OrderNotFound({ orderId }));
340
+ }
341
+ const placed = order.place(this.ids.next(), this.clock.now()); // [!code ++]
342
+ if (!placed.ok) { // [!code ++]
343
+ return placed; // [!code ++]
344
+ } // [!code ++]
345
+ await this.orders.save(order); // [!code ++]
346
+ return ok();
347
+ }
348
+ }
349
+ ```
350
+
351
+ ### 4. Combine several results
352
+
353
+ When a use case needs several results, `combine` takes an array or an object of them and returns
354
+ all the values, or the first failure. `map`, `mapErr` and `andThen` transform or chain a single
355
+ one: see the [API](#api).
356
+
357
+ ```ts
358
+ const prices = combine({
359
+ unit: Money.of(input.unitPrice, currency),
360
+ shipping: Money.of(input.shipping, currency),
361
+ });
362
+ if (!prices.ok) {
363
+ return prices;
364
+ }
365
+ const total = prices.value.unit.add(prices.value.shipping);
366
+ ```
367
+
368
+ ### 5. Turn it into a response
369
+
370
+ A driving adapter turns the failure into what its transport expects. Domain errors become HTTP
371
+ errors there, and nowhere else.
372
+
373
+ ```ts [src/ordering/driving/http/controllers/orders.controller.ts]
374
+ const placed = await this.placeOrder.handle({ orderId });
375
+ if (!placed.ok) {
376
+ return {
377
+ status: 422,
378
+ body: {
379
+ error: placed.error.type,
380
+ details: placed.error.payload,
381
+ },
382
+ };
383
+ }
384
+ return { status: 204 };
385
+ ```
386
+
387
+ ### 6. Check it
388
+
389
+ Run the checks. One rule keeps business failures as values:
390
+
391
+ ```sh
392
+ npx alveolus arch check
393
+ ```
394
+
395
+ <div class="al-cards">
396
+ <div class="al-card"><span class="al-card-title"><a href="../../rules/tactical/no-thrown-failure"><code>tactical/no-thrown-failure</code></a></span>Public methods of aggregates and entities return a <code>Result</code>, and nothing is thrown in the domain or the application.</div>
397
+ </div>
398
+
399
+ A failure thrown instead of returned is reported:
400
+
401
+ ```
402
+ src/ordering/domain/aggregates/order.aggregate.ts
403
+ 58 error tactical/no-thrown-failure: A failure is thrown: return it in a
404
+ Result instead.
405
+ ```
406
+
407
+ ## See also
408
+
409
+ - [Domain errors](../domain/domain-errors.md), what a failure carries
410
+ - [Aggregates](../domain/aggregates.md) and [Command handlers](../application/command-handlers.md),
411
+ which return results
412
+ - [Unit of Work](../application/unit-of-work.md), which commits on `ok` and rolls back on `err`
413
+ - Rules: [`tactical/no-thrown-failure`](../../rules/tactical/no-thrown-failure.md)
@@ -0,0 +1,68 @@
1
+ ---
2
+ description: "Give Claude Code, Cursor or Codex the Alveolus documentation offline: npx alveolus init writes a skill and AGENTS.md, npx alveolus explain prints any page in the terminal."
3
+ ---
4
+
5
+ # Coding agents
6
+
7
+ An agent that writes code in your project needs the same two things as a new teammate: what each
8
+ building block is for, and what a rule reports and how to fix it. Both are installed with
9
+ `@alveolus/arch`, so the agent reads them in the terminal rather than on the web, and reads the
10
+ page of the version it has.
11
+
12
+ <dl class="al-glance">
13
+ <dt>Read a page</dt><dd><code>npx alveolus explain &lt;topic&gt;</code>: a rule id, a building block or a guide</dd>
14
+ <dt>List the topics</dt><dd><code>npx alveolus explain</code></dd>
15
+ <dt>Tell the agent</dt><dd><code>npx alveolus init</code> writes <code>.claude/skills/alveolus/SKILL.md</code> and a section of <code>AGENTS.md</code></dd>
16
+ </dl>
17
+
18
+ ## Read the documentation in the terminal
19
+
20
+ Every page of this site, except the home page, ships in the package under `docs/`:
21
+
22
+ ```sh
23
+ npx alveolus explain layers/no-impure-domain # a rule, by the id the report prints
24
+ npx alveolus explain aggregates # a building block
25
+ npx alveolus explain project-layout # a guide
26
+ npx alveolus explain rules # an overview, by its folder
27
+ npx alveolus explain # every topic
28
+ ```
29
+
30
+ A name matches the end of a topic: `no-impure-domain`, `layers/no-impure-domain` and
31
+ `rules/layers/no-impure-domain` print the same page. When a name matches several pages, the
32
+ command lists them.
33
+
34
+ The report of `alveolus arch check` ends with the command, so an agent that reads a violation
35
+ knows where the fix is written:
36
+
37
+ ```
38
+ src/ordering/domain/aggregates/order.aggregate.ts
39
+ 12 error layers/no-impure-domain: The domain reads the system clock with
40
+ Date.now: receive the time from the Clock port.
41
+
42
+ 1 error in 142 files
43
+
44
+ Why, and how to fix it: npx alveolus explain <rule>
45
+ ```
46
+
47
+ ## Tell the agent where to look
48
+
49
+ Nothing in `node_modules` is read by an agent on its own: the project has to say so. `npx alveolus
50
+ init` writes two short files, and leaves alone any that exists:
51
+
52
+ | File | Read by | What it says |
53
+ | --- | --- | --- |
54
+ | `.claude/skills/alveolus/SKILL.md` | Claude Code, and any tool that follows the [Agent Skills](https://agentskills.io) format | Read the building block before writing a class, run the checks after each change, read the rule before fixing a violation, never silence a rule without asking. |
55
+ | `AGENTS.md`, a section | Cursor, Codex, Copilot, Claude Code when there is no `CLAUDE.md` | The same, in a paragraph, with a pointer to the skill. |
56
+
57
+ The skill is a map, not the documentation: ten lines that say which command to run and when. The
58
+ pages stay in the package, so they follow its version and never load into the agent's context
59
+ unless it needs one.
60
+
61
+ If the project has a `CLAUDE.md`, Claude Code reads it instead of `AGENTS.md`: add a line with
62
+ `@AGENTS.md` to it, and `init` reminds you.
63
+
64
+ ## See also
65
+
66
+ - [Getting started](./getting-started.md), the configuration and the checks
67
+ - [Rules](../rules/index.md), what each one reports
68
+ - [Learning path](./learning-path.md), the order in which to read the docs
@@ -0,0 +1,105 @@
1
+ ---
2
+ description: "Adopt the Alveolus architecture checks on an existing TypeScript project, context by context, without stopping the team."
3
+ ---
4
+
5
+ # Adopt it on an existing project
6
+
7
+ An existing project rarely has bounded contexts, layers and building blocks already. The checks
8
+ can still start today: record what is, choose what to fix first, and move one context at a time.
9
+ Nothing here requires a rewrite.
10
+
11
+ <dl class="al-glance">
12
+ <dt>Time to the first green check</dt><dd>An hour</dd>
13
+ <dt>Tools</dt><dd>The baseline, the levels of the rules, <code>layout.extraFolders</code>, <code>ignore</code></dd>
14
+ <dt>Order</dt><dd>Declare, record, lower, raise, move</dd>
15
+ </dl>
16
+
17
+ ## 1. Declare what exists
18
+
19
+ Name the bounded contexts as the code has them today, even when they are folders named
20
+ `modules/orders` or `features/billing`: `boundedContexts` takes any folder under `root`. Say under
21
+ `subdomains` which ones are the [core domain](./project-layout.md#core-supporting-generic): only
22
+ those are checked inside; a supporting or generic context is checked at its boundary only. Leave
23
+ the context map for later. Put under `ignore` what has nothing to do with the architecture:
24
+ scripts, generated code, migrations.
25
+
26
+ ```ts [alveolus.config.ts]
27
+ export default defineConfig({
28
+ boundedContexts: { billing: "features/billing", orders: "modules/orders" },
29
+ ignore: ["src/migrations/**", "src/generated/**"],
30
+ root: "src",
31
+ subdomains: { core: ["orders"], supporting: ["billing"] },
32
+ });
33
+ ```
34
+
35
+ ::: tip A legacy module is a supporting context first
36
+ A module you will not rewrite soon goes under `supporting`: the check keeps it closed and makes
37
+ it reach the others through their open host services, and leaves the rest alone. Move it to `core`
38
+ the day you model it.
39
+ :::
40
+
41
+ Run `npx alveolus arch check`: it fails, and the number of violations is your starting point.
42
+
43
+ ## 2. Record the baseline
44
+
45
+ `npx alveolus arch baseline` writes every current violation to `alveolus.baseline.json`. Commit
46
+ it: from now on, `check` fails only on what is new. The team keeps shipping, and the baseline
47
+ can only shrink: `baseline` refuses to grow unless asked to.
48
+
49
+ ## 3. Lower what you will not fix yet
50
+
51
+ A rule that reports hundreds of violations the team will fix over months goes to `warn`: it keeps
52
+ reporting, without failing the check. A rule that does not fit a part of the project yet goes to
53
+ `info`, or `off` with a date to come back.
54
+
55
+ ```ts [alveolus.config.ts]
56
+ rules: {
57
+ "tactical/no-public-field": "warn",
58
+ "tactical/no-thrown-failure": "warn",
59
+ "layers/no-outward-import": "info",
60
+ },
61
+ ```
62
+
63
+ Keep as errors, from day one, the rules that stop the worst: `strategic/no-cross-context-import`,
64
+ `layers/no-impure-domain`, `tactical/no-aggregate-reference`. New code follows them; old code is
65
+ in the baseline.
66
+
67
+ ## 4. Keep the folders you have
68
+
69
+ Folders that will stay, such as `domain/specifications/` or `application/dto/`, go in
70
+ `layout.extraFolders`. Folders that will go stay reported, in the baseline.
71
+
72
+ ## 5. Move one context
73
+
74
+ Pick the context the team touches most. In this order, each step leaving the check green:
75
+
76
+ | Step | Rules that go green | What moves |
77
+ | --- | --- | --- |
78
+ | Layers | `layers/no-outward-import`, `layers/no-driving-shortcut` | Files into `domain/`, `application/`, `driven/<technology>/`, `driving/<technology>/`; controllers call handlers. |
79
+ | Purity | `layers/no-impure-domain`, `layers/no-portless-adapter` | Framework and database out of the domain, behind ports. |
80
+ | Building blocks | `tactical/no-loose-code`, `tactical/no-misplaced-class`, `tactical/no-public-field` | Each class extends its block, in its folder, with its state private. |
81
+ | Behaviour | `tactical/no-thrown-failure`, `tactical/no-foreign-*-dependency` | Failures as `Result`, handlers receive what they should. |
82
+ | Boundaries | `strategic/*` | Open host services, anti-corruption layers, the context map. |
83
+
84
+ After each step, `npx alveolus arch baseline` drops what is fixed, and the `fixed` count in the
85
+ summary tells the team how far it got.
86
+
87
+ ## 6. Raise the levels
88
+
89
+ When a rule reports nothing for the contexts already moved, it goes back to `error`. When every
90
+ context is moved, delete the baseline.
91
+
92
+ ## What to expect
93
+
94
+ - The first `check` on a large project reports a lot: that is the point of the baseline. Read
95
+ the counts per rule in `--format json` before deciding what to lower.
96
+ - A rule reported where the team disagrees with it is a conversation, not a `disable`: lower it,
97
+ write why in the configuration, and come back to it.
98
+ - `--format sarif` in the pull requests shows new violations where they are written, which is
99
+ what keeps the baseline from growing again.
100
+
101
+ ## See also
102
+
103
+ - [Getting started](./getting-started.md), the configuration and the commands
104
+ - [Project layout](./project-layout.md), where things end up
105
+ - [Rules](../rules/index.md), what each one reports and what it cannot see