@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
@@ -30,7 +30,7 @@ as it does today.
30
30
 
31
31
  Turn it on per resource:
32
32
 
33
- ```ts
33
+ ```ts fragment
34
34
  export class ProductResource extends Resource {
35
35
  static override history = true;
36
36
  }
@@ -50,7 +50,7 @@ ago should not also undo the description somebody fixed yesterday.
50
50
  The support request nobody can reproduce is usually solved by seeing what the person
51
51
  actually sees.
52
52
 
53
- ```ts
53
+ ```ts fragment
54
54
  export class UserResource extends Resource {
55
55
  static override impersonatable = true;
56
56
 
@@ -79,7 +79,7 @@ Every bit of list state already lives in the URL: search, filters, tab, sort, co
79
79
  visibility, grouping, page size. So a saved view is a saved query string, and
80
80
  restoring one is a link.
81
81
 
82
- ```ts
82
+ ```ts fragment
83
83
  Panel.savedViews({
84
84
  async list(resource) {
85
85
  /* … */
@@ -103,7 +103,7 @@ A file upload field puts a file somewhere and stores a path. That works until th
103
103
  logo is needed on twenty products, or somebody wants to know what is still pointing at
104
104
  a file before deleting it.
105
105
 
106
- ```ts
106
+ ```ts fragment
107
107
  import { databaseMedia, mediaPicker } from "@zerotal/admin";
108
108
 
109
109
  Panel.media(databaseMedia());
@@ -117,7 +117,7 @@ somewhere else.
117
117
  Configured, the panel gains a Media page — a grid with upload, search, folder
118
118
  filtering, alt-text editing and deletion — and forms can use the picker:
119
119
 
120
- ```ts
120
+ ```ts fragment
121
121
  mediaPicker("imageUrl").label("Image");
122
122
  ```
123
123
 
@@ -134,7 +134,7 @@ Authorization already works without a UI: a resource's `can()` answers every que
134
134
  the panel asks. What is missing is the other direction — seeing who can do what, and
135
135
  changing it, without editing code.
136
136
 
137
- ```ts
137
+ ```ts fragment
138
138
  import { authRoles } from "@zerotal/admin";
139
139
 
140
140
  Panel.roles(authRoles({ superusers: ["admin"] }));
@@ -162,7 +162,7 @@ week.
162
162
  What belongs at the top of a dashboard differs by role, and neither the finance lead
163
163
  nor support wants to scroll past the other's widget every morning.
164
164
 
165
- ```ts
165
+ ```ts fragment
166
166
  Panel.dashboardLayout({
167
167
  async load() {
168
168
  return Auth.user()?.dashboard ?? null;
@@ -193,7 +193,7 @@ stale key mentions it.
193
193
  Two people editing the same record is normal; one of them losing their work silently
194
194
  is not.
195
195
 
196
- ```ts
196
+ ```ts fragment
197
197
  export class ProductResource extends Resource {
198
198
  static override optimisticLock = "version";
199
199
  }
@@ -207,7 +207,7 @@ overwriting loses somebody's work, and merging blind is worse.
207
207
 
208
208
  The expensive mistake is editing production believing it is staging.
209
209
 
210
- ```ts
210
+ ```ts fragment
211
211
  import { environmentIndicator } from "@zerotal/admin";
212
212
 
213
213
  Panel.renderHook("body.start", environmentIndicator());
@@ -18,7 +18,7 @@ Scaffold one with the generator, which writes `app/admin/<Name>Resource.ts`:
18
18
  bun zt make:admin-resource Post
19
19
  ```
20
20
 
21
- ```ts
21
+ ```ts fragment
22
22
  class PostResource extends Resource {
23
23
  static model = Post;
24
24
 
@@ -71,7 +71,7 @@ class PostResource extends Resource {
71
71
  A resource does nothing until the panel knows about it. Register it where the panel
72
72
  is configured:
73
73
 
74
- ```ts
74
+ ```ts fragment
75
75
  Panel.register(PostResource);
76
76
  ```
77
77
 
@@ -93,7 +93,7 @@ any of its links changing.
93
93
  Override `can(ability, record?)` to gate the built-in actions. Abilities used by the
94
94
  presets: `create`, `update`, `delete`, `restore`, `forceDelete`.
95
95
 
96
- ```ts
96
+ ```ts fragment
97
97
  static can(ability: string, record?: AdminRecord) {
98
98
  return Gate.allows(ability, record ?? this.model);
99
99
  }
@@ -21,7 +21,7 @@ real back office takes.
21
21
  A cluster gives a group of resources a shared URL segment and a single sidebar
22
22
  entry. Declare one, then point resources at it:
23
23
 
24
- ```ts
24
+ ```ts fragment
25
25
  import { Cluster } from "@zerotal/admin";
26
26
 
27
27
  export class ShopCluster extends Cluster {
@@ -42,7 +42,7 @@ the cluster segment on its own. Nothing in a resource needs to know its own path
42
42
 
43
43
  A cluster's `ability` gates the whole section:
44
44
 
45
- ```ts
45
+ ```ts fragment
46
46
  export class FinanceCluster extends Cluster {
47
47
  static override slug = "finance";
48
48
  static override title = "Finance";
@@ -59,7 +59,7 @@ Some records have no meaning apart from their parent. Comments belong to a post;
59
59
  there is no useful screen listing every comment in the database. Declare the
60
60
  parent and the resource moves inside it:
61
61
 
62
- ```ts
62
+ ```ts fragment
63
63
  export class CommentResource extends Resource {
64
64
  static override model = Comment;
65
65
  static override parent = { resource: () => PostResource, foreignKey: "post_id" };
@@ -83,7 +83,7 @@ on whichever side of the import cycle evaluated first.
83
83
  When the parent declares the child with `hasMany`, its view page links into the
84
84
  nested pages instead of rendering an inline table:
85
85
 
86
- ```ts
86
+ ```ts fragment
87
87
  export class PostResource extends Resource {
88
88
  static override relations() {
89
89
  return [hasMany(CommentResource, "post_id").title("Comments")];
@@ -96,7 +96,7 @@ export class PostResource extends Resource {
96
96
  Site settings are one row. A list of one, with a view page and an edit page behind
97
97
  it, is three screens too many:
98
98
 
99
- ```ts
99
+ ```ts fragment
100
100
  export class SettingsResource extends Resource {
101
101
  static override model = Setting;
102
102
  static override singular = true;
@@ -123,7 +123,7 @@ than an error.
123
123
  Most applications have one panel, and `Panel.configure(...)` / `Panel.register(...)`
124
124
  write to it. When a second audience needs a second set of screens, make another:
125
125
 
126
- ```ts
126
+ ```ts fragment
127
127
  // The back office — everything.
128
128
  Panel.configure({ brand: "Acme", path: "/admin", middleware: [AuthMiddleware] });
129
129
  Panel.register(ProductResource, OrderResource, UserResource);
@@ -7,7 +7,7 @@ description: Columns, filters, sorting, search, and bulk actions on the list scr
7
7
 
8
8
  `columns()` returns `Column`s built with `text(key)` plus chainable modifiers.
9
9
 
10
- ```ts
10
+ ```ts fragment
11
11
  static columns() {
12
12
  return [
13
13
  text("title").label("Title").sortable().searchable(),
@@ -42,14 +42,14 @@ static columns() {
42
42
  formatter) render a `<tfoot>` total computed over the **full filtered dataset**, plus
43
43
  per-group subtotals when grouping is active.
44
44
 
45
- ```ts
45
+ ```ts fragment
46
46
  text("amount").align("end").sum("Revenue", (n) => `$${n.toFixed(2)}`),
47
47
  text("id").count("Orders"),
48
48
  ```
49
49
 
50
50
  ## Filters
51
51
 
52
- ```ts
52
+ ```ts fragment
53
53
  import { selectFilter, ternaryFilter } from "@zerotal/admin";
54
54
 
55
55
  static filters() {
@@ -70,7 +70,7 @@ Fixed filters work when you can name the useful questions in advance. A catalogu
70
70
  or a ledger is queried in too many ways for that, so `queryBuilder` lets the user
71
71
  stack their own comparisons and nest AND/OR groups:
72
72
 
73
- ```ts
73
+ ```ts fragment
74
74
  import {
75
75
  queryBuilder, textConstraint, numberConstraint,
76
76
  selectConstraint, booleanConstraint, dateConstraint,
@@ -112,7 +112,7 @@ link someone can send to a colleague.
112
112
  `tab(key)` adds quick-filter tabs above the table, each scoping the query and
113
113
  optionally showing a count badge.
114
114
 
115
- ```ts
115
+ ```ts fragment
116
116
  import { tab } from "@zerotal/admin";
117
117
 
118
118
  static tabs() {
@@ -129,7 +129,7 @@ static tabs() {
129
129
  `group(column)` adds a "Group by" menu; the page renders header rows per bucket with
130
130
  counts (and per-group summary subtotals).
131
131
 
132
- ```ts
132
+ ```ts fragment
133
133
  import { group } from "@zerotal/admin";
134
134
 
135
135
  static groups() {
@@ -143,7 +143,7 @@ static defaultGroup = "status"; // optional
143
143
  Set `reorderable` to an integer position column to show up/down handles that persist
144
144
  order:
145
145
 
146
- ```ts
146
+ ```ts fragment
147
147
  static reorderable = "sort";
148
148
  ```
149
149
 
@@ -158,7 +158,7 @@ automatically and are URL-driven.
158
158
  A blank table teaches nobody anything. Override `emptyState()` to say why the list
159
159
  is empty and what will fill it:
160
160
 
161
- ```ts
161
+ ```ts fragment
162
162
  static emptyState() {
163
163
  return {
164
164
  heading: "No orders yet",
@@ -26,7 +26,7 @@ works alongside the admin-specific ones below.
26
26
  | `AdminTest.view(Resource, id)` | View page | Infolist entries and header actions for a record |
27
27
  | `AdminTest.form(Resource, mode?)` | Create / Edit form | Fields, validation and saving |
28
28
 
29
- ```ts
29
+ ```ts fragment
30
30
  // tests/admin/users.test.ts
31
31
  import { AdminTest, assertHasColumn, assertHasAction } from "@zerotal/admin/testing";
32
32
  import { UserResource } from "../../app/admin/UserResource.ts";
@@ -41,7 +41,7 @@ list.assertSee("ada@example.com");
41
41
  default) or `"edit"`. Drive the form through its `form` property, which holds
42
42
  every field's value:
43
43
 
44
- ```ts
44
+ ```ts fragment
45
45
  const form = await AdminTest.form(UserResource, "create");
46
46
  assertHasField(form, UserResource, "name");
47
47
  await form.set("form", { name: "" });
@@ -56,7 +56,7 @@ those values as the second argument to `AdminTest.list()` and the page mounts as
56
56
  though the reader had arrived on that URL — which is how you assert on a filtered
57
57
  or sorted table without first driving the clicks that would produce it.
58
58
 
59
- ```ts
59
+ ```ts fragment
60
60
  const list = await AdminTest.list(UserResource, {
61
61
  search: "ada",
62
62
  sortBy: "createdAt",
@@ -125,7 +125,7 @@ Every helper accepts a panel as its last argument, defaulting to
125
125
  `Panel.default()`. Apps that register more than one panel pass the one under test,
126
126
  so the page resolves that panel's own configuration and navigation:
127
127
 
128
- ```ts
128
+ ```ts fragment
129
129
  const shop = Panel.get("shop");
130
130
 
131
131
  const list = await AdminTest.list(OrderResource, {}, shop);
package/docs/ai.md CHANGED
@@ -116,7 +116,7 @@ a price with `registerModelPrice()`.
116
116
 
117
117
  ## Generating text
118
118
 
119
- ```ts
119
+ ```ts fragment
120
120
  import { Ai } from "@zerotal/ai";
121
121
 
122
122
  // Just the text.
@@ -152,7 +152,7 @@ Reach for `effort` instead. It trades thoroughness against cost and latency:
152
152
 
153
153
  ### Streaming
154
154
 
155
- ```ts
155
+ ```ts fragment
156
156
  for await (const chunk of Ai.stream({ prompt, signal })) {
157
157
  if (chunk.type === "text") process.stdout.write(chunk.text);
158
158
  if (chunk.type === "done") console.log(chunk.response.usage);
@@ -166,7 +166,7 @@ generation. See [Flow](/docs/flow) for streaming straight into a component.
166
166
 
167
167
  ### Structured output
168
168
 
169
- ```ts
169
+ ```ts fragment
170
170
  const review = await Ai.object({ prompt: `Classify this review:\n\n${text}` }, (rule) => ({
171
171
  sentiment: rule.string().in(["positive", "neutral", "negative"]),
172
172
  summary: rule.string().max(140),
@@ -189,13 +189,13 @@ schema is defined, not when the request is sent.
189
189
  `strippedConstraints()` names exactly what the model will not see, if you want to
190
190
  check that a load-bearing constraint is visible to it:
191
191
 
192
- ```ts
192
+ ```ts fragment
193
193
  strippedConstraints({ title: rule.string().min(3) }); // → ["title: min"]
194
194
  ```
195
195
 
196
196
  ## Tools and the agent loop
197
197
 
198
- ```ts
198
+ ```ts fragment
199
199
  import { Ai, tool } from "@zerotal/ai";
200
200
 
201
201
  const lookupOrder = tool({
@@ -238,7 +238,7 @@ back and re-requests.
238
238
 
239
239
  ### Locking a run
240
240
 
241
- ```ts
241
+ ```ts fragment
242
242
  await Ai.agent({
243
243
  prompt: "Refund order 4821 if it shipped over 30 days ago.",
244
244
  tools: [lookupOrder, issueRefund],
@@ -264,7 +264,7 @@ checking crashes on a response the API considers fine.
264
264
 
265
265
  This package checks the stop reason first and raises a typed error:
266
266
 
267
- ```ts
267
+ ```ts fragment
268
268
  import { AiRefusedError } from "@zerotal/ai";
269
269
 
270
270
  try {
@@ -283,7 +283,7 @@ with `drivers.anthropic.fallbacks: false`.
283
283
 
284
284
  ## Embeddings
285
285
 
286
- ```ts
286
+ ```ts fragment
287
287
  const { embeddings } = await Ai.embed(["first chunk", "second chunk"]);
288
288
  ```
289
289
 
@@ -294,7 +294,7 @@ One vector per input, in input order.
294
294
  A queued generation is serialized, so its completion handler is registered by **name**
295
295
  — a closure cannot survive the trip to a worker process:
296
296
 
297
- ```ts
297
+ ```ts fragment
298
298
  // in a service provider's onBooted(), so the worker registers it too
299
299
  Ai.onGenerated("summarize-ticket", async (response, meta) => {
300
300
  await Ticket.query().where("id", meta.ticketId).update({ summary: response.text });
@@ -313,7 +313,7 @@ in-process where its tools are.
313
313
  `AiFake` replaces the container binding and answers from a script. No API key, no
314
314
  network, no flakiness:
315
315
 
316
- ```ts
316
+ ```ts fragment
317
317
  import { AiFake } from "@zerotal/ai";
318
318
 
319
319
  const ai = AiFake.install();
@@ -361,7 +361,7 @@ suffix, a gateway that rewrites the base URL.
361
361
  Implement `AiDriver` — `text`, `stream`, `object`, `countTokens`, `verify` — and
362
362
  register it:
363
363
 
364
- ```ts
364
+ ```ts fragment
365
365
  // in a service provider's onBooted()
366
366
  const ai = app.container.makeSync("ai");
367
367
  ai.extend("bedrock", () => new BedrockDriver(config));
@@ -371,3 +371,110 @@ Nothing else is needed. Spend ceilings, redaction, telemetry, the lock, and the
371
371
  loop all live above the driver, so a new provider is a translation layer and nothing
372
372
  more. `agent()` is optional on the interface and none of the built-in three implement
373
373
  it — they all run the same shared loop.
374
+
375
+ ## Reference
376
+
377
+ Every exported name, grouped by the job it belongs to. The behaviour is in the
378
+ sections above; this is the index.
379
+
380
+ ### Requests and responses
381
+
382
+ | Name | Description |
383
+ | ------------------- | ------------------------------------------------------------------------------ |
384
+ | `AiRequest` | What every generation call takes. `prompt` and `messages` are interchangeable. |
385
+ | `AiResponse` | A finished, non-streaming generation. |
386
+ | `AiStreamChunk` | One event from a streaming generation. |
387
+ | `AiObjectResponse` | A structured-output generation: the parsed value plus the usual accounting. |
388
+ | `AiMessage` | One turn of a conversation. |
389
+ | `AiRole` | Who said it. Tool results ride inside a `user` turn, as the providers expect. |
390
+ | `AiUsage` | Token accounting for one request. Fields a provider does not report stay 0. |
391
+ | `AiStopReason` | Why generation stopped. `refusal` is a successful HTTP response, not an error. |
392
+ | `AiEffort` | How hard the model should work before answering. Mapped per-driver. |
393
+ | `AiProviderOptions` | Per-driver escape hatch, keyed by driver name and passed through untouched. |
394
+ | `AiEmbedRequest` | A vector embedding request. |
395
+ | `AiEmbedResponse` | Embeddings, one vector per input, in input order. |
396
+
397
+ ### Tools and the agent loop
398
+
399
+ | Name | Description |
400
+ | ---------------- | -------------------------------------------------------------------------------------- |
401
+ | `AiToolCall` | A tool call the model asked for, lifted out of whatever block shape the provider used. |
402
+ | `AiToolResult` | The answer to one `AiToolCall`. |
403
+ | `AiToolContext` | What a tool handler is told about the turn that invoked it. |
404
+ | `AiToolCalled` | Emitted once per tool call inside an agent run. |
405
+ | `AiAgentRequest` | An agent run, plus the two things only the caller can decide. |
406
+ | `AgentOptions` | What the agent loop needs from the caller, beyond the request itself. |
407
+ | `AiAgentResult` | The result of running the agent loop to completion. |
408
+ | `AiAgentStep` | One tool call and its result within an agent run. |
409
+
410
+ ### Errors
411
+
412
+ | Name | Description |
413
+ | -------------------------- | --------------------------------------------------------------------------- |
414
+ | `AiError` | Base class for all `@zerotal/ai` errors. |
415
+ | `AiConfigError` | Thrown at boot, or on first use, for a config combination that cannot work. |
416
+ | `AiRequestError` | Thrown for any other non-2xx from the provider, carrying its status. |
417
+ | `AiRateLimitError` | Thrown when the provider rate-limits. The SDKs already retried. |
418
+ | `AiSpendLimitError` | Thrown when a request would breach a configured spend ceiling. |
419
+ | `AiAgentLimitError` | Thrown when the agent loop hits its step or resume ceiling. |
420
+ | `AiCancelledError` | Thrown when the caller's `AbortSignal` fired before the call finished. |
421
+ | `AiDriverUnavailableError` | Thrown when a driver's optional peer package is not installed. |
422
+ | `UnknownAiDriverError` | Thrown for a driver name the manager does not know. |
423
+
424
+ ### Configuration
425
+
426
+ | Name | Description |
427
+ | ----------------------- | ----------------------------------------------------------------------------- |
428
+ | `AiConfigInput` | What `AiConfig()` accepts — every key optional, all the way down. |
429
+ | `AiConfigFromEnv` | The zero-config fallback: an Anthropic driver built from `ANTHROPIC_API_KEY`. |
430
+ | `AiLimitsConfigShape` | Spend ceilings, enforced before the request leaves. |
431
+ | `AiAgentConfigShape` | How the agent loop behaves. |
432
+ | `AnthropicConfigShape` | Anthropic driver settings. |
433
+ | `OpenAiConfigShape` | OpenAI driver settings. |
434
+ | `OllamaConfigShape` | Ollama driver settings — a local server, so no key. |
435
+ | `EmbeddingsConfigShape` | Embeddings are their own block with their own driver. |
436
+
437
+ ### Drivers and pricing
438
+
439
+ | Name | Description |
440
+ | ------------------------ | ---------------------------------------------------------------------------- |
441
+ | `AnthropicDriver` | The Anthropic driver. |
442
+ | `OpenAiDriver` | The OpenAI driver — Chat Completions over `fetch`, no SDK. |
443
+ | `OllamaDriver` | The Ollama driver — a local model server, so no API key and no billing. |
444
+ | `EmbeddingsDriver` | What an embeddings provider implements. |
445
+ | `OpenAiEmbeddingsDriver` | OpenAI embeddings over `fetch`. No SDK, no dependency. |
446
+ | `OllamaEmbeddingsDriver` | Ollama embeddings — a local server, so no key and no per-token cost. |
447
+ | `DriverStatus` | What `zt ai:test` prints for one driver. |
448
+ | `ModelPrice` | USD per million tokens. |
449
+ | `modelPrice` | The price for a model, or `undefined` when we have none. |
450
+ | `estimateCost` | Estimated USD for one request's usage. Returns 0 for an unpriced model. |
451
+ | `modelRejectsSampling` | Whether a Claude model rejects `temperature` / `top_p` / `top_k` with a 400. |
452
+
453
+ ### Spend and statistics
454
+
455
+ | Name | Description |
456
+ | -------------------- | --------------------------------------------------------- |
457
+ | `spentToday` | USD recorded so far today, in this process. |
458
+ | `resetSpend` | Reset the ledger. Tests, and the `ai:spend --reset` path. |
459
+ | `AiDelivery` | One recorded generation. |
460
+ | `modelStats` | Per-model roll-up over everything still in the buffer. |
461
+ | `ModelStat` | Rolled-up figures for one model. |
462
+ | `recentGenerations` | The most recent generations, newest first. |
463
+ | `refusalRate` | Share of recorded calls that the provider declined, 0–1. |
464
+ | `resetStats` | Reset the buffer. Tests. |
465
+ | `CapturedGeneration` | One recorded call, as `AiFake` captures it. |
466
+
467
+ ### Queued generation
468
+
469
+ | Name | Description |
470
+ | ---------------- | -------------------------------------------- |
471
+ | `AiQueueOptions` | What `Ai.queue()` needs beyond the request. |
472
+ | `AiQueueHandler` | What a queued generation's handler receives. |
473
+
474
+ ### Structured-output schemas
475
+
476
+ | Name | Description |
477
+ | ----------------- | --------------------------------------------------------------------------- |
478
+ | `SchemaInput` | Either shape callers have on hand: the builder map, or the raw definitions. |
479
+ | `toSchema` | Normalise either input shape to raw definitions. |
480
+ | `translateSchema` | Translate a validator schema into the JSON Schema the providers accept. |
@@ -73,7 +73,7 @@ helper reaches it anywhere.
73
73
  > **Note** — `Application.create({ providers })` and
74
74
  > `Application.create({ providers, config, env })` accept explicit providers and
75
75
  > config for lightweight in-process test setups that bypass auto-discovery. See
76
- > [Testing](/docs/testing/index).
76
+ > [Testing](/docs/testing).
77
77
 
78
78
  ### The current application
79
79
 
@@ -159,7 +159,7 @@ Framework packages (`@zerotal/orm`, `@zerotal/auth`, `@zerotal/notifications`,
159
159
  their own providers. List them in the **providers array** in
160
160
  `bootstrap/providers.ts` and pass that array to `Application.create({ providers })`:
161
161
 
162
- ```typescript
162
+ ```typescript fragment
163
163
  // bootstrap/providers.ts
164
164
  import { DatabaseProvider } from "@zerotal/orm";
165
165
  import { AuthProvider } from "@zerotal/auth";
@@ -170,7 +170,7 @@ const providers = [DatabaseProvider, AuthProvider, AppServiceProvider];
170
170
  export default providers;
171
171
  ```
172
172
 
173
- ```typescript
173
+ ```typescript fragment
174
174
  // bootstrap/app.ts
175
175
  import { Application, basePath } from "zerotal";
176
176
  import providers from "./providers.ts";
@@ -190,7 +190,7 @@ A deferred provider boots lazily — only the first time one of its container
190
190
  bindings is resolved. This keeps cold-start fast when a service isn't used on
191
191
  every request.
192
192
 
193
- ```typescript
193
+ ```typescript fragment
194
194
  // bootstrap/app.ts — defer() is called on the app instance (and returns it)
195
195
  const app = Application.create({ providers });
196
196
 
@@ -216,7 +216,7 @@ config-driven wiring, middleware. But when you just need to register a binding o
216
216
  two, that's a lot of ceremony. For those cases, `bind()` lets you register
217
217
  straight from `bootstrap/app.ts`:
218
218
 
219
- ```typescript
219
+ ```typescript fragment
220
220
  // bootstrap/app.ts
221
221
  import { Application, basePath } from "zerotal";
222
222
  import { Clock, SystemClock } from "../app/services/clock.ts";
@@ -249,7 +249,7 @@ Declare where your routes live; the application loads them at boot, after every
249
249
  provider has registered its middleware groups (so a route file can always
250
250
  reference `web`, `api`, or any provider-supplied group).
251
251
 
252
- ```typescript
252
+ ```typescript fragment
253
253
  // bootstrap/app.ts
254
254
  Application.create().routing({
255
255
  web: "./routes/web.ts",
@@ -262,7 +262,7 @@ middleware group, `api` mounts at `/api` with the `api` group. Any other group
262
262
  name must declare both `prefix` and `middleware` explicitly (or an error is thrown
263
263
  at boot):
264
264
 
265
- ```typescript
265
+ ```typescript fragment
266
266
  // bootstrap/app.ts
267
267
  .routing({
268
268
  web: "./routes/web.ts",
@@ -273,7 +273,7 @@ at boot):
273
273
  For directory-based routing, use `fileBasedRouting()` — same key semantics, but
274
274
  each value is a directory that's scanned for exported HTTP-method handlers:
275
275
 
276
- ```typescript
276
+ ```typescript fragment
277
277
  // bootstrap/app.ts
278
278
  Application.create().fileBasedRouting({ web: "./app/routes" });
279
279
  ```
@@ -285,7 +285,7 @@ root regardless of the calling file.
285
285
 
286
286
  Register global middleware that runs on every request, in array order:
287
287
 
288
- ```typescript
288
+ ```typescript fragment
289
289
  // bootstrap/app.ts
290
290
  import { CorsMiddleware, SecureHeadersMiddleware } from "zerotal";
291
291
 
@@ -296,7 +296,7 @@ The resolved pipeline runs provider-registered middleware first, then everything
296
296
  you added via `.use()`. You can read the final ordering back off the app instance
297
297
  (handy in a test or a diagnostic):
298
298
 
299
- ```typescript
299
+ ```typescript fragment
300
300
  // in a test or diagnostic
301
301
  currentApp().globalMiddleware; // PipeClass[] in execution order
302
302
  ```
@@ -315,7 +315,7 @@ class to push it onto the global pipeline automatically. See
315
315
  These are the remaining configuration methods, all called on the app instance in
316
316
  `bootstrap/app.ts` (and all returning it, so they chain):
317
317
 
318
- ```typescript
318
+ ```typescript fragment
319
319
  // bootstrap/app.ts
320
320
  import { Handler } from "../app/exceptions/Handler.ts";
321
321
 
@@ -360,7 +360,7 @@ provider hooks through a fixed set of phases.
360
360
  5. **Started** — `Bun.serve()` is live; `onStarted()` fires. A health endpoint, a
361
361
  PID file, and `SIGTERM`/`SIGINT`/`SIGUSR2` handlers are installed.
362
362
 
363
- ```typescript
363
+ ```typescript fragment
364
364
  // zt.ts (managed) ultimately does:
365
365
  const app = (await import("./bootstrap/app.ts")).default;
366
366
  await app.start(Number(env("PORT", 3000)));
package/docs/arch.md CHANGED
@@ -24,7 +24,7 @@ bun add -d @zerotal/arch
24
24
 
25
25
  Register the provider in `bootstrap/providers.ts`:
26
26
 
27
- ```typescript
27
+ ```typescript fragment
28
28
  // bootstrap/providers.ts
29
29
  import { ArchProvider } from "@zerotal/arch";
30
30
 
@@ -189,7 +189,7 @@ The transport is exported separately from the tools, on the `@zerotal/arch/mcp`
189
189
  A tool is a plain object with a schema and a `run`, so you can serve your own alongside
190
190
  these — or serve these over a transport of your own.
191
191
 
192
- ```typescript
192
+ ```typescript fragment
193
193
  import { McpServer, serveStdio } from "@zerotal/arch/mcp";
194
194
  import { archTools, vendoredDocsDir, spawnProbe } from "@zerotal/arch";
195
195
 
@@ -256,8 +256,30 @@ The transport is on the `@zerotal/arch/mcp` subpath: `McpServer`, `McpServerOpti
256
256
  `JsonRpcRequest`, `JsonRpcResponse`, `JsonRpcSuccess`, `JsonRpcFailure`, `JsonRpcId`,
257
257
  `RpcError`, `Meta`, `MODERN_VERSION`, `LEGACY_VERSIONS` and `SUPPORTED_VERSIONS`.
258
258
 
259
+ ## What `stable` covers here
260
+
261
+ The usual promise — anything importable without an `@internal` marker keeps its
262
+ shape for the rest of the 1.x line — and one more, because this package's real
263
+ interface is not its exports.
264
+
265
+ **The MCP tool contract is covered.** Tool names, the arguments they accept, and
266
+ the shape of what they return do not change within 1.x. That is the surface an
267
+ agent client is configured against: a renamed tool or a dropped field breaks
268
+ every `.mcp.json` pointing at this server, and none of it is visible to a
269
+ type-level check — `archTools = (ctx) => ArchTool[]` is byte-identical whatever
270
+ the tools are called. `mcp-surface.md` records all nine and CI diffs it.
271
+
272
+ **The protocol revision is not.** Which version of the Model Context Protocol the
273
+ server speaks follows the protocol, not this package's major version. A revision
274
+ that requires a transport change will land in a minor release, described in the
275
+ notes.
276
+
277
+ **The writers behind `arch:install` are not.** `detectAgents`, `applyMcpConfig`,
278
+ `applyBlock`, `buildGuidelines` and the rest are `@internal`: they exist for the
279
+ command, and the format of the files it writes is not a promise.
280
+
259
281
  ## Next steps
260
282
 
261
283
  - [Commands](/docs/commands) — the full `bun zt` vocabulary an agent is told about.
262
284
  - [Package Development](/docs/package-development) — contributing doctor checks of your own.
263
- - [Support policy](/docs/support-policy#maturity-levels) — what `beta` promises.
285
+ - [Support policy](/docs/support-policy#maturity-levels) — what `stable` promises.