@hozu/cli 0.25.0 → 0.26.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 (48) hide show
  1. package/CHANGELOG.md +1510 -0
  2. package/dist/commands/browse-page.d.ts.map +1 -1
  3. package/dist/commands/browse-page.js +7 -1
  4. package/dist/commands/browse-page.js.map +1 -1
  5. package/dist/commands/browse-tab.d.ts +1 -1
  6. package/dist/commands/browse-tab.d.ts.map +1 -1
  7. package/dist/commands/browse-tab.js +20 -9
  8. package/dist/commands/browse-tab.js.map +1 -1
  9. package/dist/commands/browse-world.d.ts.map +1 -1
  10. package/dist/commands/browse-world.js +73 -9
  11. package/dist/commands/browse-world.js.map +1 -1
  12. package/dist/commands/browse.d.ts +2 -0
  13. package/dist/commands/browse.d.ts.map +1 -1
  14. package/dist/commands/browse.js +4 -3
  15. package/dist/commands/browse.js.map +1 -1
  16. package/dist/commands/explain.d.ts.map +1 -1
  17. package/dist/commands/explain.js +17 -3
  18. package/dist/commands/explain.js.map +1 -1
  19. package/dist/commands/request.d.ts +1 -0
  20. package/dist/commands/request.d.ts.map +1 -1
  21. package/dist/commands/request.js +14 -4
  22. package/dist/commands/request.js.map +1 -1
  23. package/dist/commands/scaffold.js +1 -1
  24. package/dist/commands/scaffold.js.map +1 -1
  25. package/dist/commands/target.d.ts +17 -0
  26. package/dist/commands/target.d.ts.map +1 -0
  27. package/dist/commands/target.js +168 -0
  28. package/dist/commands/target.js.map +1 -0
  29. package/dist/commands/validate.d.ts.map +1 -1
  30. package/dist/commands/validate.js +10 -3
  31. package/dist/commands/validate.js.map +1 -1
  32. package/dist/contract.d.ts +2 -0
  33. package/dist/contract.d.ts.map +1 -1
  34. package/dist/main.d.ts.map +1 -1
  35. package/dist/main.js +17 -1
  36. package/dist/main.js.map +1 -1
  37. package/dist/migrate/steps.d.ts.map +1 -1
  38. package/dist/migrate/steps.js +20 -0
  39. package/dist/migrate/steps.js.map +1 -1
  40. package/package.json +9 -8
  41. package/schema/inspect.schema.json +0 -7
  42. package/schema/why.schema.json +6 -1
  43. package/skill/example/features/bookmarks/views.ts +3 -1
  44. package/skill/topics/components.md +2 -2
  45. package/skill/topics/deploy.md +14 -12
  46. package/skill/topics/machine.md +3 -2
  47. package/skill/topics/testing.md +3 -1
  48. package/skill/topics/views.md +7 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,1510 @@
1
+ # Changelog
2
+
3
+ ## 0.26.0 — Deploying is one command (ADR 0073)
4
+
5
+ Two things people met in 0.25: deploying anywhere but Node took a hand-written entry, a bundler and a platform file,
6
+ and a shortcut could only send an event. `hozu migrate` raises the packages and lists each `ui.send(…, { keys })` to
7
+ move by hand.
8
+
9
+ ### Deploying
10
+ - **`hozu build --target workers | vercel | node`.** Workers: `dist/workers/` with one bundled `worker.mjs` (no
11
+ `node:` import), `assets/` and `wrangler.jsonc`; then `npx wrangler deploy`. Vercel: `.vercel/output/` (Build
12
+ Output API, an Edge Function); then `npx vercel deploy --prebuilt`. Node: a `Dockerfile` for `hozu serve`. Hozu never
13
+ contacts a platform. Needs `@hozu/bundle`.
14
+ - **What the platform needs is printed**, read from the declarations: the server env, `SESSION_SECRET`, a KV
15
+ namespace bound as `SESSIONS` on Workers, a shared session store on Vercel, a stream limit for live queries.
16
+ - **`hozu browse --build dist/workers`** drives the bundled entry and its static files, so what you upload is what you
17
+ verified; `--session` signs in through its KV.
18
+ - `hozu export` stays the static form; `--target static` points to it.
19
+
20
+ ### Views
21
+ - **Keyboard shortcuts belong to the control they press** (breaking): `ui.input({ name: 'q', keys: ['/'] })` focuses
22
+ the field, `ui.button({ type: 'submit', keys: ['Mod+s'] }, ['Save'])` clicks it, so a form submits and no machine is
23
+ needed. Only visible, enabled controls count, inside an open modal only its own. The server writes
24
+ `aria-keyshortcuts`; a page with shortcuts loads `keys.js` (about 0.7 KB), islands or not. `ui.send(…, { keys })`
25
+ and one key on two controls always shown together are HZ014. `hozu browse --do 'press Mod+s'` presses it.
26
+
27
+ ### Fixes
28
+ - **Safari could not sign in under `hozu serve` on this machine:** session cookies were `Secure` over plain HTTP,
29
+ which Safari refuses on `127.0.0.1`. `Secure` is now left out only over HTTP on a loopback host.
30
+ - `hozu check`'s coverage line and `hozu why` count a shared `on` copied into every state as one decision, as HZ016
31
+ does (the trial apps showed 26/35 and 7/8 with no error); `why` says the lock reviews a transition that does not
32
+ decide, instead of "uncovered".
33
+ - `hozu browse`: a capped list that prepends a row is no longer a flash (positions are compared); `--js both` split
34
+ lines show their own arrival; a covering element is named by id, `aria-label` or class; `--select` prints `class`
35
+ last and cut (`--json` has it whole).
36
+ - `@hozu/cli` ships `CHANGELOG.md`.
37
+
38
+ ### Guide and site
39
+ - A staff tool may show `Unexpected`'s message (production's `Internal error (call <id>)` matches the log line); a
40
+ customer page shows a fixed text. Browse never shows `prerendered` (Chrome turns prerendering off under DevTools).
41
+ - Arc blanks the window on every document load (a plain multi-page site too); "How it works" says so and why Hozu
42
+ keeps document loads (ADR 0073 D).
43
+ - The deploy topic and the site's Deploying page use `--target`.
44
+
45
+ ## 0.25.0 — What mainstream frameworks do, at Hozu's cost (ADR 0072)
46
+
47
+ Judged three ways: what a React / Vue / Svelte app gives a person without asking, where Hozu must stay ahead in bytes,
48
+ and what an agent can verify cheaply. No source change is needed: `hozu migrate` raises the packages.
49
+
50
+ ### Views
51
+ - **`c ? a : b` keeps its element** when both branches are one element of the same tag and shape
52
+ (`ctx.paused ? resumeButton : pauseButton`): the text, classes, attributes and listener follow `c`, focus stays, and
53
+ the element is kept. The IR still holds both branches for every check; the renderers draw one element. Branches whose
54
+ differing values compute (a `fn`, a template string) or link to different routes are drawn as before.
55
+ - **Keyboard shortcuts:** `ui.send(Open, {}, { keys: ['Mod+k', '/'] })` on `keydown` / `keyup` sends only on those
56
+ presses and stops the browser's own (`Mod` is ⌘ on Apple, Ctrl elsewhere). A printable key without a modifier
57
+ waits while the person types in a field inside the listener (`/` still types a slash; `Escape` still fires), and
58
+ nothing fires while an input method composes. A bad list is HZ014.
59
+ - **`current(route, params)`** compares those params with the page shown and ignores search:
60
+ `current(shop, { category: 'apparel' })` marks a store's category in its header. A param the route lacks is HZ007.
61
+
62
+ ### Performance
63
+ - The client keeps its 9 KiB budget (P7 9 199 B): dialogs bound to the machine, `aria-current` of links in islands
64
+ and shortcut matching load in a small chunk only on pages that have them.
65
+ - **New budgets S1 / S2:** every example with a machine has `browse.json`, the steps a person takes; `pnpm bench`
66
+ runs them and requires no element rebuilt unchanged and no layout shift above 0.01 (11 runs, both 0).
67
+
68
+ ### Tools
69
+ - `hozu browse` fails a click that would land on another element, naming it (`the click would land on <h3>, which
70
+ contains it (a ::before or ::after above it, …), above <a href="/products/mug">`); an ancestor covering its link counted as a hit before.
71
+ - `hozu browse` names how a navigation arrived and its time to the first paint (`→ /products/mug (loaded, 32 ms)`).
72
+ - An element that moves to another parent (a Load more button under the next page) is no longer reported as a flash.
73
+ - `hozu get --select` / `browse --select` print `class`.
74
+
75
+ ### Guide
76
+ - A mode the person sets (paused, a view mode) is a context field, not a machine state: busy states keep it.
77
+ `examples/watchlist` holds `paused` in its context, and its Pause button no longer turns into Resume while adding.
78
+ - A menu is a constant list mapped to links, with `current(a) || current(b)` per section (it lowers inside `.map`).
79
+ - One form for "show this in that state": `is(['adding']) && …` and `disabled: is(['adding'])`; `when(states,
80
+ children, motion)` only when an enter / leave motion is needed. The example app, the scaffold and the topics use it.
81
+ - Topic fixes: `head.render`'s `locale` is its third argument; public queries take no `access` (their mutations do);
82
+ `createHandler(app, { session })`; the calm state kept across pages (shared view or same address) is one story in
83
+ machine, patterns and pages; contracts list `refresh` / `copy` / `replace` effects; testing covers the 0.25 browse
84
+ output; deploy lists every `app()` option; diagnostic summaries updated (HZ005, HZ007, HZ041, HZ045, HZ049, HZ059,
85
+ HZ093).
86
+
87
+ ### Site
88
+ - Six new docs pages: Components and kits, Machines and contracts, Forms, Resolvers in Go, Languages, Verify and
89
+ test. Every page brought to 0.25 (shortcuts, `current(route, params)`, `{ ...search }` links, parsed params,
90
+ `hozu gen`, production error masking, the DevTools dock).
91
+ - The home page shows a backend in Go and how agents check their own work; "How it works" corrected (freshness
92
+ `{ poll }` and public `'request'`, state kept across pages, what `hozu plan` prints, where effects run).
93
+ - Speed table re-run on 0.25.0 (bench/meta, 2026-10-08).
94
+
95
+ ## 0.24.0 — Sections are yours to say (ADR 0071)
96
+
97
+ The 0.23 retest asks, judged by the framework rather than taken as given. `hozu migrate` raises the packages.
98
+ Behaviour to check: a link to a section (`/orders` while on `/orders/7` or `/orders?status=open`) no longer gets
99
+ `aria-current` by itself; mark it with `current(route)`.
100
+
101
+ ### Views
102
+ - **`current(route)` in a view's render** is true on that route's pages (any params and search):
103
+ `'aria-current': current(orders) || current(orderDetail)` marks a menu's section on the list, a filtered list and a
104
+ detail page. The framework marks only the address shown (`aria-current="page"`); 0.22 and 0.23 guessed sections
105
+ from URL prefixes and every guess misfired somewhere (a next-page link, a "Back to editor" link, a filtered list).
106
+ - `aria-current: false` writes no attribute, so `aria-[current]:` styles only marked links, and `true` is written
107
+ `"page"` on the address itself (a screen reader then says "current page").
108
+ - A form whose submit reads `current(route)` still posts without JavaScript.
109
+
110
+ ### Tools
111
+ - `hozu get --select` takes descendant and child combinators (`nav a[aria-current]`).
112
+ - `hozu call <endpoint> --write` prints the tags it invalidated.
113
+ - HZ093 says when a Go contract is in the pre-0.23 format (run `hozu gen`), instead of "every effect is missing".
114
+ - `hozu migrate` lists each step's changes as bullets.
115
+ - `hozu browse` / `get` say once that a production server shows `Internal error` where they show a message.
116
+ - `hozu gen` notes untitled enums with the same members and suggests `.meta({ title })` to make them one Go type.
117
+
118
+ ### Guide
119
+ - Expected failures (a row gone, a state that forbids the change) are declared errors, handled in `failed` with and
120
+ without JavaScript; a thrown error is `Unexpected`, a server fault.
121
+ - A query that another app's writes change needs a tag, or only its freshness time refreshes it.
122
+
123
+ ## 0.23.0 — What the 0.22 retest and a Go backend found (ADR 0070)
124
+
125
+ The two trial agents upgraded their CMS / shop admin and storefront to 0.22 (`hozu migrate` rewrote nothing), and the
126
+ admin moved its order lifecycle, inventory and dashboard to a Go service. This release answers what they found.
127
+ `hozu migrate` raises the packages. If you have a Go service: run `hozu gen` and rebuild it (the contract now has a
128
+ fingerprint per effect).
129
+
130
+ ### Fixes
131
+ - **Canonical URLs and links leave defaults out after an optional segment.** `/shop/:category?` with search defaults
132
+ produced `<link rel="canonical" href="/shop/apparel?availability=all&page=1&…">`, and links built from search values
133
+ kept the defaults (since 0.17).
134
+ - **Route params are parsed:** `params: z.object({ id: z.coerce.number() })` now gives resolvers a number, as its type
135
+ says (it was the string from the URL).
136
+ - **`aria-current` marks the page and the sections above it only**: a next-page link (same path, another search) gets
137
+ nothing; an `aria-current` you set wins.
138
+ - `hozu get` / `browse` print each server error once per step.
139
+
140
+ ### Forms and links
141
+ - **A multi-step form keeps its step without JavaScript.** After a native post, every form Hozu posts (no `method` or
142
+ `action` of yours) carries the machine's state in a hidden field (`__hozu_state`), and the next post continues from
143
+ it: no more re-posting every earlier field. The state is bound to the visitor's session and the machine's shape,
144
+ lasts a day, is checked against the context schema, and is signed with `SESSION_SECRET` when the server has one.
145
+ - **`ui.link(route, params, { ...search, page: 2 })`** keeps the current search and changes one field.
146
+ - **Every access but `'anyone'` types the resolver's `session` as present** (`{ allow }` and `{ owner }` too).
147
+
148
+ ### Resolvers in Go
149
+ - A service that does not answer names the effect, the URL and what to start; every call carries `x-hozu-call`, and
150
+ the Go handler answers its error's first line with that id (`resolver failed: …`), so `onError` shows the cause.
151
+ - **One fingerprint per effect:** changing one declaration makes only that effect answer 409 until `hozu gen`, and
152
+ HZ093 names it. The Go contract's `Fingerprint` is a function of the effect.
153
+ - String enums are named Go types with constants; `hozu gen` notes number fields that look like ids or counts
154
+ (`z.int()` makes them `int64`); `hozu docs data --more` links `examples/notes-go` and shows a `main.go`.
155
+ - A wrong answer's schema issues are collapsed (`rows.*.tone: … (10×)`).
156
+
157
+ ### Errors in production
158
+ - **The browser no longer sees error messages in production** (`NODE_ENV=production`): an `Unexpected` answer says
159
+ `Internal error` (with the call id of a Go service), and `onError` keeps the full message. Development, `hozu get`,
160
+ `browse` and `call` show it as before.
161
+
162
+ ### Tools
163
+ - A changed lock line lists only the assignments that differ (`assign - total := …, + total := …`) and leaves out a
164
+ `now:` longer than 120 characters.
165
+ - `hozu browse --js both` names the words that differ (`"#1307" vs "#1306"`) when each mode wrote its own row.
166
+ - A native `commandfor` / `popovertarget` button is not `js-only`: without JavaScript the dialog still opens.
167
+ - `--json` changes: `hozu gen` effects are `{ ref, fingerprint }[]` with `notes`; browse adds `differences`.
168
+
169
+ ## 0.22.0 — Resolvers in Go, and what a CMS, a shop admin and a storefront asked for (ADR 0068, 0069)
170
+
171
+ Two agents built a CMS with a shop back office and its storefront on 0.21.1, sharing one MySQL database, then
172
+ reviewed Hozu. This release answers them. `hozu migrate` raises the packages; run `hozu build` again before
173
+ deploying (component fingerprints changed). Behaviour to check: kept state now follows only a view two pages share,
174
+ or the visitor coming back to the same address.
175
+
176
+ ### Resolvers in another language (ADR 0068)
177
+ - **`remote(options, [decls])`** implements server effects in a service of another language over HTTP; `hozu gen`
178
+ writes its Go contract (types, the `Resolvers` interface, `Handler`, gofmt-clean, standard library only).
179
+ `access`, caching, tags and the output check stay in the Hozu server. HZ093: a missing or stale contract, a
180
+ browser-run effect, a non-JSON endpoint, an undeclared env variable, or a missing or short secret.
181
+ - The secret is required (16+ characters): the service trusts the session it is sent. Endpoints forward their
182
+ request headers (no cookie), every call carries `preview` and the uploads (`ctx.File(token)` in Go).
183
+ - `examples/notes-go` is the notes app with every resolver in Go; `hozu docs data --more` has the loop.
184
+
185
+ ### Data and the server
186
+ - **A seed reads server data:** `seed: ({ params, search, query }) => ({ email: query(me, {}).email })` starts a
187
+ machine prefilled from a query (server render, hydration, no-JS posts; the render plan counts the query; a failed
188
+ one leaves `initialContext`).
189
+ - **A resolver may answer `fail('Forbidden', { message })`**, and with `access: 'signedIn'` its `session` is typed
190
+ as present.
191
+ - **A query input that fails its schema reaches `onError`** with the field and a hint (`z.coerce.number()` for route
192
+ params and form fields), instead of a silent 500.
193
+ - **`head.input` and `head.render` get `search`**, so `/journal?topic=makers` has its own title.
194
+ - **`part()` shares an access rule:** `const staffOnly = part(({ session }) => …)`, `access: { allow: staffOnly }`.
195
+ - `hozu docs data --more` covers databases: the pool and `app({ dispose })`, transactions, migrations, numeric ids,
196
+ staff-shared data, a second app writing the same database. `hozu docs http` lists every CSP key.
197
+
198
+ ### Views
199
+ - **Links to the page shown get `aria-current`** (`page`, or `true` for the same path with another search and for a
200
+ section above it), on the server and in the browser: a menu needs no current-route logic.
201
+ - **`ui.dialog({ open: is(['editing']) })`** opens as a modal and closes with the machine.
202
+ - **`ui.format.plural(n, { one: '# item', other: '# items' })`.**
203
+ - `null` and `false` render nothing inside a constant list too; `rel` is allowed on `a`, `area` and `form`.
204
+ - HZ033 accepts a hidden input whose value is a context field of the same enum.
205
+ - **Kept state** (0.21) follows a machine to another page only through a view both pages show, or back to the same
206
+ address: the quantity chosen on one product no longer appears on the next.
207
+
208
+ ### Tools
209
+ - One-shot commands (`hozu get`, `call`, `browse`, `check`, …) exit when done even if the app holds a database pool;
210
+ `app({ dispose })` closes it (also on `hozu serve` shutdown).
211
+ - `hozu get` and `hozu browse` list the server errors of each page and step.
212
+ - A flash is an element removed and an equal one (tag, class, text, `name`, `id`, `href`, `src`, `type`, parent
213
+ path) added in the same step; the report names them (`main > form > input[name=card]`).
214
+ - `--json` changes: browse's `flashes` is `{ count, elements }`, and `get` / `browse` add `serverErrors`.
215
+ - `hozu add feature --with auth` writes a valid config; `--select` takes `^= $= *= ~=`; `fill` values take `\n`;
216
+ `project({ routes })` with a non-route value is HZ014 naming the key.
217
+
218
+ ## 0.21.1
219
+
220
+ - **`ui.set` works on a field that stays visible while the machine is busy.** A state with `invoke` drops every
221
+ declared event it does not handle, but not the event `ui.set` adds, so `on: { input: ui.set(ctx.draft,
222
+ ui.dom.value) }` on an input shown during `adding` was HZ005. Busy states now drop it too, like any event.
223
+ - **The guide uses the 0.21 forms:** the example app (`examples/bookmarks`, the skill's `example/`) binds its title
224
+ input with `ui.set` instead of a `Draft` event; `hozu docs forms` and `patterns` (search as you type, toggle
225
+ buttons, shared controls) teach `ui.set` for a control that only sets a field, and an event for a transition that
226
+ decides or does more.
227
+ - **hozu.org uses them too:** the home page's variant picker is `ui.set` and its demo swaps its button with
228
+ `is(['broken']) ? … : …`; the How it works lab sets scope, freshness and binding with `ui.set`, so changing one
229
+ while the walkthrough runs no longer restarts the current step.
230
+
231
+ ## 0.21.0 — Continuity: a page that never flashes (ADR 0067)
232
+
233
+ `hozu migrate` raises the packages; run `hozu build` again before deploying (component fingerprints changed). One
234
+ behaviour to check: a transition's `navigate`, `refresh` and `copy` now read the context after its `assign` (below);
235
+ `hozu check` shows a `navigate` that changes through its contract. Hozu knows the whole page before it runs, so it
236
+ keeps the page calm with no code from you.
237
+
238
+ ### A calm page
239
+ - **A query region settles instead of being replaced.** When its input changes (a filter, one more item), the rows
240
+ on screen stay, marked `aria-busy`, and update by key; only the new row is inserted. `pending` shows only before the
241
+ first answer. (Adding a symbol to the watchlist rebuilt 14 elements and flashed `pending`; it now inserts one row.)
242
+ - **What an update adds fades in** (160 ms, rising 4 px): a region that was empty, rows added to
243
+ a list. A swap does not fade. Nothing animates on the first render or with reduced motion; a `motion` name still
244
+ chooses your own.
245
+ - **A request that fails no longer leaves the page waiting:** a query read that cannot reach the server (offline, a
246
+ 502 page) shows the `Unexpected` branch instead of the old rows, and a mutation that cannot reach it goes to
247
+ `failed.Unexpected` instead of staying in its busy state.
248
+ - **Views two pages share keep still across a page change:** their root gets a derived `view-transition-name`, so a
249
+ header or a side panel stays while the rest cross-fades.
250
+ - **A machine the next page shows too keeps its state** across the page change (the tab's `sessionStorage`, for the
251
+ same visitor, under half an hour, calm states only, not on a reload; fields the address seeds come from the
252
+ address). The page hydrates the server's view, then enters the kept state; a prerendered page does so when shown.
253
+ State that belongs to one item (a draft on `/posts/:id`) should be seeded from the address. In an app with a
254
+ session, cacheable pages keep nothing (they cannot know who is visiting). The code loads as a small chunk next to
255
+ hydration; the initial client is 8 935 B.
256
+ - **`hozu browse` proves it:** a step that rebuilds elements unchanged reports a flash (`N elements rebuilt unchanged
257
+ (a flash)`), and layout that moves without input reports `layout shift X` (as CLS counts it). The examples were
258
+ fixed where it found flashes: controls are disabled while busy instead of hidden.
259
+
260
+ ### Shorter forms for common UI
261
+ - **`is([...])` works for structure:** `is(['paused']) ? resume : pause`, `!is(['idle']) && saving`; HZ005 reads the
262
+ states each branch can show in. (A `!is(…)` was evaluated as JavaScript before.)
263
+ - **`ui.set(ctx.field, value)`** in a view's `on`: a control that only sets a context field needs no event. The build
264
+ adds the event and a shared `on` that stays, the IR of the long form; a native post checks the value against the
265
+ field's schema.
266
+ - **`replace: () => ui.link(…)`** on a transition writes the address without loading a page, so a reload or a shared
267
+ link keeps a search (`examples/stations`). A link that copies context fields decides nothing (no contract).
268
+ - **Every effect of a transition reads the context after its `assign`** (`navigate`, `refresh`, `copy`, `replace`),
269
+ like the `invoke` input of the state it enters. `navigate` read it from before until 0.20. A `replace` to another
270
+ route is HZ014: that is a `navigate`.
271
+
272
+ ### Fixes and tools
273
+ - Component fingerprints hash the recorded render, `ui.each` items included (a bundler cannot change them; a
274
+ constant the render reads does, GitHub issue #1 point 3); the manifest keeps `fn` fingerprints only.
275
+ - The runtime makes no random value at module load (Cloudflare Workers refuse one at startup): `httpBus` picks its id
276
+ when it first sends.
277
+ - `replace` to a route that no page of the machine shows is HZ014; `aria-busy` is counted per parent and released
278
+ when a busy region goes away.
279
+ - `hozu migrate` leaves out the paths it cannot predict before it counts, so a real IR difference is never hidden
280
+ behind 50 fingerprint lines.
281
+ - A changed lock entry lists only the fields that changed (`guard was …, now …`) before its `now:` line.
282
+ - `hozu browse`: a same-document address change (`replace`) is `in place`, not a page load; `release;` followed by
283
+ another step splits correctly.
284
+ - The guide: `fn` for computed attributes (an SVG path), `vars` with arbitrary-value classes for sizes and colours,
285
+ the Chart.js client component in `examples/showcase`, and a Vue / React → Hozu table (`hozu docs views --more`).
286
+
287
+ ## 0.20.2
288
+
289
+ - **A bundled app with components or `fn`s matches its build manifest** (GitHub issue #1). The IR fingerprinted
290
+ component renders and `fn` bodies from their function text, which a bundler reprints, so
291
+ `createHandler(app, { manifest, render })` refused an `app.ts` bundled with `hozuTransform()`. `hozu build` now
292
+ records those fingerprints in the manifest and a build with a manifest reads them. Run `hozu build` again after
293
+ upgrading, and build and bundle from the same source on every deploy (an edited `fn` body alone no longer fails
294
+ the manifest check).
295
+ - **`@hozu/bundle` keeps Node out of edge bundles:** it loads `node:path` and esbuild only when it builds.
296
+ - **DevTools: Copy for AI saves the request too.** A pasted request had no file, so the agent could not mark it done
297
+ (`hozu requests done`). Copy for AI now saves `.hozu/requests/NNNN-….md` and adds a last line with the file and the
298
+ `hozu requests done <n>` command; Copy and Save of the same request share one file.
299
+
300
+ ## 0.20.1
301
+
302
+ - **`hold` works for browser-run mutations too:** `hozu browse --do 'hold watchlist.addSymbol'` keeps a `runs:
303
+ 'browser'` (or `'either'`) mutation from running until `release`, so the busy UI of a localStorage app can be read
304
+ and screenshot. `hozu browse` serves the feature's fetch module through a wrapper; the production runtime is
305
+ unchanged.
306
+ - **`target: 'previous'` returns to the last state without `invoke`:** A → looking → saving → `previous` comes back to
307
+ A (it went back to `looking` and ran its lookup again). A return from a state entered right after a busy one now
308
+ skips that busy state too, and a state entered only through busy states has nothing to return to (HZ007 for its
309
+ `done` / `failed` / `after` returns; a contract's `given.previous` that invokes is HZ007).
310
+ - **`hozu dev` reloads only for files that changed since it started:** it records each file's time at start, so a
311
+ late or repeated file event (common under load) no longer reloads the page.
312
+ - **`browse` targets ignore symbols** when no name matches exactly: `click 暫停` finds `❚❚ 暫停` or `⏸️ 暫停` (still one match only).
313
+ - **A machine may invoke a query** (the guide said only mutations; the runtime always ran both): "ask the server,
314
+ then save in the browser" is `invoke(serverQuery)` → `invoke(browserMutation)` with `done: 'previous'`, as the
315
+ recipes topic shows. An invoked browser-run query no longer reads the page's queries again by its tags.
316
+
317
+ ## 0.20.0 — Simple requests stay simple (ADR 0064)
318
+
319
+ No source change is needed (`hozu migrate` raises the packages). An `accept` entry for HZ036 on a form that starts a
320
+ `runs: 'browser'` mutation is now stale (HZ087): delete it. A `machine({ on })` entry without `target` now stays
321
+ without entering its state again: run `hozu check --update-lock` if HZ057 lists such entries (`--> stays`).
322
+
323
+ - **`refresh` on a transition** reads the page's queries with those tags again, with no write:
324
+ `on(RefreshNow, { refresh: () => [quotesTag()] })`, or every 30 s while a `live` state lasts
325
+ (`after: [{ ms: 30_000, target: 'live', refresh: … }]`; Pause is another state). It replaces the no-op mutation
326
+ whose only job was `invalidates`. Contracts expect `{ refresh: [quotesTag()] }`. `freshness: { poll }` stays for data
327
+ that is always kept fresh.
328
+ - **An `on` without `target` stays where it is:** its state's timers keep running and an `invoke` keeps going, as in
329
+ XState v5 or plain `setInterval` code. A Copy click no longer restarts a refresh timer, typing no longer keeps a
330
+ toast open, and a busy state can take an event without restarting. Naming the state enters it again (a debounce, a
331
+ repeating timer).
332
+ - **`copy` on a transition** writes text to the clipboard: `on(CopyLink, { copy: (e) => e.url })`.
333
+ - **`is([...])` in a render** follows the machine state as a value: `disabled: is(['saving'])`.
334
+ - **Warnings about real problems only.** A form that starts a browser-run mutation no longer warns (HZ036); HZ036
335
+ now says what actually goes wrong (a submit before the page has loaded is lost). HZ005 suggests handling the event
336
+ first (`machine({ on })`), since `ignore` drops the click.
337
+ - **`hozu browse` runs with JavaScript by default**; `--js off` / `--js both` are for a page that must also work without
338
+ it. The skill's verify line, `hozu map` and the docs follow.
339
+ - **The guide** shows native dialogs, popovers and menus (`command` / `commandfor`, `popover`, `<details>`), a toast
340
+ that keeps the page usable, refresh controls, dark mode, and asks where data lives only when it could be shared or
341
+ follow a user across devices.
342
+ - **P7** (initial client JavaScript) budget rises to 9 KiB.
343
+ - `hozu dev` compares file times with the wall clock, so files saved just before it started no longer reload the
344
+ page once it runs.
345
+
346
+ ## 0.19.0 — What an agent building a dashboard found (ADR 0063)
347
+
348
+ No source change is needed (`hozu migrate` raises the packages).
349
+
350
+ - **The guide no longer teaches keeping app data in server memory.** Every example kept its data in a module-level
351
+ array and nothing said it was a stand-in, so an agent stored each visitor's watchlist in one list on the server,
352
+ shared by everyone, without asking. `SKILL.md` now says where data lives is the person's call (ask when the request
353
+ does not say); `hozu docs data` opens with "whose data is it?" (the visitor's own → the browser, a user's →
354
+ session + database, everyone's → a database); stand-ins are named `demo…` in the examples and the scaffold; a new
355
+ recipe, "A personal list without sign-in"; and `examples/watchlist` keeps the list in `localStorage` with quotes
356
+ from the server.
357
+ - **`freshness: { poll: seconds }`** reads a query again on a timer while a page shows it (5 s to a day; any `scope`
358
+ and `runs`). It skips hidden pages and in-flight effects; public data is cached on the server for half the
359
+ interval, user data never.
360
+ - **`target: 'previous'`** (also `done: 'previous'`) returns to the state the machine came from, so a busy state
361
+ entered from two modes needs no copy per mode. Contracts take `given: { state, previous }`; HZ016 suggests it, and
362
+ a `done`, `failed` or `after` return from a state nothing enters from another state is HZ007 (the machine would
363
+ stay there).
364
+ - **A field alone is a guard:** `guard: () => ctx.auto`.
365
+ - **Removing a guard is reviewed by the lock alone.** A transition that stops deciding no longer asks for a covering
366
+ contract (HZ018); HZ057 says to accept it and delete the contracts HZ058 names.
367
+ - **`hozu browse` says what each step did:** the page reloaded, navigated, or changed in place (`--full` adds how many
368
+ elements were redrawn). `hold <feature>.<effect>` keeps an effect's answer until `release`, to read and screenshot
369
+ the pending state. Click and fill targets match the accessible name (`aria-hidden` glyphs left out).
370
+ - **SVG shapes** (`path`, `circle`, `rect`, `line`, `stop`, …) take their children argument as optional.
371
+ - **`@hozu/css` moves `@import url(…)` rules to the top** of the compiled stylesheet; the content topic recommends
372
+ local fonts and shows a remote one with its CSP sources.
373
+ - **`ui.format.*` is listed in the views topic.**
374
+ - **`@hozu/cli` no longer depends on `create-hozu`**, so a release is installable as soon as the `@hozu/*` packages
375
+ are on npm.
376
+
377
+ ## 0.18.2
378
+
379
+ - **`hozu dev` no longer reloads because the app wrote a file.** It reloaded the page, and restarted the app, for
380
+ any `.ts`, `.css` or `.json` change in the project, so a resolver that keeps data in `data/*.json` reloaded the page
381
+ after every mutation (instead of refreshing the invalidated queries in place) and, by restarting, signed everyone
382
+ out of the default in-memory sessions. It now reloads only for files the app loaded or the browser bundle read
383
+ (client components, `fetch.ts`), stylesheets (still swapped in place), env files (now watched too),
384
+ `package.json` and `tsconfig.json`. The page logs which files changed
385
+ (`[hozu dev] reloaded: lib.ts changed`). ADR 0062.
386
+ - **Releases run in GitHub Actions** (ADR 0061): a version tag builds, tests and packs, then publishes after the
387
+ owner approves, with npm Trusted Publishing and provenance; `create-hozu` goes out only once every `@hozu/*` package
388
+ is on npm, and a fresh install is checked.
389
+ - Two tests that passed on macOS only now pass on Linux too.
390
+
391
+ ## 0.18.1
392
+
393
+ - **DevTools on a client component says why you cannot select inside it.** A client component draws its inside in
394
+ the browser (its `client` module), so DevTools selects it as one part. The inspector now says which module draws it
395
+ ("Drawn in the browser by features/site/editor.client.ts …"), and no longer offers Inside / Child, which did
396
+ nothing there. `hozu why` on such a node gives the module as `component.client`.
397
+
398
+ ## 0.18.0 — What a backend engineer's app found (ADR 0060)
399
+
400
+ No source change is needed (`hozu migrate` raises the packages).
401
+
402
+ - **Renew a session while the app only reads: `app({ refreshSession })`.** When the session holds a token that
403
+ expires, `refreshSession: async (session, { env }) => …` runs once per request, before any resolver reads the
404
+ session. Return the new value (it replaces the old one on the server under the same id, so the cookie stays),
405
+ `null` to sign out, or `undefined` to keep it. Within one process, requests of one session share one call (and its
406
+ result for ten seconds), a sign-out while it runs wins, a throw keeps the session and reaches `onError`, and the
407
+ value is checked against the session schema. Its types come from
408
+ `resolvers(project, …)`. `SessionStore` gains an optional `update(request, value)`, which `memorySessions` and
409
+ `kvSessions` have. Queries still only read.
410
+ - **DevTools in your language.** `npx hozu devtools messages > devtools.zh-TW.json` prints every DevTools string to
411
+ translate; `hozu dev --devtools-messages <file>`, or `HOZU_DEVTOOLS_MESSAGES=<file>` in your shell for every
412
+ project, shows it. A missing string stays English, `hozu dev` says how many are missing, and `--check <file>`
413
+ lists missing, stale and wrongly placed `{placeholders}`. The request Markdown and the CLI stay English. A
414
+ complete Traditional Chinese file is in `examples/studio/devtools.zh-TW.json`.
415
+ - **A 404 or 410 page is titled with the site name**, not `null · <site>`: a failed head query no longer evaluates
416
+ the head fields.
417
+ - **HZ014 for a condition inside `navigate`** now says so and gives the fix: one guarded transition per link.
418
+ - **`hozu browse --viewport 390x844`** opens at that size (a phone below 768 px wide), for `--screenshot` and layout
419
+ checks; `--do 'screenshot …'` points at `--screenshot <file>`.
420
+
421
+ ## 0.17.2
422
+
423
+ Deploying, and what trial 0024's re-run found (ADR 0059). No app changes how it is written; `hozu migrate` raises the
424
+ packages.
425
+
426
+ - **`npx hozu export`** writes every page for a static host (GitHub Pages, Netlify, Cloudflare Pages, Vercel) to
427
+ `dist/`, with `.nojekyll`, and exits 1 naming each page and server effect a static host cannot answer. New apps
428
+ include `@hozu/adapter-static`; older ones `npm install @hozu/adapter-static`.
429
+ - **Cloudflare Workers:** a bundle made with `hozuTransform()` now starts (it threw `Invalid URL string`: a Worker
430
+ has no `import.meta.url`, which core and every `hozu.config.ts` use). The plugin gives each app file its own URL;
431
+ no `define` is needed.
432
+ - **`kvSessions(kv, { secret })`** keeps sessions in a shared key-value store, so several instances, or a Worker
433
+ with a KV binding, agree on who is signed in. Same contract as `memorySessions`: an opaque signed id in the
434
+ cookie, the value on the server, deleted on sign-out. On Workers: `createHandler(app, { …, session:
435
+ kvSessions(env.SESSIONS, { secret: env.SESSION_SECRET }) })`.
436
+ - **`hozu build` writes `server/render.d.ts`**, so an edge entry that imports the render module passes `tsc` and
437
+ `hozu check`.
438
+ - **A static export under `basePath`** writes `sitemap.xml` and `404.html` under the base, where `robots.txt` points
439
+ (a GitHub project site uploads `dist/<repo>`).
440
+ - **`hozu migrate` 0.14 → 0.15** renames an error the app named `Forbidden` (the framework's access error since 0.15)
441
+ to `NotAllowed`, and still proves the IR unchanged.
442
+ - **DevTools:** a request counts a message used by the page head or an attribute as another place, so it says
443
+ "shared by 2 places; give this one its own message" instead of sending the agent to change the tab title too.
444
+ - **Guide:** the deploy topic covers `hozu export`, Docker, Workers and shared sessions; the testing topic shows how
445
+ to post a stale form with `hozu browse` (`remember … @action`, then `post $name`).
446
+ - The site's Deploying page has tested recipes: GitHub Pages, Cloudflare Pages / Netlify / Vercel, Docker and
447
+ Cloudflare Workers with KV sessions.
448
+
449
+ ## 0.17.1
450
+
451
+ - **A visitor's cached client no longer breaks the page after a deploy.** `/_hozu/client.js` was referenced under a
452
+ fixed URL while its chunks carry content hashes, so a browser that kept the previous `client.js` (Safari keeps it
453
+ past the host's `max-age`) asked for a chunk the new deploy no longer had (404, `Importing a module script
454
+ failed`) and no island hydrated: on hozu.org the home page's AI CHANGE did nothing in Safari. Pages now reference
455
+ `/_hozu/client.js?v=<content hash>`, and a client whose chunk fails to load reloads the page once.
456
+ - Client budget P7: 8 011 B of 8 192 (the reload guard).
457
+ - `client.js` under its current `?v=` is served `immutable`, its chunks too; a bare `/_hozu/client.js` is `no-cache`.
458
+ - **DevTools:** the Design panel reads a value from the element's own classes first, so a part selected under the
459
+ pointer no longer shows its `hover:` colour; Assets tiles are at least 320 px wide (phone layouts fit); the shortcut
460
+ tip hides while a panel is open, instead of covering it.
461
+ - The site's header shows the menu button below 1024 px, keeps the links on one line above it, and the menu opens
462
+ with a short slide (none under reduced motion).
463
+
464
+ ## 0.17.0
465
+
466
+ DevTools for Figma hands (ADR 0058). Everything here is DevTools, loaded only under `hozu dev`: production pages,
467
+ the client budget and the authoring surface do not change, so 0.16 apps upgrade without a source change
468
+ (`hozu migrate` raises the packages).
469
+
470
+ ### Keys and measuring, as in Figma
471
+ - **`Shift+Enter` selects the surrounding part, `Enter` the first part inside, `Tab` / `Shift+Tab` the next or
472
+ previous part beside it.** ↑ / ↓ still work. **Alt+click no longer selects the parent**: Alt measures now.
473
+ - **Hold Alt to measure:** with a part selected, red lines show the distance in px to the part under the pointer
474
+ (the gap between two parts, or the four insets when one holds the other); with nothing selected, the part under the
475
+ pointer is measured against the part around it.
476
+ - **The selection shows its size**, `W × H` in CSS px.
477
+
478
+ ### The Design panel
479
+ - **Look is now Design, in Figma's order:** Frame (W, H, corner radius), Auto layout (gap, horizontal and vertical
480
+ padding), Layer (opacity), Fill, Stroke (weight, colour), Effects (drop shadow), Text (size, weight, colour).
481
+ - **New properties:** width, height, gap, opacity, border width, border colour and shadow, each turned into the theme
482
+ utility the agent should write (`w-80`, `w-full`, `gap-4`, `opacity-50`, `border-2`, `border-red`, `shadow-lg`),
483
+ with the nearest theme step when a value is off the scale.
484
+ - **Builder shows design tokens first** (`2xl · 24px`, `red · #fb3a0e`); Developer keeps classes first.
485
+
486
+ ### Assets: every component on one page
487
+ - **A new dock button, Assets**, opens a full-screen board: every component of the app, each variant on its own and
488
+ the named previews, rendered live from the IR with your stylesheet (props filled from the schema), so there is no
489
+ showcase page to write by hand and no Storybook. Search, a detail view (variants, properties, slots, the file and
490
+ line), **Where used** with **Show the instances** (frames every use on the page, or opens a page that has one),
491
+ and **Change the main component**, which adds a request for every use.
492
+ - **Styles** shows the design tokens: colours, text sizes, corner radius, shadows and the spacing unit.
493
+ - DevTools keeps its own scrolling and pointer: libraries that hijack the wheel or lock the page (Lenis, modal
494
+ scroll locks) no longer scroll the page under a panel.
495
+
496
+ ### `previews.ts`: screens for people
497
+ - **`project({ previews: new URL('./previews.ts', import.meta.url) })`** names named component states
498
+ (`p.component(ui.Button, 'Long label', { children: '…' })`) and page screens whose queries answer with the data
499
+ given (`p.page(home, 'No notes', [p.data(listNotes, [])])`, `p.fail(listNotes, 'Unexpected')`), from
500
+ `@hozu/core/preview`.
501
+ - **It never ships:** only `hozu dev` and `hozu check` load it; `hozu build`, a production server and
502
+ an edge bundle never import it, and a production server ignores the DevTools cookie that picks a screen.
503
+ - **Layers → Previews** and **Assets → Screens** open a page screen under `hozu dev` (uncached, `noindex`); the dock
504
+ shows it until you exit.
505
+ - **HZ092** keeps previews honest: data off its query's output schema, an error the query does not declare, a route
506
+ without a page, or a component use that does not build, each at its `file:line`; a previews module that is
507
+ missing, throws or exports something else is HZ014.
508
+ - **Agents leave it alone:** `hozu map` does not list it, and the skill says to read it only when asked or when
509
+ HZ092 names a line. `examples/notes`, `examples/bookmarks` (the skill example) and the site have one.
510
+
511
+ ### Figma's words
512
+ - Scope: **This instance only** / **Main component · every Button (6 places)**.
513
+ - Agent notes and saved requests: **Resolve** (was Done). The Workbench is **Frame**.
514
+ - The request Markdown your agent reads and the CLI (`hozu requests done`) are unchanged.
515
+
516
+ ### Fixes
517
+ - **`feature({ styles: new URL(…) })` is HZ014** with the list form as the fix; before, `hozu check` crashed with
518
+ `flatMap is not a function`.
519
+ - **Stopping `hozu dev` stops its app:** `kill <pid>` (the line `hozu dev` prints), Ctrl+C or a closed terminal left
520
+ the app process on the second port, so the next `hozu dev` said the port was in use. The app now exits with
521
+ `hozu dev`, also when `hozu dev` is killed outright.
522
+
523
+ ## 0.16.0 — Ship less, measure fairly, learn faster (ADR 0057)
524
+
525
+ 0.16 closes a security hole the 0.15 dogfood found, makes pages smaller on the wire and faster to render, and fixes
526
+ what four apps built from scratch with 0.15 ran into. No breaking change: upgrade the `@hozu/*` packages.
527
+
528
+ **Upgrade now if a mutation uses `access: { owner: { load, … } }`** (see Security below).
529
+
530
+ ### Security: an owner rule's load fails closed
531
+ **Upgrade if a mutation uses `access: { owner: { load, … } }`.** In 0.15.0, when the `load` query failed with a
532
+ declared error (for example `NotFound`), the mutation's resolver still ran, so the owner check could be bypassed
533
+ by naming a row the load refuses. Found by the 0.15 dogfood.
534
+ - **A failing `load` now answers `Forbidden`, whatever the reason, and the resolver does not run.** That includes a
535
+ declared `NotFound` and an unexpected error in the load: a mutation guarded by an owner rule answers 403 for a
536
+ row it cannot see, never 404 or 500, so a caller cannot tell a missing row from someone else's.
537
+ - **An owner both sides lack never matches:** a missing row field and a missing session field are no longer equal.
538
+
539
+ ### Share cards and the sitemap
540
+ - **The share card is derived from the image:**
541
+ - `og:image:width` and `og:image:height` are read from the file;
542
+ - `og:image:alt` is the page title;
543
+ - `twitter:card` is `summary_large_image` from 600 px wide, `summary` below. X used to show the small card.
544
+ - **`entries.lastmod: (item) => item.updatedAt`** adds `<lastmod>` to the sitemap. It takes an ISO date, and an
545
+ invalid one is left out.
546
+ - **`site.url: { env: 'SITE_URL' }`** reads the origin at startup, in the handler and the static export. The
547
+ variable must be declared (HZ085); a missing or non-origin value stops the start.
548
+ - **Every package lists `funding`** (`npm fund`).
549
+
550
+ ### Fixes (found by the 0.15 dogfood)
551
+ - **A form holding a `ui.query` posts without JavaScript again:** the streaming render path left out its `method`
552
+ and `action`, so the form fell back to GET.
553
+ - **A refused native post redirects like the page:** signed out, a forged post to a page whose head maps
554
+ `Forbidden` to a route answered 303 without a `Location`; it now redirects there.
555
+ - **`invoke` takes what the mutation's schema takes in:** a field declared `z.coerce.number()` accepts the form's
556
+ text, as the forms guide says (before, `invoke` wanted the parsed `number`). Resolvers and `fetch.ts`
557
+ implementations still receive the parsed input.
558
+ - **`hozu show` and `hozu why` take `views.ts:42`** (or `features/notes/views.ts:42:9`): the outermost view node
559
+ written there, so an agent needs no dev server to find an id; a line that two files share is refused with both
560
+ paths. `hozu show … --in "<text>"` frames one row of a list. Listing the notes marks one `STALE` when its id now
561
+ names another part, and still lists them while the project does not load. SKILL.md's change loop ends with it.
562
+ - **`hozu serve` and `hozu dev` print how to stop them** (`stop: kill <pid>`), so an agent stops its own server
563
+ instead of every Hozu server on the machine.
564
+ - **A head field Hozu does not know is HZ014:** `head.render` returning `twitter` or `jsonLd` was silently dropped.
565
+ A render that returns the head query's value as a whole is still recorded.
566
+ - **An endpoint at `/sitemap.xml`, `/robots.txt` or (with a `site`) `/manifest.webmanifest` is HZ046:** it hid the
567
+ derived file.
568
+ Shape it with `entries` (now with `lastmod`), `noindex` and `site` instead.
569
+ - **`hozu get --select script` reads the head's scripts**, raw, so the JSON-LD can be checked without a server.
570
+ - **Pages without machines get a lock too:** an app whose head maps errors, or that has endpoints, redirects or
571
+ access, reports the lock missing, and `--update-lock` writes it. Before, those were never locked.
572
+ - **HZ054 knows exclusive branches:** two controls of one name in different branches of a query or a condition
573
+ (a `<select>` when ready, a hidden input when it failed) never post together, so they are one value.
574
+ - **HZ057 on `/pages` names access** among what the pages section locks.
575
+ - **Line numbers stay right after a multi-line `?:`, `&&` or `??`:** the transform moved the newlines between the
576
+ operands to the end, so every node after one reported an earlier line in `hozu why`, `hozu show`, DevTools and
577
+ diagnostics (the dogfood saw a list row reported on its `<tbody>`'s line).
578
+ - **`hozu browse` takes the forms agents write:** `in "<text>"` before or after a fill's value, several steps in one
579
+ `--do` joined with `;` (outside balanced quotes, before a verb and a space), and a missing target prints `Did you mean "<closest label>"?`. In trial 0024, a quarter
580
+ of the agents' browse runs failed on such a guess and re-ran a whole chain.
581
+ - **`hozu browse` treats a page's 401, 403, 404 or 410 as the step's answer:** a step that loads such a page
582
+ shows `→ /notes/n1 (403)` and is not an error, so an access check exits 0. The start page still must load, and
583
+ such an answer inside an iframe stays an error.
584
+ - **`hozu browse` ignores the view-transition abort** a browser reports when a step posts to a JSON endpoint.
585
+ - **`--with auth,detail`:** the detail page maps `Forbidden` to the sign-in page and lists no user data in the
586
+ sitemap.
587
+ - **The guide answers what the dogfood asked:** `SESSION_SECRET` length, how a refused page renders, the order of
588
+ input, access and resolver checks, `exports` / `imports`, what endpoints cannot do yet, why a link resets context,
589
+ per-language collections and images named in front matter.
590
+ - **`exports` in the old record form** (`exports: { queries: [...] }`) is HZ014 with the list form, not a crash;
591
+ HZ006's fix shows both edits in source form (`imports: [owner]`, `exports: [name]`), each with its feature.
592
+ - **The deploy guide no longer says `public/` is served:** files a page shows are `ui.asset`, files named in data
593
+ are served by a GET endpoint with `output: 'response'`.
594
+
595
+ ### Compression in adapter-node
596
+ - **Answers are compressed as they stream** (gzip, or brotli when only that is accepted), flushed whenever the
597
+ stream waits, so the head still arrives first and a streamed text answer is never held back.
598
+ - **Framework files are compressed once:** `hozu build` writes `.br` and `.gz` next to each file of `dist/public`
599
+ over 1 KB; an immutable `/_hozu/` file without them is compressed once and kept. Nothing else is kept: an answer
600
+ that is private or sets a cookie is compressed for its own request only. Every answer that could be compressed
601
+ carries `Vary: Accept-Encoding`, compressed or not, so a CDN keeps both.
602
+ - **Not compressed:** live streams, HEAD, 204 / 206 / 304, `Cache-Control: no-transform`, and already-compressed
603
+ types. The web-standard handler (edge) leaves compression to the platform.
604
+ - **A body that fails mid-stream** (an endpoint's own `Response`) cuts the connection; before, the rejected send
605
+ could stop the Node process.
606
+
607
+ ### Faster server rendering
608
+ - **SSR is back at the 0.9 level: 47.3 k → 55.0 k renders/s** on the frameworks bench. 0.11 and 0.12 each added a walk
609
+ of every island node on every render (the browser-run queries the page reads, the routes it links to); each
610
+ answer is now kept per IR object.
611
+
612
+ ### Less JavaScript on every page
613
+ - **The initial client is 7 884 B gzipped, down from 8 123 B:** a client component use and a keyed list's move
614
+ animation now load only on the pages that have one.
615
+
616
+ ## 0.15.0 — Say who may read and change what, test it as two visitors, and the 0.14 dogfood fixes (ADR 0056)
617
+
618
+ In 0.14, nothing in an app said who may run a query or a mutation: the rule lived in each resolver, so a missing check
619
+ was invisible to `hozu check`. 0.15 makes it a declaration, like `runs`. The tools can now test it as two visitors
620
+ in one command. Four apps built with 0.14 found the bugs fixed below, and a performance regression from 0.12 is
621
+ found and fixed.
622
+
623
+ **Upgrade:** `npx -p @hozu/cli@latest hozu migrate`, install, then `npx hozu migrate` again. The step adds
624
+ `access: 'anyone'` (the 0.14 behaviour, so the IR does not change) to every server-run `scope: 'user'` query and
625
+ every server-run mutation. HZ090 then lists each user query to tighten. Run `hozu check --update-lock` to record
626
+ access in the lock.
627
+
628
+ ### Breaking
629
+ - **`access` is required** on every `runs: 'server'` `scope: 'user'` query and every `runs: 'server'` mutation. A
630
+ missing `access` is a type error, and HZ088 in untyped code.
631
+ - **`Forbidden` is a reserved error name**, like `Invalid`.
632
+ - **`hozu impact`, `explain` and `locate` are removed:** `hozu why` answers each (deprecated in 0.14).
633
+ - **`hozu serve` (`npm start`) runs as production** unless `NODE_ENV` is set: a session app without
634
+ `SESSION_SECRET` now refuses to start, as it would in production.
635
+
636
+ ### Declared access
637
+ - `access: 'signedIn'`: any signed-in visitor.
638
+ - `access: { owner: { row: (n) => n.owner, session: (s) => s.user } }`: the framework checks the output.
639
+ - One row that is not the visitor's is `Forbidden`.
640
+ - A list holding such rows is HZ091, because the resolver read too much. It is an error in development; in
641
+ production the rows are dropped and logged once per query.
642
+ - On a mutation, `{ owner: { load: getNote, input: (i) => ({ id: i.id }), row, session } }` reads the row and
643
+ checks it before the resolver runs.
644
+ - `access: { allow: ({ session, input }) => session.role === 'admin' }`.
645
+ - `access: 'anyone'`: on user data it is HZ090 (a warning, which can be accepted with a reason).
646
+ - The callbacks are lowered like guards, so the IR holds paths. There are no new exports.
647
+ - **Refused** is the framework error `Forbidden`, raised before the resolver runs. It is optional in `failed`.
648
+ - A page whose head query is refused answers 403, unless `head.failed` maps it (`{ Forbidden: login }`).
649
+ - **Diagnostics:**
650
+ - HZ088: missing access, or an owner field the row or session does not have.
651
+ - HZ089: access where nothing enforces it (a public or browser-run effect).
652
+ - HZ090: user data that anyone may read.
653
+ - HZ091: a list with rows the visitor does not own.
654
+ - **Reviewed and visible:**
655
+ - Access is in `hozu.lock.json`, so changing it is a reviewed change.
656
+ - `hozu why` and `hozu map` show it.
657
+ - **Examples:**
658
+ - `examples/notes` declares `'signedIn'`, with a role error mapped to 403.
659
+ - blog and cart declare their user data.
660
+ - **Not in 0.15:** generated cross-user checks in `hozu check` (ADR 0056 C5). They need rows and sessions the app
661
+ would have to supply. The runtime check and the `browse` chain below cover it.
662
+
663
+ ### Test it as two visitors
664
+ - **`hozu call` on endpoints:**
665
+ - `hozu call api.who --input '{"room":"a"}' --header 'Authorization: Bearer t'` prints the status and the body.
666
+ - A POST endpoint needs `--write`.
667
+ - **`hozu browse --header 'Name: value'`:** before the first `--as` it applies to every actor; after an `--as`, to
668
+ that actor only.
669
+ - **`remember <name> from url|<selector> [@attr]`:** keeps a value; later steps read it as `$name`, in any actor.
670
+ For example, ada remembers her note's link, then bob opens `$note` and gets 403.
671
+ - **`post <path> a=1&b=2`:** a forged native form post as the current actor, without the page.
672
+
673
+ ### Your agent shows you what it changed
674
+ - **`hozu show <part> --note "<text>"`:** the part is a DevTools id, an IR pointer or `page:<route>`.
675
+ - Under `hozu dev`, the part gets a numbered red frame on the page, and an **Agent** button appears in the dock.
676
+ - Its panel steps through the notes, scrolling to each part.
677
+ - Clicking a frame's label opens that note in full; hovering shows it too.
678
+ - **Send reply** saves a request, which the agent reads with `hozu requests`. **Done** removes the note.
679
+ - **Managing notes:** `hozu show` lists them; `--done <n>` removes one, and `--clear` removes them all.
680
+ - **Storage:** notes live in `.hozu/notes.json`, and only `hozu dev` serves them, to this machine. Production has
681
+ nothing of it.
682
+ - **The DevTools dock:**
683
+ - it keeps its width at the window's edge (its buttons no longer wrap) and stays 8 px inside;
684
+ - on a narrow window it takes two rows;
685
+ - Select's help is a tip above it (`Click`, `Shift`, `Alt`, `Esc` as keys), not a faint line inside it.
686
+ - **The Workbench:** its side columns narrow with the window, and its toolbar takes two rows instead of hiding the
687
+ buttons that do not fit.
688
+ - **The Workbench below 1 100 px:** the Layers column folds into a toolbar button and opens over the page.
689
+ - **`hozu dev` prints one URL:** the app process's own `… on http://127.0.0.1:<port + 1>` line is gone.
690
+ - **`.hozu/` no longer triggers reloads:** changes there (check caches, notes) no longer reload the app under
691
+ `hozu dev`.
692
+
693
+ ### Fixes (found by the 0.14 dogfood)
694
+ - **Links:** a `ui.link` attribute built from machine context now updates on the client when the context changes.
695
+ - **Endpoints:**
696
+ - an endpoint `fail('E', data)` returns every field of `data` in the response;
697
+ - a disallowed endpoint error status is one HZ046 that lists the allowed statuses.
698
+ - **Env:** an env variable set to the empty string is unset, so `optional` and `default` apply.
699
+ - **CLI:**
700
+ - `hozu plan` accepts a path (`hozu plan /products/mug`);
701
+ - `hozu check --update-lock` prints the accepted `now:` lines (`--json`: `accepted`);
702
+ - `hozu <command> --help` prints that command's usage.
703
+ - **SEO:**
704
+ - `og:locale` carries the likely region (`en` → `en_US`);
705
+ - the sitemap lists `xhtml:link` alternates when `site.locales` is set.
706
+ - **`hozu browse --js both`** compares pages by route, so two modes that create different ids are not a
707
+ difference.
708
+ - **Scaffold:** the scaffold stores error codes in the machine (`Problem`), not English text.
709
+ - **Docs:** views (query branches return one node), pages and i18n (the locale argument of `head.input`), content
710
+ (install, slugs, dates), env (server resolvers read server variables; `internal` applies to `fetch.ts`).
711
+
712
+ ### Performance
713
+ - **Cause:**
714
+ - `bench:frameworks` had been broken since 0.8, so a regression went unmeasured: the Hozu row was interactive at
715
+ 59–61 ms (4× CPU), against 27.8 ms in benchmark 0001.
716
+ - 33 of the 40 ms of hydration were spent waiting for `import()` of the fn module that 0.12 split out.
717
+ - **Fix:** fn modules are now ordered `<script type="module">` tags, placed before the client, that register by URL.
718
+ Hydration reads them synchronously.
719
+ - **Result:** hydrate 41 → 6 ms; interactive 62 → 26.5 ms.
720
+ - **Guard:** `bench:frameworks` works again, and `pnpm bench` B2 runs the Hozu row with a 50 ms budget.
721
+
722
+ ## 0.14.0 — Easier to learn: one form, one check command, quieter checks (ADR 0053)
723
+
724
+ The largest cost of building with Hozu is that models do not know it yet: every session learns it from the guide.
725
+ 0.14 makes less to learn. It removes a hidden default and a duplicate command, and lets a warning be kept on purpose.
726
+ Diagnostics are documented from one registry, and `hozu docs` prints about half as much.
727
+
728
+ **Upgrade:** `npx -p @hozu/cli@latest hozu migrate`, install, then `npx hozu migrate` again. The step:
729
+ - adds `runs: 'either'` where `runs` is omitted (the old default, so the IR does not change);
730
+ - rewrites `hozu validate` in package.json scripts to `hozu check`;
731
+ - removes `hozu graph` scripts.
732
+
733
+ ### Breaking
734
+ - **`runs` is required** on every `query` and `mutation`: `'server' | 'browser' | 'either'`. A missing `runs` is a
735
+ type error, and HZ081 in untyped code.
736
+ - **`hozu validate` is removed.** `hozu check --no-types` runs the rules and contracts without TypeScript;
737
+ `hozu check --update-lock` accepts a behaviour change.
738
+ - **`hozu graph` is removed**, together with `graphOf` / `mermaid` from `@hozu/cli`. Use `hozu why` or
739
+ `hozu inspect`.
740
+
741
+ ### Keep a warning on purpose
742
+ - `project({ accept: [{ code: 'HZ036', at: 'lab.SaveDraft', reason: 'drafts live in localStorage' }] })`.
743
+ - An accepted warning does not count: `check` prints `0 errors, 0 warnings (1 accepted)` and lists each one with its
744
+ reason.
745
+ - **HZ087** (warning): an entry that matches no warning, names an error, or has no reason. Errors cannot be accepted.
746
+
747
+ ### Diagnostics from one registry
748
+ - Every code has a summary, a fix and a topic in `@hozu/core`.
749
+ - `pnpm skill` generates the guide's diagnostics topic and the site's table from them.
750
+ - A test fails when a code has none.
751
+ - **`hozu docs HZ083`** prints one code: its cause, its fix and the topic to read.
752
+ - **Fixes that matched their cause:**
753
+ - HZ084 no longer offers a rename as the way out;
754
+ - HZ021 says to remove one of two implementations, or an implementation of an unknown declaration.
755
+
756
+ ### A shorter guide
757
+ - Each topic is the shortest correct form; **`hozu docs <topic> --more`** adds options and edge cases.
758
+ - What `hozu docs` prints by default is 27.0 KB over every topic, down from 72.7 KB (37 %). The tested examples are unchanged.
759
+ - The `runs` examples in the data and fetch topics now state `runs`.
760
+
761
+ ### `hozu why`
762
+ - `hozu why <target>` says what a target is, where it is (`file:line`), what uses it and what it affects.
763
+ - The target can be a declaration (`cart.addItem`), a component (`ui.Button`), or a state (`cart.idle`, with its
764
+ transitions and covering contracts). It can also be a view node (a DevTools id or an IR pointer) or a page
765
+ (`page:home`).
766
+ - **Deprecated:** `hozu impact`, `explain` and `locate` still answer, with a line on stderr; they are removed in 0.15.
767
+ - DevTools requests point at `hozu why`.
768
+
769
+ ### Docs
770
+ - The README, the site and trial 0021 say why the comparison is with Nuxt: Nuxt is in the training data, Hozu is
771
+ learned in each session, and everything else is equal.
772
+ - ADR 0055 pre-registers trial 0024, which separates the cost of learning Hozu from the cost of its structure. It runs
773
+ after this release.
774
+
775
+ ## 0.13.0 — Test your API while you build, and environment conventions (ADR 0051, 0052)
776
+
777
+ 0.13 turns the DevTools API tab into a drawer for testing while you build, fixes the CSP that blocked 0.11's
778
+ browser-run effects from calling other origins, and sets the conventions for the environment. Two trial apps built
779
+ from scratch with 0.13 found the bugs fixed below.
780
+
781
+ **Upgrade:** `npx -p @hozu/cli@latest hozu migrate`, install, then `npx hozu migrate` again. Nothing is rewritten.
782
+ - **Browser-run effects:** if `fetch.ts` calls another origin, add `feature({ connect: [...] })`. `hozu check`
783
+ names each missing origin (HZ083).
784
+ - **Env files:** to have the CLI read `.env` files, add `env: { files: ['.env', '.env.local'] }` and ignore them
785
+ in git (HZ086).
786
+
787
+ ### DevTools API drawer
788
+ - **The drawer:** **API** opens a drawer docked at the bottom, in the overlay and in the Workbench (which had no
789
+ API button).
790
+ - **Rows:**
791
+ - each row says **read** or **write** and where it runs (coloured);
792
+ - it shows its freshness and the `file:line` that implements it;
793
+ - input fields are inline, and **JSON** sends any input, also one the schema rejects.
794
+ - **Results:**
795
+ - a table or JSON, with the status, the time and where it ran;
796
+ - **Copy as hozu call**;
797
+ - a **History** tab for the session.
798
+ - **Mutations** ask in their row. `Invalid` marks the field. The page then re-reads what the mutation invalidated
799
+ in place: the development client offers DevTools the machine's own effect path, and the production client grows
800
+ by 9 B (P7 8,056 B). Browser-run effects go through the page's runner, so they are schema-checked too.
801
+ - **Requests it sent:** what a call really sent out, from the server (a development `fetch` trace that never waits
802
+ for a body) and from the browser. It shows headers, bodies, status and time, with **Copy as curl**.
803
+ - **Act as** sets the browser's session in development, checked against the session schema.
804
+ - **Endpoints** sends a request to each declared endpoint with path parameters, a query or JSON body, and your own
805
+ headers (a bearer token). Queries and mutations still read no request headers: identity is the session.
806
+
807
+ ### Browser-run effects and CSP (ADR 0051)
808
+ - **`feature({ connect })`:** `connect: ['https://api.github.com', { env: 'POSTS_API' }]` lists the origins
809
+ `fetch.ts` calls from the browser. Hozu adds them to `connect-src`. Until now, the default `connect-src 'self'`
810
+ blocked `'browser'` and `'either'` calls to other origins on adapter-node and the edge.
811
+ - **HZ083** (warning): an absolute URL in fetch.ts, or a public env URL read as `env.NAME`, that `connect` does not
812
+ cover (comments are ignored).
813
+ - **HZ081** also covers an entry that is not an origin, and an `{ env }` naming an undeclared variable.
814
+
815
+ ### Environment (ADR 0052)
816
+ - **`env.files`** names the env files the CLI reads. A later file wins, and the shell wins over every file. New
817
+ apps list `.env` and `.env.local` and ignore both.
818
+ - **`env.internal: { POSTS_API: 'POSTS_API_INTERNAL' }`:** on the server, `'either'` effects call the internal URL
819
+ when it is set, and the public one otherwise. The browser, the payload and CSP only see the public one.
820
+ - **`hozu env [--example]`** lists every variable (side, required, default, set now, internal URL) and the ones Hozu
821
+ reserves, and writes `.env.example`.
822
+ - **New diagnostics:**
823
+ - **HZ084** (warning): a public variable named like a secret;
824
+ - **HZ085**: an internal mapping to undeclared variables;
825
+ - **HZ086** (warning): a listed env file that git would commit.
826
+
827
+ ### Fixes
828
+ - **Required server variables:** `hozu check`, `get`, `call` and `browse` failed on a required server variable
829
+ even when it was set, because they checked the app without its env. `check` no longer needs deployment secrets
830
+ at all.
831
+ - **`fetch.ts` apps:** `hozu get`, `hozu browse` and `testApp` failed to start an app with `fetch.ts` (since 0.11).
832
+ - **No-JS form posts:** a form whose mutation runs in the browser, posted without JavaScript, answers a page with the
833
+ reason and a link back (it was one line of text).
834
+ - **CLI startup:**
835
+ - `hozu` and `create-hozu` say they need Node 22.18, instead of failing on an import;
836
+ - `hozu dev` and `hozu serve` say which port is in use, instead of an `EADDRINUSE` stack.
837
+ - **`hozu docs`** prints the guide of the installed Hozu and says when the app's skill copy is older.
838
+ - **`hozu browse`** takes quoted targets (`click "Save draft"`).
839
+ - **Releases** pack from a clean build (`pnpm pack:release`): earlier tarballs carried the output of deleted sources.
840
+ - **Messages:** HZ021 suggests `runs: 'server'` for a server resolver of a non-server effect; HZ045 and
841
+ `hozu build` say to install `@hozu/bundle`.
842
+ - **Docs:** `ctx.request` in endpoints, `testApp(app, { env })`, the scope of browser-held data.
843
+
844
+ ### Examples
845
+ - **`examples/playground`** has an effect of each `runs`, endpoints with a bearer check, and `env.files` /
846
+ `env.internal`.
847
+ - **`examples/stars`** declares its `connect`.
848
+
849
+ ## 0.12.0 — Large apps and many servers, `hozu call` and the DevTools API tab (ADR 0050)
850
+
851
+ 0.12 measured Hozu at 50, 200 and 500 generated features ([benchmark 0003](docs/benchmarks/0003-scale.md)), then
852
+ fixed what grew with the app instead of the page, and what a deployment of several instances needs.
853
+
854
+ **Upgrade:** `npx -p @hozu/cli@latest hozu migrate`, install, then `npx hozu migrate` again. The only rewrite is
855
+ `.hozu/` in the app's `.gitignore`, where 0.12 keeps its caches. **One thing to check by hand:** `/_hozu/fns.js` is
856
+ gone (see below); a CDN or CSP rule that names it should name `/_hozu/f/*` instead.
857
+
858
+ ### Caches and many instances
859
+ - **Bounded caches:**
860
+ - public query results are an LRU of at most 10,000 entries (`app({ dataCache: memoryDataCache({ maxEntries }) })`,
861
+ `DataCache` interface in `@hozu/data`);
862
+ - cached pages are an LRU of at most 5,000 pages (`memoryCache({ maxPages })`), and invalidating a tag touches only
863
+ the pages that carry it;
864
+ - one million distinct keys hold 5.3 MB instead of 702 MB (budget P13);
865
+ - `server.stats()` returns `{ dataEntries, pages, evictions }`.
866
+ - **Invalidation bus:**
867
+ - `app({ bus })` tells the other instances which tags a mutation, an endpoint, a native post or
868
+ `server.revalidate` invalidated; they drop the same pages and data and push to their own live clients;
869
+ - `httpBus({ peers, secret })` is built in: a signed `POST /_hozu/invalidate`, zero dependencies;
870
+ - a broker (Redis, NATS, Postgres `LISTEN`) is a few lines against `InvalidationBus`;
871
+ - `app({ staticTtl })` re-reads `'static'` data and pages after that many seconds, a safety net for lost messages
872
+ (off by default).
873
+
874
+ ### Pages no longer grow with the app
875
+ - **`fn` modules:**
876
+ - `fns.js` becomes one module per feature, `/_hozu/f/<feature>-<hash>.js` (immutable), holding only the `fn`s the
877
+ browser can call; builtins share a `hozu` module;
878
+ - a page loads only the modules of its machine-bound views;
879
+ - module helpers are emitted once.
880
+ - **Routes:** the payload's `routes` lists only what the page's islands link or navigate to.
881
+ - **Result, at 500 features:** the same page's payload equals the one at 50 features (it was 71 % larger), and it
882
+ loads 237 B of `fn`s instead of 161.8 KB. `fnModules()` replaces `fnsModule()`.
883
+
884
+ ### A faster `hozu check`
885
+ - The type check runs in a child process from the start, in parallel with loading and validating;
886
+ `tsc --incremental` keeps its state in `.hozu/check/`.
887
+ - `@hozu/transform` caches transformed sources in `.hozu/transform/` (CLI, `hozu serve`, `hozu dev`;
888
+ `HOZU_TRANSFORM_CACHE=0` turns it off).
889
+ - At 500 features, a check after a one-line edit takes 1.91 s instead of 4.61 s (budget P12, `pnpm bench:scale`); at
890
+ 50 features 0.41 s instead of 0.83 s.
891
+ - `--json` adds `timings: { types, load, validate }`.
892
+
893
+ ### Tools
894
+ - **`hozu call <feature>.<effect>`:** runs one query or mutation through the app's own handler, in process.
895
+ - It takes `--input` and `--session`, and a mutation needs `--write`.
896
+ - It prints the value or the declared error, the invalidated tags and the queries they refresh.
897
+ - **DevTools API tab:** the queries a page reads and the mutations its machines start, with `runs`, scope,
898
+ freshness, tags and errors. It runs them with an input built from their schema; mutations ask first.
899
+ - **`runs` everywhere:** `inspect`, `impact`, `explain` and DevTools Layers show where an effect runs.
900
+ - **`hozu migrate`:**
901
+ - `--dry-run` says it *would* save the old IR;
902
+ - a failed check in the verify pass names the type-check state.
903
+
904
+ ## 0.11.0 — Where queries and mutations run, and `hozu migrate` (ADR 0049)
905
+
906
+ Before 0.11 every query and mutation ran on a Hozu server. A pure front end on a static host could not read
907
+ per-request data or mutate, a public API was proxied through the app (two hops, twice the egress), and a token that
908
+ lives in the browser had to travel to the server. 0.11 makes where an implementation runs one more declared fact:
909
+ the framework derives the rest, and the schemas, declared errors, tags and states stay.
910
+
911
+ **Upgrade:** run `npx -p @hozu/cli@latest hozu migrate`, install, then `npx hozu migrate` again. The first run adds
912
+ `runs: 'server'` to every query and mutation (0.11 defaults to `'either'`) and raises `@hozu/*`; the second
913
+ checks that the IR is unchanged and runs `hozu check`. The lock is never written.
914
+
915
+ ### `runs`
916
+ - `query({ …, runs })` / `mutation({ …, runs })`: `'server'` (a database, a secret, the session; resolvers as
917
+ before), `'browser'` (the visitor's credentials) or `'either'` (the default: a public API or your own API with
918
+ CORS). `'either'` needs `scope: 'public'`.
919
+ - `feature({ fetch: new URL('./fetch.ts', import.meta.url) })` implements the `'browser'` and `'either'` effects:
920
+ `export const x = implement<typeof model.x>(async (input, { fail, signal, env }) => …)` from
921
+ `@hozu/core/fetch`, one export per effect; `env` is the parsed public environment.
922
+ - **`'either'`:** server-rendered on first paint (cached per `freshness`), then in-page reads and mutations call the
923
+ API from the browser directly, never through the app's server.
924
+ - **`'browser'`:** the server renders the `pending` branch (render-plan mode `browser`) and never runs it:
925
+ `/_hozu/query`, `/_hozu/effect` and native form posts answer 400.
926
+ - **In the browser:** a lazy runner chunk (P11, 1.9 KB) loads each feature's fetch bundle once, checks input and
927
+ output against the JSON Schemas (stripping undeclared keys like a parse), turns `fail` into the declared branch,
928
+ aborts on `pagehide`, and re-reads queries by tag after a local or a server mutation (`EffectResponse.tags`).
929
+ The initial client stays at 8.0 KB (P7).
930
+ - **Bundling:** `@hozu/bundle` builds `fetch-<feature>-<hash>.js`; the handler refuses to start without it, and
931
+ `hozu build` writes it to the manifest.
932
+
933
+ ### Static hosts
934
+ - `exportStatic` writes pages whose data is `'browser'`, or `'either'` but not cacheable at export time: those
935
+ render `pending` and read in the browser. The parsed public env goes into the page (`env` option).
936
+ - `needsServer` lists the server effects a written page still calls; `site/export.ts` and `examples/stars/export.ts`
937
+ fail on it.
938
+
939
+ ### Diagnostics
940
+ - **HZ081** `invalid-effect-runtime`: a missing or extra `fetch.ts` export, no fetch module, `'either'` with user
941
+ data, or a Node-only import in `fetch.ts`.
942
+ - **HZ082** `effect-needs-server`: a `'browser'` query in a page `head` or `entries`; a browser mutation that
943
+ invalidates a tag a server-cached query reads.
944
+ - **HZ036** also warns on a form that starts a `'browser'` mutation; **HZ045** covers an app with `fetch.ts` and no
945
+ `components`; **HZ020** no longer asks for a session for a user-scoped query that runs in the browser.
946
+
947
+ ### `hozu migrate`
948
+ - Upgrades from 0.10.0 on, one step per release. Pass 1 records the old IR with the app's own installed packages,
949
+ rewrites the source and raises the ranges; pass 2 compares the IR through each step's normalisation, refreshes
950
+ the skill and agent guide, and runs `hozu check`. `--dry-run` and `--json` (`migrate.schema.json`).
951
+
952
+ ### Tools and guide
953
+ - `hozu map` shows `runs` per query and mutation and the feature's `fetch.ts`; `hozu add feature` writes
954
+ `runs: 'server'` for its resolvers.
955
+ - Skill: the `runs` rule in `SKILL.md`; new `hozu docs fetch` (runs, `fetch.ts`, browser tokens, CORS, static
956
+ hosts); `hozu docs deploy` says how to upgrade; `data`, `auth` and `diagnostics` updated.
957
+
958
+ ### Examples
959
+ - `examples/stars`: a GitHub client that runs entirely in the browser (a token in `localStorage`, an `'either'`
960
+ search, star / unstar) and exports to a static directory; its test hydrates the export against a fake GitHub API
961
+ and checks no request reaches `/_hozu/`.
962
+ - Every example, the site and the skill example are migrated (`runs: 'server'`).
963
+
964
+ ## 0.10.0 — Hozu DevTools (ADR 0047)
965
+
966
+ A vibe coder sees something wrong on the screen and describes it in words; the agent then searches the code for it.
967
+ Hozu already knew where every node comes from, so 0.10 lets the person point instead: under `npm run dev` they
968
+ select the part, say what should change, and hand the agent a request that names the file, line and the Hozu way to
969
+ make the change. The tool never edits source; the agent edits and Hozu checks.
970
+
971
+ **Upgrade:** additive, no change to the authoring surface, the IR or the lock. Add `"dev": "hozu dev"` and the
972
+ `@hozu/dev` dev dependency to an app's `package.json` (new apps have them), and `.hozu/` to `.gitignore`.
973
+
974
+ ### DevTools (`hozu dev`)
975
+ - **Overlay:** a dock with Browse / Select, Changes, Page, Layers, Workbench and settings. Select a part (click; Alt
976
+ goes up; double-click picks a text) to see where it is, its component and how many places use it, where its text
977
+ comes from (literal, message, data, context), when it is shown and what it sends.
978
+ - **Look and Text:** preview font size, weight, colours, padding and corners, or other words (Longer, 中文, English),
979
+ on the page only. A request turns styles into the class to replace and the project's theme utility.
980
+ - **Layers and states:** the page's parts from the IR, and the states that are not on screen — query `pending` and
981
+ `failed.<Error>` branches, `when` and busy states, and context conditions (`ctx.error !== null`) — each previewed
982
+ without running a resolver or a mutation.
983
+ - **Workbench:** the page in an exact-size frame (devices, rotate, drag to resize), Layers on the left, the
984
+ inspector on the right.
985
+ - **Builder or Developer:** plain words by default, or files, excerpts, transitions and node ids
986
+ (`hozu dev --devtools developer`). Light and dark follow the system.
987
+
988
+ ### Requests
989
+ - One request holds every described part; Copy for AI or Save writes Markdown with Want, Where, Scope, Style, Text,
990
+ Shown when, Mind (only where a plain edit goes wrong) and Locate.
991
+ - Saved requests live in `.hozu/requests/`. `hozu requests` lists them, `hozu requests --full` prints every open one
992
+ as one prompt, `hozu requests done <n> --result "<what changed>"` removes one. `hozu docs requests` tells agents how
993
+ to work them.
994
+ - `hozu locate <id|pointer|page:route>` re-finds a node after edits moved its lines.
995
+
996
+ ### Zero production cost
997
+ - Markers (`data-hz`), the dev endpoints and the DevTools script exist only under `hozu dev`; production renders and
998
+ the production client carry none, and budget P7 is unchanged (7868 B). Dev endpoints answer loopback `Host`s only,
999
+ and `hozu serve` binds 127.0.0.1 under `HOZU_DEV`.
1000
+
1001
+ ### Examples
1002
+ - `examples/studio`: a task board with a kit, counts, filters, validation, a saved notice, a confirm dialog and a
1003
+ detail page, to test DevTools.
1004
+
1005
+ ## 0.9.0 — declared UI components (ADR 0045, breaking)
1006
+
1007
+ A button, a field or a card used by several features had no declaration in 0.8: a `part()` disappears when the view
1008
+ is recorded, so no tool could list it, and an agent could not tell it from a helper. Its classes fought by Tailwind's
1009
+ sort order, so an override or a toggle silently lost (the showcase tabs worked by luck). 0.9 makes UI a declaration:
1010
+ `ui.component` in a kit, used through `ui.use`, styled with tailwind-variants at record time, and checked property by
1011
+ property. `ui.widget` is the same declaration with a `client` module.
1012
+
1013
+ **Upgrade:** there is no migration tool before the first stable release, and `hozu migrate` is removed. A 0.7 app
1014
+ upgrades with the 0.8.0 CLI first (`npx @hozu/cli@0.8 migrate 0.8`). A 0.8 app upgrades by hand with the list
1015
+ below, then runs `npx hozu check`, applies the patches HZ079 and HZ074 print, and accepts nothing new in the lock (views
1016
+ are not locked).
1017
+
1018
+ ### Upgrading by hand from 0.8
1019
+ 1. **`ui.widget` → `ui.component({ client })`.** `events` become `emits`, `wraps` goes, and the render is the server
1020
+ HTML the module takes over:
1021
+ ```ts
1022
+ // 0.8
1023
+ export const Map = ui.widget({ tag: 'div', props: z.object({ lat: z.number() }),
1024
+ events: { picked: z.object({ id: z.string() }) }, client: new URL('./map.client.ts', import.meta.url),
1025
+ load: 'visible', wraps: false })
1026
+ ui.use(Map, { props: { lat: ctx.lat }, on: { picked: (d) => ui.send(Pick, { id: d.id }) } }, [])
1027
+ // 0.9
1028
+ export const Map = ui.component({ tag: 'div', props: z.object({ lat: z.number() }),
1029
+ emits: { picked: z.object({ id: z.string() }) }, client: new URL('./map.client.ts', import.meta.url),
1030
+ load: 'visible', render: () => ui.div({}, []) })
1031
+ ui.use(Map, { props: { lat: ctx.lat }, on: { picked: (d) => ui.send(Pick, { id: d.id }) } })
1032
+ ```
1033
+ - A widget with `wraps: true`, or whose uses passed children, declares `children: true` and renders them:
1034
+ `render: ({ children }) => ui.div({}, children)`. A use without children passes no third argument.
1035
+ - `ui.use` options `toggle` and `vars` are gone (HZ014): the render sets them on its root from a prop.
1036
+ - The root of a client render takes no attributes and no `on` (HZ014): put a role or label on a wrapping element.
1037
+ 2. **`@hozu/core/widget` → `@hozu/core/component`** in every client module:
1038
+ ```ts
1039
+ import { implement } from '@hozu/core/widget' // 0.8
1040
+ import { implement } from '@hozu/core/component' // 0.9
1041
+ ```
1042
+ `WidgetDecl`, `WidgetLoad`, `WidgetUse` are `ComponentDecl`, `ComponentLoad`, `ComponentUse`.
1043
+ 3. **The bundle in `app.ts`:**
1044
+ ```ts
1045
+ import { bundleWidgets } from '@hozu/bundle' // 0.8
1046
+ export default app({ resolvers, widgets: bundleWidgets })
1047
+ import { bundleComponents } from '@hozu/bundle' // 0.9
1048
+ export default app({ resolvers, components: bundleComponents })
1049
+ ```
1050
+ The same rename applies to `createHandler({ widgets })`, `exportStatic({ widgets })`, `AppHost.widgets`,
1051
+ `WidgetBundle` / `assertWidgetBundle` (`ComponentBundle` / `assertComponentBundle`), `usedWidgets` / `widgetsIn`
1052
+ (`usedClientComponents` / `clientComponentsIn`) and `hydrate({ loadWidget })` (`loadComponent`).
1053
+ 4. **`hozu add widget <feature> <Name>` → `hozu add component <feature> <Name> --client`.** The old form is a usage
1054
+ error naming the new one.
1055
+ 5. **`data-hozu-widget*` → `data-hozu-component*`** on mounted hosts, in your own browser tests:
1056
+ ```ts
1057
+ page.locator('[data-hozu-widget="stations.StationMap"][data-hozu-widget-state="mounted"]') // 0.8
1058
+ page.locator('[data-hozu-component="stations.StationMap"][data-hozu-component-state="mounted"]') // 0.9
1059
+ ```
1060
+ Bundles are served from `/_hozu/c/…` (was `/_hozu/w/…`); `hozu browse` prints `component <id>:` lines and its JSON
1061
+ has `components` (was `widgets`).
1062
+ 6. **HZ079 on existing code:** two classes of one element that set the same property under the same variant are an
1063
+ error, base classes against toggles included. Apply the patch, or style the state through its attribute:
1064
+ ```ts
1065
+ // 0.8: bg-white always wins over the toggle, by Tailwind's sort order
1066
+ ui.button({ class: 'bg-white', toggle: { 'bg-indigo-600 text-white': ctx.tab === t } }, [t])
1067
+ // 0.9, the patch: a complementary toggle
1068
+ ui.button({ toggle: { 'bg-white': ctx.tab !== t, 'bg-indigo-600 text-white': ctx.tab === t } }, [t])
1069
+ // 0.9, or one source for the look and the accessibility
1070
+ ui.button({ class: 'bg-white aria-selected:bg-indigo-600 aria-selected:text-white', 'aria-selected': ctx.tab === t }, [t])
1071
+ ```
1072
+ A leading `!` is HZ074 anywhere: `!bg-red-500` → `bg-red-500!` (patch).
1073
+ 7. **`hozu migrate` is removed.** It answers a usage error naming `npx @hozu/cli@0.8 migrate 0.8`. `hozu skill` still
1074
+ rewrites the marked Hozu block of `CLAUDE.md` / `AGENTS.md`; run it after upgrading the packages.
1075
+ 8. **IR version 3.** Tools that read the IR or the CLI JSON: `FeatureIR.widgets` is `FeatureIR.components`
1076
+ (`ComponentIR`, the client in `client: { load, sourceHash }`), `ProjectIR.kits` is new, a widget node
1077
+ (`kind: 'widget'`, `widget`) is a `ComponentNode` (`kind: 'component'`, `use.component`), and the root of a pure use
1078
+ carries `use`. `hozu inspect` and `hozu impact` output is a union (feature or component), and `hozu check --json`
1079
+ always has `overrides`. The JSON Schemas are regenerated.
1080
+
1081
+ ### New
1082
+ - **Components (A–C):** `ui.component({ tag, styles?, props?, slots?, children?, events?, extend?, render })` in a kit
1083
+ (`ui.kit({ id, components, styles? })`, `project({ kits })`, id `ui.Button`) or private to a feature
1084
+ (`notes.Composer`, HZ006 from another feature). `ui.use(C, { variant, props, slots, on, class }, children)` is the
1085
+ only call form, typed from the declaration. A pure use is inlined at record time: 0 B of client JavaScript, and the
1086
+ IR equals the hand-written tree apart from `use`.
1087
+ - **Closed render (C):** a render reads only `props`, `slots`, `children`, `on` and `classes`; a declaration it reaches
1088
+ is HZ070. Variants are literals (HZ071). Every component is rendered once when it is declared, so an unused one is
1089
+ checked too.
1090
+ - **Styles (D–F):** `@hozu/variants` (tailwind-variants 3.3.1, tailwind-merge 3.7.0) runs at record time only;
1091
+ `hozu add kit <id>` writes `<id>/tv.ts` with the tailwind-merge config of the project's design tokens, HZ078 when
1092
+ it is stale, `--sync` to regenerate. The CSS stage reads the properties of every class from Tailwind: HZ072 (a
1093
+ caller sets an owned property; a trailing `!` is the one override), HZ073 (`!` inside a component), HZ074, HZ075,
1094
+ HZ076, HZ077, HZ079 on every element, HZ080 (a part's view inlined by two features). `hozu check` prints one line
1095
+ per component with overrides (`ui.Button: 1 override — account`).
1096
+ - **Record-time literals (G):** an operation with no reference operand runs as JavaScript, so a part or a render
1097
+ called with literals gives the inline form's IR.
1098
+ - **Tools (I):** `hozu docs components` (the topic, then the app's components: id, tag, variants), `hozu render <id>
1099
+ --variant k=v --props '<json>' --slot name=text` (HTML, root class, owned properties, diagnostics; exit 1 on
1100
+ errors), `hozu inspect` / `hozu impact <component id>` (the declaration and every use with its added classes and
1101
+ overrides), `hozu map` (`kits: ui 3` and `· uses ui.Button ui.Input` per page), `hozu add component <kit|feature>
1102
+ <Name> [--client]`.
1103
+ - **The guide:** `topics/components.md` replaces `widgets.md`; SKILL.md gains the UI row and stays at 3 519 B.
1104
+ - `examples/notes` uses a `ui/` kit (Button, Input, Field) on tv, with one `!` override.
1105
+
1106
+ ### Measured
1107
+ - P7 (initial client JS, min+gz): **7 872 B** (0.8.0: 7 893 B; the client ref lost `wraps`). No page of any example
1108
+ gains client JavaScript.
1109
+ - `hozu docs components` on notes: 4 537 B (budget 5 120 B). `hozu map`: notes 3 343 B (budget 3 584 B), bookmarks
1110
+ 1 450 B and trial-0007 1 516 B (budget 2 048 B).
1111
+ - HZ079 on the 0.8 examples: 6 real pairs (the showcase tabs) and none of the 24 exclusive toggle pairs.
1112
+ - `hozu check` cold: notes 0.56 → 0.64 s, showcase 0.60 → 0.77 s (the CSS stage reads class properties).
1113
+
1114
+ ### Behaviour changes with no diagnostic
1115
+ - A client component with children hydrates them on claim and on a client render (`wraps` is derived per node);
1116
+ island roots still ship without children, so every page hydrates what it hydrated before.
1117
+ - The browser console says `Component <id> failed`, and `Hozu: component <id> has no client code (bundleComponents)`.
1118
+ - The order of the classes on a component's root follows tv: owned classes, then the caller's.
1119
+ - `examples/notes`: the sign-in button's corner radius is 0.5rem (the `rounded-lg!` demonstration).
1120
+ - `hozu map` puts `kits:` after the files and no longer prints the ignore list of a state with `invoke`: it is
1121
+ derived (every event the state does not handle).
1122
+
1123
+ ### Also
1124
+ - An inline `styles: tv({ … })` next to a destructuring render types correctly: `@hozu/variants` types `tv()` with
1125
+ an intersection result, because TypeScript skips a generic call that returns a plain function type while it
1126
+ infers the surrounding call.
1127
+ - `hozu browse`'s 20 s budget per run is asserted only when its test file runs alone (`HOZU_BUDGET=1`); the bench
1128
+ runs it once (row B1).
1129
+ - `@hozu/ui-kit` is reserved for the official component library.
1130
+
1131
+ ## 0.8.0 — close the escape hatches (ADR 0043, breaking)
1132
+
1133
+ Trial 0020 ran twenty sequential changes. Hozu 0.7 kept 10× less client JS than Nuxt, but its cost per change doubled
1134
+ over the second half, and from step 16 both runs carried regressions that `hozu check` did not see: a deleted
1135
+ account came back, a page hand-wrote its 403, bulk forms dropped values. 0.8 closes each hatch those apps left
1136
+ through, and teaches an agent at the moment of a mistake instead of in a longer guide. Trial 0021 judges the release.
1137
+
1138
+ **Upgrade:** run `npx hozu migrate 0.8` before upgrading the packages. It lists the lock entries already stale under
1139
+ 0.7, rewrites what it can (below), rewrites the Hozu block of `CLAUDE.md` / `AGENTS.md`, and prints what it cannot.
1140
+ Then upgrade, run `npx hozu check` and accept the lock with `npx hozu check --update-lock`. It never writes the lock
1141
+ and never deletes a contract.
1142
+
1143
+ ### Breaking changes
1144
+ - **Data (A):** a user-scoped query is `freshness: 'request'` or `'live'` (HZ049, patch to `'request'`);
1145
+ `'request'` also replaces `{ revalidate: 0 }` for public data and makes the page per-request. `'live'` needs tags
1146
+ (HZ050). No per-session cache and no cross-request dedup. `server.revalidate([tag()])` takes tag uses and returns
1147
+ `{ entries, pages }`. Endpoints may declare `invalidates` (HZ062 on a GET endpoint, a warning).
1148
+ - **Sessions (B):** a server-side store with an opaque signed id (`memorySessions()` by default); the cookie holds no
1149
+ payload, and sign-out revokes it. Production without `SESSION_SECRET` refuses to start. The effect response re-reads
1150
+ queries with the session after the mutation, and `/_hozu/live` sends a page only its own tags.
1151
+ - **Forms (C):** `ui.dom.formAll(name)` reads every value; `ui.dom.form(name)` is the first value on both sides; the
1152
+ pressed submit button is part of the payload. `ui.formRef()` joins controls outside the form (a string `form`
1153
+ attribute is HZ014 with a patch). New: HZ054 single value for a list, HZ055 / HZ063 unknown field, HZ056 a submit
1154
+ button with a click send, HZ061 limits on a form payload. An endpoint form body is multi-valued where its input
1155
+ schema declares an array.
1156
+ - **Pages and endpoints (D):** `head.redirects` is `head.failed`, which maps every declared error of the head query to
1157
+ a parameterless route (303) or 403 / 404 / 410 (HZ051). An endpoint's `output` is a schema, `'redirect'` or
1158
+ `'response'`; HTML from an endpoint is a 500 with HZ053. Endpoints gain `errors`, `failed`, `input: 'raw'`,
1159
+ `ui.link(endpoint, input)` and `exports`. A route no page renders is HZ052.
1160
+ - **One app module (E):** `project({ app: new URL('./app.ts', import.meta.url) })`, default-exporting
1161
+ `app({ resolvers, session?, widgets? })`. `hozu serve` (`npm start`), `hozu check`, `hozu get` / `browse` and
1162
+ `testApp(app)` build from it; `serve.ts` and `createResolvers()` leave the apps. A default export that is not an
1163
+ `app(…)` is HZ045, now an error.
1164
+ - **i18n (F):** `site.lang` keeps its unprefixed URLs, the other locales are prefixed, `/en/x` answers 308 `/x`.
1165
+ A route that starts with a locale segment is HZ060.
1166
+ - **Lock and contracts (G):** the lock is version 2 and must equal the computed lock (HZ057, accepted with
1167
+ `hozu check --update-lock`). A deciding change is accepted only when a covering contract fails against the previous
1168
+ record. A contract over only copy-only transitions is HZ058 (a warning), an identical one HZ064. `ui.link(route,
1169
+ params)` takes `search` only when it differs from the defaults (`null` and `{}` are type errors).
1170
+ - **Authoring (H):** `op.*` and the motion-less `ui.if` are removed: `c ? a : b` and `c && a` (a branch may be a
1171
+ list). Reusable view logic is `part((…) => …)`; a plain function or a global that receives a reference is HZ059, and
1172
+ the server refuses to start. `list.includes(v)` and the removal of a primitive (`filter((x) => x !== v)`) lower.
1173
+ - **No soft navigation (I):** every internal link loads a document, with speculation prerender and the cross-document
1174
+ View Transition. `navigate.js`, `payload.soft` and budget P8 are gone.
1175
+ - **Verification (J):** `hozu browse --js on|off|both` (default both), `--as <name>` actors each with their own
1176
+ `--session`, `in "<text>"` targets, `check` / `uncheck`, `submit "<form>"`. `hozu post` is removed (a usage error
1177
+ names `browse`). `hozu get`, `hozu browse` and `testApp` exit 1 with the diagnostics when the build has errors.
1178
+ - **The guide (K):** SKILL.md is at most 4 KB (tested): the change loop, what to touch, the rules no diagnostic
1179
+ checks, and the topic index. `changing.md` is gone; `hozu docs feature` has the build example. `hozu map` starts
1180
+ with the session shape, the verify line and the files. The app's `CLAUDE.md` / `AGENTS.md` block sits between
1181
+ `<!-- hozu: … -->` markers that `hozu skill` and `hozu migrate 0.8` rewrite; a guide they do not recognise is printed
1182
+ and the command exits 1.
1183
+ - **IR version 2**, lock version 2 and regenerated JSON Schemas.
1184
+
1185
+ ### Behaviour changes with no diagnostic
1186
+ - The Accept-Language negotiation is gone, and prefixed default-locale URLs answer 308.
1187
+ - Everyone signs in once more after the upgrade (sessions move to the server-side store).
1188
+ - `ui.dom.form` is first-wins on the client too, and JavaScript payloads now include the submitter.
1189
+ - An invalid native post answers 400 and re-renders the page with the framework `Invalid` error.
1190
+ - User data is no longer cached (budget P9, report-only, moves).
1191
+ - Soft navigation is removed: every internal link loads a document.
1192
+
1193
+ ### Also
1194
+ - The examples, the site and the skill example drop the contracts HZ058 flags (95 in all; the negative
1195
+ specifications stay), and every example is clean under `hozu check`.
1196
+ - `examples/notes` gains the 403 admin page, the bulk form (`formAll` + `formRef`) and German under (c).
1197
+ - Every new diagnostic carries a patch or an exact snippet, except HZ051, where 403 vs 404 is an intent decision.
1198
+
1199
+ ### Found while migrating the trial reference to 0.8
1200
+ - HZ016 counts only the `machine({ on })` copies of one entry as covered together; an identical transition or `done`
1201
+ branch of another state needs its own contract.
1202
+ - `hozu migrate` keeps the 0.7 IR in `.hozu/migrate-0.7.json` (a reinstall keeps it) and says when the comparison
1203
+ cannot run; it prints every hand-built redirect or 4xx `Response` and create-on-read reached through another
1204
+ module; its rewrites keep the file's indentation, quotes, semicolons and import layout.
1205
+ - HZ025 is silent for a page whose head query is user-scoped (private pages stay out of the sitemap); HZ046 no
1206
+ longer offers "one of (none)".
1207
+ - Query branches and `ui.each` items may return `c ? a : [b, c]`.
1208
+ - A clean checkout builds in one `pnpm build` (the CLI's project references include `@hozu/transform`).
1209
+
1210
+ ## 0.7.0 — write less (ADR 0041)
1211
+
1212
+ A study of trials 0016–0018 found the remaining cost is what an agent has to *write*.
1213
+ - On the notes task the scaffold writes most of the app, and a build outputs about half of what Nuxt does.
1214
+ - On the widget task nothing is generated: apps came out at 1.5–1.9× Nuxt's lines, and a change added 214–529 lines
1215
+ against 61.
1216
+
1217
+ Five things forced that code:
1218
+ - a machine could not start from the URL (19–23 `ctx.typed ? ctx.search : search.q` per app);
1219
+ - `fn` bodies could not share a helper (the same predicate 3–6 times);
1220
+ - every declaration was imported and listed again;
1221
+ - modes repeated their shared transitions;
1222
+ - `changing.md` carried 5.6 KB of recipes into every change.
1223
+
1224
+ **Measured (trial 0019, two Claude runs per task, every check passing):**
1225
+ - widgets: build 1.75× Nuxt (was 2.15×) and change 2.03× (was 3.46×);
1226
+ - notes: build 1.14× (was 1.38×) and change 1.38× (was 1.45×).
1227
+ - A first run of the change measured 1.68×, because a recipe had left `changing.md`.
1228
+ - The re-run with that row restored is the 1.38× above.
1229
+
1230
+ ### Changes
1231
+ - **`seed`**: `ui.view({ machine, route, seed: ({ search }) => ({ q: search.q }) })`.
1232
+ - The page's machine starts with those context fields in the server render, hydration and no-JS posts.
1233
+ - Views read `ctx.q` only.
1234
+ - HZ048 reports an unknown field, a seed without a machine or route, and two seeding views on one page.
1235
+ - **`fn` bodies may call helpers from their module:** functions and JSON constants that are themselves self-contained.
1236
+ - They are shipped with the `fn` in `fns.js`, and their source is part of the fn's `sourceHash`.
1237
+ - Imported names and `let` state stay HZ047.
1238
+ - **`declarations` is a list of modules:** `feature({ id, intent, declarations: [model, views] })` with namespace
1239
+ imports, in a new `feature.ts`.
1240
+ - Every exported declaration is registered under its name. Schemas and helpers are ignored.
1241
+ - A name two modules declare is HZ013.
1242
+ - **The record form `declarations: { … }` is removed** (HZ014, with the module form as the fix).
1243
+ `hozu add feature` and `hozu add widget` write the new layout.
1244
+ - **`machine({ on })`**: transitions shared by every state that is not busy or final and does not handle or ignore the
1245
+ event itself.
1246
+ - Without `target`, a shared transition stays in the state it fires in.
1247
+ - One contract covers every identical copy (HZ016).
1248
+ - **`changing.md` is 3 KB:** the loop and a table of change kinds. The worked recipes are `hozu docs recipes`.
1249
+ - **Fewer rejected first attempts** (counted from the `hozu check` output of trials 0017 and 0018):
1250
+ - **A contract's `given.context` is a patch over `initialContext`** (30 hits of TS2740 / HZ017).
1251
+ - `given: { state: 'touring', context: { touring: true } }` now works; nested objects merge and arrays replace, like
1252
+ `expect.changes`.
1253
+ - A full context still means the same as before, and the IR is unchanged.
1254
+ - **`ui.use(W, { props })` needs no `on: {}`** (13 hits of TS2741).
1255
+ - **A `ui.query` branch may return `null` to render nothing** (HZ014 and TS2322).
1256
+ - `failed: { Unexpected: () => null }` inside a `<select>` now leaves only the other options.
1257
+ - `ready` may return `null` too.
1258
+ - **The widgets topic says where a role or label goes:** on a wrapping element. `ui.use` stays the widget's props, events
1259
+ and classes, so there is one form (4 hits of TS2353).
1260
+
1261
+ ## 0.6.0 — verify what the browser runs (ADR 0040)
1262
+
1263
+ Trial 0017 built a widget-heavy app (Leaflet, Chart.js, GSAP, Three.js).
1264
+ - Every Hozu run was correct, but cost 2.65× Nuxt to build.
1265
+ - Part of that went to what `check`, `get` and `post` cannot see: code that runs only in the browser.
1266
+
1267
+ **Measured (trial 0018, same task, two Claude runs, all checks passing):**
1268
+ - building costs 2.15× Nuxt, was 2.65× (−19 %);
1269
+ - changing is unchanged at about 3.5×;
1270
+ - both runs verified with `hozu browse`, and neither wrote a browser script.
1271
+
1272
+ - **`hozu browse <path>`: a real browser, still without a server.**
1273
+ - It drives the installed Chrome, Chromium or Edge over the DevTools protocol. There are no dependencies and no
1274
+ port: requests go to the in-process handler.
1275
+ - It runs `--do` steps in order (`fill`, `select`, `check`, `click`, `press`, `wait`, `goto`, all addressed by the
1276
+ names a user reads).
1277
+ - It reports exceptions, `console.error` calls, failed requests, every widget (mounted, failed or not mounted,
1278
+ with its size and canvases), the text, `--select` elements and an optional `--screenshot`.
1279
+ - The exit code is 1 when anything failed.
1280
+ - **HZ047: a `fn` body that uses a helper from outside `impl`.**
1281
+ - `fn` bodies are sent to the browser as source text. A module-level helper worked on the server and silently
1282
+ stopped every island in the browser, while `hozu check` stayed green.
1283
+ - It is now a build error with the names found, and the server refuses to start.
1284
+ - **No favicon request without `site.icon`:** the head carries `<link rel="icon" href="data:,">`, so the console no
1285
+ longer shows a 404 that looks like a bug.
1286
+ - **Widget hosts are marked** with `data-hozu-widget="<feature>.<Name>"` and `data-hozu-widget-state`
1287
+ (`loading`, `mounted` or `failed`), for `browse` and for any browser test.
1288
+ - **`hozu add widget` next to a `file:` core tarball** now adds the bundle tarball, not the core one.
1289
+
1290
+ ### Found while building `examples/stations` (the reference app for the widget trial)
1291
+ - **A widget that first renders after a client-side change now loads.**
1292
+ - The page payload listed only the widgets the server rendered. A widget shown later (for example a details panel
1293
+ that fades in once a station is selected) had no client code, so it never mounted.
1294
+ - Every widget an island can render is now listed.
1295
+ - **`fn()` calls are typed as their value,** like data in callbacks since 0.5, so `ctx.selected = nextStop({ … })`
1296
+ type-checks.
1297
+
1298
+ ## 0.5.0 — ordinary TypeScript, a shorter guide, less to write (ADR 0037–0039)
1299
+
1300
+ A study of every trial transcript (ADR 0038) found that an agent's extra cost is mostly **reading the guide**: 45–75 %
1301
+ of the gap to Nuxt. The calls it takes multiply that cost, while writing was already at parity. 0.5 removes the rules
1302
+ the guide had to teach, and makes the rest findable in one step.
1303
+
1304
+ **Measured (trial 0016, notes app, four Claude runs per step plus one Codex run):**
1305
+ - building costs 1.38× Nuxt and changing 1.45× (before 0.5: 1.80× and 1.81×);
1306
+ - every run passed all 36 acceptance checks.
1307
+
1308
+ ### Ordinary TypeScript in builder callbacks (ADR 0039)
1309
+ - **What you can write:**
1310
+ - `===`, `!==`, `<`, `&&`, `||`, `!`, `??`, `c ? a : b` and template strings;
1311
+ - `+`, `-` and `.length`;
1312
+ - in `assign`: `ctx.x = v`, `ctx.n += 1`, `ctx.list.push(v)` and `ctx.list = ctx.list.filter((i) => i.id !== e.id)`.
1313
+ - **How it works:** `@hozu/transform` lowers them to the same IR as the explicit `op.*` / `ui.if` forms, which keep
1314
+ working. `x ? node : node` and `x && node` become conditional nodes.
1315
+ - **What is not lowered:** methods on data (`.map`, `.toUpperCase()`). They are HZ014, with the fix: `ui.each`, or a
1316
+ `fn()`.
1317
+ - **Where it runs:**
1318
+ - `npm start` runs `node --import @hozu/transform/register serve.ts`, and the CLI registers it itself;
1319
+ - `hozuTransform()` is available for Vite / Vitest (`@hozu/transform/vite`) and esbuild (`@hozu/transform/esbuild`).
1320
+ - **HZ044:** a view or machine loaded without the transform. The server refuses to start, instead of silently
1321
+ comparing placeholders.
1322
+ - **Types:** data in callbacks is typed as its value (`ctx.error: string | null`). Type instantiations for the cart
1323
+ fell from 61.7 k to 55.0 k.
1324
+
1325
+ ### The guide (ADR 0038 R1, R2)
1326
+ - **The skill is smaller:** `SKILL.md` went from 10.2 KB to about 5.8 KB. It has a complete feature example, checked by
1327
+ a test, and a task index.
1328
+ - **Topics:** `hozu docs <topic>` prints one short topic (views, machine, data, forms, auth, …). With no topic, it
1329
+ lists them.
1330
+ - **Pointers:** every diagnostic ends with `see: hozu docs <topic>`.
1331
+ - **No server needed:** `hozu get` / `post` print `set-cookie` attributes, and the guide says they replace a running
1332
+ server for checks. A `post` is a no-JS form post.
1333
+ - **HZ015 on `effects`** gives the list to paste, in authoring form.
1334
+ - **`hozu post` is easier to use:**
1335
+ - a button can follow `&` in `--next` (`'POST / id=n1&@Pin'`);
1336
+ - `--select` takes a comma list;
1337
+ - a post to a page that redirects (to sign-in) says so.
1338
+
1339
+ ### Contracts, busy states, endpoints (ADR 0037)
1340
+ - **Contracts for decisions only.** A transition with a guard, `navigate` or a `fn` value needs a contract (HZ016).
1341
+ - `hozu.lock.json` records every transition readably, e.g. `idle --Draft--> idle · draft := event.text`.
1342
+ - HZ018 shows `was: … now: …`, and `--update-lock` accepts copy-only changes.
1343
+ - Scaffolds write contracts only where required.
1344
+ - **Busy states by rule.** A state with `invoke` drops unhandled events, so `ignore` there is HZ014. `done` / `failed`
1345
+ take a state name, a transition, or a guarded list.
1346
+ - **Clearer view errors.** `Invalid view child` says what it got, and `ui.query`'s `pending` is optional.
1347
+ - **`serve.ts` is checked.** HZ045 warns when the widget bundle or the session store is missing.
1348
+ - **`hozu add widget <feature> <Name>`** writes the declaration, the client module, the bundle and the dependency.
1349
+ - **Declared endpoints.**
1350
+ - `endpoint({ method, path, input, output })` is implemented in resolvers. It answers JSON, or a `Response` with
1351
+ `output: 'response'`, and can call `setSession`.
1352
+ - HZ046 checks paths, with patches.
1353
+ - `examples/notes` serves `GET /api/notes`.
1354
+
1355
+ ### Migrating from 0.4
1356
+ - **Run apps with the transform:**
1357
+ - add `@hozu/transform` to the dependencies;
1358
+ - start with `node --import @hozu/transform/register serve.ts`;
1359
+ - add `hozuTransform()` to Vitest.
1360
+ - **Delete `ignore` in states with `invoke`.** The HZ014 patch does it.
1361
+ - **Old forms still work.** Explicit `op.*` / `ui.if` code needs no change, and contracts on copy-only transitions
1362
+ stay valid as examples.
1363
+ - **Refresh the lock:** run `hozu check --update-lock` once for the readable summaries.
1364
+ - **Refresh the skill:** run `hozu skill`.
1365
+ - **`hozu validate --json` coverage:** `covered` / `total` now count deciding transitions, and `transitions` counts
1366
+ all of them.
1367
+
1368
+ ## 0.4.2 — no JS download on pages that do not run it, no silent widgets
1369
+
1370
+ - **The client runtime is preloaded only where an island renders (ADR 0036).**
1371
+ - Some islands can be left out of a page: those inside `ui.each`, `ui.if`, `when` or a query branch.
1372
+ - A route whose islands are all of this kind no longer preloads `client.js` in `<head>`. The preload is written
1373
+ right before the first island that renders, and not at all on a page that renders none.
1374
+ - Before, such pages downloaded about 8 KiB they never ran.
1375
+ - Pages with an island outside such branches are unchanged. This is true of every example app.
1376
+ - **`hozu plan`** shows `js: 2 islands (always)` or `js: 1 island (only when rendered)`. In `--json`, `js` is
1377
+ `'always' | 'conditional' | false`; it was a boolean.
1378
+ - **Static export** writes `/_hozu/client.js` only when an exported page runs it.
1379
+ - **A missing widget bundle is an error.** If views use `ui.use` but `createServer`, `createHandler` or `exportStatic`
1380
+ got no `widgets`, startup throws and names the widgets and the fix (`widgets: await bundleWidgets(build)`).
1381
+ - Before, the scaffolded `serve.ts` rendered empty hosts that never mounted, with no error anywhere.
1382
+ - A bundle that lacks a widget (a failed HZ029 build) is an error too.
1383
+ - The client logs any widget without client code.
1384
+ - `testApp` and `hozu get` do not run client code, so they need no bundle.
1385
+ - **The Widgets guide** says bundling is a separate step, and that a library's CSS goes in `app.css`.
1386
+
1387
+ ## 0.4.1 — accessibility and widget docs
1388
+
1389
+ - **`role` on SVG elements.** `ui.svg({ role: 'img', 'aria-label': '…' }, …)` type-checks and validates. Before, it
1390
+ was TS2353 and HZ014.
1391
+ - **`ui.noscript`,** for content shown only without JavaScript.
1392
+ - **Widgets in the guide.** `reference.md` explains that there is no `widget` export. It shows the whole path:
1393
+ `ui.widget` in the feature's `declarations`, `ui.use`, a client module with `implement<typeof W>` from
1394
+ `@hozu/core/widget`, and `bundleWidgets` (`hozu build` does it for you).
1395
+
1396
+ ## 0.4.0 — what the official site found
1397
+
1398
+ Gaps found while building [hozu.org](https://hozu.org) with Hozu (ADR 0032).
1399
+ - **No flash between pages.** The stylesheet turns on cross-document view transitions, so links between pages
1400
+ without islands cross-fade instead of flashing, with no JS (Chrome/Edge 126+, Safari 18.2+; other browsers are
1401
+ unchanged). With `prefers-reduced-motion` the pages swap without animation. Turn them off with
1402
+ `@view-transition { navigation: none; }` in your stylesheet.
1403
+ - **Static output contains every file its pages link to.** `exportStatic` and `hozu build` now write
1404
+ `/manifest.webmanifest`, and `/sw.js` with `/_hozu/sw-register.js` when `site.offline` is set. Before, pages linked
1405
+ them but only the server generated them.
1406
+ - **`head.image` accepts `ui.asset(...)`,** for a share image on a static host. It is linked by absolute URL and
1407
+ copied with the other assets.
1408
+
1409
+ ## 0.3.0 — less reading, less rewriting
1410
+
1411
+ - **`hozu map`:** a compact outline of the app with `file:line` for every entry. It covers routes and their pages,
1412
+ queries and mutations with their errors and tags, events and their fields, and the machine's states with their
1413
+ transitions, `invoke`, `ignore` and `after`, views and contracts. The example apps map in about 1.2 KB. `--json`
1414
+ follows `map.schema.json`.
1415
+ - **`hozu add feature <name> --with detail,toggle,filter,remove`:** composable parts on top of the list and add
1416
+ form:
1417
+ - `detail`: a detail page with a 404, and its route, head and `entries`;
1418
+ - `toggle`: a done field and a per-item button that works without JS;
1419
+ - `filter`: in-page All / Open / Done buttons and an empty state;
1420
+ - `remove`: a per-item delete.
1421
+
1422
+ Each of the 16 combinations checks clean in a fresh app.
1423
+ - **Recipes in `changing.md`,** verified by applying them to a scaffolded app in a test:
1424
+ - an enum field chosen in the add form;
1425
+ - an action button that works on many items;
1426
+ - a field shown on the detail page;
1427
+ - adding a detail page.
1428
+
1429
+ The change loop starts with `hozu map`.
1430
+ - **`hozu get` / `hozu post` show more without a server:**
1431
+ - `--select <selector>` prints matching elements with their attributes. The selectors are `tag`, `#id`,
1432
+ `[attr]`, `[attr=value]` and `tag[attr=value]`, e.g. `button[aria-pressed=true]`.
1433
+ - `--forms` lists each form's action, fields with their defaults, and submit buttons.
1434
+ - **`--with auth`:** sign-in and sign-out (`features/account`), a signed `HttpOnly` session cookie in `serve.ts`,
1435
+ per-user queries and resolvers, and a redirect to `/login` when signed out. A second feature with `auth` reuses the
1436
+ account. `hozu get` / `hozu post` keep a real session cookie across steps, so sign-in flows can be tried without a
1437
+ server.
1438
+ - **`hozu add feature` prints what to edit:** the generated declarations by kind, and every user-facing text with its
1439
+ `file:line`. The guides say not to print the generated files.
1440
+
1441
+ ## 0.2.0 — a cheaper loop for agents
1442
+
1443
+ ### Commands
1444
+ - **`hozu check`:** type-checks the app with its own TypeScript and runs every rule and contract. One command and
1445
+ one summary line; `--json` follows `check.schema.json`.
1446
+ - **`hozu get <path>...`:** requests pages in-process, with no server. It prints the status, title, every
1447
+ `role="alert"` text and the visible text (capped at 1,500 characters).
1448
+ - **`hozu post <path> --field name=value [--next <step>]...`:** fills the page's form like a browser, posts it,
1449
+ follows the redirect, then runs the next steps in the same process.
1450
+ - A step is `'/path'`, `'GET /path'`, `'POST /path a=1&b=2'` or `'POST /path @Button label'`.
1451
+ - `--button <label>` picks a form by its submit button, for action forms without fields.
1452
+ - **`hozu add feature <name> [--page <path>]`:** scaffolds a working feature and wires it into `hozu.config.ts`,
1453
+ `server.ts` and, with `--page`, `routes.ts`:
1454
+ - a list query and an add mutation;
1455
+ - a machine with a busy state;
1456
+ - a no-JS form with field errors;
1457
+ - the contracts;
1458
+ - in-memory resolvers.
1459
+
1460
+ ### Other changes
1461
+ - **The skill and the app guide teach this loop:** `add` → edit → `check` → `get` / `post`. `patterns.md` points
1462
+ at the part of the example each pattern uses.
1463
+ - **`create-hozu` apps** also depend on `@hozu/testing`, which `get` / `post` use. Their `check` script is
1464
+ `hozu check`.
1465
+ - **`<html data-hozu-ready>`** is set once the page has hydrated, for browser tests.
1466
+
1467
+ ## 0.1.0 — first public release
1468
+
1469
+ All packages are published under `@hozu/*`, plus [`create-hozu`](https://www.npmjs.com/package/create-hozu).
1470
+ The framework was developed under the working name Tenon (see `docs/adr` 0001–0025).
1471
+
1472
+ ### Authoring
1473
+ - **Declarations:**
1474
+ - features, one state machine per feature, typed events, queries, mutations, tags, `fn`s, views, widgets and
1475
+ messages;
1476
+ - `feature({ id, intent, declarations })` sorts declarations by kind (ADR 0022).
1477
+ - **Contracts:** given / when / expect for every transition. `expect.changes` states only what changes. A behaviour
1478
+ lock catches drift (HZ016, HZ018).
1479
+ - **Views:** typed element trees with every HTML/SVG element, typed attributes and DOM events, `toggle` and `vars`,
1480
+ Tailwind classes checked against the generated CSS, `ui.if`, `ui.each`, `ui.query` and motion.
1481
+ - **Routes:**
1482
+ - typed `params` and `search`, with the `:x?`, `:x+` and `:x*` modifiers;
1483
+ - `ui.link` is the only form of an internal URL;
1484
+ - soft navigation keeps UI alive across links.
1485
+ - **Forms:** work without JavaScript. Field errors come from the `Invalid` error, and there is a pattern for
1486
+ optimistic items.
1487
+ - **Other features:**
1488
+ - i18n with typed messages and `Intl` formatting;
1489
+ - typed `env`;
1490
+ - sessions, CSP and cross-site POST checks;
1491
+ - Markdown collections;
1492
+ - image `srcset` and share images;
1493
+ - preview mode, PWA and an offline page;
1494
+ - `@hozu/testing`.
1495
+
1496
+ ### Rendering
1497
+ - **Render plans are derived per node:** static, ISR, SWR, streamed or client. User-scoped data cannot reach a
1498
+ cacheable region.
1499
+ - **Server HTML comes from generated JavaScript** (ADR 0024). `hozu build` writes it for edge runtimes.
1500
+ - **The client runtime** hydrates only machine-bound islands. It is 7.5 KB gzipped, with a compact payload and
1501
+ modulepreload (ADR 0023).
1502
+
1503
+ ### Tools
1504
+ - `hozu validate | inspect | graph | explain | impact | plan | build | skill`, all with `--json` and JSON Schemas.
1505
+ - 43 diagnostic codes, each with a location, a cause and a fix.
1506
+ - `create-hozu --agent claude|agents|both`: writes `CLAUDE.md` or `AGENTS.md`, plus the versioned authoring skill
1507
+ with a verified example.
1508
+
1509
+ ### Requirements
1510
+ - Node 22.18 or newer.