@zerotal/arch 1.11.1 → 1.12.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/ai.md +35 -2
- package/docs/changelog.md +169 -0
- package/docs/orm/index.md +37 -0
- package/docs/support-policy.md +40 -24
- package/docs/upgrade.md +79 -0
- package/package.json +3 -3
package/docs/ai.md
CHANGED
|
@@ -138,9 +138,28 @@ response.stopReason; // "end_turn" | "max_tokens" | "tool_use" | …
|
|
|
138
138
|
|
|
139
139
|
Current Claude models **reject** `temperature`, `top_p`, and `top_k` with a 400 — a
|
|
140
140
|
generic sampling parameter forwarded blindly fails every request. The Anthropic
|
|
141
|
-
driver
|
|
141
|
+
driver drops `temperature` on those models and warns once.
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
**On models that accept it, it is sent.** The 4.6 and 4.5 generations take sampling
|
|
144
|
+
parameters perfectly well, and there the configured or per-request `temperature`
|
|
145
|
+
reaches the API. Ask `modelCapabilities(model)` if you want to know which you are on:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { modelCapabilities } from "@zerotal/ai";
|
|
149
|
+
|
|
150
|
+
const caps = modelCapabilities("claude-haiku-4-5");
|
|
151
|
+
// { sampling: true, effort: false, thinking: "budget" }
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
That table is also what keeps the driver from sending a model something it rejects.
|
|
155
|
+
`effort` is a 400 on the 4.5 generation, and those models want an explicit thinking
|
|
156
|
+
budget rather than the adaptive form — so the driver builds a different request for
|
|
157
|
+
them rather than one request for everything. Models it does not recognise are treated
|
|
158
|
+
as current generation, because the ones that differ are a closed set that ages out
|
|
159
|
+
while new models keep arriving.
|
|
160
|
+
|
|
161
|
+
Reach for `effort` where the model has it. It trades thoroughness against cost and
|
|
162
|
+
latency:
|
|
144
163
|
|
|
145
164
|
| Effort | Use it for |
|
|
146
165
|
| -------- | -------------------------------------------------------- |
|
|
@@ -150,6 +169,18 @@ Reach for `effort` instead. It trades thoroughness against cost and latency:
|
|
|
150
169
|
| `xhigh` | Hard coding and agentic tasks |
|
|
151
170
|
| `max` | When correctness matters more than the bill |
|
|
152
171
|
|
|
172
|
+
### The thinking stream
|
|
173
|
+
|
|
174
|
+
A streamed `thinking` chunk carries the model's reasoning as it happens. The API
|
|
175
|
+
**omits that text by default** on the current generation, so the driver asks for it:
|
|
176
|
+
`drivers.anthropic.thinkingDisplay` defaults to `"summarized"`.
|
|
177
|
+
|
|
178
|
+
Set it to `"omitted"` to get the API's own default back. The thinking happens — and is
|
|
179
|
+
billed — either way; the setting only decides whether you are shown it. Before 1.11.2
|
|
180
|
+
the driver never asked, so the documented `thinking` chunk fired forever with
|
|
181
|
+
`text: ""` and no error, and a "thinking…" view built against the 4.6 models stopped
|
|
182
|
+
working when their users moved to 5 without anything saying so.
|
|
183
|
+
|
|
153
184
|
### Streaming
|
|
154
185
|
|
|
155
186
|
```ts fragment
|
|
@@ -531,6 +562,8 @@ sections above; this is the index.
|
|
|
531
562
|
| `modelPrice` | The price for a model, or `undefined` when we have none. |
|
|
532
563
|
| `estimateCost` | Estimated USD for one request's usage. Returns 0 for an unpriced model. |
|
|
533
564
|
| `modelRejectsSampling` | Whether a Claude model rejects `temperature` / `top_p` / `top_k` with a 400. |
|
|
565
|
+
| `modelCapabilities` | What a model accepts: sampling, `effort`, and which thinking shape. |
|
|
566
|
+
| `ModelCapabilities` | The three answers `modelCapabilities` returns. |
|
|
534
567
|
|
|
535
568
|
### Spend and statistics
|
|
536
569
|
|
package/docs/changelog.md
CHANGED
|
@@ -27,6 +27,175 @@ 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.12.0 — 2026-08-31
|
|
31
|
+
|
|
32
|
+
One change, deliberately alone: the minor exists to carry it.
|
|
33
|
+
|
|
34
|
+
A field report from an app running in production found a feature flag reading as
|
|
35
|
+
enabled for every record that had it turned off. Nothing errored, nothing logged, and
|
|
36
|
+
the database was doing exactly what it had been asked to.
|
|
37
|
+
|
|
38
|
+
### Changed — BREAKING
|
|
39
|
+
|
|
40
|
+
- **A boolean written to a column declared to hold text is refused.**
|
|
41
|
+
|
|
42
|
+
A bare `@column()` resolves to `{ type: "string" }` — the right default for the
|
|
43
|
+
common case, and the wrong one for a boolean. A text column has text affinity, so
|
|
44
|
+
`false` was stored as the string `"0"`, and `"0"` is truthy in JavaScript. Every
|
|
45
|
+
`if (model.flag)` on such a column took the wrong branch for a stored `false`, on
|
|
46
|
+
every row, silently.
|
|
47
|
+
|
|
48
|
+
There is no correct coercion. `0` becomes `"0"`; `"false"` is truthy too. The value
|
|
49
|
+
cannot survive the round trip, so the only honest options were to refuse the write or
|
|
50
|
+
to keep letting a stored `false` read back as `true`. It now raises
|
|
51
|
+
`ColumnTypeError`, naming the property and the fix:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
[Zerotal ORM] Widget.active is declared as a `string` column and was given a boolean.
|
|
55
|
+
A text column stores that as "0"/"1", and "0" is truthy in JavaScript — so a stored
|
|
56
|
+
`false` would read back as true and every `if (…)` on it would take the wrong branch.
|
|
57
|
+
Declare the column's type instead: `@column("boolean")`.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The decorator cannot pick for you: `declare active: boolean` erases the TypeScript
|
|
61
|
+
type at runtime, so the property looks identical to a decorator whether it holds a
|
|
62
|
+
boolean or a string. Declaring the type is the only signal there is — which is why
|
|
63
|
+
the mistake is worth refusing loudly rather than guessing at.
|
|
64
|
+
|
|
65
|
+
An explicit `@column({ type: "string", cast: "boolean" })` is still honoured. That is
|
|
66
|
+
someone stating what they meant; the guard is for the column that says nothing.
|
|
67
|
+
|
|
68
|
+
### Before you upgrade
|
|
69
|
+
|
|
70
|
+
- **Find the boolean properties whose `@column()` declares no type.** Nothing can find
|
|
71
|
+
them for you, for the reason above — a search of your models for a bare `@column()`,
|
|
72
|
+
read against the property types beside them, is the reliable way.
|
|
73
|
+
- **The rows you already wrote are still text.** This stops new bad writes; it does not
|
|
74
|
+
migrate old ones. Those rows keep reading truthy until they are converted. The
|
|
75
|
+
[upgrade guide](/docs/upgrade#1-11-to-1-12) has the statement.
|
|
76
|
+
|
|
77
|
+
### Added
|
|
78
|
+
|
|
79
|
+
- **`ColumnTypeError`** — exported, so an app can catch it by class.
|
|
80
|
+
|
|
81
|
+
## 1.11.2 — 2026-08-31
|
|
82
|
+
|
|
83
|
+
`@zerotal/ai` is `stable`, and the release that promotes it is the one that fixes five
|
|
84
|
+
bugs its first production users found. That ordering is the point: a `stable` promise
|
|
85
|
+
about an API nothing has pushed against is a promise nobody has tested.
|
|
86
|
+
|
|
87
|
+
Also here: two gates that were not doing their job, one of which had let two releases
|
|
88
|
+
publish over a red build.
|
|
89
|
+
|
|
90
|
+
A patch. Nothing here breaks — `@zerotal/ai`'s surface was narrowed _before_ the label,
|
|
91
|
+
while narrowing was still free.
|
|
92
|
+
|
|
93
|
+
### `@zerotal/ai` — the review, answered
|
|
94
|
+
|
|
95
|
+
The package shipped `experimental` with a stated precondition — _it graduates in the
|
|
96
|
+
release after its first real users_ — and a review date of 1.11.0 enforced by the
|
|
97
|
+
package linter rather than by a promise. Its first production users, running it against
|
|
98
|
+
Anthropic, sent a field review of the driver. So the precondition was met rather than
|
|
99
|
+
waived, and the answer is **promote**.
|
|
100
|
+
|
|
101
|
+
**Fixed, all from that review:**
|
|
102
|
+
|
|
103
|
+
- **Sonnet 5 was priced as Sonnet 4.6** — 3/15 rather than 2/10, 50% high. The same
|
|
104
|
+
table feeds `limits.perRequestUsd` and `perDayUsd`, so an app on that model was
|
|
105
|
+
refused requests comfortably inside its budget by an error that said "spend limit"
|
|
106
|
+
and sent it to its config rather than to the row. `AiSpendLimitError` now quotes the
|
|
107
|
+
rate it priced with and names `registerModelPrice()`, so a wrong table is legible
|
|
108
|
+
from the refusal and correctable without waiting for a release.
|
|
109
|
+
|
|
110
|
+
- **`effort` and `thinking` are model-aware.** Both went on every call. `effort` is a
|
|
111
|
+
400 on the 4.5 generation and those models want an explicit thinking budget rather
|
|
112
|
+
than the adaptive form — so the package listed `claude-haiku-4-5` in its pricing
|
|
113
|
+
table while the driver could not successfully call it. `modelCapabilities()` answers
|
|
114
|
+
what a model takes, and the driver builds the request that model accepts.
|
|
115
|
+
|
|
116
|
+
- **`temperature` never reached the API, on any model.** Not in the review — it turned
|
|
117
|
+
up while testing the item below. The driver warned about dropping `temperature` and
|
|
118
|
+
had no branch that set it, so the configured default and `AiRequest.temperature` were
|
|
119
|
+
both inert everywhere, including on the models that accept them. The old predicate
|
|
120
|
+
warned for almost every model, which is exactly what made the silence look deliberate
|
|
121
|
+
on the few it did not.
|
|
122
|
+
|
|
123
|
+
- **The streamed `thinking` chunk was always empty.** The API omits thinking text by
|
|
124
|
+
default on the current generation, so a documented chunk type fired forever with
|
|
125
|
+
`text: ""` and no error — and a "thinking…" view built against the 4.6 models, where
|
|
126
|
+
it defaulted on, stopped working when users moved to 5 with nothing to say so.
|
|
127
|
+
`drivers.anthropic.thinkingDisplay` defaults to `"summarized"`.
|
|
128
|
+
|
|
129
|
+
- **An app with no AI configured now boots.** `AiConfig` threw when no driver was
|
|
130
|
+
declared and threw again on an empty `apiKey`, so a deployment with no key could not
|
|
131
|
+
express itself either way. One app declared an Ollama server it did not run purely to
|
|
132
|
+
satisfy the validator, with a comment explaining that the config was lying. "AI is
|
|
133
|
+
off" is a coherent deployment and is now expressible; the first call raises
|
|
134
|
+
`AiDriverUnavailableError`, whose `transient` is already `false`.
|
|
135
|
+
|
|
136
|
+
- **`countTokens` returns `null` where a provider cannot count**, rather than `0`. Only
|
|
137
|
+
Anthropic has a counting endpoint, and `0` is also a real count for an empty prompt.
|
|
138
|
+
|
|
139
|
+
**How it was promoted**, because the order is the part that matters:
|
|
140
|
+
|
|
141
|
+
The surface was narrowed **first** — narrowing after `stable` is itself a breaking
|
|
142
|
+
change. `toSchema`, `strippedConstraints`, `resetSpend` and `resetStats` are `@internal`
|
|
143
|
+
now: still exported, so nothing breaks at runtime, but no longer promised.
|
|
144
|
+
`translateSchema` stayed public despite having no caller outside the package, for the
|
|
145
|
+
same reason `AiDriver` is public — the point of a driver contract is that someone else
|
|
146
|
+
implements it, and implementing structured output means translating a schema.
|
|
147
|
+
`AiDelivery` stayed too, being the element type of `recentGenerations()`.
|
|
148
|
+
|
|
149
|
+
Then the two modules it would have been embarrassing to freeze untested: the SSE
|
|
150
|
+
parser, which reads a remote provider's framing off the network, and prompt redaction,
|
|
151
|
+
which is the only thing between a user's prompt and a log that outlives the request.
|
|
152
|
+
Both hold up — the parser reassembles a frame whose terminator is split across chunks
|
|
153
|
+
and a UTF-8 sequence cut mid-character.
|
|
154
|
+
|
|
155
|
+
#### **BREAKING** — `countTokens` can return `null`
|
|
156
|
+
|
|
157
|
+
`Ai.countTokens()` and `AiDriver.countTokens()` return `number | null` rather than
|
|
158
|
+
`number`. Only Anthropic has a counting endpoint; the other drivers returned `0`, which
|
|
159
|
+
is also a real count for an empty prompt, so the old value was a number you could divide
|
|
160
|
+
by and budget against without ever being told it meant "unsupported".
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// before
|
|
164
|
+
const tokens = await Ai.countTokens(prompt);
|
|
165
|
+
if (tokens > 1000) shorten();
|
|
166
|
+
|
|
167
|
+
// after
|
|
168
|
+
const tokens = await Ai.countTokens(prompt);
|
|
169
|
+
if (tokens !== null && tokens > 1000) shorten();
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
A custom `AiDriver` implementation compiles unchanged — returning `number` still
|
|
173
|
+
satisfies `Promise<number | null>`. It is callers who need the check.
|
|
174
|
+
|
|
175
|
+
**This should have been a minor.** It shipped in a patch, which the versioning scheme
|
|
176
|
+
says cannot carry a break; see [the note in the upgrade guide](/docs/upgrade#1-11-2).
|
|
177
|
+
|
|
178
|
+
#### **INTERNAL** — four `@zerotal/ai` exports left the promised surface
|
|
179
|
+
|
|
180
|
+
`toSchema`, `strippedConstraints`, `resetSpend` and `resetStats` are `@internal`. They
|
|
181
|
+
are still exported and still work, so nothing breaks — they are simply no longer
|
|
182
|
+
covered by the compatibility promise, which is the narrowing that had to happen before
|
|
183
|
+
the package could be promoted at all. Reach for `AiFake` where a test used `resetSpend`
|
|
184
|
+
or `resetStats`; it is the seam built for that.
|
|
185
|
+
|
|
186
|
+
### Fixed — the gates
|
|
187
|
+
|
|
188
|
+
- **The release workflow ran three checks; the pull-request workflow ran fifteen.** So
|
|
189
|
+
every convention, surface and documentation gate guarded the cheap, reversible action
|
|
190
|
+
and not the permanent one. 1.11.0 and 1.11.1 both published over a CI that had been
|
|
191
|
+
red since the first of them, and nothing in the release objected, because nothing in
|
|
192
|
+
the release looked. `release.yml` now runs the same set.
|
|
193
|
+
|
|
194
|
+
- **One failing check hid eleven others.** When the `@zerotal/ai` review fell due, the
|
|
195
|
+
package-conventions step failed and every later step in that job was skipped —
|
|
196
|
+
reported as "skipped", which reads like "not applicable" rather than "never ran".
|
|
197
|
+
Each check is now guarded so it reports its own result.
|
|
198
|
+
|
|
30
199
|
## 1.11.1 — 2026-08-31
|
|
31
200
|
|
|
32
201
|
Two things the framework could not do, both reported by teams who had already
|
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,19 @@ 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. Six 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, and the refusal to write a boolean into a text column in 1.12.0.
|
|
83
|
+
- **One of those five is in the wrong place, and it stays on the record.** 1.11.2
|
|
84
|
+
is a patch, and by the rule above a patch cannot carry a break. It did: the
|
|
85
|
+
`countTokens` signature changed in the same release that promoted `@zerotal/ai`
|
|
86
|
+
to `stable`, and the reasoning that allowed it — the package was still
|
|
87
|
+
`experimental` when the change was made, earlier in that release — is not a
|
|
88
|
+
distinction anyone installing 1.11.2 can observe. What they get is a patch that
|
|
89
|
+
breaks. It is listed here rather than argued away, because a policy that quietly
|
|
90
|
+
excuses its own exceptions is not one you can plan against.
|
|
82
91
|
- **Provenance:** packages are published with npm provenance, so you can verify
|
|
83
92
|
a tarball was built by this repository's release workflow rather than someone's
|
|
84
93
|
laptop.
|
|
@@ -111,32 +120,39 @@ has worn the label for a year is not being cautious, it is unowned. So each one
|
|
|
111
120
|
below `stable` names the release by which it is reviewed, and the review has three
|
|
112
121
|
outcomes — promote, keep with a new date and the reason, or withdraw.
|
|
113
122
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
`@zerotal/ai`
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
Nothing is below `stable` today. The table that lived here is empty, which is the
|
|
124
|
+
outcome the mechanism is for rather than the absence of one — it is how the review
|
|
125
|
+
looks when every date has been answered.
|
|
126
|
+
|
|
127
|
+
`@zerotal/ai` was the last entry, `experimental` and due by 1.11.0. It was promoted in
|
|
128
|
+
1.11.2, and the precondition it carried was met rather than waived: it graduates in the
|
|
129
|
+
release after its first real users, and its first production users sent a field review
|
|
130
|
+
of the driver against Anthropic. That review is why the promotion is worth anything —
|
|
131
|
+
five bugs came back with it, and a `stable` promise about an API nothing has pushed
|
|
132
|
+
against is a promise nobody has tested.
|
|
133
|
+
|
|
134
|
+
The order was deliberate. Its surface was narrowed **before** the label, because
|
|
135
|
+
narrowing after `stable` is itself a breaking change: `toSchema`, `strippedConstraints`,
|
|
136
|
+
`resetSpend` and `resetStats` are `@internal` now. `translateSchema` stayed public
|
|
137
|
+
despite having no caller outside the package, for the same reason `AiDriver` did — the
|
|
138
|
+
whole point of a driver contract is that someone else implements it, and implementing
|
|
139
|
+
structured output requires translating a schema. Then its two riskiest modules were
|
|
140
|
+
tested: the SSE parser, which reads a remote provider's framing off the network, and
|
|
141
|
+
prompt redaction, which is the only thing between a user's prompt and a log that
|
|
142
|
+
outlives the request.
|
|
129
143
|
|
|
130
144
|
`@zerotal/arch` held `beta` with the same date, was reviewed ahead of it, and is
|
|
131
145
|
`stable` — the release that carried the promotion is the one its
|
|
132
|
-
[changelog](/docs/changelog) names. Its surface was narrowed first
|
|
133
|
-
`arch:install` are `@internal
|
|
146
|
+
[changelog](/docs/changelog) names. Its surface was narrowed first too: the writers
|
|
147
|
+
behind `arch:install` are `@internal`, because they had no caller outside the package
|
|
134
148
|
and freezing them would have promised the shape of `.mcp.json` writing to nobody.
|
|
135
149
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
150
|
+
Neither `ai` nor `arch` is in the `zerotal` meta-package. `arch` stays out because
|
|
151
|
+
`arch:install` writes configuration and instruction files into a project, which is an
|
|
152
|
+
opinion about someone's toolchain and stays their choice to invite. `ai` stays out for
|
|
153
|
+
its own reason rather than by omission: it is the only package with an optional peer
|
|
154
|
+
on a vendor SDK, and pulling it into the meta-package would put a provider dependency
|
|
155
|
+
in front of every app that installs `zerotal`, including the ones with no AI in them.
|
|
140
156
|
|
|
141
157
|
That table used to be the whole of the commitment, which meant the version could
|
|
142
158
|
sail past it and the only consequence would be this paragraph quietly becoming
|
package/docs/upgrade.md
CHANGED
|
@@ -289,6 +289,85 @@ 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.11 to 1.12
|
|
293
|
+
|
|
294
|
+
One breaking change, and it is the intended kind: a minor, announced, with the reason.
|
|
295
|
+
|
|
296
|
+
**A boolean written to a text column is refused.** A bare `@column()` resolves to
|
|
297
|
+
`{ type: "string" }`, so a boolean property decorated with one was stored as text —
|
|
298
|
+
and a text column has text affinity, so `false` was stored as `"0"`, which is truthy
|
|
299
|
+
in JavaScript. Every `if (model.flag)` on such a column took the wrong branch for a
|
|
300
|
+
stored `false`, on every row, with nothing in the app or the database registering a
|
|
301
|
+
fault. An app found it when a feature flag read as enabled for every record that had
|
|
302
|
+
it turned off.
|
|
303
|
+
|
|
304
|
+
```ts fragment
|
|
305
|
+
// in a model class body — before, and silently wrong
|
|
306
|
+
@column() declare active: boolean;
|
|
307
|
+
|
|
308
|
+
// after
|
|
309
|
+
@column("boolean") declare active: boolean;
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
There is no correct coercion: `0` becomes `"0"` and `"false"` is truthy too, so the
|
|
313
|
+
value cannot survive the round trip. The write now throws `ColumnTypeError`, naming
|
|
314
|
+
the property and the fix.
|
|
315
|
+
|
|
316
|
+
### What to do before upgrading
|
|
317
|
+
|
|
318
|
+
**Find your boolean properties whose `@column()` declares no type.** The decorator
|
|
319
|
+
cannot find them for you — `declare active: boolean` erases the TypeScript type at
|
|
320
|
+
runtime, so a bare `@column()` on a boolean is indistinguishable from one on a string
|
|
321
|
+
until a value arrives. A search of your models for `@column()` with no argument, read
|
|
322
|
+
against the property types beside them, is the reliable way.
|
|
323
|
+
|
|
324
|
+
**The rows you already wrote are still text.** This release stops new bad writes; it
|
|
325
|
+
does not migrate old ones. A column that has been holding `"0"` and `"1"` needs both
|
|
326
|
+
the decorator fixed and the stored values converted — on SQLite,
|
|
327
|
+
`UPDATE widgets SET active = CAST(active AS INTEGER)` after the column type is
|
|
328
|
+
corrected. Until then those rows keep reading truthy, which is the behaviour you are
|
|
329
|
+
upgrading to escape.
|
|
330
|
+
|
|
331
|
+
**If a text column really should hold a boolean**, say so explicitly and it is
|
|
332
|
+
honoured: `@column({ type: "string", cast: "boolean" })`. The guard is for the column
|
|
333
|
+
that says nothing, not for every string column.
|
|
334
|
+
|
|
335
|
+
## 1.11.2
|
|
336
|
+
|
|
337
|
+
One breaking change, and it is in a release that should not have carried one.
|
|
338
|
+
|
|
339
|
+
**`countTokens` returns `number | null`.** `Ai.countTokens()` and
|
|
340
|
+
`AiDriver.countTokens()` used to return `number`, with `0` standing in for "this
|
|
341
|
+
provider cannot count". Only Anthropic has a counting endpoint, and `0` is also a real
|
|
342
|
+
count for an empty prompt — so the old return value could not tell you which it meant,
|
|
343
|
+
and a budget built on it was quietly wrong for every other provider.
|
|
344
|
+
|
|
345
|
+
```ts fragment
|
|
346
|
+
// before
|
|
347
|
+
const tokens = await Ai.countTokens(prompt);
|
|
348
|
+
if (tokens > 1000) shorten();
|
|
349
|
+
|
|
350
|
+
// after
|
|
351
|
+
const tokens = await Ai.countTokens(prompt);
|
|
352
|
+
if (tokens !== null && tokens > 1000) shorten();
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
A custom `AiDriver` needs no change — returning `number` still satisfies
|
|
356
|
+
`Promise<number | null>`. Only callers do.
|
|
357
|
+
|
|
358
|
+
**Why this is in a patch.** It was made while `@zerotal/ai` was still `experimental`
|
|
359
|
+
and therefore outside the compatibility promise, in the same release that then promoted
|
|
360
|
+
the package to `stable`. That ordering is real and it is not a distinction anyone
|
|
361
|
+
installing 1.11.2 can observe: what arrives is a patch that breaks a build. The rule
|
|
362
|
+
stands as written — a patch does not break — and this release is recorded as the
|
|
363
|
+
exception rather than as a reinterpretation of it. See
|
|
364
|
+
[the support policy](/docs/support-policy#releases-and-versioning).
|
|
365
|
+
|
|
366
|
+
Everything else in 1.11.2 is additive. `@zerotal/ai`'s other surface change — `toSchema`,
|
|
367
|
+
`strippedConstraints`, `resetSpend` and `resetStats` becoming `@internal` — leaves those
|
|
368
|
+
exports working; they are no longer covered by the promise, which is different from
|
|
369
|
+
being gone.
|
|
370
|
+
|
|
292
371
|
## The managed zt.ts
|
|
293
372
|
|
|
294
373
|
`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.12.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.12.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "^5.8.0",
|
|
42
|
-
"@zerotal/orm": "1.
|
|
42
|
+
"@zerotal/orm": "1.12.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": [
|