6.0.0: Solid reactivity that works, Bun.Image, and four defects that shipped green - #263
Conversation
…d PNG codec
`packages/core/src/image/` was a from-scratch codec — Huffman tables, DCT,
scanline filters, a Lanczos resampler. 1,679 source lines and 1,292 test lines,
deleted. `pipeline.ts:20` gave the reason it existed:
Bun ships no image API and the contract forbids `sharp`, so WebP and AVIF
are probed and served, never synthesised here.
That stopped being true in Bun 1.3.14 — the version already pinned. `Bun.Image`
is statically-linked libjpeg-turbo / libspng / libwebp with SIMD resize kernels.
Still zero dependencies.
WebP encodes now, so `@ultimat3/storage`'s default variant format and its default
`.webp` key extension finally agree: a `srcset` entry can be served, not merely
named.
`Bun.Image.backend` is pinned to `'bun'` on every call. That forces the static
codecs and Highway geometry everywhere, which is what `variantKey`'s
content-addressed cache requires — a platform-dependent variant is a cache that
never hits. The cost is that AVIF and HEIC are refused on every platform rather
than working on two of them, which is one behaviour instead of two.
`blurDataUrl` is `Bun.Image.placeholder()` — a deterministic ThumbHash at most
32px on its long edge. `BLUR_PLACEHOLDER_WIDTH` is deleted; there is no width to
name. ThumbHash quantises the aspect ratio, and `responsiveImage()` paints the
LQIP as `background-size: cover` inside a box sized from the real dimensions, so
nothing shifts.
With no `format`, the SOURCE format is kept. The old rule guessed from the pixels
and turned an opaque PNG into a JPEG; a heuristic that can flatten a logo
eventually does.
`png-pixels.ts` (183 lines) survives as a raw-pixel seam, and writing its test
first found a real bug: unfiltering into a `Uint8ClampedArray` CLAMPS where PNG
filters are mod-256, so `255 + 1` gave 255 instead of 0. Invisible against our
own filter-0 writer, wrong against every adaptive one — it forced alpha to 255,
flattening every transparent PWA icon. `Bun.Image` has no compositor and no
raw-pixel terminal, and the PWA maskable safe zone needs both, which is why the
seam exists at all.
Bun agrees with Pillow byte-for-byte on five reference decodes (RGBA, greyscale,
grey+alpha, 16-bit, palette+tRNS). Determinism is asserted on both the fast and
composite paths, since the content-addressed cache depends on it.
Refs #252
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KgsU1WBJMAjnLaazvfuJmD
BREAKING. Bun 1.4 ships ICU 78, where `new Intl.DateTimeFormat('en',
{timeZone:'CET'})` no longer throws. `resolve()` delegated the IANA-ness
judgement to `Intl`, so a runtime upgrade alone reopened the framework's
"no date without an explicit IANA timeZone" rule — silently, and in the
direction that fails dangerous:
isValidTimeZone('CET') -> true (was false) also EST, EST5EDT, GMT, MST
`Intl` answers "can I format this", never "is this IANA", and at ICU 78 the two
stopped agreeing. The judgement is now structural: `UTC`, or a name containing
`/`. It cannot move with the runtime again.
43 names change answer, enumerated with their replacements in CHANGELOG.md. The
24 geographic `backward` links and the three UTC aliases swap textually — each
verified against seven probe instants including both 2026 DST transitions. The
GMT family maps to `Etc/GMT`, not `UTC`, because `UTC` renders a different zone
label. The eleven abbreviations have no mechanical replacement, and that is the
defect: an abbreviation names no jurisdiction and carries no DST rule.
No denylist. A list of refused abbreviations grows with every tzdata release, and
there is no structural rule that keeps `CET` out and lets `Japan` in.
`X_TIMEZONE_INVALID`'s `cause:` was itself false — `Japan` IS an IANA name, a
`backward` link in shipped tzdata — so it now reads "not an IANA Area/Location
zone name", which is true for all four refused classes. The `fix:` instructs both
classes: the mechanical swap for a legacy link, and "name the city" for an
abbreviation, because only the author can choose one.
Three `format.test.ts` expectations move to anchored patterns rather than pinned
ICU-78 literals, so the suite is green on ICU 75 and 78 both and this does not
have to merge with the Bun upgrade.
Refs #251
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KgsU1WBJMAjnLaazvfuJmD
… worked
BREAKING. Every `spa` route ever declared served an empty document — 200, correct
headers, blank page:
<body><div id="x-root"></div></body>
`dev-render.ts:205` passed `chunks: []`, hardcoded; `prerender.ts` refused the
mode outright; the container mounted the same table. No document in the
framework's history ever carried a <script>. And `renderSpa` never read
`entry.component`, so a `spa` page's own component was dead code in every mode —
along with its `hydrate` and `budget.js`, which nothing could honour.
The mode's own header claimed the shell was "identical for every actor and
therefore cacheable" while it emitted `private, max-age=0, must-revalidate` and
required a `policy`. `MODE_SPECS.spa.prerenderable` was `true` while
`isPrerenderable` is `render === 'static'`.
The deployed demo had already rejected it in writing, one release earlier:
`ssr`, not `spa`: the shell has data to render, and a `spa` shell would ship
that decision to the browser to be asked again.
With `solid-loader.ts` landed, `island({ src })` is a working client model with a
bundler, a content-addressed URL, a boot runtime and a budget measured in real
bytes. `spa` was a second path to interactivity that delivered strictly less. Not
a different thing — a worse island.
`createRouter` goes with it: 236 lines exported from `@ultimat3/render`, 490
lines of tests, zero callers. Its header called it "load-bearing for every app
page"; nothing loaded it. It existed to drive a `spa` shell, and it was built on
a premise `solid-loader.ts` disproves — "inject reactive primitives, import no
solid-js" — because Solid's reactivity is a compile-time contract no injected
runtime can stand in for.
`SPA_ROOT_ID` was never a `spa` symbol: `routeBody` wraps EVERY mode's body in
it. It returns as `ROOT_ELEMENT_ID` in `render-html.ts`, same value.
The guard is a loop over `RENDER_MODES` itself — every mode puts the route
component inside the hydration root — so a fifth mode arriving without a renderer
is a build error, not a blank page.
`@ultimat3/admin`'s generated views join its custom pages at `ssr` +
`hydrate: 'never'`; admin views are pure functions of props with no local state,
so there was never page-level state to hydrate.
Four more hand-copied `RenderMode` unions still admitted 'spa' and typechecked
green after the deletion — `http`, `seo`, `manifest`, `pwa`. All corrected.
`pwa`'s mapped `spa -> cache-first`, the only mode that gave an `app/` route a
shared cache entry. Five copies of one closed set with nothing comparing them is
filed as #261.
Refs #245, #247, #261
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KgsU1WBJMAjnLaazvfuJmD
BREAKING. Observed in a real app on 5.0.1 — every user-facing string on every
page rendered as a placeholder:
curl /play -> <h1>⟦app.play.title⟧</h1>
The keys existed. The app's catalog module called `defineCatalogs()` and nothing
imported it, so registration never ran. Both guards said green: `x verify` passed
and `x i18n check` answered "18 keys used — no gaps", because it audited the
catalog file against source usage and never asked whether anything LOADED it.
The loud-miss design worked exactly as intended and the guard still missed it,
because it checked the wrong question.
The fix is a base layer, not a check. `framework.ts` installs the framework
catalog at MODULE SCOPE, so you cannot call `t()` without having imported the
module that registers `errors.*`, `auth.*` and `ui.*`. `registerFrameworkCatalog()`
is deleted rather than kept as a no-op — there is nothing to replace it with.
Base entries merge UNDER whatever an app registers, so it is order-independent
and an app override always wins.
`x i18n check` gained the runtime half: it calls the same `loadApp` the server
calls at boot, then asks the registry what actually arrived. And it is now the
19th step of `x verify` — a named step, not a rider on `boundaries`, because
"did this import cross a line?" and "did this declaration reach the running app?"
are different questions with different fixes. Skipped, never passed, on a repo
that is not an app.
The same bug, one package over: `registerMailCatalog()` had ZERO production
callers, so every `mail.*` string in every running app was `⟦key⟧`. Deleting it
turned three existing mail tests red — only the package entry reached
`catalog.ts`, so any deep import of `render.ts` rendered misses, and the tests
that would have noticed were the ones calling `registerMailCatalog()` themselves.
The renderer now depends on the strings it renders.
Root cause of both: the framework shipped two idioms and the generators shipped
the broken one. `examples/dummy` used `useT()` from the app's own module in 100%
of its pages; `x new` and `x g route|resource|admin:page` emitted `t` from
`@ultimat3/i18n` at six sites. Corrected.
The 14 `@ultimat3/ui` assertions that read `⟦ui.*⟧` were doing double duty —
proving which key a component asked for AND that it had not written its own
English. Replacing the marker with a literal would have kept the first and lost
the second, so they assert `uiString(UI_KEYS.x)`, a lookup by key. Mutation-proved
both ways.
Closes #249
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KgsU1WBJMAjnLaazvfuJmD
…till compiles
Five changes to `@ultimat3/cli`, all consequences of the slices around them.
`x verify` gains `i18n` as its 19th step (`verify-step.ts`, `cmd-verify.ts`), so
a catalog that never reaches the runtime registry is a gate failure rather than a
green run shipping ⟦key⟧ to every user. `i18n-registration.ts` composes both
halves of one question — a key missing from a catalog, and a catalog no module
registered — and `x i18n check` renders the same call as a table, so the command
and the gate cannot disagree.
The generators emit `useT()` from the app's own catalog module instead of `t`
from `@ultimat3/i18n`. An app with no catalog package keeps the old import:
emitting one that cannot resolve trades a wrong idiom for a file that does not
compile.
`cmd-verify.ts` (511) and `cmd-generate.ts` (509) were over the 500 ceiling and
split by name rather than by length: which checks run / what running them means /
the command, and which files a generator emits / what happens to them on disk /
the command. Re-exports keep one import path per symbol.
`--external @babel/preset-typescript` on every `--compile` site, which repairs a
regression this branch shipped. Adding `@babel/core` put a runtime
`require("@babel/preset-typescript/package.json")` in the binary's static graph.
It sits inside a `catch`, in a path unreachable at runtime because the loader
passes `babelrc: false, configFile: false` — but the bundler walks it anyway, and
Bun 1.3.14 fails the build where 1.4.0 bundles it as a runtime throw. That is
also why the local gate was green while CI was red.
Measured on Bun 1.3.14, not inferred from 1.4: without the flag, CI's error
verbatim; with it, `bundle 952 modules` and `./app --version` answering `5.0.1`.
`--external @babel/core` was tried and rejected — the binary then dies at boot,
because `serve.ts:294` calls `buildIslands` unconditionally, so a compiled binary
genuinely does need to build islands. A lazy `await import` was tried and
rejected too: Bun follows a literal dynamic specifier into `--compile`.
`docker/Dockerfile` carries the same flag, and `compile-externals.test.ts` now
fails if either site drifts from the other. The Dockerfile had already been the
one place a compile flag went missing once before, for `--define`, one release
earlier. This time it is mechanised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KgsU1WBJMAjnLaazvfuJmD
`examples/dummy` exercises every primitive once, idiomatically — and until now it
demonstrated NO working island with reactivity. That absence is why the client
bundle shipped broken through five majors: nothing in the repo compiled a `.tsx`
that reached a browser.
`/settings` was the worst case. It declared `render: 'spa'`, so it served a blank
page, and its `Page()` was a full Solid component that was dead code in every
mode — and uncompilable besides, against `SelectProps` that has no `label`, no
`hint` and no children.
It is now `ssr` chrome plus `island({ src: './settings.island.tsx' })`. The island
carries five signals, a `<For>`, a `createEffect` that writes and removes
`<html data-theme>`, and a fetch to a server-minted endpoint. `Page()` renders a
summary of the saved row as the island's shell, so the screen paints before a
byte of the module arrives and `mount` replaces it.
`offline: 'precache'` became `'runtime'`: precaching a per-request authed
document is a shared cache entry holding one member's HTML.
The test builds the real chunk through the same `buildIslands` `x build` uses and
drives `mount` against a micro-DOM. Hoisting the preview out of its accessor
fails two cases — that is the compile-time reactivity contract, asserted rather
than assumed.
Measured, and the honest shape of #254: the chunk is 19,368 bytes, of which
15,020 is the Solid runtime floor before rendering anything. The plain-DOM
`contact-sales` island is 875. An `app/` route fits one reactive island, which is
why the budget says 20kb rather than the fictional 45kb it declared before.
The island does NOT import `@ultimat3/ui`, and that was measured rather than
preferred: `@ultimat3/ui`'s barrel does not tree-shake, so importing anything at
all costs 51 kB and one `<Switch>` costs 82 kB — and worse, a `.module.scss`
inside an island resolves to a STRING, so every class renders empty and the build
reports success. Both filed as #259. Until that is fixed, `setSolidRuntime` still
has no non-test caller and #246 is not closed in substance.
Refs #243, #245, #254, #259
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KgsU1WBJMAjnLaazvfuJmD
…rift again The gate had 17 steps in 20 markdown files, 18 in the code since the SEO step landed, and 19 by the end of this branch. A number wrong across a whole major release, in the file every agent reads first. `scripts/gate-steps.ts` compares every documented count AND every documented step list against `VERIFY_STEP_NAMES`, and runs inside the `manifest` step — so it is enforcement, not a script someone remembers. Two rules, because a page is stale in two ways: a list can be wrong while its count is right, which is exactly what `wiki/Getting-Started.md` was. `DOC_GLOBS` widens from 99 to 166 pages — `*.md` and `packages/*/*.md` were never scanned, which is how the `x g` kind list stayed four short. Every root file was already clean; the rot was entirely in package pages, ratcheted at 23 pins that may only shrink. The README is rebuilt for two readers at once. A human skimming for ninety seconds sees one `action` and the six artifacts it projects, a real error with its runnable `fix:`, the eight primitives and the one-command gate — demonstration rather than adjectives. An agent gets `## Navigate`, the 30-package table derived from `list-workspaces.ts`, and a second-person line telling it to read `llms.txt` first. No number appears without the command that re-derives it. `llms.txt` is regenerated from the on-disk indexes: 210 links, 16 wiki pages and 4 architecture docs that were missing, and no version number at all — only the commands that resolve one. `wiki/Upgrading.md` said "three majors to cross" with a table ending at 4.0.0, while its own body documented 4.1.0 -> 5.0.0. Now four, 70 entries, counted from `CHANGELOG.md` rather than asserted — and a new `5.x -> 6.0.0` section, structured so entries append without rewriting it. `docs/architecture/19-cutting-a-major.md` records the convention itself, which existed only by imitation. Also corrected: `x i18n check` does NOT silently answer ok on this repo (`X_NOT_IN_APP` refuses it); the real vacuous green was an app with no catalogs at all. The demo app is 16 of 18 with two pinned red, not 15 of 17. `examples/dummy` is pinned on 4 steps, not 6. `packages/ui/CATALOG.md` says 52 components; four pages said 46. Migration advice that named a codemod, in a repo that has never shipped one, now names the manual edit. Refs #251, #252, #245, #249 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KgsU1WBJMAjnLaazvfuJmD
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 21 minutes Limit details: You’ve used the included review currently available. Your 77 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. You’re in a promotional period — use the checkbox below to run this review for free:
On-demand reviews are free for the next 31 days. After that, they cost $0.25 per reviewed file. How can I continue?Run this review now using the option above, or comment You can also wait for the limit to reset, then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (163)
Comment |
… sorts its imports Two consequences of the `i18n` gate step, both caught by CI on #263. `x.verify.json` is each app's committed floor — the steps it has proved it can run — so a suite that vanishes reads as a skip rather than a regression. Both apps ran `i18n` and neither named it: `X_REFERENCE_APP_NO_FLOOR`, with the exact edit in its `fix:`. `x g admin:page` emitted its two imports in a fixed order, and the correct order DEPENDS on the app's scope: an app catalog (`@myapp/i18n`) sorts BEFORE `@ultimat3/admin`, while the fallback `@ultimat3/i18n` sorts AFTER it. Either hardcoded order is a lint error in exactly one of the two cases. Each case is covered by a different job, which is why one order looked correct: `templates`' own linter test generates without an app catalog module and pinned the fallback ordering, while only `scaffold-smoke` reaches the app-scoped one — because it runs every generator against a real scaffold rather than stopping at `x new`. Moving the import unconditionally traded one red job for the other, and both were needed to see the whole rule. The imports are now sorted by specifier, so neither case can regress. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KgsU1WBJMAjnLaazvfuJmD
Second of two. Stacked on #262 — base retargets to
mainwhen that merges.101 files1. A catalog that never registers now fails the gate — #249 · BREAKING
Observed in a real app on 5.0.1 — every user-facing string on every page:
The keys existed. The catalog module called
defineCatalogs()and nothing imported it. Both guards said green —x verifypassed andx i18n checkanswered "18 keys used — no gaps", because it audited the catalog file against source usage and never asked whether anything loaded it.The loud-miss design worked exactly as intended and the guard still missed it, because it checked the wrong question.
The fix makes it impossible, not detectable. The framework catalog installs itself at module scope, so you cannot call
t()without having imported the module that registers it.registerFrameworkCatalog()is deleted rather than kept as a no-op.The same bug, one package over:
registerMailCatalog()had zero production callers — everymail.*string in every running app was⟦key⟧. Deleting it turned three existing mail tests red, because the tests that would have caught it were the ones calling it themselves. Identical trap.Root cause, which is not what the issue said: the framework shipped two idioms and the generators shipped the broken one.
examples/dummyuseduseT()in 100% of its pages;x newandx g route|resource|admin:pageemittedtfrom@ultimat3/i18nat six sites.2.
i18nis the 19th gate stepA named step, not a rider on
boundaries— "did this import cross a line?" and "did this declaration reach the running app?" are different questions with different fixes. Skipped, never passed, on a repo that is not an app.Without this, everything above is detection that never fires.
3. The reference app's first genuinely reactive island — #243
examples/dummyexercises every primitive once — and demonstrated no working island with reactivity. That absence is why the client bundle shipped broken through five majors./settingsis nowssrchrome plusisland({ src }). Proven in a real browser, driving the real served chunk:A signal drove an effect that patched the DOM.
Measured, and the honest shape of #254: 19,368 bytes, of which 15,020 is the Solid runtime floor before rendering anything. The plain-DOM island is 875.
The island deliberately does not import
@ultimat3/ui— measured, not preferred. The barrel does not tree-shake (51 kB for anything, 82 kB for one<Switch>), and a.module.scssinside an island resolves to a string, so every class renders empty while the build reports success. Both filed as #259. SosetSolidRuntimestill has no non-test caller and #246 is not closed in substance — stated rather than claimed.4. A step count that cannot drift again
The gate had 17 steps in 20 markdown files, 18 in the code since the SEO step landed, and 19 by the end of this branch. Wrong across a whole major release, in the file every agent reads first.
scripts/gate-steps.tscompares every documented count and every documented list againstVERIFY_STEP_NAMES, inside themanifeststep. Two rules, because a list can be wrong while its count is right — whichwiki/Getting-Started.mdwas.DOC_GLOBSwidens 99 → 166 pages. Every root file was already clean; the rot was entirely in package pages, ratcheted at 23 shrink-only pins.5. A README for two audiences
A human skimming for ninety seconds sees one
actionand the six artifacts it projects, a real error with its runnablefix:, the eight primitives, the one-command gate. Demonstration, not adjectives. An agent gets## Navigate, the 30-package table derived fromlist-workspaces.ts, and a second-person line telling it to readllms.txtfirst.No number appears without the command that re-derives it.
wiki/Upgrading.mdsaid "three majors to cross" with a table ending at 4.0.0 — while its own body documented 4.1.0 → 5.0.0. Now four, 70 entries, counted fromCHANGELOG.md.docs/architecture/19-cutting-a-major.mdrecords the convention, which existed only by imitation.Gate
bun run verify— 14 of 19 passed, 5 skipped, 0 failed.Also fixes a regression #253 shipped:
--external @babel/preset-typescripton every--compilesite, verified on Bun 1.3.14 (bundle 952 modules,./app --version→5.0.1).docker/Dockerfilecarries the same flag and a test now fails if either site drifts — the Dockerfile had already been the one place a compile flag went missing once before, for--define.🤖 Generated with Claude Code
https://claude.ai/code/session_01KgsU1WBJMAjnLaazvfuJmD
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.