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