@zerotal/arch 1.7.4 → 1.8.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 (126) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/README.md +4 -2
  3. package/api-surface.md +2 -0
  4. package/docs/about.md +16 -16
  5. package/docs/admin/actions.md +9 -9
  6. package/docs/admin/auth.md +2 -2
  7. package/docs/admin/dashboard.md +5 -5
  8. package/docs/admin/extending-ui.md +12 -12
  9. package/docs/admin/extending.md +16 -7
  10. package/docs/admin/forms.md +4 -4
  11. package/docs/admin/index.md +2 -2
  12. package/docs/admin/operations.md +9 -9
  13. package/docs/admin/resources.md +3 -3
  14. package/docs/admin/structure.md +6 -6
  15. package/docs/admin/tables.md +8 -8
  16. package/docs/admin/testing.md +4 -4
  17. package/docs/ai.md +118 -11
  18. package/docs/application.md +12 -12
  19. package/docs/arch.md +25 -3
  20. package/docs/assets.md +11 -11
  21. package/docs/audit.md +13 -13
  22. package/docs/authentication.md +39 -39
  23. package/docs/authorization.md +13 -13
  24. package/docs/broadcasting/channels.md +2 -2
  25. package/docs/broadcasting/client.md +2 -2
  26. package/docs/broadcasting/events.md +9 -10
  27. package/docs/broadcasting/index.md +1 -1
  28. package/docs/broadcasting/testing.md +5 -5
  29. package/docs/cache.md +15 -13
  30. package/docs/carbon.md +35 -35
  31. package/docs/changelog.md +268 -1
  32. package/docs/client/index.md +13 -13
  33. package/docs/commands.md +36 -4
  34. package/docs/components.md +116 -115
  35. package/docs/config-system.md +4 -4
  36. package/docs/container.md +27 -27
  37. package/docs/context.md +27 -27
  38. package/docs/contributing.md +28 -0
  39. package/docs/controllers.md +10 -10
  40. package/docs/conventions.md +23 -15
  41. package/docs/cookies.md +3 -3
  42. package/docs/csrf.md +6 -6
  43. package/docs/database.md +22 -17
  44. package/docs/deployment.md +99 -2
  45. package/docs/devtools.md +16 -16
  46. package/docs/email-verification.md +8 -8
  47. package/docs/encryption.md +7 -7
  48. package/docs/errors.md +6 -6
  49. package/docs/events.md +12 -11
  50. package/docs/flow/components.md +32 -32
  51. package/docs/flow/decorators.md +18 -18
  52. package/docs/flow/events.md +16 -16
  53. package/docs/flow/forms.md +17 -17
  54. package/docs/flow/icons.md +5 -5
  55. package/docs/flow/index.md +21 -21
  56. package/docs/flow/layouts.md +99 -35
  57. package/docs/flow/lifecycle.md +16 -16
  58. package/docs/flow/models.md +9 -9
  59. package/docs/flow/pagination.md +8 -8
  60. package/docs/flow/performance.md +7 -7
  61. package/docs/flow/references.md +3 -3
  62. package/docs/flow/routing.md +26 -26
  63. package/docs/flow/testing.md +20 -20
  64. package/docs/getting-started.md +18 -13
  65. package/docs/health.md +17 -8
  66. package/docs/helpers.md +17 -17
  67. package/docs/i18n.md +10 -10
  68. package/docs/inertia/devtools.md +4 -4
  69. package/docs/inertia/index.md +6 -6
  70. package/docs/inertia/props.md +21 -21
  71. package/docs/inertia/rendering.md +9 -9
  72. package/docs/inertia/ssr.md +2 -2
  73. package/docs/lifecycle.md +2 -2
  74. package/docs/lock.md +11 -11
  75. package/docs/logger.md +12 -12
  76. package/docs/media.md +22 -22
  77. package/docs/middleware.md +17 -17
  78. package/docs/migrations.md +13 -13
  79. package/docs/monitor.md +6 -6
  80. package/docs/notifications.md +24 -24
  81. package/docs/orm/casts.md +14 -14
  82. package/docs/orm/factories.md +8 -8
  83. package/docs/orm/index.md +22 -20
  84. package/docs/orm/lifecycle.md +13 -13
  85. package/docs/orm/queries.md +28 -28
  86. package/docs/orm/relationships.md +16 -16
  87. package/docs/orm/serialization.md +11 -11
  88. package/docs/package-development.md +10 -10
  89. package/docs/pagination.md +14 -14
  90. package/docs/password-reset.md +6 -6
  91. package/docs/providers.md +11 -11
  92. package/docs/query-builder.md +18 -18
  93. package/docs/queue.md +14 -12
  94. package/docs/rate-limiting.md +6 -6
  95. package/docs/responses.md +14 -14
  96. package/docs/roles-and-2fa.md +9 -9
  97. package/docs/routing.md +44 -44
  98. package/docs/scaffolding.md +1 -1
  99. package/docs/scheduler.md +10 -10
  100. package/docs/seeding.md +6 -6
  101. package/docs/session.md +16 -14
  102. package/docs/social.md +14 -14
  103. package/docs/storage.md +25 -13
  104. package/docs/structure.md +2 -2
  105. package/docs/support-policy.md +23 -10
  106. package/docs/telemetry.md +12 -12
  107. package/docs/tenancy.md +34 -40
  108. package/docs/testing/browser.md +6 -6
  109. package/docs/testing/console.md +3 -3
  110. package/docs/testing/database.md +10 -10
  111. package/docs/testing/flow-browser.md +6 -6
  112. package/docs/testing/http.md +12 -12
  113. package/docs/testing/index.md +29 -3
  114. package/docs/testing/mocking.md +8 -8
  115. package/docs/upgrade.md +1 -1
  116. package/docs/validator.md +19 -19
  117. package/docs/view.md +14 -14
  118. package/package.json +4 -4
  119. package/src/config.ts +8 -0
  120. package/src/index.ts +25 -1
  121. package/src/install/ArchInstallCommand.ts +46 -3
  122. package/src/install/guidelines.ts +84 -0
  123. package/src/install/shape.ts +177 -0
  124. package/src/install/skills.ts +271 -0
  125. package/src/provider/ArchProvider.ts +148 -0
  126. package/src/tools/searchDocs.ts +7 -0
@@ -94,7 +94,7 @@ Dot-notation maps to the config file path and the key within it. `config('databa
94
94
 
95
95
  `config()` is type-aware. Each path resolves to the type declared in the owning package's `*ConfigShape`, with autocomplete on the path string:
96
96
 
97
- ```typescript
97
+ ```typescript fragment
98
98
  // in application code
99
99
  config("app.name"); // string
100
100
  config("app.port"); // number
@@ -105,7 +105,7 @@ config.set("app.debug", "yes"); // type error: expected boolean
105
105
 
106
106
  This works exactly like the container's `ContainerBindings`: there's a `ConfigRegistry` interface that every config-owning package augments by namespace. Core registers `app` and `health`; each package registers its own next to its `*ConfigShape`:
107
107
 
108
- ```typescript
108
+ ```typescript fragment
109
109
  // in a package's config registry declaration
110
110
  declare module "zerotal" {
111
111
  interface ConfigRegistry {
@@ -140,7 +140,7 @@ Because every file is loaded as a normal module, a config file may `import` and
140
140
 
141
141
  ## Overriding config in tests
142
142
 
143
- ```typescript
143
+ ```typescript fragment
144
144
  // in a test setup file
145
145
  import { config } from "zerotal";
146
146
 
@@ -227,7 +227,7 @@ SESSION_SECRET=super-secret
227
227
 
228
228
  Use `APP_ENV` to differentiate behaviour:
229
229
 
230
- ```typescript
230
+ ```typescript fragment
231
231
  // in application code
232
232
  if (config("app.env") === "production") {
233
233
  // production-only logic
package/docs/container.md CHANGED
@@ -34,7 +34,7 @@ one line) and decoupling (a class depends on a `Mailer` contract, not on
34
34
 
35
35
  Inside a `ServiceProvider`, the container lives at `this.app.container`:
36
36
 
37
- ```typescript
37
+ ```typescript fragment
38
38
  // in a ServiceProvider method
39
39
  this.app.container.singleton(CacheManager, () => new CacheManager());
40
40
  const cache = await this.app.container.make(CacheManager);
@@ -42,7 +42,7 @@ const cache = await this.app.container.make(CacheManager);
42
42
 
43
43
  Anywhere else, reach it through the application singleton:
44
44
 
45
- ```typescript
45
+ ```typescript fragment
46
46
  // in application code outside a provider
47
47
  import { Application } from "zerotal";
48
48
 
@@ -97,7 +97,7 @@ Created once, then cached for the lifetime of the application. Every caller gets
97
97
  the same instance. The factory receives the container so it can resolve its own
98
98
  dependencies:
99
99
 
100
- ```typescript
100
+ ```typescript fragment
101
101
  // in a ServiceProvider's onRegister()
102
102
  this.app.container.singleton(CacheManager, async (c) => {
103
103
  const cfg = await c.make("config");
@@ -115,7 +115,7 @@ Created once per HTTP request and isolated between concurrent requests via
115
115
  `AsyncLocalStorage`. Resolving a scoped binding outside of a request context
116
116
  throws `ScopedOutsideRequestError`:
117
117
 
118
- ```typescript
118
+ ```typescript fragment
119
119
  // in a ServiceProvider's onRegister()
120
120
  this.app.container.scoped(UserSession, (c) => new UserSession());
121
121
  ```
@@ -124,7 +124,7 @@ this.app.container.scoped(UserSession, (c) => new UserSession());
124
124
 
125
125
  A fresh instance on every `make()`:
126
126
 
127
- ```typescript
127
+ ```typescript fragment
128
128
  // in a ServiceProvider's onRegister()
129
129
  this.app.container.bind(ReportGenerator, () => new ReportGenerator());
130
130
  ```
@@ -133,7 +133,7 @@ this.app.container.bind(ReportGenerator, () => new ReportGenerator());
133
133
 
134
134
  Bind a pre-built instance directly — no factory, no lazy construction:
135
135
 
136
- ```typescript
136
+ ```typescript fragment
137
137
  // in a ServiceProvider's onRegister()
138
138
  this.app.container.value("config", configObject);
139
139
  ```
@@ -142,7 +142,7 @@ this.app.container.value("config", configObject);
142
142
 
143
143
  Bindings are registered in a `ServiceProvider`'s `onRegister()`:
144
144
 
145
- ```typescript
145
+ ```typescript fragment
146
146
  // app/providers/AppServiceProvider.ts
147
147
  import { ServiceProvider } from "zerotal";
148
148
  import { CacheManager } from "../services/CacheManager.ts";
@@ -197,7 +197,7 @@ can't register itself, and only reach for a provider when you need lifecycle hoo
197
197
  `App` is the canonical surface for application code — resolve from anywhere
198
198
  (pages, controllers, jobs) without importing `Application`:
199
199
 
200
- ```typescript
200
+ ```typescript fragment
201
201
  // in a controller, page, or job
202
202
  import { App } from "zerotal";
203
203
  import { UsersService } from "@app/services/users-service.ts";
@@ -211,7 +211,7 @@ It also exposes registration (`bind`, `singleton`, `scoped`, `value`, `alias`,
211
211
  helpers (`environment()`, `isProduction()`, `isLocal()`). The `make()` and
212
212
  `app()` global helpers are shorthands:
213
213
 
214
- ```typescript
214
+ ```typescript fragment
215
215
  // in application code
216
216
  import { make, app } from "zerotal";
217
217
 
@@ -225,7 +225,7 @@ const same = await app(UsersService); // = make(UsersService)
225
225
  Register bindings in `bootstrap/app.ts` without a provider. The callback runs at
226
226
  boot, before any provider's `onRegister()`, so providers can still override:
227
227
 
228
- ```typescript
228
+ ```typescript fragment
229
229
  // bootstrap/app.ts — the callback receives the live container
230
230
  Application.create({ providers })
231
231
  .bind((container) => {
@@ -238,7 +238,7 @@ Application.create({ providers })
238
238
  This is the place for **interface → implementation** bindings, since a class
239
239
  can't register itself against a different token:
240
240
 
241
- ```typescript
241
+ ```typescript fragment
242
242
  // bootstrap/app.ts
243
243
  Application.create({ providers }).bind((container) =>
244
244
  container.singleton(Mailer, () => new SendGridMailer(env("SENDGRID_KEY"))),
@@ -250,7 +250,7 @@ Application.create({ providers }).bind((container) =>
250
250
  Any class under `app/services/` is auto-discovered at boot. Declare its lifetime
251
251
  next to the class with a `static lifetime` flag and it's registered for you:
252
252
 
253
- ```typescript
253
+ ```typescript fragment
254
254
  // app/services/users-service.ts
255
255
  @inject(Auth)
256
256
  export class UsersService {
@@ -288,7 +288,7 @@ binding at boot and the container hands each request its own instance.
288
288
  `make()` is the primary way to resolve a binding. It's async because factories
289
289
  may be async and deferred providers may need to boot first:
290
290
 
291
- ```typescript
291
+ ```typescript fragment
292
292
  // in application code
293
293
  const cache = await container.make(CacheManager);
294
294
  const cfg = await container.make("config");
@@ -305,7 +305,7 @@ Anything else throws `SyncResolutionError`. This is what [facades](#facades) use
305
305
  internally — which is why providers pre-resolve (warm) their singleton in
306
306
  `onBooted()` before any facade call happens:
307
307
 
308
- ```typescript
308
+ ```typescript fragment
309
309
  // in application code
310
310
  const cache = container.makeSync(CacheManager); // throws if not yet resolved
311
311
  ```
@@ -316,7 +316,7 @@ const cache = container.makeSync(CacheManager); // throws if not yet resolved
316
316
  instead of throwing when the token isn't registered. Useful for optional
317
317
  services that exist only in certain runtime modes:
318
318
 
319
- ```typescript
319
+ ```typescript fragment
320
320
  // in a ServiceProvider — CommandRunner is only bound in console mode
321
321
  const runner = container.tryMake("commandRunner");
322
322
  if (runner) runner.register(MyCommand);
@@ -334,7 +334,7 @@ Pass a class's dependency tokens straight to `@inject(...)`. The container
334
334
  resolves each token — recursively, and **in parallel** — before constructing the
335
335
  class:
336
336
 
337
- ```typescript
337
+ ```typescript fragment
338
338
  // app/repositories/PostRepository.ts
339
339
  import { inject } from "zerotal";
340
340
  import { CacheManager } from "../services/CacheManager.ts";
@@ -351,7 +351,7 @@ export class PostRepository {
351
351
 
352
352
  No registration needed — the container auto-wires on first `make()`:
353
353
 
354
- ```typescript
354
+ ```typescript fragment
355
355
  // in application code
356
356
  const repo = await container.make(PostRepository);
357
357
  ```
@@ -365,7 +365,7 @@ classes, or string keys from `ContainerBindings`.
365
365
  A token is the key the container resolves against — a class constructor, an
366
366
  abstract class, or a string key declared in `ContainerBindings`:
367
367
 
368
- ```typescript
368
+ ```typescript fragment
369
369
  // in application code
370
370
  // Class token (most common)
371
371
  container.singleton(CacheManager, factory);
@@ -379,7 +379,7 @@ const cfg = await container.make("config");
379
379
  Packages extend the `ContainerBindings` interface via declaration merging so
380
380
  string tokens stay fully type-safe:
381
381
 
382
- ```typescript
382
+ ```typescript fragment
383
383
  // in a package's types.ts
384
384
  declare module "zerotal" {
385
385
  interface ContainerBindings {
@@ -409,7 +409,7 @@ After construction, any [`resolving()`](#resolving-hooks) hooks for the token fi
409
409
 
410
410
  Hand a different implementation of the same dependency to different consumers:
411
411
 
412
- ```typescript
412
+ ```typescript fragment
413
413
  // in a ServiceProvider or bootstrap bind() callback
414
414
  // PostController gets the Redis cache; ReportController gets the file cache
415
415
  container.for(PostController).give(CacheDriver, () => new RedisCache());
@@ -434,7 +434,7 @@ of its `@inject` tokens.
434
434
  Bind one token as an alias for another. Resolving the alias returns the target's
435
435
  instance — handy for binding an interface/contract token to a concrete class:
436
436
 
437
- ```typescript
437
+ ```typescript fragment
438
438
  // in a ServiceProvider or bootstrap bind() callback
439
439
  container.alias(CacheContract, CacheManager);
440
440
  const cache = await container.make(CacheContract); // → the CacheManager singleton
@@ -448,7 +448,7 @@ alias cycles.
448
448
  Run a callback every time a token resolves — for post-construction setup without
449
449
  subclassing or wrapping the factory:
450
450
 
451
- ```typescript
451
+ ```typescript fragment
452
452
  // in a ServiceProvider or bootstrap bind() callback
453
453
  container.resolving(Logger, (logger) => {
454
454
  logger.setChannel("app");
@@ -463,7 +463,7 @@ instance is constructed.
463
463
  Register a provider so it boots only when one of its tokens is first resolved —
464
464
  keeping cold-start fast for services not used on every request:
465
465
 
466
- ```typescript
466
+ ```typescript fragment
467
467
  // in a ServiceProvider or bootstrap bind() callback
468
468
  container.defer(SearchClient, SearchServiceProvider);
469
469
  ```
@@ -542,7 +542,7 @@ synchronously via `makeSync`. That's why providers pre-resolve their singleton i
542
542
 
543
543
  Core ships a few, importable from `zerotal`:
544
544
 
545
- ```typescript
545
+ ```typescript fragment
546
546
  // in application code (after boot)
547
547
  import { Config, Events, Artisan } from "zerotal";
548
548
 
@@ -560,12 +560,12 @@ await Artisan.call("migrate");
560
560
  Packages ship their own — for example `Cache` from `@zerotal/cache`, `Auth` from
561
561
  `@zerotal/auth`:
562
562
 
563
- ```typescript
563
+ ```typescript fragment
564
564
  // in application code (after boot)
565
565
  import { Cache } from "@zerotal/cache";
566
566
  import { Auth } from "@zerotal/auth";
567
567
 
568
- await Cache.put("key", value, 300);
568
+ await Cache.set("key", value, 300);
569
569
  Auth.check(); // → boolean
570
570
  Auth.user(); // → AuthenticatedUser (throws if guest)
571
571
  Auth.userOrNull(); // → AuthenticatedUser | undefined
@@ -575,7 +575,7 @@ Auth.userOrNull(); // → AuthenticatedUser | undefined
575
575
 
576
576
  Build one with `createFacade<T>(token)`, passing the same token the provider binds:
577
577
 
578
- ```typescript
578
+ ```typescript fragment
579
579
  // src/facades/Cache.ts
580
580
  import { createFacade } from "zerotal";
581
581
  import type { CacheManager } from "../CacheManager.ts";
package/docs/context.md CHANGED
@@ -32,7 +32,7 @@ tests, build one with [`HttpContext.fake()`](#testing).
32
32
  Every method on the context is bound to its instance, so you can destructure
33
33
  exactly what a handler needs — methods included — without losing `this`:
34
34
 
35
- ```typescript
35
+ ```typescript fragment
36
36
  // routes: Router.get('/posts/:post/:tab', PostController, 'show')
37
37
 
38
38
  async show({ view, params: { post, tab } }: HttpContext<{ post: Post; tab: string }>) {
@@ -55,7 +55,7 @@ instead (`async show(ctx: HttpContext)`) when you prefer `ctx.view(...)` /
55
55
  `ctx.params` is `Record<string, string>` — the raw matched segments. Pass a type
56
56
  argument to describe what the route resolves, including model bindings:
57
57
 
58
- ```typescript
58
+ ```typescript fragment
59
59
  // raw params only — every value is a string
60
60
  async index(ctx: HttpContext) {
61
61
  const page = ctx.params.page; // string | undefined
@@ -81,7 +81,7 @@ name (and exposes it via `ctx.model<T>()`).
81
81
  Usually there is nothing to declare: a param whose name matches an auto-registered
82
82
  model binds on its own, so `:post` resolves through `Post` by primary key.
83
83
 
84
- ```typescript
84
+ ```typescript fragment
85
85
  // routes/web.ts — :post is already bound to Post
86
86
  Router.get("/posts/:post", PostController, "show");
87
87
 
@@ -91,7 +91,7 @@ Router.get("/posts/:post", PostController, "show").bind("post", (slug) =>
91
91
  );
92
92
  ```
93
93
 
94
- ```typescript
94
+ ```typescript fragment
95
95
  // in PostController — ctx.params.post is the resolved Post, not the raw id
96
96
  async show(ctx: HttpContext<{ post: Post }>) {
97
97
  return ctx.json(ctx.params.post);
@@ -114,7 +114,7 @@ custom resolvers, scoped bindings — lives in [Routing](/docs/routing).
114
114
  The scalar helpers check route params first, then the query string, and coerce to
115
115
  the type the method name promises. Each takes an optional fallback.
116
116
 
117
- ```typescript
117
+ ```typescript fragment
118
118
  // in a controller
119
119
  ctx.query("page", "1"); // string | undefined — query string only
120
120
  ctx.string("sort", "asc"); // string | undefined — param then query
@@ -131,7 +131,7 @@ is absent, or the value is not a valid integer (`NaN`). Because parsing is base-
131
131
  validate with a [form request](/docs/validator) when you need to reject malformed
132
132
  input rather than coerce it.
133
133
 
134
- ```typescript
134
+ ```typescript fragment
135
135
  // in a controller — GET /posts?page=3
136
136
  ctx.integer("page"); // 3
137
137
  ctx.integer("page", 1); // 3 (fallback unused)
@@ -147,7 +147,7 @@ of `'1'`, `'true'`, `'yes'`, or `'on'` (compared case-insensitively). Any other
147
147
  present value is `false`; an absent value returns the fallback, which defaults to
148
148
  `false`.
149
149
 
150
- ```typescript
150
+ ```typescript fragment
151
151
  // in a controller
152
152
  ctx.boolean("active"); // ?active=true / ?active=1 / ?active=ON → true
153
153
  ctx.boolean("active"); // ?active=0 / ?active=no / ?active= → false
@@ -164,14 +164,14 @@ ctx.boolean("active", true); // absent → true (custom fallback)
164
164
  `body()` parses and caches the body (JSON, form-urlencoded, or multipart fields) so
165
165
  repeated calls are free, and returns `{}` on an absent or invalid body:
166
166
 
167
- ```typescript
167
+ ```typescript fragment
168
168
  const data = await ctx.body<{ title: string; body: string }>();
169
169
  ```
170
170
 
171
171
  `input()` reads a single merged value in priority order — route params → cached
172
172
  body → query string — without awaiting:
173
173
 
174
- ```typescript
174
+ ```typescript fragment
175
175
  ctx.input("id"); // route :id or ?id=
176
176
  ctx.input("q", "all"); // with fallback
177
177
  ```
@@ -183,7 +183,7 @@ ctx.input("q", "all"); // with fallback
183
183
  Read a header with `header()` (case-insensitive) or pull a Bearer token with
184
184
  `bearerToken()`:
185
185
 
186
- ```typescript
186
+ ```typescript fragment
187
187
  ctx.header("x-forwarded-for"); // string | null
188
188
  ctx.bearerToken(); // string | null — strips the "Bearer " prefix
189
189
  ```
@@ -193,7 +193,7 @@ ctx.bearerToken(); // string | null — strips the "Bearer " prefix
193
193
  `file()` returns the first `UploadedFile` for a form field (or `null`); `files()`
194
194
  returns all of them. Both parse and cache the multipart body on first call.
195
195
 
196
- ```typescript
196
+ ```typescript fragment
197
197
  const avatar = await ctx.file("avatar"); // UploadedFile | null
198
198
  const attachments = await ctx.files("attachments"); // UploadedFile[]
199
199
  ```
@@ -201,7 +201,7 @@ const attachments = await ctx.files("attachments"); // UploadedFile[]
201
201
  An `UploadedFile` describes the upload and knows how to persist itself. Validate
202
202
  before storing, then hand it a disk — `store()` returns the stored path:
203
203
 
204
- ```typescript
204
+ ```typescript fragment
205
205
  // in a controller — single file
206
206
  const avatar = await ctx.file("avatar");
207
207
 
@@ -220,7 +220,7 @@ if (avatar) {
220
220
  }
221
221
  ```
222
222
 
223
- ```typescript
223
+ ```typescript fragment
224
224
  // in a controller — multiple files
225
225
  for (const file of await ctx.files("attachments")) {
226
226
  await file.store("uploads", Storage.disk());
@@ -239,7 +239,7 @@ The response helpers set `ctx.response` for you and return nothing — they are
239
239
  terminal. You can also `return` a value from a controller; see
240
240
  [Responses](/docs/responses) for the full set.
241
241
 
242
- ```typescript
242
+ ```typescript fragment
243
243
  ctx.json({ user }); // 200 application/json
244
244
  ctx.json({ errors }, 422); // custom status
245
245
  ctx.view(WelcomeView, { name }); // full HTML document (prepends <!DOCTYPE html>)
@@ -253,7 +253,7 @@ ctx.back(303); // same, with a 303 status
253
253
  `view()` accepts either pre-rendered markup, or a **view component plus its props**.
254
254
  A view component receives the request `HttpContext` first and your props second:
255
255
 
256
- ```typescript
256
+ ```typescript fragment
257
257
  // resources/views/Welcome.tsx
258
258
  export default function Welcome(ctx: HttpContext, { title }: { title: string }) {
259
259
  return (
@@ -307,7 +307,7 @@ the pipeline, ordering, and registration.
307
307
  the matching record, and assigns it to `ctx.user`; `AuthMiddleware` reads it to guard
308
308
  routes. It is `undefined` for guests.
309
309
 
310
- ```typescript
310
+ ```typescript fragment
311
311
  if (!ctx.user) throw new UnauthorizedError();
312
312
  return ctx.json({ email: ctx.user.email });
313
313
  ```
@@ -317,7 +317,7 @@ registered auth model — see [Authentication](/docs/authentication).
317
317
 
318
318
  ## Client IP
319
319
 
320
- ```typescript
320
+ ```typescript fragment
321
321
  ctx.ip(); // string | null — socket-level IP from Bun's server.requestIP()
322
322
  ```
323
323
 
@@ -331,7 +331,7 @@ with `HttpContext.fake()`).
331
331
 
332
332
  ## URL & matching helpers
333
333
 
334
- ```typescript
334
+ ```typescript fragment
335
335
  ctx.path(); // "/posts" — pathname only
336
336
  ctx.fullUrl(); // "https://app.test/posts?page=2"
337
337
  ctx.host(); // "app.test"
@@ -343,7 +343,7 @@ ctx.subdomains; // { tenant: "acme" }
343
343
  `isJson()` / `wantsJson()` inspect the `Content-Type` / `Accept` headers — handy in
344
344
  an exception handler, or anywhere one route serves both browsers and API clients:
345
345
 
346
- ```typescript
346
+ ```typescript fragment
347
347
  // in a controller or middleware
348
348
  if (ctx.wantsJson()) {
349
349
  ctx.json({ message: "Unauthorized" }, 401);
@@ -358,13 +358,13 @@ Flash writes a value to the session for the **next** request only — ideal for
358
358
  post-redirect success and error messages. It requires `SessionMiddleware` and
359
359
  silently no-ops without it.
360
360
 
361
- ```typescript
361
+ ```typescript fragment
362
362
  // before redirecting
363
363
  ctx.flash("success", "Post saved!");
364
364
  return ctx.redirect("/posts", 303);
365
365
  ```
366
366
 
367
- ```typescript
367
+ ```typescript fragment
368
368
  // on the next request
369
369
  const msg = ctx.flashed<string>("success"); // 'Post saved!'
370
370
  ```
@@ -378,7 +378,7 @@ Register work to run **after** the response has been sent — fire-and-forget si
378
378
  effects that shouldn't delay the client. `afterResponse()` returns `this`, so calls
379
379
  chain.
380
380
 
381
- ```typescript
381
+ ```typescript fragment
382
382
  ctx.afterResponse(async () => {
383
383
  await analytics.track(ctx.requestId, ctx.url.pathname);
384
384
  });
@@ -413,7 +413,7 @@ request — a service shared with CLI commands or queue workers — uses
413
413
  `RequestContext.remember(key, factory)` runs `factory` at most once per request
414
414
  for a given key and hands every later caller the same answer:
415
415
 
416
- ```typescript
416
+ ```typescript fragment
417
417
  import { RequestContext } from "zerotal";
418
418
 
419
419
  const settings = await RequestContext.remember(`household:${id}:settings`, () =>
@@ -446,7 +446,7 @@ earlier in the same request.
446
446
  `HttpContext.fake()` builds a context without a live server — perfect for unit
447
447
  testing controllers and middleware:
448
448
 
449
- ```typescript
449
+ ```typescript fragment
450
450
  const ctx = HttpContext.fake("http://localhost/posts?page=2", {
451
451
  method: "GET",
452
452
  headers: { Authorization: "Bearer token" },
@@ -459,7 +459,7 @@ expect(ctx.response?.status).toBe(200);
459
459
  Pass a `body` to exercise handlers that read one, and assign `ctx.params` directly
460
460
  when the handler expects route params a real match would have provided:
461
461
 
462
- ```typescript
462
+ ```typescript fragment
463
463
  // in a test — a POST with a JSON body
464
464
  const ctx = HttpContext.fake("http://localhost/posts", {
465
465
  method: "POST",
@@ -470,14 +470,14 @@ ctx.params = { id: "42" };
470
470
  ```
471
471
 
472
472
  The signature is `HttpContext.fake(url?, init?, container?)`; all three are optional.
473
- See [Testing](/docs/testing/index) for the full harness, and
473
+ See [Testing](/docs/testing) for the full harness, and
474
474
  [HTTP testing](/docs/testing/http) for the higher-level request helpers.
475
475
 
476
476
  ## References
477
477
 
478
478
  ### `HttpContext<TParams>`
479
479
 
480
- ```typescript
480
+ ```typescript fragment
481
481
  class HttpContext<TParams extends Record<string, unknown> = Record<string, string>> {
482
482
  constructor(request: Request, container: ScopedResolver);
483
483
  }
@@ -149,6 +149,34 @@ that keeps it complete.
149
149
  The same rule governs a page's own length: a section that has grown into a second
150
150
  subject is a sign it wants to be its own page, or to move to the one that owns it.
151
151
 
152
+ ### Code examples are compiled
153
+
154
+ Every TypeScript block in `docs/` is compiled against the real packages by
155
+ `bun run docs:examples:check`, which CI runs on every pull request. An example that
156
+ names a member the API no longer has fails the build rather than waiting for a reader
157
+ to find it.
158
+
159
+ Plenty of blocks are not whole programs on purpose — a class body, a set of columns,
160
+ a method shown without the class around it. Say so in the fence:
161
+
162
+ ````text
163
+ ```ts fragment
164
+ @column("string") name!: string;
165
+ @column("boolean") active!: boolean;
166
+ ```
167
+ ````
168
+
169
+ `fragment` works on `ts`, `tsx` and `typescript`, and readers never see it: a fence's
170
+ info string carries only its first word into the rendered page. It is never guessed —
171
+ a block is a fragment because you wrote that it is, and a run of
172
+ `bun run docs:examples` records it so a new one is a deliberate change rather than a
173
+ silent exemption.
174
+
175
+ Two lists back that up. `fragments` in `docs-examples-baseline.json` are the
176
+ intentional ones. `drift` is the other kind: a block that resolves every import and
177
+ still fails, which means the page is wrong. Neither list may grow, and `drift` is
178
+ meant to reach zero.
179
+
152
180
  ## Changelogs
153
181
 
154
182
  Each package keeps its own `CHANGELOG.md`. Add what you changed under
@@ -16,7 +16,7 @@ A controller is a plain class. Each action receives the request `HttpContext`
16
16
  directly, reads input from it, and sets the response by calling a helper on it —
17
17
  actions return `void`, not a value.
18
18
 
19
- ```typescript
19
+ ```typescript fragment
20
20
  // app/controllers/PostController.ts
21
21
  import type { HttpContext } from "zerotal";
22
22
  import { Post } from "../models/Post.ts";
@@ -48,7 +48,7 @@ export class PostController {
48
48
 
49
49
  Map routes to actions by passing the controller class and the action name:
50
50
 
51
- ```typescript
51
+ ```typescript fragment
52
52
  // routes/index.ts
53
53
  import { Router } from "zerotal";
54
54
  import { PostController } from "../app/controllers/PostController.ts";
@@ -71,7 +71,7 @@ dependencies. Decorate the **class** with `@inject(...)`, listing the tokens in
71
71
  constructor order — the container resolves each token and passes it to the
72
72
  constructor:
73
73
 
74
- ```typescript
74
+ ```typescript fragment
75
75
  // app/controllers/PostController.ts
76
76
  import type { HttpContext } from "zerotal";
77
77
  import { inject } from "zerotal";
@@ -127,12 +127,12 @@ declaration — the resolved instance arrives on `ctx.params` under the param's
127
127
  or you can read it explicitly with `ctx.model()`. `.bind()` and the model's own
128
128
  `resolveRouteBinding` are for overriding that default:
129
129
 
130
- ```typescript
130
+ ```typescript fragment
131
131
  // routes/index.ts — :post already resolves through Post
132
132
  Router.get("/posts/:post", PostController, "show");
133
133
  ```
134
134
 
135
- ```typescript
135
+ ```typescript fragment
136
136
  // app/controllers/PostController.ts — via ctx.params:
137
137
  async show(ctx: HttpContext<{ post: Post }>): Promise<void> {
138
138
  ctx.json(ctx.params.post);
@@ -176,7 +176,7 @@ export class StorePostRequest extends FormRequest {
176
176
  returns the validated data fully typed from your `rules()` return — it throws a
177
177
  validation error on failure:
178
178
 
179
- ```typescript
179
+ ```typescript fragment
180
180
  // app/controllers/PostController.ts
181
181
  import { StorePostRequest } from "../requests/posts/StorePostRequest.ts";
182
182
 
@@ -228,7 +228,7 @@ export class ArticleController {
228
228
 
229
229
  Register all seven with a single call, or narrow them with `.only()` / `.except()`:
230
230
 
231
- ```typescript
231
+ ```typescript fragment
232
232
  // routes/index.ts
233
233
  Router.resource("articles", ArticleController);
234
234
  Router.resource("articles", ArticleController).only(["index", "show"]);
@@ -243,7 +243,7 @@ Router.resource("articles", ArticleController).except(["create", "edit"]);
243
243
  Controllers set `ctx.response` via helper methods — they do not return a value
244
244
  (the return type is `Promise<void>`):
245
245
 
246
- ```typescript
246
+ ```typescript fragment
247
247
  // in a controller action
248
248
  ctx.json(data); // 200 JSON
249
249
  ctx.json(data, 201); // 201 JSON
@@ -281,7 +281,7 @@ With `@zerotal/auth` installed, the authenticated user is available at
281
281
  `ctx.user` after `AuthMiddleware` runs. The `Auth` facade exposes the same data
282
282
  from anywhere in the async tree — handy inside services called by the controller:
283
283
 
284
- ```typescript
284
+ ```typescript fragment
285
285
  // in a controller action
286
286
  import { Auth } from "@zerotal/auth";
287
287
 
@@ -304,7 +304,7 @@ Register a callback to run after the response has been sent — useful for
304
304
  expensive side effects that shouldn't block the client. `afterResponse` returns
305
305
  `this`, so it chains:
306
306
 
307
- ```typescript
307
+ ```typescript fragment
308
308
  // in a controller action
309
309
  async store(ctx: HttpContext): Promise<void> {
310
310
  const post = await Post.create(data);