@zerotal/arch 1.7.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 (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
package/docs/ai.md ADDED
@@ -0,0 +1,373 @@
1
+ ---
2
+ title: AI
3
+ description: Text, streaming, structured output, typed tools, and an agent loop behind one provider-agnostic facade.
4
+ ---
5
+
6
+ # AI
7
+
8
+ `@zerotal/ai` gives an application one way to talk to a language model: `Ai.text()`
9
+ for a completion, `Ai.stream()` for tokens as they arrive, `Ai.object()` for a value
10
+ that satisfies a validator schema, and `Ai.agent()` for a loop that calls your tools
11
+ until the model is done. The provider is chosen in `config/ai.ts`, not at each call
12
+ site, so moving from Claude to a local model is a config change.
13
+
14
+ ::: warning Experimental
15
+ This package is `experimental`. Its API may change in a minor release — treat it as
16
+ a preview and pin the version if that matters to you.
17
+ :::
18
+
19
+ ## Getting Started
20
+
21
+ ```bash
22
+ bun add @zerotal/ai
23
+ ```
24
+
25
+ The provider SDKs are **optional peers**, imported lazily. Install only the one you
26
+ use:
27
+
28
+ ```bash
29
+ bun add @anthropic-ai/sdk # only for the anthropic driver
30
+ ```
31
+
32
+ The OpenAI and Ollama drivers use `fetch` directly and need nothing extra.
33
+
34
+ ## Register the provider
35
+
36
+ ```ts
37
+ // bootstrap/providers.ts
38
+ import { AiProvider } from "@zerotal/ai";
39
+
40
+ const providers = [
41
+ // …your other providers
42
+ AiProvider,
43
+ ];
44
+
45
+ export default providers;
46
+ ```
47
+
48
+ Registering the provider switches on the following:
49
+
50
+ - `onRegister` — binds an `AiManager` as a lazy singleton on the `"ai"` container key.
51
+ - `onBooted` — subscribes the observability bridges, contributes the monitor's **AI**
52
+ section, and registers the `ai:test` and `ai:spend` commands.
53
+ - `onStopping` — unsubscribes the bridges.
54
+
55
+ With no `config/ai.ts` at all, the provider falls back to an Anthropic driver built
56
+ from `ANTHROPIC_API_KEY`. That is enough to try the package; everything below assumes
57
+ a real config file.
58
+
59
+ ## Configuration
60
+
61
+ ```ts
62
+ // config/ai.ts
63
+ import { AiConfig } from "@zerotal/ai";
64
+
65
+ export default AiConfig({
66
+ default: "anthropic",
67
+
68
+ drivers: {
69
+ anthropic: {
70
+ apiKey: Bun.env["ANTHROPIC_API_KEY"] ?? "",
71
+ model: "claude-opus-5",
72
+ effort: "high",
73
+ },
74
+
75
+ ollama: {
76
+ model: "llama3.2",
77
+ baseUrl: "http://127.0.0.1:11434",
78
+ },
79
+ },
80
+
81
+ // Embeddings are their own block with their own driver — see below.
82
+ embeddings: {
83
+ default: "openai",
84
+ drivers: {
85
+ openai: { apiKey: Bun.env["OPENAI_API_KEY"] ?? "" },
86
+ },
87
+ },
88
+
89
+ limits: { perRequestUsd: 0.5, perDayUsd: 25 },
90
+ });
91
+ ```
92
+
93
+ Only the drivers you declare exist. An app that talks to Ollama alone declares no
94
+ `anthropic` block, installs no SDK, and needs no API key.
95
+
96
+ ### Why embeddings are configured separately
97
+
98
+ Anthropic has no embeddings endpoint. If `embed()` hung off the generation driver,
99
+ the normal pairing — Claude for generation, something cheaper for vectors — could not
100
+ be expressed at all. So `embeddings` is its own block with its own `default`.
101
+
102
+ ### Spend ceilings
103
+
104
+ `limits.perRequestUsd` is checked **before** the request is sent, from a real token
105
+ count, and bounds the blast radius of one runaway prompt.
106
+ `limits.perDayUsd` is checked from reported usage as it accumulates.
107
+
108
+ Both are process-scoped and estimated from public list prices. That is a real
109
+ limitation stated plainly: N workers hold N daily ceilings, and an account with
110
+ negotiated rates pays less than the estimate. They are a guard against a runaway
111
+ loop, not a billing system — the provider's dashboard remains the authority.
112
+
113
+ A model this package has no price for is never blocked, because a ceiling with
114
+ nothing to compare against should stand aside rather than refuse everything. Teach it
115
+ a price with `registerModelPrice()`.
116
+
117
+ ## Generating text
118
+
119
+ ```ts
120
+ import { Ai } from "@zerotal/ai";
121
+
122
+ // Just the text.
123
+ const summary = await Ai.text(`Summarize in one sentence:\n\n${article}`);
124
+
125
+ // The text plus the accounting.
126
+ const response = await Ai.generate({
127
+ prompt: "Explain event sourcing to a backend developer.",
128
+ system: "You are terse. No preamble.",
129
+ effort: "low",
130
+ });
131
+
132
+ response.text;
133
+ response.usage.outputTokens;
134
+ response.stopReason; // "end_turn" | "max_tokens" | "tool_use" | …
135
+ ```
136
+
137
+ ### Effort, not temperature
138
+
139
+ Current Claude models **reject** `temperature`, `top_p`, and `top_k` with a 400 — a
140
+ generic sampling parameter forwarded blindly fails every request. The Anthropic
141
+ driver therefore drops `temperature` and warns once.
142
+
143
+ Reach for `effort` instead. It trades thoroughness against cost and latency:
144
+
145
+ | Effort | Use it for |
146
+ | -------- | -------------------------------------------------------- |
147
+ | `low` | Classification, extraction, short latency-sensitive work |
148
+ | `medium` | A cost-conscious default |
149
+ | `high` | The default — most intelligence-sensitive work |
150
+ | `xhigh` | Hard coding and agentic tasks |
151
+ | `max` | When correctness matters more than the bill |
152
+
153
+ ### Streaming
154
+
155
+ ```ts
156
+ for await (const chunk of Ai.stream({ prompt, signal })) {
157
+ if (chunk.type === "text") process.stdout.write(chunk.text);
158
+ if (chunk.type === "done") console.log(chunk.response.usage);
159
+ }
160
+ ```
161
+
162
+ The last chunk is always `{ type: "done" }` carrying the assembled response, so a
163
+ caller that only wants tokens can ignore it and one that needs usage does not have to
164
+ add up the pieces. Pass an `AbortSignal` and a cancelled caller actually stops the
165
+ generation. See [Flow](/docs/flow) for streaming straight into a component.
166
+
167
+ ### Structured output
168
+
169
+ ```ts
170
+ const review = await Ai.object({ prompt: `Classify this review:\n\n${text}` }, (rule) => ({
171
+ sentiment: rule.string().in(["positive", "neutral", "negative"]),
172
+ summary: rule.string().max(140),
173
+ score: rule.number().min(1).max(5),
174
+ }));
175
+
176
+ review.sentiment; // typed, validated
177
+ ```
178
+
179
+ The schema is the same [validator](/docs/validator) schema you use for forms.
180
+
181
+ The JSON Schema subset these APIs accept is narrow: `additionalProperties: false` is
182
+ required on every object, and `minLength` / `maxLength` / `minimum` / `maximum` /
183
+ recursive schemas are **not supported**. So constraints the provider cannot express
184
+ are stripped from the schema it receives and **re-checked here** against the original
185
+ — `max(140)` is enforced by the validator on the way back in, and a violation raises
186
+ `AiSchemaError`. A recursive schema has no client-side rescue and is refused when the
187
+ schema is defined, not when the request is sent.
188
+
189
+ `strippedConstraints()` names exactly what the model will not see, if you want to
190
+ check that a load-bearing constraint is visible to it:
191
+
192
+ ```ts
193
+ strippedConstraints({ title: rule.string().min(3) }); // → ["title: min"]
194
+ ```
195
+
196
+ ## Tools and the agent loop
197
+
198
+ ```ts
199
+ import { Ai, tool } from "@zerotal/ai";
200
+
201
+ const lookupOrder = tool({
202
+ name: "lookup_order",
203
+ description:
204
+ "Fetch one order by id. Call this whenever the user mentions an order number — " +
205
+ "do not answer from the conversation alone.",
206
+ input: (rule) => ({ id: rule.string() }),
207
+ handle: async ({ id }) => await Order.find(id),
208
+ });
209
+
210
+ const result = await Ai.agent({
211
+ prompt: "Where is order 4821?",
212
+ tools: [lookupOrder],
213
+ });
214
+
215
+ result.text;
216
+ result.steps; // every call, its result, and how long it took
217
+ ```
218
+
219
+ Say **when** to call a tool, not just what it does — the trigger condition is the half
220
+ that moves the call rate.
221
+
222
+ A handler that throws does not end the run: the error becomes an error-flagged result
223
+ the model can react to. A call to a tool that does not exist gets the same treatment,
224
+ naming the tools that do.
225
+
226
+ ### Ceilings
227
+
228
+ Two, because a model deciding when to stop is not a termination proof:
229
+
230
+ - `agent.maxSteps` (default 25) caps tool-calling round trips.
231
+ - `agent.maxResumes` (default 5) caps `pause_turn` restarts.
232
+
233
+ `pause_turn` is worth knowing about. A provider running a long server-side tool can
234
+ end a turn with `stop_reason: "pause_turn"`, meaning "ask me again" — not an error and
235
+ not a completion. Left unhandled it reads as a finished answer, so the user sees a
236
+ silently truncated response with no warning anywhere. The loop pushes the paused turn
237
+ back and re-requests.
238
+
239
+ ### Locking a run
240
+
241
+ ```ts
242
+ await Ai.agent({
243
+ prompt: "Refund order 4821 if it shipped over 30 days ago.",
244
+ tools: [lookupOrder, issueRefund],
245
+ lock: "refund:4821",
246
+ });
247
+ ```
248
+
249
+ Naming a run makes it exclusive **for that name** — two workers cannot refund the same
250
+ order, while unrelated runs proceed in parallel. A shared key would serialize every
251
+ agent run in the app, which is why the lock is opt-in and named rather than automatic.
252
+
253
+ The lock refreshes for as long as the loop runs, so `agent.lockTtl` (default 120s)
254
+ stops meaning "how long the job might take" — unanswerable — and becomes "how long
255
+ after a crash before another worker may take over". If the lock is ever lost the
256
+ loop's signal aborts, because at that point somebody else may be doing the same work.
257
+ See [Locking](/docs/lock) for the mechanism.
258
+
259
+ ## Refusals
260
+
261
+ A provider's safety classifiers can decline a request. That arrives as a **successful
262
+ HTTP 200** with empty or partial content — so code that reads `content[0]` without
263
+ checking crashes on a response the API considers fine.
264
+
265
+ This package checks the stop reason first and raises a typed error:
266
+
267
+ ```ts
268
+ import { AiRefusedError } from "@zerotal/ai";
269
+
270
+ try {
271
+ await Ai.text(prompt);
272
+ } catch (error) {
273
+ if (error instanceof AiRefusedError) {
274
+ error.category; // "cyber" | "bio" | … | null
275
+ error.partialText; // whatever arrived before a mid-stream decline
276
+ }
277
+ }
278
+ ```
279
+
280
+ The Anthropic driver ships `fallbacks: "default"` on by default, which re-runs a
281
+ declined request on the provider's recommended fallback model server-side. Turn it off
282
+ with `drivers.anthropic.fallbacks: false`.
283
+
284
+ ## Embeddings
285
+
286
+ ```ts
287
+ const { embeddings } = await Ai.embed(["first chunk", "second chunk"]);
288
+ ```
289
+
290
+ One vector per input, in input order.
291
+
292
+ ## Background generation
293
+
294
+ A queued generation is serialized, so its completion handler is registered by **name**
295
+ — a closure cannot survive the trip to a worker process:
296
+
297
+ ```ts
298
+ // in a service provider's onBooted(), so the worker registers it too
299
+ Ai.onGenerated("summarize-ticket", async (response, meta) => {
300
+ await Ticket.query().where("id", meta.ticketId).update({ summary: response.text });
301
+ });
302
+
303
+ // anywhere
304
+ await Ai.queue({ prompt }, { handler: "summarize-ticket", meta: { ticketId } });
305
+ ```
306
+
307
+ `tools` and `signal` are stripped at dispatch rather than silently arriving as
308
+ `undefined` — a queued generation is a one-shot completion, and `Ai.agent()` stays
309
+ in-process where its tools are.
310
+
311
+ ## Testing
312
+
313
+ `AiFake` replaces the container binding and answers from a script. No API key, no
314
+ network, no flakiness:
315
+
316
+ ```ts
317
+ import { AiFake } from "@zerotal/ai";
318
+
319
+ const ai = AiFake.install();
320
+ ai.respondWith("A one-sentence summary.");
321
+
322
+ await service.summarize(article);
323
+
324
+ ai.assertPrompted(/Summarize/);
325
+ ai.assertPromptCount(1);
326
+
327
+ ai.restore(); // in afterEach
328
+ ```
329
+
330
+ The assertions are about **what your application asked for** — the part you wrote and
331
+ the part that can be wrong. Whether the model's prose is good is not a unit test.
332
+
333
+ `ai.refuse()` makes the next call decline, which is worth exercising deliberately: a
334
+ refusal is an HTTP 200, so that handling path is the one most likely never to have run.
335
+
336
+ ## Observability
337
+
338
+ Every generation emits `AiGenerated` on the framework event bus, and a decline also
339
+ emits `AiRefused`. With `@zerotal/monitor` installed, the **AI** section shows spend
340
+ against the daily ceiling, tokens in and out, cache reads, latency percentiles per
341
+ model, and the refusal rate.
342
+
343
+ **Prompts are redacted by default.** A prompt is user data, and the observability path
344
+ is the one place it would otherwise be durably kept — so what is recorded is a shape,
345
+ `[redacted 38 chars]`, not the text. Set `redact: false` to record a truncated preview
346
+ instead.
347
+
348
+ ## Commands
349
+
350
+ | Command | What it does |
351
+ | --------------------- | -------------------------------------------------------- |
352
+ | `zt ai:test [driver]` | Reach the provider once and print the **resolved model** |
353
+ | `zt ai:spend` | This process's token spend today, by model |
354
+
355
+ `ai:test` exists because AI configuration fails in ways unit tests cannot reach: a key
356
+ with no access to the model, a model id that 404s because someone appended a date
357
+ suffix, a gateway that rewrites the base URL.
358
+
359
+ ## Adding a provider
360
+
361
+ Implement `AiDriver` — `text`, `stream`, `object`, `countTokens`, `verify` — and
362
+ register it:
363
+
364
+ ```ts
365
+ // in a service provider's onBooted()
366
+ const ai = app.container.makeSync("ai");
367
+ ai.extend("bedrock", () => new BedrockDriver(config));
368
+ ```
369
+
370
+ Nothing else is needed. Spend ceilings, redaction, telemetry, the lock, and the agent
371
+ loop all live above the driver, so a new provider is a translation layer and nothing
372
+ more. `agent()` is optional on the interface and none of the built-in three implement
373
+ it — they all run the same shared loop.