@zerotal/arch 1.13.5 → 1.14.1

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.
package/docs/ai.md CHANGED
@@ -287,6 +287,48 @@ after a crash before another worker may take over". If the lock is ever lost the
287
287
  loop's signal aborts, because at that point somebody else may be doing the same work.
288
288
  See [Locking](/docs/lock) for the mechanism.
289
289
 
290
+ ## Prompt caching
291
+
292
+ A long, stable prefix is worth caching: a cached read costs a tenth of an ordinary input
293
+ token. A _write_ costs more than one, so caching something that changes every request is
294
+ strictly more expensive than not caching it.
295
+
296
+ **The system prompt is cached for you** when `cacheSystem` is on (the default) and it is
297
+ long enough to be worth a breakpoint.
298
+
299
+ **Anything else you mark yourself.** Put `cache: true` on the last message of the stable
300
+ prefix — a retrieved document, a fixed few-shot block — and everything up to and including
301
+ it is cached:
302
+
303
+ ```ts fragment
304
+ await Ai.generate({
305
+ messages: [
306
+ { role: "user", content: policyDocument, cache: true },
307
+ { role: "user", content: question },
308
+ ],
309
+ });
310
+ ```
311
+
312
+ The marker goes on the **last** message of the prefix, not on every message in it: a
313
+ breakpoint caches everything before it, so marking each one spends four breakpoints to
314
+ describe one boundary. Anthropic allows four per request and the framework refuses a fifth
315
+ by name rather than letting the provider return a 400 about a field you did not write.
316
+
317
+ Before 1.14.1 only the system prompt could be cached, so a long document in the message
318
+ history could not be — which is the case caching is most worth having for.
319
+
320
+ ### What it costs
321
+
322
+ `estimateCost` prices a cache read at 0.1× input and a **5-minute** write at 1.25×. A
323
+ **1-hour** write costs 2×, and the two are tracked separately in `AiUsage`
324
+ (`cacheWrite1hTokens`, counted inside `cacheWriteTokens`) because one number cannot be
325
+ priced two ways.
326
+
327
+ Zerotal never requests a 1-hour cache, so that figure is `0` unless you reach past the
328
+ driver with `providerOptions`. It used to be priced at 1.25× regardless, which
329
+ underestimated by 37.5% — in the unsafe direction for a spend ceiling, since a limit that
330
+ under-counts lets spend through rather than blocking it.
331
+
290
332
  ## Refusals
291
333
 
292
334
  A provider's safety classifiers can decline a request. That arrives as a **successful
package/docs/changelog.md CHANGED
@@ -27,6 +27,119 @@ 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.14.1 — 2026-09-01
31
+
32
+ Clearing the deferred list. Three of the four are fixed; the other two were sized and moved
33
+ to 2.0 with the reasoning recorded, because deciding is also addressing.
34
+
35
+ ### Added
36
+
37
+ - **Prompt-cache breakpoints on messages.** Caching reached exactly one place — the system
38
+ prompt — so a long stable document in the message history could not be cached, which is
39
+ the case caching is most worth having for.
40
+
41
+ ```ts
42
+ await Ai.generate({
43
+ messages: [
44
+ { role: "user", content: policyDocument, cache: true },
45
+ { role: "user", content: question },
46
+ ],
47
+ });
48
+ ```
49
+
50
+ The marker goes on the **last** message of the prefix, because a breakpoint caches
51
+ everything before it — marking each message spends four breakpoints describing one
52
+ boundary. Anthropic allows four, and a fifth is refused by name rather than surfacing as
53
+ a provider 400 about a field you never wrote.
54
+
55
+ ### Fixed
56
+
57
+ - **A 1-hour cache write is priced at 2×, not 1.25×.** Every write was multiplied by the
58
+ 5-minute rate, underestimating a 1-hour write by 37.5% — in the unsafe direction for a
59
+ ceiling, since a limit that under-counts lets spend through rather than blocking it.
60
+ `AiUsage.cacheWrite1hTokens` carries the split, read from the `cache_creation` breakdown
61
+ Anthropic returns when a 1-hour cache was used. Optional on the type, because a custom
62
+ driver constructs `AiUsage` and making it required would break every one for an
63
+ accounting detail their provider probably does not report.
64
+
65
+ - **A command that declares `needsApp = false` no longer boots the application.**
66
+ `zt version`, `key:generate`, every `make:*` scaffolder and the gate commands paid for
67
+ providers, a database connection and a schedule registry they never touch — and could not
68
+ run when booting was the thing that was broken. A CLI you cannot reach when the app is
69
+ down is missing exactly when it is wanted.
70
+
71
+ Config is still bound, through the new `Application.bindConfig()`. "Does not need the
72
+ app" and "does not need config" are different claims, and only the first is true of a
73
+ scaffolder writing to a configured path.
74
+
75
+ It also makes `zt version --json` pipeable, which previously needed the `--version` form
76
+ to dodge the boot log on stdout.
77
+
78
+ ### Decided, not done
79
+
80
+ Two entries on the 2.0 ledger were filed as alias retirements and are mass renames:
81
+
82
+ - **`BaseModel` → `Model`** is **617 references across `api-surface.md` files and 354 in
83
+ non-test source.** `BaseModel` is the class's declared name, so TypeScript prints it into
84
+ every signature mentioning a model — retiring it means renaming the class and
85
+ regenerating every snapshot, which the breaking-change gate reads as hundreds of removals.
86
+ The "trivial" codemod in the ledger is the app-side rewrite, which is genuinely trivial
87
+ and already written; the framework side was never sized.
88
+ - **Prefixing the 269 `@internal` exports with `_`** is the same shape, minus the classes
89
+ that [cannot be renamed at all](#1131--2026-08-31).
90
+
91
+ Both move to 2.0 on the ledger's own rule — a break worth taking once, wanting one
92
+ migration and one `zt upgrade` run rather than hundreds of app-visible type positions
93
+ churned so there is one name instead of two. They should cross together, so an app pays for
94
+ one rename pass rather than two.
95
+
96
+ ## 1.14.0 — 2026-09-01
97
+
98
+ Rendering and the SSR endpoint become separate decisions. Small in practice — most apps
99
+ change nothing — but it is a change to what a config flag does, so it takes a minor and
100
+ says so.
101
+
102
+ ### Changed — BREAKING
103
+
104
+ - **`inertia.ssr: true` no longer registers `POST /__ssr`.**
105
+
106
+ ```ts
107
+ // config/inertia.ts
108
+ export default InertiaConfig({
109
+ ssr: true, // server-render every first page load. Renders nothing else.
110
+ ssrEndpoint: true, // expose POST /__ssr, for a renderer outside this process
111
+ });
112
+ ```
113
+
114
+ **If you set `ssr: true` for server rendering, you need no change.** You keep exactly
115
+ that and stop exposing a route you were not using. Add `ssrEndpoint: true` only when
116
+ something outside the web process calls `/__ssr` — a separate renderer, a second host.
117
+
118
+ The endpoint exists because upstream Inertia runs on hosts with no JavaScript runtime:
119
+ PHP cannot import a `.tsx`, so it posts `{ component, props, url }` to a Node process and
120
+ gets `{ body, head }` back. That hop is forced by the host language, not chosen. Bun _is_
121
+ a JavaScript runtime, so [1.13.5](#1135--2026-09-01) made `ssr` import the component and
122
+ render it inline — no serialisation, no second process, no network.
123
+
124
+ So the endpoint solves a problem this framework does not have, and it stays only for the
125
+ case that is still real: deliberately moving render CPU off the web process. That is a
126
+ deployment choice, and it now has to be made rather than inherited.
127
+
128
+ Turning rendering on should not open a route that renders arbitrary components from POST
129
+ input, however well guarded. One switch, one thing — the same reasoning that separated
130
+ `secureHeaders: false` from the site gate in 1.13.3.
131
+
132
+ ### A note on what this costs
133
+
134
+ Worth stating plainly, because it is easy to expect the opposite: **turning `ssr` on adds
135
+ CPU to the web process.** Before 1.13.5 the flag rendered nothing, so there was no round
136
+ trip to remove — a page load did no rendering at all. Now it renders every first load.
137
+
138
+ The comparison where in-process rendering _is_ cheaper is against the way other frameworks
139
+ do SSR: no HTTP hop, no JSON round trip of the whole page object, no second process to run
140
+ and supervise. Against the framework's own previous behaviour, it is new work in exchange
141
+ for HTML a crawler and a link preview can read.
142
+
30
143
  ## 1.13.5 — 2026-09-01
31
144
 
32
145
  **`inertia.ssr: true` server-renders now.** One config line, every page, no controller
@@ -56,15 +56,30 @@ server renderer for the framework your app uses.
56
56
  client-rendered document is served instead, because taking a route down because an
57
57
  _optimisation_ failed would make `ssr: true` a liability rather than an improvement.
58
58
 
59
- ## What `POST /__ssr` is for
59
+ ## What `POST /__ssr` is for, and why you probably do not want it
60
60
 
61
- `ssr: true` also registers it. It accepts `{ component, props, url }` and returns
62
- `{ body, head }` — the same contract as the Inertia Node SSR server — and it exists for
63
- an **external** caller: a separate renderer process, or a deployment that renders
64
- somewhere other than the web process.
61
+ Set `ssrEndpoint: true` and the app exposes `POST /__ssr`, which accepts
62
+ `{ component, props, url }` and returns `{ body, head }`.
65
63
 
66
- You do not need it for the switch above, which renders in-process. It is throttled and
67
- loopback-gated; see `ssrSecret` for reaching it from another host.
64
+ That is the contract upstream Inertia uses, and it exists there for a reason that does not
65
+ apply here: **PHP and Ruby have no JavaScript runtime**, so the web framework cannot import
66
+ a `.tsx` and must hand rendering to a separate Node process. The HTTP hop is forced by the
67
+ host language.
68
+
69
+ Bun _is_ a JavaScript runtime. `ssr: true` imports the component and renders it inline, in
70
+ the same process, with no serialisation and no network. So the endpoint solves a problem
71
+ this framework does not have.
72
+
73
+ What it is still good for is the one case that remains real: **deliberately moving render
74
+ CPU off the web process**, onto another process or another host, on an app where rendering
75
+ competes with request handling. Then the hop is the point rather than the cost.
76
+
77
+ It is throttled, answers a 404 to anything that is not loopback, and takes `ssrSecret` for
78
+ a renderer on another host.
79
+
80
+ > **`ssr: true` does not register it, since 1.14.0.** It used to, which meant turning
81
+ > rendering on opened a route the app never asked for. See
82
+ > [the upgrade note](/docs/upgrade#1-13-to-1-14).
68
83
 
69
84
  ## Streaming SSR — a different question
70
85
 
@@ -75,12 +75,13 @@ dependency order, from CI. Never mix versions across packages.
75
75
  tilde if you would rather cross a minor deliberately.
76
76
  - **A break is never silent.** Every one is called out in the release notes as
77
77
  **BREAKING**, with the reason and the migration steps, and the version gets its
78
- own section in the Upgrade Guide. Seven have shipped so far — the
78
+ own section in the Upgrade Guide. Eight have shipped so far — the
79
79
  `ComponentWith` / `BaseModelWith` removal in 1.3.0, Flow's `socket:` listener
80
80
  prefix in 1.7.2, the removal of Flow's `this.title(…)` in 1.7.3, SQLite
81
81
  foreign-key enforcement in 1.11.0, `countTokens` returning `number | null` in
82
82
  1.11.2, the refusal to write a boolean into a text column in 1.12.0, and the removal of
83
- Flow's `Component.client(…)` alongside two retired aliases in 1.13.0.
83
+ Flow's `Component.client(…)` alongside two retired aliases in 1.13.0, and
84
+ `inertia.ssr` no longer registering `POST /__ssr` in 1.14.0.
84
85
  - **One of those five is in the wrong place, and it stays on the record.** 1.11.2
85
86
  is a patch, and by the rule above a patch cannot carry a break. It did: the
86
87
  `countTokens` signature changed in the same release that promoted `@zerotal/ai`
package/docs/upgrade.md CHANGED
@@ -296,6 +296,34 @@ doing something quiet.
296
296
  If it really is a new migration, give it a name that does not collide once the
297
297
  leading digits are removed.
298
298
 
299
+ ## 1.13 to 1.14
300
+
301
+ One breaking change, and it is small in practice.
302
+
303
+ **`inertia.ssr: true` no longer registers `POST /__ssr`.** Rendering and the endpoint are
304
+ separate decisions now:
305
+
306
+ ```ts fragment
307
+ // config/inertia.ts — before
308
+ export default InertiaConfig({ ssr: true }); // rendered, and opened the route
309
+
310
+ // after
311
+ export default InertiaConfig({ ssr: true }); // renders, and opens nothing
312
+ export default InertiaConfig({ ssrEndpoint: true }); // opens the route, for an
313
+ // external renderer
314
+ ```
315
+
316
+ **Most apps need no change.** If you set `ssr: true` for server rendering — which is what
317
+ the option is for — you keep exactly that, and you stop exposing a route you were not
318
+ using. The endpoint had no in-process caller before 1.13.5, so an app that set the flag was
319
+ getting the route and nothing else.
320
+
321
+ **Add `ssrEndpoint: true` only if something outside the web process calls `/__ssr`** — a
322
+ separate renderer, a second host. If you are not running one, you were not using it.
323
+
324
+ Why split them: turning rendering on should not open a route that renders arbitrary
325
+ components from POST input, however well guarded. One switch, one thing.
326
+
299
327
  ## 1.12 to 1.13
300
328
 
301
329
  Three retirements in one crossing, deliberately together: each is a small migration, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/arch",
3
- "version": "1.13.5",
3
+ "version": "1.14.1",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -35,11 +35,11 @@
35
35
  "typecheck": "tsc --noEmit"
36
36
  },
37
37
  "dependencies": {
38
- "@zerotal/core": "1.13.5"
38
+ "@zerotal/core": "1.14.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "typescript": "^5.8.0",
42
- "@zerotal/orm": "1.13.5"
42
+ "@zerotal/orm": "1.14.1"
43
43
  },
44
44
  "description": "The Zerotal agent surface — an MCP server that hands coding agents the framework's machine-readable truth: exact API signatures, live routes and schema, version-matched docs, and `zt doctor`.",
45
45
  "keywords": [