@zerotal/arch 1.7.3 → 1.7.5

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 (127) hide show
  1. package/CHANGELOG.md +34 -2
  2. package/README.md +4 -2
  3. package/api-surface.md +4 -4
  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 +165 -1
  32. package/docs/client/index.md +277 -70
  33. package/docs/commands.md +4 -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 +46 -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 +2 -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 +199 -0
  55. package/docs/flow/index.md +20 -20
  56. package/docs/flow/layouts.md +35 -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 +12 -7
  70. package/docs/inertia/props.md +22 -22
  71. package/docs/inertia/rendering.md +86 -7
  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 +36 -8
  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 +3 -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/index.ts +25 -1
  120. package/src/tools/searchDocs.ts +7 -0
  121. package/docs/client/auth.md +0 -113
  122. package/docs/client/errors.md +0 -139
  123. package/docs/client/files.md +0 -118
  124. package/docs/client/references.md +0 -58
  125. package/docs/client/requests.md +0 -131
  126. package/docs/client/resilience.md +0 -141
  127. package/docs/client/testing.md +0 -146
@@ -125,7 +125,7 @@ import {
125
125
  Extend `BroadcastingEvent`, implement `broadcastOn()`, and dispatch — every subscribed client on
126
126
  that channel receives the payload:
127
127
 
128
- ```ts
128
+ ```ts fragment
129
129
  // app/events/OrderShipmentStatusUpdated.ts
130
130
  import { BroadcastingEvent, privateChannel } from "@zerotal/broadcasting";
131
131
  import type { Order } from "../models/Order.ts";
@@ -11,7 +11,7 @@ live manager for an in-memory recorder, so the code under test broadcasts exactl
11
11
  as it normally would and the test inspects what came out — no Redis, no Pusher
12
12
  credentials, and no waiting on a socket.
13
13
 
14
- ```ts
14
+ ```ts fragment
15
15
  // in a test
16
16
  import { Broadcast } from "@zerotal/broadcasting";
17
17
 
@@ -31,7 +31,7 @@ Broadcast.resetFake();
31
31
  `afterEach` — otherwise the first test to fake broadcasting silently mutes every
32
32
  test that follows it.
33
33
 
34
- ```ts
34
+ ```ts fragment
35
35
  afterEach(() => Broadcast.resetFake());
36
36
  ```
37
37
 
@@ -76,7 +76,7 @@ Proving a broadcast did _not_ happen is often the more valuable test, because a
76
76
  stray broadcast reaches real users. Both negative assertions are worth reaching
77
77
  for:
78
78
 
79
- ```ts
79
+ ```ts fragment
80
80
  it("does not broadcast when validation fails", async () => {
81
81
  const fake = Broadcast.fake();
82
82
 
@@ -102,7 +102,7 @@ no special handling — save the model and assert. The event name is whatever
102
102
  `broadcastAs()` returns and the payload whatever `broadcastWith()` builds, so
103
103
  asserting on both is what pins that mapping down:
104
104
 
105
- ```ts
105
+ ```ts fragment
106
106
  const fake = Broadcast.fake();
107
107
 
108
108
  await Post.create({ title: "Hello" });
@@ -117,7 +117,7 @@ its socket id along. The fake records the broadcast either way, so a test that o
117
117
  asserts the event fired will pass whether or not `toOthers()` was used. To pin that
118
118
  behaviour down, read the recorded entries directly:
119
119
 
120
- ```ts
120
+ ```ts fragment
121
121
  const [entry] = fake.recorded();
122
122
  expect(entry.event).toBe("PostUpdated");
123
123
  ```
package/docs/cache.md CHANGED
@@ -45,7 +45,9 @@ import { CacheConfig } from "@zerotal/cache";
45
45
  import { env } from "zerotal";
46
46
 
47
47
  export default CacheConfig({
48
- driver: env("CACHE_DRIVER", "sqlite"), // 'sqlite' | 'redis' | 'memory'
48
+ // One of 'sqlite' | 'redis' | 'memory', written literally: the field is that
49
+ // union and `env()` returns a plain string.
50
+ driver: "sqlite",
49
51
  prefix: env("CACHE_PREFIX", "zerotal:"), // prepended to every key
50
52
  ttl: env("CACHE_TTL", 3600), // default TTL in seconds (1 hour)
51
53
 
@@ -66,7 +68,7 @@ export default CacheConfig({
66
68
 
67
69
  ## Basic operations
68
70
 
69
- ```ts
71
+ ```ts fragment
70
72
  // in a controller or service
71
73
  import { Cache } from "@zerotal/cache";
72
74
 
@@ -93,13 +95,13 @@ await Cache.flush();
93
95
 
94
96
  `remember()` checks the cache and, on a miss, calls the factory, stores the result, and returns it. Under high concurrency, multiple callers for the same key coalesce — the factory runs exactly once:
95
97
 
96
- ```ts
98
+ ```ts fragment
97
99
  function remember<T>(key: string, ttl: number, fn: () => Promise<T> | T): Promise<T>;
98
100
  ```
99
101
 
100
102
  > **Tip** — Reach for `remember()` instead of a manual `get`/`set` pair. It avoids the cache stampede where many requests miss at once and all hit the database.
101
103
 
102
- ```ts
104
+ ```ts fragment
103
105
  // in a controller
104
106
  import { Cache } from "@zerotal/cache";
105
107
  import { Post } from "../app/models/Post.ts";
@@ -130,7 +132,7 @@ const stats = await Cache.remember(`user:${userId}:stats`, 60, async () => {
130
132
 
131
133
  Group related keys under named tags so you can invalidate them together. `Cache.tags()` returns a `TaggedCache` whose keys all share a tag prefix — useful when several cache entries depend on the same underlying data:
132
134
 
133
- ```ts
135
+ ```ts fragment
134
136
  // in a controller
135
137
  import { Cache } from "@zerotal/cache";
136
138
 
@@ -156,7 +158,7 @@ await Cache.tags([`user:${userId}`]).flush();
156
158
 
157
159
  ### Tag-based invalidation in model hooks
158
160
 
159
- ```ts
161
+ ```ts fragment
160
162
  // app/models/Post.ts — inside a lifecycle hook or observer
161
163
  async afterCreate(post: Post) {
162
164
  await Cache.tags(["posts"]).flush();
@@ -171,7 +173,7 @@ async afterUpdate(post: Post) {
171
173
 
172
174
  There is no multi-get primitive — batch with `Promise.all` to cut round-trips to the backend:
173
175
 
174
- ```ts
176
+ ```ts fragment
175
177
  // in a controller
176
178
  import { Cache } from "@zerotal/cache";
177
179
 
@@ -211,7 +213,7 @@ await Promise.all([
211
213
 
212
214
  The `Cache` facade always uses the configured driver. To use a different driver for one use case, construct a `CacheManager` directly:
213
215
 
214
- ```ts
216
+ ```ts fragment
215
217
  // in a service
216
218
  import { CacheManager, MemoryDriver } from "@zerotal/cache";
217
219
 
@@ -220,7 +222,7 @@ const local = new CacheManager(new MemoryDriver(), "req:", 30);
220
222
  await local.set("computed-total", total);
221
223
  ```
222
224
 
223
- ```ts
225
+ ```ts fragment
224
226
  new CacheManager(driver: CacheDriver, prefix?: string, defaultTtl?: number)
225
227
  ```
226
228
 
@@ -230,7 +232,7 @@ new CacheManager(driver: CacheDriver, prefix?: string, defaultTtl?: number)
230
232
 
231
233
  `with()` needs a `CacheManager` instance — resolve the framework's bound manager from the container with `app.container.makeSync("cache")`:
232
234
 
233
- ```ts
235
+ ```ts fragment
234
236
  // routes/api.ts
235
237
  import { IdempotencyMiddleware, CacheManager } from "@zerotal/cache";
236
238
 
@@ -283,7 +285,7 @@ A replayed response includes `Idempotency-Replay: true` so clients can distingui
283
285
 
284
286
  Pre-populate the cache at boot so the first real request is always fast. Call from a provider's `onStarted()`, which runs after the application has finished booting:
285
287
 
286
- ```ts
288
+ ```ts fragment
287
289
  // bootstrap/providers/AppServiceProvider.ts
288
290
  import { ServiceProvider } from "zerotal";
289
291
  import { Cache } from "@zerotal/cache";
@@ -305,7 +307,7 @@ Use the `memory` driver in tests for speed and isolation — it never persists b
305
307
 
306
308
  To swap the cache for a single suite, rebind the `cache` singleton on the container before resolving the facade, then flush between tests to avoid bleed:
307
309
 
308
- ```ts
310
+ ```ts fragment
309
311
  // in test setup
310
312
  import { Application } from "zerotal";
311
313
  import { Cache, CacheManager, MemoryDriver } from "@zerotal/cache";
@@ -354,7 +356,7 @@ Cache errors extend `CacheError`, which extends the framework's `ZerotalError`.
354
356
  | `CacheSerializationError` | `E_CACHE_SERIALIZATION` | A value cannot be serialised for storage. |
355
357
  | `CacheDeserializationError` | `E_CACHE_DESERIALIZATION` | A stored value cannot be read back — usually a shape change. |
356
358
 
357
- ```typescript
359
+ ```typescript fragment
358
360
  // in a service
359
361
  import { CacheDeserializationError } from "@zerotal/cache";
360
362
 
package/docs/carbon.md CHANGED
@@ -28,7 +28,7 @@ import { Carbon, CarbonInterval } from "zerotal/carbon";
28
28
  The constructor accepts a string, a millisecond timestamp, a native `Date`, a
29
29
  Temporal type, or another Carbon. A second argument sets the timezone.
30
30
 
31
- ```typescript
31
+ ```typescript fragment
32
32
  // anywhere in your app
33
33
  // Current date and time (system timezone)
34
34
  const now = new Carbon();
@@ -60,7 +60,7 @@ const d8 = new Carbon(Temporal.Now.instant());
60
60
  Named static factories cover the common boundary cases (all accept an optional
61
61
  timezone string):
62
62
 
63
- ```typescript
63
+ ```typescript fragment
64
64
  // anywhere in your app
65
65
  Carbon.today(); // today at 00:00:00
66
66
  Carbon.tomorrow(); // tomorrow at 00:00:00
@@ -79,7 +79,7 @@ Carbon.create(input); // same as new Carbon(input)
79
79
  Every modifier returns a **new** Carbon. Chain freely — the base instance never
80
80
  changes.
81
81
 
82
- ```typescript
82
+ ```typescript fragment
83
83
  // anywhere in your app
84
84
  const base = new Carbon("2026-01-15");
85
85
  const nextWeek = base.addDays(7);
@@ -95,7 +95,7 @@ deadline.format("YYYY-MM-DD"); // '2026-02-15'
95
95
  A Carbon carries its timezone; converting produces a new instance pointing at the
96
96
  same instant.
97
97
 
98
- ```typescript
98
+ ```typescript fragment
99
99
  // anywhere in your app
100
100
  // Read the timezone of an instance
101
101
  const tz = Carbon.now("Asia/Tokyo").timezone; // 'Asia/Tokyo'
@@ -117,7 +117,7 @@ Reading a Carbon never changes it. Calendar fields are plain properties, so they
117
117
  carry no parentheses; the three counts that depend on the surrounding calendar are
118
118
  methods, because they compute an answer rather than expose a stored field.
119
119
 
120
- ```typescript
120
+ ```typescript fragment
121
121
  // anywhere in your app
122
122
  const d = new Carbon("2026-06-15 09:30:45.123");
123
123
 
@@ -162,7 +162,7 @@ out and comparing them by hand.
162
162
 
163
163
  ### Predicates
164
164
 
165
- ```typescript
165
+ ```typescript fragment
166
166
  // anywhere in your app
167
167
  d.isToday();
168
168
  d.isTomorrow();
@@ -221,7 +221,7 @@ ranges, where inclusive bounds would place a boundary value in both buckets.
221
221
 
222
222
  All arithmetic returns a new Carbon.
223
223
 
224
- ```typescript
224
+ ```typescript fragment
225
225
  // anywhere in your app
226
226
  d.addNanoseconds(1) d.subtractNanoseconds(1)
227
227
  d.addMicroseconds(1) d.subtractMicroseconds(1)
@@ -245,7 +245,7 @@ either direction from a computed value without branching.
245
245
 
246
246
  To add a [CarbonInterval](#carboninterval), use `add()` / `subtract()`:
247
247
 
248
- ```typescript
248
+ ```typescript fragment
249
249
  // anywhere in your app
250
250
  const interval = CarbonInterval.days(3).andHours(6);
251
251
 
@@ -259,7 +259,7 @@ Months and years are calendar units rather than fixed spans, so adding one lands
259
259
  on the same day number in the target month — and clamps when that day does not
260
260
  exist there:
261
261
 
262
- ```typescript
262
+ ```typescript fragment
263
263
  new Carbon("2026-01-31").addMonths(1); // → 2026-02-28
264
264
  new Carbon("2024-02-29").addYears(1); // → 2025-02-28
265
265
  ```
@@ -267,7 +267,7 @@ new Carbon("2024-02-29").addYears(1); // → 2025-02-28
267
267
  Clamping discards information, which makes month arithmetic asymmetric. Adding a
268
268
  month and taking it back does not always return the original date:
269
269
 
270
- ```typescript
270
+ ```typescript fragment
271
271
  new Carbon("2026-01-31").addMonths(1).subtractMonths(1); // → 2026-01-28
272
272
  ```
273
273
 
@@ -285,7 +285,7 @@ and always reverse cleanly.
285
285
 
286
286
  Snap to the start or end of a time period — all return a new Carbon.
287
287
 
288
- ```typescript
288
+ ```typescript fragment
289
289
  // anywhere in your app
290
290
  d.startOfMinute(); // :00.000
291
291
  d.endOfMinute(); // :59.999999999
@@ -325,7 +325,7 @@ day without also catching midnight the next morning.
325
325
 
326
326
  The `with` prefix signals a new instance is returned.
327
327
 
328
- ```typescript
328
+ ```typescript fragment
329
329
  // anywhere in your app
330
330
  d.withYear(2030);
331
331
  d.withMonth(12); // 1-indexed
@@ -351,7 +351,7 @@ of setters.
351
351
  All numeric diff methods return a **number** — positive when `this` is after
352
352
  `other`.
353
353
 
354
- ```typescript
354
+ ```typescript fragment
355
355
  // anywhere in your app
356
356
  const a = new Carbon("2026-01-01");
357
357
  const b = new Carbon("2026-06-15");
@@ -373,7 +373,7 @@ b.diffInYears(a); // ~0.42
373
373
  For a calendar-aware breakdown use `diffAsCarbonInterval`, which delegates to
374
374
  Temporal's `until()`:
375
375
 
376
- ```typescript
376
+ ```typescript fragment
377
377
  // anywhere in your app
378
378
  const age = birthday.diffAsCarbonInterval(Carbon.now(), "year");
379
379
  // → CarbonInterval { years: 28, months: 3, days: 12, … }
@@ -394,7 +394,7 @@ The `largestUnit` defaults to `'day'` and accepts `'year'`, `'month'`, `'week'`,
394
394
  Returns a human-readable relative string using `Intl.RelativeTimeFormat`. With no
395
395
  argument it compares against now.
396
396
 
397
- ```typescript
397
+ ```typescript fragment
398
398
  // anywhere in your app
399
399
  const posted = new Carbon("2026-06-08");
400
400
  posted.diffForHumans(); // '1 week ago'
@@ -407,7 +407,7 @@ a.diffForHumans(b); // '5 months ago'
407
407
 
408
408
  Pass an options object to tune the output:
409
409
 
410
- ```typescript
410
+ ```typescript fragment
411
411
  // anywhere in your app
412
412
  a.diffForHumans(b, {
413
413
  parts: 2, // include up to 2 units → '5 months, 1 week ago'
@@ -419,7 +419,7 @@ a.diffForHumans(b, {
419
419
  });
420
420
  ```
421
421
 
422
- ```typescript
422
+ ```typescript fragment
423
423
  // anywhere in your app
424
424
  const future = Carbon.now().addDays(3);
425
425
  future.diffForHumans(); // '3 days from now'
@@ -434,7 +434,7 @@ future.diffForHumans({ absolute: true }); // '3 days'
434
434
 
435
435
  ### Token-based
436
436
 
437
- ```typescript
437
+ ```typescript fragment
438
438
  // anywhere in your app
439
439
  d.format(); // '2026-06-15 09:30:00' (default)
440
440
  d.format("YYYY-MM-DD"); // '2026-06-15'
@@ -472,7 +472,7 @@ d.format("YYYY-MM-DDTHH:mm:ssZ"); // '2026-06-15T09:30:00+02:00'
472
472
 
473
473
  `intlFormat` delegates to `Intl.DateTimeFormat`:
474
474
 
475
- ```typescript
475
+ ```typescript fragment
476
476
  // anywhere in your app
477
477
  d.intlFormat("en-US", { dateStyle: "full" });
478
478
  // → 'Monday, June 15, 2026'
@@ -489,7 +489,7 @@ d.intlFormat("ja-JP", { year: "numeric", month: "long", day: "numeric" });
489
489
 
490
490
  ### Convenience formatters
491
491
 
492
- ```typescript
492
+ ```typescript fragment
493
493
  // anywhere in your app
494
494
  d.toDateString(); // '2026-06-15'
495
495
  d.toDateTimeString(); // '2026-06-15 09:30:00'
@@ -509,7 +509,7 @@ d.toLongDate(); // '15 June 2026'
509
509
 
510
510
  ## Serialisation
511
511
 
512
- ```typescript
512
+ ```typescript fragment
513
513
  // anywhere in your app
514
514
  d.toDate(); // native Date
515
515
  d.toISOString(); // '2026-06-15T07:30:00+00:00' (UTC)
@@ -529,7 +529,7 @@ d.toPlainDate(); // Temporal.PlainDate (loses time + timezone)
529
529
 
530
530
  `valueOf()` enables direct comparison with `<`, `>`, `-`:
531
531
 
532
- ```typescript
532
+ ```typescript fragment
533
533
  // anywhere in your app
534
534
  const a = new Carbon("2026-01-01");
535
535
  const b = new Carbon("2026-06-15");
@@ -544,7 +544,7 @@ Math.min(+a, +b) === +a; // true
544
544
  `CarbonInterval` is an immutable duration value object backed by
545
545
  `Temporal.Duration`. Every method returns a new instance.
546
546
 
547
- ```typescript
547
+ ```typescript fragment
548
548
  // anywhere in your app
549
549
  import { CarbonInterval } from "zerotal/carbon";
550
550
 
@@ -575,7 +575,7 @@ const built = new CarbonInterval({ years: 1, months: 6, days: 3 });
575
575
 
576
576
  Chain `and*` methods to compose multi-unit intervals:
577
577
 
578
- ```typescript
578
+ ```typescript fragment
579
579
  // anywhere in your app
580
580
  CarbonInterval.days(3).andHours(6).andMinutes(30);
581
581
  // → 3 days 6 hours 30 minutes
@@ -592,7 +592,7 @@ Available: `andYears`, `andMonths`, `andWeeks`, `andDays`, `andHours`,
592
592
 
593
593
  ### Interval getters
594
594
 
595
- ```typescript
595
+ ```typescript fragment
596
596
  // anywhere in your app
597
597
  const i = CarbonInterval.fromISO("P1Y2M3DT4H5M6S");
598
598
 
@@ -613,7 +613,7 @@ i.isZero; // false
613
613
 
614
614
  ### Interval arithmetic
615
615
 
616
- ```typescript
616
+ ```typescript fragment
617
617
  // anywhere in your app
618
618
  const a = CarbonInterval.hours(2);
619
619
  const b = CarbonInterval.minutes(30);
@@ -630,7 +630,7 @@ CarbonInterval.abs(a.negate()); // 2 hours (all fields positive)
630
630
  `cascade()` rolls excess sub-units up into higher ones. It needs a reference date
631
631
  for calendar-aware units (months, years) and defaults to now in UTC.
632
632
 
633
- ```typescript
633
+ ```typescript fragment
634
634
  // anywhere in your app
635
635
  CarbonInterval.seconds(90).cascade();
636
636
  // → { minutes: 1, seconds: 30 }
@@ -653,7 +653,7 @@ CarbonInterval.days(32).cascade(Carbon.today().inTimezone("UTC").toZonedDateTime
653
653
 
654
654
  Calendar units (years, months) are approximated as average lengths.
655
655
 
656
- ```typescript
656
+ ```typescript fragment
657
657
  // anywhere in your app
658
658
  CarbonInterval.hours(2).andMinutes(30).totalMinutes(); // 150
659
659
  CarbonInterval.days(3).andHours(6).totalHours(); // 78
@@ -667,7 +667,7 @@ i.totalWeeks();
667
667
 
668
668
  ### Interval comparison
669
669
 
670
- ```typescript
670
+ ```typescript fragment
671
671
  // anywhere in your app
672
672
  const a = CarbonInterval.hours(2);
673
673
  const b = CarbonInterval.minutes(90);
@@ -681,7 +681,7 @@ CarbonInterval.compare(a, b); // 1 (a > b), -1 (a < b), 0 (equal)
681
681
 
682
682
  ### Human-readable output
683
683
 
684
- ```typescript
684
+ ```typescript fragment
685
685
  // anywhere in your app
686
686
  CarbonInterval.days(1).andHours(2).andMinutes(30).forHumans();
687
687
  // → '1 day 2 hours 30 minutes'
@@ -697,7 +697,7 @@ i.toString(); // alias for forHumans()
697
697
 
698
698
  ### Interval serialisation
699
699
 
700
- ```typescript
700
+ ```typescript fragment
701
701
  // anywhere in your app
702
702
  CarbonInterval.days(1).andHours(2).toISO();
703
703
  // → 'P1DT2H'
@@ -715,7 +715,7 @@ An interval is what [`add()` and `subtract()`](#add-and-subtract) accept, and wh
715
715
  [`diffAsCarbonInterval()`](#differences) returns — the two directions between the
716
716
  types:
717
717
 
718
- ```typescript
718
+ ```typescript fragment
719
719
  // anywhere in your app
720
720
  const interval = CarbonInterval.days(3).andHours(6);
721
721
 
@@ -732,7 +732,7 @@ immutable and pure, so it needs no application — but a test that reaches for
732
732
  **Pin the instant.** Pass a fixed input rather than using the current time, and
733
733
  the assertion holds forever:
734
734
 
735
- ```typescript
735
+ ```typescript fragment
736
736
  // tests/dates/BillingPeriod.test.ts
737
737
  import { test, expect } from "bun:test";
738
738
  import { Carbon } from "zerotal/carbon";
@@ -751,7 +751,7 @@ test("a mid-month signup bills to the end of the month", () => {
751
751
  calls `Carbon.now()` internally can only be tested by waiting or by mocking; one
752
752
  that accepts a `now` parameter is tested by passing a date:
753
753
 
754
- ```typescript
754
+ ```typescript fragment
755
755
  // app/services/billing.ts
756
756
  export function periodFor(signedUp: Carbon, now: Carbon = Carbon.now()): Period {
757
757
  // …
@@ -766,7 +766,7 @@ month end.
766
766
  transitions are where date code breaks, and none of them appear in a test written
767
767
  around today:
768
768
 
769
- ```typescript
769
+ ```typescript fragment
770
770
  // tests/dates/BillingPeriod.test.ts
771
771
  test("handles a leap day", () => {
772
772
  expect(Carbon.create("2028-02-29T12:00:00Z").addYears(1).toDateString()).toBe("2029-02-28");
package/docs/changelog.md CHANGED
@@ -27,9 +27,173 @@ the section for every version you cross and apply its migration notes, not only
27
27
  majors. [Releases and versioning](/docs/support-policy#releases-and-versioning) explains
28
28
  when that carve-out ends.
29
29
 
30
+ ## 1.7.5 — 2026-08-23
31
+
32
+ Two bugs that shipped to every deployed app, a package promoted to `stable`, and
33
+ the gates that would have caught both.
34
+
35
+ ### Changed
36
+
37
+ - **`@zerotal/arch` is `stable`.** Reviewed ahead of its 1.9.0 date. The API follows
38
+ SemVer strictly from here, and that promise covers the **MCP tool contract** — tool
39
+ names, their arguments, and the shape of what they return. That is what an agent
40
+ client is configured against, and nothing type-level can see it: `archTools` has the
41
+ same signature however the tools are named. `mcp-surface.md` records all nine and CI
42
+ diffs it on every change. The protocol revision the server speaks is not covered; it
43
+ follows the protocol.
44
+
45
+ - **INTERNAL — the writers behind `arch:install` are no longer public API.**
46
+ `detectAgents`, `applyMcpConfig`, `applyBlock`, `buildGuidelines` and the rest are
47
+ `@internal`: still exported, still working, no longer promised. Their only caller is
48
+ the install command, and freezing them would have committed the shape of `.mcp.json`
49
+ writing to the rest of the 1.x line on behalf of a caller who never arrived.
50
+
51
+ - **INTERNAL — `api-surface.md` honours `@internal` across every package.** The
52
+ contract has always read "anything importable without an `@internal` marker keeps its
53
+ shape", and the generator did not read the tag — so symbols already marked internal
54
+ were recorded as though promised. 374 entries across 13 packages are omitted now,
55
+ every one verified marked. Nothing changes at runtime or in the types; the file
56
+ listing the promises now lists the promises.
57
+
58
+ - **A modal locks the page behind it.** `<Modal>` and `<Drawer>` trapped focus
59
+ correctly while the page underneath kept scrolling, which on a phone reads as the
60
+ dialog having broken the page.
61
+
62
+ - **Flow marks the active nav link for everyone.** `<Link navigate>` set
63
+ `data-current`, which styles a link, and nothing that announces it. It sets
64
+ `aria-current="page"` alongside now, so a screen reader can tell which of thirty nav
65
+ items is the current page.
66
+
67
+ ### Fixed
68
+
69
+ - **Assets were cache-busted in development and not in production.** `asset()` appended
70
+ `?v=` only when a dev version was set, so every deployed Zerotal app served the
71
+ previous build's JavaScript and CSS to anyone with a warm cache — indefinitely, since
72
+ the URL never changed. The version is now derived from the built files themselves, so
73
+ it is stable across restarts and moves when the files do.
74
+
75
+ - **DevTools mounted on production pages.** The provider is gated on the environment, so
76
+ the endpoints are absent outside development — and the client took that to mean it
77
+ could start anyway, pinning a floating panel to the page whose tabs read
78
+ `Could not read the map — HTTP 404`. It now mounts only when the server half says it
79
+ is there, via a `<meta>` the middleware writes, and makes no request at all on a
80
+ public hostname.
81
+
82
+ - **Browser tests drove an unstyled site.** `Router.static("/", public)` is registered
83
+ only for the `web` environment, and a test app is not one — so every `FlowBrowser`
84
+ suite served pages without their stylesheet. Invisible to assertions that read text;
85
+ fatal for anything measuring layout.
86
+
87
+ ### Documented
88
+
89
+ - **Every TypeScript example in the documentation is compiled against the real
90
+ packages**, on every pull request. 1,593 blocks. The gate found examples importing
91
+ symbols that do not exist (`currentUser`, `Layout` from the wrong package), calling
92
+ methods that were renamed (`Cache.put`), and configuring fields with `env()` where the
93
+ type is a literal union. Blocks deliberately written as fragments say so in their
94
+ fence and are recorded by key, so a new one is a deliberate act rather than a silent
95
+ exemption.
96
+
97
+ - **A break cannot ship without a release note.** `api:surface:check` demands a
98
+ regenerated snapshot when an export changes and then goes quiet, so the changelog was
99
+ defended by remembering — and 1.7.3 shipped the removal of Flow's `this.title(…)` with
100
+ no BREAKING entry. That entry is now in 1.7.3's notes, the support policy counts three
101
+ breaks rather than two, and `breaking:check` reads the snapshot diff so the next one
102
+ cannot pass silently.
103
+
104
+ - **A maturity label falls due.** The review release for a package below `stable` lives
105
+ in its `package.json` as `maturityReview`, and the package-conventions gate fails once
106
+ the version reaches it.
107
+
108
+ ## 1.7.4 — 2026-08-21
109
+
110
+ A debug panel that was reaching production, a column type MySQL would not index, and
111
+ 2,060 icons.
112
+
113
+ ### Fixed
114
+
115
+ - **DevTools no longer appears on a production page.** The provider is gated on the
116
+ environment, so in production its routes are absent — and the browser client took that as
117
+ permission to start anyway and "connect to nothing". It did not: it mounted the panel first
118
+ and discovered the absence afterwards, so an app calling `DevTools.start()` unconditionally
119
+ served a floating DevTools bar to every visitor, its tabs reading
120
+ `Could not read the map — HTTP 404`.
121
+
122
+ `start()` now probes for the routes and builds nothing unless they answer — no shell, no
123
+ shadow root, no `EventSource`, no listeners. Any failure (404, offline, CSP) is read as
124
+ absent. **If your app calls `DevTools.start()`, take this release.**
125
+
126
+ - **A string column could not carry an index on MySQL.** `table.string()` compiled to `TEXT`
127
+ on every engine and discarded its `length`, and MySQL refuses to key a TEXT column without
128
+ a prefix length — so `table.string("email").unique()` failed at `CREATE TABLE`. MySQL now
129
+ gets `VARCHAR(length)`; SQLite and PostgreSQL keep `TEXT`. `char()` had the same bug and the
130
+ same fix. Found by the new MySQL suite on its first run against a real server.
131
+
132
+ ### Added
133
+
134
+ - **`<Icon name="inbox" />` — 2,060 icons, bundled, typed by name.** The set ships inside
135
+ `@zerotal/flow-ui`, so there is nothing to install and no generator to run: a fresh app gets
136
+ autocomplete over every name and a compile error on a typo. Rendered on the server as inline
137
+ SVG, so there is no icon font, no sprite, no request per glyph, and nothing for a strict CSP
138
+ to block. Four icons are drawn for sign-in flows the set has no name for — `passkey`,
139
+ `two-factor`, `otp`, `magic-link` — and three brand marks ship for the social-login providers
140
+ `@zerotal/auth` supports. See [Icons](/docs/flow/icons).
141
+
142
+ - **The ORM suite runs against MySQL 8 in CI, and the job blocks merges.** The same smoke
143
+ suite that covers PostgreSQL — schema DDL and `ALTER`, identity columns, CRUD, type
144
+ round-trips, unique and NOT NULL enforcement, row locks, transaction rollback. MySQL moves
145
+ from _experimental_ to _supported, hardening_; see the
146
+ [Support Policy](/docs/support-policy).
147
+
148
+ ### Changed
149
+
150
+ - **The starters link by route name.** Every hard-coded `href="/about"` in the React and Vue
151
+ templates now goes through `route()`, and the templates ship the generated route table so a
152
+ freshly scaffolded app type-checks before its first `zt dev`.
153
+
154
+ ### Documented
155
+
156
+ - **The HTTP client guide is one page.** Eight pages became one, written from where the
157
+ package is used — your app calling somebody else's service — with straight URLs instead of
158
+ a route map threaded through every example. See [HTTP Client](/docs/client).
159
+
160
+ - **`route()` in Inertia**, for links and for form submissions, including the one thing Inertia
161
+ adds: a page renders in two processes, so `defineRoutes()` has to run in the SSR entry too.
162
+ See [Building URLs](/docs/inertia/rendering#building-urls-with-route).
163
+
164
+ - **Every package changelog has the release headings it was missing.** `[Unreleased]` had
165
+ accumulated four releases of shipped work — `@zerotal/flow-ui`'s newest heading read
166
+ `[1.5.0]` while 1.7.3 was on npm. Cutting a release now moves them.
167
+
30
168
  ## 1.7.3 — 2026-08-20
31
169
 
32
- Two fields that accepted input and threw it away, and a CI job that was testing nothing.
170
+ Two fields that accepted input and threw it away, a CI job that was testing nothing, and a
171
+ name given back to applications.
172
+
173
+ ### Changed
174
+
175
+ - **BREAKING — `this.title(…)` is removed from Flow components.** Declare `static title`
176
+ instead, as a string or a function of the component:
177
+
178
+ ```ts fragment
179
+ // Before
180
+ override async mount(): Promise<void> {
181
+ this.title(`Search: ${this.query}`);
182
+ }
183
+
184
+ // After
185
+ static title = (c: SearchPage) => (c.query ? `Search: ${c.query}` : "Search");
186
+ ```
187
+
188
+ The instance method held a name four separate components wanted for their own data — a
189
+ media row, a guide, a review, an issue — for a one-line accessor that belongs on the class.
190
+ The static form is also the better one: it is resolved on the server for every render and
191
+ every patch, so a title that depends on state follows it without an action remembering to
192
+ update it.
193
+
194
+ A call to `this.title(…)` on a component that declares its own `title` field now sets that
195
+ field instead of the document title, which is silent. Search your components for
196
+ `this.title(` before upgrading; every hit is either a migration or was already shadowed.
33
197
 
34
198
  ### Fixed
35
199