@zerotal/arch 1.11.2 → 1.13.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.
- package/docs/changelog.md +147 -0
- package/docs/flow/index.md +1 -1
- package/docs/orm/index.md +37 -0
- package/docs/support-policy.md +13 -3
- package/docs/upgrade.md +137 -0
- package/package.json +3 -3
package/docs/changelog.md
CHANGED
|
@@ -27,6 +27,122 @@ 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.13.0 — 2026-08-31
|
|
31
|
+
|
|
32
|
+
Three retirements, taken together on purpose. Each is a small migration, and three minors
|
|
33
|
+
each asking an app to move costs more than one that asks properly — so this is one crossing
|
|
34
|
+
and one `zt upgrade` run.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bun zt upgrade --to 1.13.0
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Removed — BREAKING
|
|
41
|
+
|
|
42
|
+
- **Flow's `Component.client(…)`.** Use the `` this.$`…` `` tagged template.
|
|
43
|
+
|
|
44
|
+
This is a security fix wearing an ergonomics change's clothes, which is why it did not wait
|
|
45
|
+
for 2.0. `client()` took a **string** and queued it to be evaluated in the browser, so the
|
|
46
|
+
caller owned the escaping — and its own docblock had to warn _never interpolate unescaped
|
|
47
|
+
user input_. A method whose documentation has to tell you not to hold it that way is a
|
|
48
|
+
footgun with a label on. `$` is a tagged template, so every `${…}` is encoded as a JS
|
|
49
|
+
literal before it reaches the page.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// before — escaping was yours to remember
|
|
53
|
+
this.client(`toast(${JSON.stringify(this.search)})`);
|
|
54
|
+
|
|
55
|
+
// after — encoded for you
|
|
56
|
+
this.$`toast(${this.search})`;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The codemod rewrites a call whose argument is a single literal. **One whose argument is a
|
|
60
|
+
variable or a concatenation is reported rather than rewritten**: those are precisely the
|
|
61
|
+
ones the warning was about, and wrapping a finished string as `` $`${expr}` `` would encode
|
|
62
|
+
it as a string literal and stop running it as code. A codemod that quietly did that would
|
|
63
|
+
leave an app compiling, running, and no longer doing anything where it used to run a script.
|
|
64
|
+
|
|
65
|
+
Removing it also frees `client` as a property name on a component — the same benefit
|
|
66
|
+
removing `title` gave in 1.7.3.
|
|
67
|
+
|
|
68
|
+
### Changed — BREAKING
|
|
69
|
+
|
|
70
|
+
- **`LockDriver.extend()` is required.** Only affects a custom lock driver; all three
|
|
71
|
+
built-in drivers already implement it.
|
|
72
|
+
|
|
73
|
+
It shipped optional in 1.5.0 with `acquire(key, owner, ttl)` as the fallback, and that
|
|
74
|
+
fallback was correct only by coincidence. `acquire` happens to be an owner-guarded refresh
|
|
75
|
+
on every built-in driver, and nothing in the interface ever said it had to be — so a
|
|
76
|
+
third-party driver whose `acquire` takes a _free_ lock, which is the ordinary reading of
|
|
77
|
+
the word, would have had `refresh()` silently take a lock another holder owned. That is the
|
|
78
|
+
one thing a lock exists to prevent. Requiring the method turns an assumption the contract
|
|
79
|
+
never stated into something a driver has to answer.
|
|
80
|
+
|
|
81
|
+
- **`routes:types` and `serve --dev` are retired**, in favour of `route:types` and `dev`.
|
|
82
|
+
Both are rewritten by the codemod.
|
|
83
|
+
|
|
84
|
+
`serve --dev` **fails with a message** rather than being ignored, and the flag is still
|
|
85
|
+
declared for that reason alone. Flag parsing runs non-strict, so simply deleting it would
|
|
86
|
+
have left `serve --dev` starting a plain server — no watcher, no rebuild, no explanation. A
|
|
87
|
+
retired flag that silently changes what a command does is worse than one that is still
|
|
88
|
+
there.
|
|
89
|
+
|
|
90
|
+
### Added
|
|
91
|
+
|
|
92
|
+
- **The `client-tagged-template` codemod**, which is what makes the first item above a
|
|
93
|
+
migration rather than a search.
|
|
94
|
+
|
|
95
|
+
## 1.12.0 — 2026-08-31
|
|
96
|
+
|
|
97
|
+
One change, deliberately alone: the minor exists to carry it.
|
|
98
|
+
|
|
99
|
+
A field report from an app running in production found a feature flag reading as
|
|
100
|
+
enabled for every record that had it turned off. Nothing errored, nothing logged, and
|
|
101
|
+
the database was doing exactly what it had been asked to.
|
|
102
|
+
|
|
103
|
+
### Changed — BREAKING
|
|
104
|
+
|
|
105
|
+
- **A boolean written to a column declared to hold text is refused.**
|
|
106
|
+
|
|
107
|
+
A bare `@column()` resolves to `{ type: "string" }` — the right default for the
|
|
108
|
+
common case, and the wrong one for a boolean. A text column has text affinity, so
|
|
109
|
+
`false` was stored as the string `"0"`, and `"0"` is truthy in JavaScript. Every
|
|
110
|
+
`if (model.flag)` on such a column took the wrong branch for a stored `false`, on
|
|
111
|
+
every row, silently.
|
|
112
|
+
|
|
113
|
+
There is no correct coercion. `0` becomes `"0"`; `"false"` is truthy too. The value
|
|
114
|
+
cannot survive the round trip, so the only honest options were to refuse the write or
|
|
115
|
+
to keep letting a stored `false` read back as `true`. It now raises
|
|
116
|
+
`ColumnTypeError`, naming the property and the fix:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
[Zerotal ORM] Widget.active is declared as a `string` column and was given a boolean.
|
|
120
|
+
A text column stores that as "0"/"1", and "0" is truthy in JavaScript — so a stored
|
|
121
|
+
`false` would read back as true and every `if (…)` on it would take the wrong branch.
|
|
122
|
+
Declare the column's type instead: `@column("boolean")`.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The decorator cannot pick for you: `declare active: boolean` erases the TypeScript
|
|
126
|
+
type at runtime, so the property looks identical to a decorator whether it holds a
|
|
127
|
+
boolean or a string. Declaring the type is the only signal there is — which is why
|
|
128
|
+
the mistake is worth refusing loudly rather than guessing at.
|
|
129
|
+
|
|
130
|
+
An explicit `@column({ type: "string", cast: "boolean" })` is still honoured. That is
|
|
131
|
+
someone stating what they meant; the guard is for the column that says nothing.
|
|
132
|
+
|
|
133
|
+
### Before you upgrade
|
|
134
|
+
|
|
135
|
+
- **Find the boolean properties whose `@column()` declares no type.** Nothing can find
|
|
136
|
+
them for you, for the reason above — a search of your models for a bare `@column()`,
|
|
137
|
+
read against the property types beside them, is the reliable way.
|
|
138
|
+
- **The rows you already wrote are still text.** This stops new bad writes; it does not
|
|
139
|
+
migrate old ones. Those rows keep reading truthy until they are converted. The
|
|
140
|
+
[upgrade guide](/docs/upgrade#1-11-to-1-12) has the statement.
|
|
141
|
+
|
|
142
|
+
### Added
|
|
143
|
+
|
|
144
|
+
- **`ColumnTypeError`** — exported, so an app can catch it by class.
|
|
145
|
+
|
|
30
146
|
## 1.11.2 — 2026-08-31
|
|
31
147
|
|
|
32
148
|
`@zerotal/ai` is `stable`, and the release that promotes it is the one that fixes five
|
|
@@ -101,6 +217,37 @@ which is the only thing between a user's prompt and a log that outlives the requ
|
|
|
101
217
|
Both hold up — the parser reassembles a frame whose terminator is split across chunks
|
|
102
218
|
and a UTF-8 sequence cut mid-character.
|
|
103
219
|
|
|
220
|
+
#### **BREAKING** — `countTokens` can return `null`
|
|
221
|
+
|
|
222
|
+
`Ai.countTokens()` and `AiDriver.countTokens()` return `number | null` rather than
|
|
223
|
+
`number`. Only Anthropic has a counting endpoint; the other drivers returned `0`, which
|
|
224
|
+
is also a real count for an empty prompt, so the old value was a number you could divide
|
|
225
|
+
by and budget against without ever being told it meant "unsupported".
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
// before
|
|
229
|
+
const tokens = await Ai.countTokens(prompt);
|
|
230
|
+
if (tokens > 1000) shorten();
|
|
231
|
+
|
|
232
|
+
// after
|
|
233
|
+
const tokens = await Ai.countTokens(prompt);
|
|
234
|
+
if (tokens !== null && tokens > 1000) shorten();
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
A custom `AiDriver` implementation compiles unchanged — returning `number` still
|
|
238
|
+
satisfies `Promise<number | null>`. It is callers who need the check.
|
|
239
|
+
|
|
240
|
+
**This should have been a minor.** It shipped in a patch, which the versioning scheme
|
|
241
|
+
says cannot carry a break; see [the note in the upgrade guide](/docs/upgrade#1-11-2).
|
|
242
|
+
|
|
243
|
+
#### **INTERNAL** — four `@zerotal/ai` exports left the promised surface
|
|
244
|
+
|
|
245
|
+
`toSchema`, `strippedConstraints`, `resetSpend` and `resetStats` are `@internal`. They
|
|
246
|
+
are still exported and still work, so nothing breaks — they are simply no longer
|
|
247
|
+
covered by the compatibility promise, which is the narrowing that had to happen before
|
|
248
|
+
the package could be promoted at all. Reach for `AiFake` where a test used `resetSpend`
|
|
249
|
+
or `resetStats`; it is the seam built for that.
|
|
250
|
+
|
|
104
251
|
### Fixed — the gates
|
|
105
252
|
|
|
106
253
|
- **The release workflow ran three checks; the pull-request workflow ran fifteen.** So
|
package/docs/flow/index.md
CHANGED
|
@@ -198,7 +198,7 @@ The names in use:
|
|
|
198
198
|
| Rendering | `render` `layout` `placeholder` `slot` `hasSlot` `child` `isInteractive` |
|
|
199
199
|
| Actions & state | `bind` `validate` `resetValidation` `errors` `addError` `refresh` `$refresh` `$set` `cancelled` `signal` |
|
|
200
200
|
| Navigation | `redirect` `redirectRoute` `redirectIntended` `currentUrl` `navigateCurrent` |
|
|
201
|
-
| Events & realtime | `dispatch` `dispatchSelf` `dispatchTo` `stream`
|
|
201
|
+
| Events & realtime | `dispatch` `dispatchSelf` `dispatchTo` `stream` `$` |
|
|
202
202
|
| Misc | `flash` `download` `clearDurable` |
|
|
203
203
|
|
|
204
204
|
Anything beginning with `_` is also framework-internal, as are the statics `durable`
|
package/docs/orm/index.md
CHANGED
|
@@ -227,6 +227,34 @@ so it is a **string**. TypeScript cannot catch either — the decorator does not
|
|
|
227
227
|
constrain the property type — so an annotation that disagrees compiles fine and
|
|
228
228
|
fails at the first `.diffForHumans()` or arithmetic.
|
|
229
229
|
|
|
230
|
+
#### A boolean needs `@column("boolean")`
|
|
231
|
+
|
|
232
|
+
A bare `@column()` resolves to `{ type: "string" }` — the right default for the common
|
|
233
|
+
case, and a trap for a boolean. Writing one to a text column is **refused**:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
[Zerotal ORM] Widget.active is declared as a `string` column and was given a boolean.
|
|
237
|
+
A text column stores that as "0"/"1", and "0" is truthy in JavaScript — so a stored
|
|
238
|
+
`false` would read back as true and every `if (…)` on it would take the wrong branch.
|
|
239
|
+
Declare the column's type instead: `@column("boolean")`.
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
There is no correct coercion, which is why it refuses rather than converting. SQLite
|
|
243
|
+
gives a text column text affinity, so an integer `0` written there is stored as the
|
|
244
|
+
string `"0"`, and `"0"` is truthy in JavaScript. `"false"` is truthy too. The value
|
|
245
|
+
cannot survive the round trip in either direction, so the only honest options are to
|
|
246
|
+
refuse the write or to let a stored `false` read back as `true` — which is what used to
|
|
247
|
+
happen, on every row, with nothing in the app or the database registering a fault.
|
|
248
|
+
|
|
249
|
+
The decorator cannot infer the type for you: `declare active: boolean` erases the
|
|
250
|
+
TypeScript type at runtime, so the property looks the same to a decorator whether it
|
|
251
|
+
holds a boolean or a string. Declaring the type is the only signal there is.
|
|
252
|
+
|
|
253
|
+
If you genuinely want the text `"true"`/`"false"`, assign a string. If you want a
|
|
254
|
+
boolean stored in a text column on purpose, say so with a cast —
|
|
255
|
+
`@column({ type: "string", cast: "boolean" })` is honoured, because it is someone
|
|
256
|
+
stating what they meant.
|
|
257
|
+
|
|
230
258
|
### Indexes and uniqueness
|
|
231
259
|
|
|
232
260
|
Declare constraints on the column and schema generation emits them, so `migrate:generate` produces a schema with the guarantees your application depends on rather than a bare set of columns:
|
|
@@ -649,6 +677,15 @@ Type helpers exported from `@zerotal/orm`:
|
|
|
649
677
|
| `UpdatePayload<T>` | The shape accepted by `fill()` / `update()`. |
|
|
650
678
|
| `DatabaseConfigShape` | The `config/database.ts` configuration type. |
|
|
651
679
|
|
|
680
|
+
### Errors
|
|
681
|
+
|
|
682
|
+
| Error | Thrown when |
|
|
683
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------ |
|
|
684
|
+
| `ModelNotFoundError` | `findOrFail()` / `firstOrFail()` found no row. |
|
|
685
|
+
| `MassAssignmentError` | `fill()` / `create()` received an attribute the model's rules do not allow. |
|
|
686
|
+
| `ColumnTypeError` | A boolean was written to a column declared to hold text — see [above](#a-boolean-needs-columnboolean). |
|
|
687
|
+
| `RelationNotLoadedError` | A relation was read without being loaded, under strict relation access. |
|
|
688
|
+
|
|
652
689
|
### Commands
|
|
653
690
|
|
|
654
691
|
`@zerotal/orm` ships the migration, model, and seeding commands. Every one runs through `bun zt`:
|
package/docs/support-policy.md
CHANGED
|
@@ -75,10 +75,20 @@ 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.
|
|
78
|
+
own section in the Upgrade Guide. Seven have shipped so far — the
|
|
79
79
|
`ComponentWith` / `BaseModelWith` removal in 1.3.0, Flow's `socket:` listener
|
|
80
|
-
prefix in 1.7.2, the removal of Flow's `this.title(…)` in 1.7.3,
|
|
81
|
-
foreign-key enforcement in 1.11.0
|
|
80
|
+
prefix in 1.7.2, the removal of Flow's `this.title(…)` in 1.7.3, SQLite
|
|
81
|
+
foreign-key enforcement in 1.11.0, `countTokens` returning `number | null` in
|
|
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.
|
|
84
|
+
- **One of those five is in the wrong place, and it stays on the record.** 1.11.2
|
|
85
|
+
is a patch, and by the rule above a patch cannot carry a break. It did: the
|
|
86
|
+
`countTokens` signature changed in the same release that promoted `@zerotal/ai`
|
|
87
|
+
to `stable`, and the reasoning that allowed it — the package was still
|
|
88
|
+
`experimental` when the change was made, earlier in that release — is not a
|
|
89
|
+
distinction anyone installing 1.11.2 can observe. What they get is a patch that
|
|
90
|
+
breaks. It is listed here rather than argued away, because a policy that quietly
|
|
91
|
+
excuses its own exceptions is not one you can plan against.
|
|
82
92
|
- **Provenance:** packages are published with npm provenance, so you can verify
|
|
83
93
|
a tarball was built by this repository's release workflow rather than someone's
|
|
84
94
|
laptop.
|
package/docs/upgrade.md
CHANGED
|
@@ -289,6 +289,143 @@ doing something quiet.
|
|
|
289
289
|
If it really is a new migration, give it a name that does not collide once the
|
|
290
290
|
leading digits are removed.
|
|
291
291
|
|
|
292
|
+
## 1.12 to 1.13
|
|
293
|
+
|
|
294
|
+
Three retirements in one crossing, deliberately together: each is a small migration, and
|
|
295
|
+
three minors each asking an app to move costs more than one that asks properly. `zt upgrade`
|
|
296
|
+
does the mechanical half.
|
|
297
|
+
|
|
298
|
+
```bash fragment
|
|
299
|
+
bun zt upgrade --to 1.13.0
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### `Component.client(…)` is removed — use the `$` tagged template
|
|
303
|
+
|
|
304
|
+
The reason this did not wait: `client()` took a **string** and queued it to be evaluated in
|
|
305
|
+
the browser, so the caller owned the escaping. Its own docblock had to say _never interpolate
|
|
306
|
+
unescaped user input_, which is a warning about a footgun rather than a design. `$` is a
|
|
307
|
+
tagged template, so every `${…}` is encoded as a JS literal before it reaches the page.
|
|
308
|
+
|
|
309
|
+
```ts fragment
|
|
310
|
+
// in a component class body — before
|
|
311
|
+
this.client(`$refs.titleInput.focus()`);
|
|
312
|
+
this.client(`toast(${JSON.stringify(this.search)})`); // escaping was yours to remember
|
|
313
|
+
|
|
314
|
+
// after
|
|
315
|
+
this.$`$refs.titleInput.focus()`;
|
|
316
|
+
this.$`toast(${this.search})`; // encoded for you
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The codemod rewrites a call whose argument is a single literal. **A call whose argument is a
|
|
320
|
+
variable or a concatenation is reported rather than rewritten**, because those are exactly the
|
|
321
|
+
ones the security note was about — and wrapping the finished string as `` $`${expr}` `` would
|
|
322
|
+
encode it as a string literal and stop running it as code. Read those and interpolate through
|
|
323
|
+
`$` instead.
|
|
324
|
+
|
|
325
|
+
Removing it also frees `client` as a property name on your components, the way removing
|
|
326
|
+
`title` did in 1.7.3.
|
|
327
|
+
|
|
328
|
+
### `LockDriver.extend()` is required
|
|
329
|
+
|
|
330
|
+
Only affects a **custom lock driver**; the three built-in ones already implement it.
|
|
331
|
+
|
|
332
|
+
It shipped optional in 1.5.0 with `acquire(key, owner, ttl)` as the fallback, and the fallback
|
|
333
|
+
was correct only by coincidence: `acquire` happens to be an owner-guarded refresh on every
|
|
334
|
+
built-in driver, and nothing in the interface said it had to be. A driver whose `acquire` takes
|
|
335
|
+
a _free_ lock — the ordinary reading of the word — would have had `refresh()` silently take a
|
|
336
|
+
lock another holder owned, which is the one thing a lock exists to prevent.
|
|
337
|
+
|
|
338
|
+
Implement `extend(key, owner, ttlSeconds)`: push the deadline out, return `false` when the key
|
|
339
|
+
is free or held by someone else.
|
|
340
|
+
|
|
341
|
+
### `routes:types` and `serve --dev` are retired
|
|
342
|
+
|
|
343
|
+
`route:types` and `dev` are the real names. The codemod rewrites both in scripts and CI config.
|
|
344
|
+
|
|
345
|
+
`serve --dev` **fails loudly** rather than being ignored. The flag is still declared for exactly
|
|
346
|
+
that reason: flag parsing is non-strict, so deleting it would have left `serve --dev` starting a
|
|
347
|
+
plain server with no watcher and no message — a retired flag that silently changes what a
|
|
348
|
+
command does.
|
|
349
|
+
|
|
350
|
+
## 1.11 to 1.12
|
|
351
|
+
|
|
352
|
+
One breaking change, and it is the intended kind: a minor, announced, with the reason.
|
|
353
|
+
|
|
354
|
+
**A boolean written to a text column is refused.** A bare `@column()` resolves to
|
|
355
|
+
`{ type: "string" }`, so a boolean property decorated with one was stored as text —
|
|
356
|
+
and a text column has text affinity, so `false` was stored as `"0"`, which is truthy
|
|
357
|
+
in JavaScript. Every `if (model.flag)` on such a column took the wrong branch for a
|
|
358
|
+
stored `false`, on every row, with nothing in the app or the database registering a
|
|
359
|
+
fault. An app found it when a feature flag read as enabled for every record that had
|
|
360
|
+
it turned off.
|
|
361
|
+
|
|
362
|
+
```ts fragment
|
|
363
|
+
// in a model class body — before, and silently wrong
|
|
364
|
+
@column() declare active: boolean;
|
|
365
|
+
|
|
366
|
+
// after
|
|
367
|
+
@column("boolean") declare active: boolean;
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
There is no correct coercion: `0` becomes `"0"` and `"false"` is truthy too, so the
|
|
371
|
+
value cannot survive the round trip. The write now throws `ColumnTypeError`, naming
|
|
372
|
+
the property and the fix.
|
|
373
|
+
|
|
374
|
+
### What to do before upgrading
|
|
375
|
+
|
|
376
|
+
**Find your boolean properties whose `@column()` declares no type.** The decorator
|
|
377
|
+
cannot find them for you — `declare active: boolean` erases the TypeScript type at
|
|
378
|
+
runtime, so a bare `@column()` on a boolean is indistinguishable from one on a string
|
|
379
|
+
until a value arrives. A search of your models for `@column()` with no argument, read
|
|
380
|
+
against the property types beside them, is the reliable way.
|
|
381
|
+
|
|
382
|
+
**The rows you already wrote are still text.** This release stops new bad writes; it
|
|
383
|
+
does not migrate old ones. A column that has been holding `"0"` and `"1"` needs both
|
|
384
|
+
the decorator fixed and the stored values converted — on SQLite,
|
|
385
|
+
`UPDATE widgets SET active = CAST(active AS INTEGER)` after the column type is
|
|
386
|
+
corrected. Until then those rows keep reading truthy, which is the behaviour you are
|
|
387
|
+
upgrading to escape.
|
|
388
|
+
|
|
389
|
+
**If a text column really should hold a boolean**, say so explicitly and it is
|
|
390
|
+
honoured: `@column({ type: "string", cast: "boolean" })`. The guard is for the column
|
|
391
|
+
that says nothing, not for every string column.
|
|
392
|
+
|
|
393
|
+
## 1.11.2
|
|
394
|
+
|
|
395
|
+
One breaking change, and it is in a release that should not have carried one.
|
|
396
|
+
|
|
397
|
+
**`countTokens` returns `number | null`.** `Ai.countTokens()` and
|
|
398
|
+
`AiDriver.countTokens()` used to return `number`, with `0` standing in for "this
|
|
399
|
+
provider cannot count". Only Anthropic has a counting endpoint, and `0` is also a real
|
|
400
|
+
count for an empty prompt — so the old return value could not tell you which it meant,
|
|
401
|
+
and a budget built on it was quietly wrong for every other provider.
|
|
402
|
+
|
|
403
|
+
```ts fragment
|
|
404
|
+
// before
|
|
405
|
+
const tokens = await Ai.countTokens(prompt);
|
|
406
|
+
if (tokens > 1000) shorten();
|
|
407
|
+
|
|
408
|
+
// after
|
|
409
|
+
const tokens = await Ai.countTokens(prompt);
|
|
410
|
+
if (tokens !== null && tokens > 1000) shorten();
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
A custom `AiDriver` needs no change — returning `number` still satisfies
|
|
414
|
+
`Promise<number | null>`. Only callers do.
|
|
415
|
+
|
|
416
|
+
**Why this is in a patch.** It was made while `@zerotal/ai` was still `experimental`
|
|
417
|
+
and therefore outside the compatibility promise, in the same release that then promoted
|
|
418
|
+
the package to `stable`. That ordering is real and it is not a distinction anyone
|
|
419
|
+
installing 1.11.2 can observe: what arrives is a patch that breaks a build. The rule
|
|
420
|
+
stands as written — a patch does not break — and this release is recorded as the
|
|
421
|
+
exception rather than as a reinterpretation of it. See
|
|
422
|
+
[the support policy](/docs/support-policy#releases-and-versioning).
|
|
423
|
+
|
|
424
|
+
Everything else in 1.11.2 is additive. `@zerotal/ai`'s other surface change — `toSchema`,
|
|
425
|
+
`strippedConstraints`, `resetSpend` and `resetStats` becoming `@internal` — leaves those
|
|
426
|
+
exports working; they are no longer covered by the promise, which is different from
|
|
427
|
+
being gone.
|
|
428
|
+
|
|
292
429
|
## The managed zt.ts
|
|
293
430
|
|
|
294
431
|
`zt.ts` is framework-managed — the header says _do not modify_. If a release
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/arch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
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.
|
|
38
|
+
"@zerotal/core": "1.13.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "^5.8.0",
|
|
42
|
-
"@zerotal/orm": "1.
|
|
42
|
+
"@zerotal/orm": "1.13.0"
|
|
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": [
|