@zerotal/arch 1.12.0 → 1.13.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/changelog.md CHANGED
@@ -27,6 +27,104 @@ 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.1 — 2026-08-31
31
+
32
+ One addition, found by sizing a job rather than doing it.
33
+
34
+ The 2.0 ledger carries an entry to prefix every `@internal` export with `_` — 270 symbols,
35
+ filed as mechanical. Three of them are `Job` classes, and a job class name is not a source
36
+ symbol: `JobRegistry` keys on it and that string is written into the persisted queue payload,
37
+ so a job enqueued yesterday is resolved by today's process. Renaming one invalidates every job
38
+ already in the queue, at deploy time rather than at change time, and no test sees it because a
39
+ test enqueues and runs in the same process.
40
+
41
+ The rename is re-scoped. The hazard it exposed is fixed here.
42
+
43
+ ### Added
44
+
45
+ - **`Job.jobName`** — a declared name for the queue payload, so renaming a job class is free:
46
+
47
+ ```ts
48
+ export class SendWelcomeEmail extends Job {
49
+ static override jobName = "SendWelcomeEmail"; // survives a class rename
50
+ async handle(): Promise<void> {}
51
+ }
52
+ ```
53
+
54
+ It defaults to the class name, so a job that declares nothing behaves exactly as before.
55
+ This is the same tool [`Migration.id`](/docs/upgrade#1-10-to-1-11) is for a migration's
56
+ filename, shipped in 1.11.0 for the same reason: an identity the framework derived from a
57
+ name someone was free to change, with no way to say otherwise.
58
+
59
+ It also decouples the queue from a build that mangles names. `zt compile` does not minify
60
+ today, so that is not a live hazard — but nothing in the registry said it depended on that,
61
+ and an assumption worth relying on is worth writing down.
62
+
63
+ ## 1.13.0 — 2026-08-31
64
+
65
+ Three retirements, taken together on purpose. Each is a small migration, and three minors
66
+ each asking an app to move costs more than one that asks properly — so this is one crossing
67
+ and one `zt upgrade` run.
68
+
69
+ ```bash
70
+ bun zt upgrade --to 1.13.0
71
+ ```
72
+
73
+ ### Removed — BREAKING
74
+
75
+ - **Flow's `Component.client(…)`.** Use the `` this.$`…` `` tagged template.
76
+
77
+ This is a security fix wearing an ergonomics change's clothes, which is why it did not wait
78
+ for 2.0. `client()` took a **string** and queued it to be evaluated in the browser, so the
79
+ caller owned the escaping — and its own docblock had to warn _never interpolate unescaped
80
+ user input_. A method whose documentation has to tell you not to hold it that way is a
81
+ footgun with a label on. `$` is a tagged template, so every `${…}` is encoded as a JS
82
+ literal before it reaches the page.
83
+
84
+ ```ts
85
+ // before — escaping was yours to remember
86
+ this.client(`toast(${JSON.stringify(this.search)})`);
87
+
88
+ // after — encoded for you
89
+ this.$`toast(${this.search})`;
90
+ ```
91
+
92
+ The codemod rewrites a call whose argument is a single literal. **One whose argument is a
93
+ variable or a concatenation is reported rather than rewritten**: those are precisely the
94
+ ones the warning was about, and wrapping a finished string as `` $`${expr}` `` would encode
95
+ it as a string literal and stop running it as code. A codemod that quietly did that would
96
+ leave an app compiling, running, and no longer doing anything where it used to run a script.
97
+
98
+ Removing it also frees `client` as a property name on a component — the same benefit
99
+ removing `title` gave in 1.7.3.
100
+
101
+ ### Changed — BREAKING
102
+
103
+ - **`LockDriver.extend()` is required.** Only affects a custom lock driver; all three
104
+ built-in drivers already implement it.
105
+
106
+ It shipped optional in 1.5.0 with `acquire(key, owner, ttl)` as the fallback, and that
107
+ fallback was correct only by coincidence. `acquire` happens to be an owner-guarded refresh
108
+ on every built-in driver, and nothing in the interface ever said it had to be — so a
109
+ third-party driver whose `acquire` takes a _free_ lock, which is the ordinary reading of
110
+ the word, would have had `refresh()` silently take a lock another holder owned. That is the
111
+ one thing a lock exists to prevent. Requiring the method turns an assumption the contract
112
+ never stated into something a driver has to answer.
113
+
114
+ - **`routes:types` and `serve --dev` are retired**, in favour of `route:types` and `dev`.
115
+ Both are rewritten by the codemod.
116
+
117
+ `serve --dev` **fails with a message** rather than being ignored, and the flag is still
118
+ declared for that reason alone. Flag parsing runs non-strict, so simply deleting it would
119
+ have left `serve --dev` starting a plain server — no watcher, no rebuild, no explanation. A
120
+ retired flag that silently changes what a command does is worse than one that is still
121
+ there.
122
+
123
+ ### Added
124
+
125
+ - **The `client-tagged-template` codemod**, which is what makes the first item above a
126
+ migration rather than a search.
127
+
30
128
  ## 1.12.0 — 2026-08-31
31
129
 
32
130
  One change, deliberately alone: the minor exists to carry it.
@@ -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` `client` `$` |
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/queue.md CHANGED
@@ -141,6 +141,37 @@ export class PruneDeletedContentJob extends Job {
141
141
  JobRegistry.register(PruneDeletedContentJob as never);
142
142
  ```
143
143
 
144
+ ### Renaming a job class
145
+
146
+ A queued job is a **persisted reference to a class**. The payload in the database or in Redis
147
+ carries a string, and the worker resolves the class by it — so a job enqueued yesterday is
148
+ looked up by today's process. That makes the class name a compatibility surface, and renaming
149
+ the class a silent data migration: jobs already in the queue under the old name stop resolving
150
+ at the moment you deploy.
151
+
152
+ It is invisible to a test suite, because a test enqueues and runs in the same process.
153
+
154
+ Declare the stored name and the class is free to move:
155
+
156
+ ```ts
157
+ import { Job } from "@zerotal/queue";
158
+
159
+ export class SendWelcomeEmail extends Job {
160
+ // The name in the queue payload. Keep it when the class is renamed.
161
+ static override jobName = "SendWelcomeEmail";
162
+
163
+ async handle(): Promise<void> {}
164
+ }
165
+ ```
166
+
167
+ It defaults to the class name, so a job that says nothing behaves exactly as before. Declare it
168
+ when you rename a job class, or ahead of time on jobs you expect to rename — it is the same
169
+ tool `Migration.id` is for a migration's filename, and the same reason.
170
+
171
+ **Drain before you rename, if you have not declared one.** A rename with jobs in flight and no
172
+ `jobName` leaves rows the worker reports as an unknown job class; `zt queue:failed` lists them
173
+ and `zt queue:retry` can replay them once the name matches again.
174
+
144
175
  ## Auto-registration
145
176
 
146
177
  You don't import or wire up your jobs anywhere. Any job class placed under
@@ -75,11 +75,12 @@ 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. Six have shipped so far — the
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
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
- 1.11.2, and the refusal to write a boolean into a text column in 1.12.0.
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
84
  - **One of those five is in the wrong place, and it stays on the record.** 1.11.2
84
85
  is a patch, and by the rule above a patch cannot carry a break. It did: the
85
86
  `countTokens` signature changed in the same release that promoted `@zerotal/ai`
package/docs/upgrade.md CHANGED
@@ -289,6 +289,64 @@ 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
+
292
350
  ## 1.11 to 1.12
293
351
 
294
352
  One breaking change, and it is the intended kind: a minor, announced, with the reason.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/arch",
3
- "version": "1.12.0",
3
+ "version": "1.13.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.12.0"
38
+ "@zerotal/core": "1.13.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "typescript": "^5.8.0",
42
- "@zerotal/orm": "1.12.0"
42
+ "@zerotal/orm": "1.13.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": [