Kanzo UI
The design of Kanzo UI

How a rule lives here

Every rule in this section says what would reverse it. That sentence is the mechanism, not a flourish — a rule you cannot falsify is a rule nobody can reopen.

This section is for whoever works on the library. The pages before it teach the components; the pages after this one say why the components are shaped that way, what was measured, and — for every rule — the evidence that would say it is wrong. Three of them are about what sits beside the library rather than in it: the services, the auth layer and the navigation layer.

That last part is the whole of why the section exists. Any decision here may be reopened, and reopening one should be editing a paragraph rather than winning an argument again. So each rule ends the same way: what would reverse it. Sometimes that is a measurement with a threshold, sometimes a second consumer, sometimes a change upstream. Where the honest answer is nothing, it says nothing and says why the constraint is physical rather than chosen.

Read the layers and the seams first. It is the map the other pages hang from: which package may import which, and the one rule that governs each place two packages meet. Three pages own the transversal rules it names: the kernel (the one failure class, the one wait, the read port, disposal, the timeline mark), the engine (one read-only door onto DuckDB, abort that interrupts, a worker that may die) and relations (JoinGraph as the one read-model of a catalog). Then the arguments, in the order the sidebar lists them: admission, the references and names and slots; the surfaces (colour, preferences, the graph and its hardening, analytics and its timeline and crossfilter, reads and fields); the model (the AI surfaces and tasks); the layers beside the library (services, auth, how it fails and its hardening, navigation, the shell); and last how a failure leaves the library, the findings and the tests.

A page whose opening line says to be built states a design that is decided and not yet in the tree. Where such a page and the tree disagree the tree wins, and the disagreement is the work.

Beside a rule, where one exists, is the guard that fails when the code stops matching, written as a Held by line. A rule with a test behind it is a rule you do not have to remember; a rule without one is a rule under review.

Two spellings in a Held by line are load-bearing, and packages/ui/src/documented-evidence.test.ts runs them. A test cited by its quoted title must be a real it() or describe() in the file beside it — not a comment, and not a title as you remember it. And `!Something` means the file beside it asserts that name is off @kanzo-tech/ui's public surface; the guard imports the barrel and checks. Both exist because the sentence that reaches outside the page making it is the one that rots: no page here has been wrong about its own subject, and twenty-six were wrong about something else — a test holds this, that export was removed, this is the only one. Write the declaration whenever an argument depends on an absence: prose saying "the export has since been removed" is unfalsifiable, it was wrong about ProgressTrack within hours, and a reader who believed it deleted a shipped export.

A rule broken three times becomes a test

When a mistake recurs, write the guard rather than the paragraph, and put the reason in the test file, beside the assertion. The document carries a pointer and no restatement.

The rule this library broke most — an untokenised colour — stopped recurring on the day it became a test, and has not returned. A read of a token no theme declares is the same shape: it fails silently, and theme-tokens.test.ts is what makes it fail out loud. The rules still being broken are the ones that exist only as prose. no-literal-hues.test.ts is the model: the promise, its reason, its enforcement and its own blind spots in one file. data-slot.test.tsx is the same shape at length, and it was written in the same commit as this rule, over the case that argued for it — three rounds of a slot being omitted, one regression, a written rule, and nothing enforcing it.

What this does not yet cover: a claim that reaches outside the file making it. That recurred twenty-six times across the guidance corpus before anything caught it, and the fix was the same move one rung further out — packages/ui/src/documented-evidence.test.ts checks that a test cited by name is a real assertion and that a name declared absent is really absent.

What would reverse it. A guard that costs more to maintain than the defect costs to fix. None on the roster has; the roster is the *.test.ts files beside the source they scan, as Conventions says, and ls is the set to recount against.

Held by packages/ui/src/no-literal-hues.test.ts and packages/ui/src/data-slot.test.tsx, the two that carry their own reasoning and their own blind spots.

A count belongs in a script, not in prose

No guidance document carries a measurement. If a number matters, a test prints it or asserts it; if no test wants it, it was noise. The same goes for a file:line — cite a file and a symbol.

The export census was written into the guidance as the authoritative correction of an earlier wrong count, and three careful independent recounts then produced three different answers, each wrong in a different direction. Prose survives; numbers and paths rot.

What would reverse it: nothing, and the recounts disagreed for reasons that generalise. The repository carries whole working copies of itself under .claude/worktrees/, so a scan that does not exclude them reports every symbol as consumed and no export as dead. And an intra-module reference join has to key on absolute declaration position: keying on the name misses aliased exports, keying on the line collides with same-line parameters. A count that three passes get wrong three ways is a count nobody should retype. packages/ui/src/index.test.ts pins the surface it cares about by name, and lib/token-color.test.ts counts the slots in every shipped theme rather than trusting either constant that declares it.

Held by packages/ui/src/index.test.ts, the pinned surface; packages/ui/src/lib/token-color.test.ts, "declares a full set of slots, or none at all, in every theme".

An audit is a map, not an oracle

Verify every finding against the code before acting on it — including findings on this page, and including your own. Audit a commit, never a working tree, and say which commit in the report's first line.

The first architecture audit did not survive contact with the code: the status-token rename it asked for was applied and fully reverted, the "dead" Appearance type was live and load-bearing, and one migration step broke at runtime when applied literally. Every later round has had to correct its own premises.

The sharpest instance is a failure mode of its own, and it is why the commit clause is here. One audit ran against another session's uncommitted working tree, so several findings quote strings that exist nowhere on this branch — a ramp count, a test fixture, a whole file. Those findings were not stale; they were measuring a different tree, and no amount of care in the reading would have caught it, because the text they cite is real somewhere. The corollary for a reviewer: when a finding cites a string you cannot find, the first hypothesis is not that the finding is wrong, but that you and the auditor are looking at different trees.

What would reverse it: nothing. Each round of auditing has produced its own falsified findings, and the rule has never had a counter-example.

Held by packages/theme/src/index.ts, Appearance — the type an audit called dead, still exported and still load-bearing.

A defect visible in docs/ is a library defect until proven otherwise

Fix it at the source. A containment rule in the consumer is a fix for one consumer.

The library's stylesheet once shipped the typography plugin under .prose, whose generated selectors outrank Preflight — so every consumer that happens to use that class name (fumadocs does, and it is a common choice) had its anchors, headings, lists and tables repainted by a design system it had only asked for a Button. The first fix was a containment rule in the docs app; the same bug came back, and the real fix was renaming the plugin's class in packages/ui/src/tailwind.css, where the @plugin "@tailwindcss/typography" block still carries the reason.

The standing corollary: docs/ consumes dist/, not src/. A rename typechecks clean while the docs build fails, and a fix in src/ does not reach the docs app until the package is rebuilt.

What would reverse it for a given defect: one that is genuinely the docs app's own — its MDX pipeline, its routing, its Shiki rendering. The test is whether a consumer who never opens our docs would hit it.

Held by packages/ui/src/tailwind.css, the @plugin "@tailwindcss/typography" block and the comment above it.

The packages ship Tailwind source, and the page is one build

@kanzo-tech/ui and @kanzo-tech/ai publish tailwind.css, an entry the consumer imports after its own @import "tailwindcss". Each declares its own plugin, variant, tokens and @source, relative to where it sits in the tarball; the consumer compiles once. It is the model Tailwind v4 describes for libraries and the one daisyUI, HeroUI and Flowbite ship.

They used to ship compiled sheets, and three Tailwind builds on one page cannot be ordered. ai's .hidden came after ui's md:block and the sidebar was display: none at every width — putting both in the same layer does not help, because two separately compiled sheets still sort by which loaded last. A @custom-variant does not survive compilation either, so every host restated dark, and every host @sourced streamdown, a dependency of ai's. None of it showed here: the docs ran one build over packages/*/src, so the tested path was not the shipped one. Now docs/app/global.css is the three lines a consumer writes, and the entries' @source reads dist/ in this workspace exactly as it does in a tarball.

streamdown is a dependency of ai's, so its @source is written for the two places a dependency of the package can be installed — its own node_modules (a nested install, and this workspace) and the node_modules it is installed in (npm's hoisted tree and pnpm's virtual store alike). @source takes a path, not a specifier, so this is the resolution Tailwind leaves to the package; absent, neither path exists and nothing is emitted.

What would reverse it. A consumer that is not Tailwind v4 — the decision is that there is none. Or @source gaining module resolution, which would collapse streamdown's two lines to one.

Held by nothing. The install smoke test compiled these three lines against the packed tarballs until 2026-09-29, when it was deleted; a wrong @source path now shows up as a consumer's missing styles, not as a red gate.

A chart fails silently and well-painted

Chart work is verified in a browser against a real coordinator. A unit test is not evidence that a chart works.

The failures this layer produces render cleanly: an empty plot with correct axes, a legend for a scale nothing uses, a brush that publishes into a selection no client reads. A DuckDB binder error surfaces as a chart with no marks, and jsdom has no DuckDB, so the test covering the component passes. charts/chart-inputs.test.tsx and charts/chart-stat.test.tsx say so in their own headers: the coordinator there is a double that plays the moves a real one makes.

Two operational consequences. The docs app builds and serves charts under --webpack only — vgplot trips a temporal-dead-zone error under Turbopack. And one coordinator serves a whole page: the active-coordinator setter is process-wide, so a second one leaves the first page's charts empty. Each MosaicProvider still owns its own selections, so one example's brush never reaches the next.

What would reverse it. A harness that can run the real coordinator headlessly and assert on marks rather than on React output.

Held by packages/ui/src/charts/chart-inputs.test.tsx and packages/ui/src/charts/chart-stat.test.tsx, whose own headers state the limit.

The size limit moves when an adoption crosses it, in the commit that crosses it

A gauge that a newly adopted component pushes over is raised in the same commit, with the before and after figures written down and the component named. It is never raised on a tree that adopted nothing.

A Tailwind utility only compiles if it is written out as literal text, so a component's appearance is source that cannot be compressed, and the barrel is their sum. ImageCropperHandle is why: eight compass positions, each with its own cursor, its own two border sides and its own hover, written as sixteen literal class strings because that is the only form Tailwind's scanner reads. A template literal or a lookup keyed on position would compress to nothing in the bundle and emit no CSS at all — a defect that renders as an unstyled handle rather than as a build error.

It has fired twice, and the second time taught something the first did not. image-cropper crossed 42.5 kB (41.94 → 42.60 kB brotlied, measured by deleting its one export * line and rebuilding), and the limit went to 43 kB. Suggestions then crossed 43 kB (42.86 → 43.06 kB) and it went to 43.5 kB. Both times the new limit keeps the same ~1% of headroom the crossed one had, so the entry stays a tripwire rather than becoming a ceiling nobody meets. The overage sat in the tree failing on purpose for a session before it moved, because the component that crossed it arrived alongside a compound rewrite, and raising a budget in the middle of that would have been raising it for the wrong reason. And the figure the rule wants is not the component's own size: what the barrel pays is what did not already exist in it.

This licenses nothing about the other entries, and it excuses no component from admission. A component with no renderer costs the same bytes and buys nothing; the budget is not the gate, admission is.

What would reverse it. The same measurement rising on a tree where no module was added — that is a regression the budget exists to catch, and no adoption pays for it. Or a build step that emitted class lists from something Tailwind could scan, which would make the strings compressible after all.

Held by packages/ui/package.json, gauges; packages/ui/src/simples/image-cropper.tsx, ImageCropperHandle.

A gauge per entry, and its raises written here

Every published entry of every package has one gauge, and it measures what that entry adds: the entry bundled with every package it imports left out, and for a subpath the root's modules left out too. A root gauge measures the root; a subpath gauge measures the subpath over the root, which is what an application that imports both pays for it. Beside them stays the gauge of a tree-shaken import — { Button }, { Dashboard } — which is the cost a consumer actually meets. The entries are read from each manifest's exports by scripts/gauges.mjs, which every package's .size-limit.mjs calls, so a subpath published without a limit fails pnpm size rather than going unmeasured; the limits are each manifest's gauges.

The gauges this replaced, until 13 October 2026, measured something else. The analytics subpath's counted, in isolation, every root simples/ module its inputs render — about a third of it — which dedupe in any application that imports the root, so each adoption raised a figure that did not say what anybody pays: over the root it is 27.42 kB, where the isolated gauge read 38.22. Five entries had no gauge at all — @kanzo-tech/ui/editor and /table, @kanzo-tech/graph/section, @kanzo-tech/llm and /local, and @kanzo-tech/theme. And each gauge's ignore list was a hand-kept copy of the build's externals; every package import is left out now, by rule, because what is measured is our code — the analytics gauge counted valibot and every @uwdata/* package until they were added to its list, and that was somebody else's code.

The reason for each raise is written on this page, not in package.json. The //size-limit notes in the manifests had grown into dated ledgers — @kanzo-tech/ui's past twenty raises — the record of every decision about a budget in the one place a reader of the decisions never looks, and a changelog in all but name, which this repository does not keep. They are the table below, one row per raise; a manifest keeps its limits and one sentence pointing here. The rule above, the size limit moves when an adoption crosses it, is unchanged; only where its record lives moved.

What would reverse it: a consumer whose bundler does not tree-shake, for whom the whole barrel is the cost — then a whole-library gauge comes back for that entry, named as that consumer's.

Held by scripts/gauges.mjs, gauges; packages/ui/package.json, gauges.

What each package's gauges watch for

  • @kanzo-tech/ui: the root is the whole barrel, a change detector for what the library ships; { Button } is the regression gate for a consumer's cost, and has stayed near 1.3 kB through every raise above it. { Dashboard } counts the lazily imported editor too: that a read-only dashboard loads no editor is held by charts/dashboard-layering.test.ts, not by a number.
  • @kanzo-tech/ai: Assist and Chat tree-shake apart, so a host that only assists fields does not ship the transcript, and the markdown parser comes with Chat alone.
  • @kanzo-tech/auth and @kanzo-tech/navigation: a door reaching through another. The root must never learn a protocol or a router, and the Node doors must never learn React; a jump of kilobytes on any of them means one of those happened.
  • @kanzo-tech/mosaic: the only gauge that measures it, since graph and ui leave it out. esbuild warns that import.meta is empty under the format it measures with; that is engine.ts's new URL(…, import.meta.url), which changes what a URL resolves to and not the size.
  • Every first limit is set just above its first measurement: a budget with 70% headroom detects nothing.

The raises

Sizes are minified and brotlied. Before 13 October 2026 a subpath was measured in isolation, so its rows are not comparable with the gauges above them.

DateGaugeLimitMeasuredWhat crossed it
before 2026-07-31ui rootjust above35.27 kBfirst measurement
before 2026-07-31ui analytics (then charts)4 → 6 kB5.47 kBthe five preset charts became a composable grammar
before 2026-07-31ui analytics6 → 60 kB56.01 kBChartFilter, ChartSearch, ChartSlider brought Select, Combobox, Input, Field and Slider into the subpath — modules that dedupe with the root
before 2026-07-31ui root38 → 42 kB40.66 kBthe AI-assist compounds
2026-07-31ui analytics60 → 68 kB66.30 kBrebaselined: the gate was already red, and the growth predates the branch that measured it (useThemeTick moved it 0.03 kB)
2026-08-18ui root42 → 42.5 kB41.54 → 42.12 kBthe core's axes as a generated declaration the barrel carries (/docs/design/colour)
2026-08-20ui root42.5 kB, not raised42.12 → 42.32 kBcomplete, suggest, use-ai and the Suggestion type left for @kanzo-tech/ai; Diagnostic and Questionnaire came in
—ui root42.5 → 43 kB41.94 → 42.60 kBimage-cropper (above)
—ui root43 → 43.5 kB42.86 → 43.06 kBSuggestions (above)
2026-09-30ui root43.5 → 44 kB43.57 kBFieldRequiredIndicator's fallback, the Empty*, Stat* and Gated* parts — four issues from one consumer
2026-09-30ui root44 → 45.5 kB45.09 kBLanguagePicker, useAsyncCollection, useDebouncedCommit, the Field bridge on DatePicker and SegmentGroup, FieldHelper's tone, FieldGroup's columns, LocaleProvider
2026-09-30ui root45.5 → 46.5 kB45.98 → 46.41 kBthe shared control height, DatePicker's granularity and segmented DateInput, DatePickerTimer
2026-09-30graph root—19.07 kB; 19.13 → 15.29 kBfirst measurement; the whole-graph store deleted the tileset, and the budget followed it down
2026-10-01ui analytics68 → 89 kB66.96 → 88.37 kBthe BI kit: Dashboard, DashboardFilters, DashboardStat, ChartCard, DetailTable, FilterChips, useFieldStats
2026-10-01ui root46.5 → 47 kB46.83 kBThemePicker and ThemePreview
2026-10-15kernel root1.5 → 1.7 kB1.58 kBProblem read clause by clause from RFC 9457: the derived type, the cycle guard and depth cap, the foreign-data filter, a rebuild that ignores a wrong-typed member
2026-10-01graph root→ 18 kB17.71 kBGraphCounts, GraphSearch, GraphLooks moved in from the workspace showcase; drag-to-fix
2026-10-01auth root, ./browser, ./server, ./next2.75, 2.25, 3.9, 5.25 kB2.22 → 2.6, 1.77 → 2.1, 3.4 → 3.73, 4.59 → 5.03 kBthe failure guarantees: a failed status, a 30 s deadline on every wait, coded IdP and session failures, the problem-page redirect
2026-10-02ui analytics89 → 89.5 kB89.19 kBv0.19.0's failure model met the BI kit
2026-10-02ui analytics89.5 → 90 kB89.74 kBDenseStackMark, DetailTable formatting dates by type
2026-10-02ui root47 → 47.5 kB47.20 kBThemePicker's swatches preview under manual activation
2026-10-02ui root47.5 → 48 kB47.67 kBFindings
2026-10-02ui root48 → 49 kB47.73 → 48.55 kBProblem
2026-10-02graph root→ 18.5 kB18.01 kBthe square placement, the camera that follows a running layout
2026-10-03ui analytics90 → 100 kB89.39 → 99.91 kBdashboard spec v2: the editor as a side sheet, the RelationPicker, the chip filter bar
2026-10-03graph root→ 19 kB18.58 kBGraphSelect moved in from the hosts
2026-10-03graph root→ 22.5 kB18.8 → 22.04 kBthe Discover integration: GraphPlacement, label levels, the arrangement kept across canvases, GraphSearch as a palette, GraphInspector
2026-10-03ai ./data6 → 6.5 kB5.12 → 6.18 kBgateStatement, a security fix
2026-10-05auth root, ./server, ./next2.5 (lowered), 5, 7 kB2.6 → 2.37, 3.73 → 4.75, 5.03 → 6.65 kB./browser gone; back-channel logout, a transaction cookie per state, renewal in place, the proxy as session authority
2026-10-05graph root→ 22.3 kB (lowered)22.29 kBthe named looks left GraphLooks
2026-10-06ui root49 → 49.5 kB48.73 → 49.14 kBPref, usePref, SectionProvider (#79)
2026-10-06ui analytics, { Dashboard }32.5, 31.5 kB31.22 → 32.31, 29.88 → 31.09 kBre-gauged without what the build externalises, then block A: the schema, parseDashboards, the staged recommender, the band of figures
2026-10-06ui root49.5 → 50 kB49.42 → 49.51 kBCommandDialog's hotkey, one useHotkey (#77)
2026-10-06auth ./server, ./next5.5, 7.5 kB4.78 → 5.25, 6.71 → 7.29 kBone credential model: per-call token exchange (RFC 8693) (#97)
2026-10-07ui analytics32.5 → 32.6 kB32.31 → 32.54 kBMosaicClients, a bridged clause as its parts
2026-10-07ui analytics32.6 → 32.9 kB32.54 → 32.8 kBFilterBar
2026-10-07graph root→ 22.2 kB (lowered), then 22.5 kB22.07 → 22.12, 22.49 kBGraphSelect left for usePick; the search reading the subset
2026-10-09ui analytics, { Dashboard }33.4, 31.8 kB33.37, 31.77 kBChartTimeline (#143)
2026-10-09ui analytics, { Dashboard }33.9, 32.3 kB33.8, 32.25 kBthe timeline's sticky window, one bar a tick, years read as years
2026-10-09graph root→ 22.75 kB22.71 kBthe tools on what is in full colour (#112)
2026-10-09graph root→ 23 kB22.71 → 22.95 kBthe test hook's support
2026-10-09graph root→ 23.5 kB22.95 → 23.3 kBGraphTimeline (#143)
2026-10-09ai ./data6.5 → 6.75 kB6.22 → 6.68 kBAnswerCard reading the host's dashboards; answerRelationsOf
2026-10-10ui root50 → 51 kB49.88 → 50.75 kBthe findings model
2026-10-10ui analytics33.9 → 34.8 kB33.82 → 34.70 kBRelationPicker reading each hop as a sentence (#160)
2026-10-10ui analytics, { Dashboard }34.2 → 34.8 → 35.6, 32.5 → 33.1 → 33.9 kB34.19 → 34.72 → 35.54, 32.48 → 33.04 → 33.88 kBbrushes handed over on a rebuild; one clause source per chart; play inside the brushed range (#154)
2026-10-10ui analytics35.6 → 36.5 kB—#154 met #160 on main
2026-10-10graph root→ 24 kB, then 24.1 kB23.41 → 23.84, 24.07 kBthe map saying what it places; #154 met #156
2026-10-10ai ./data6.75 → 7.25 kB6.68 → 7.08 kBreading through Engine.query with the chat's signal (ui#55, ui#47)
2026-10-10ai ./data7.25 → 8.5 kB7.15 → 8.39 kBeach type described once, the context budget
2026-10-10mosaic root3.5 kB3.27 kBfirst measurement
2026-10-11ui analytics, { Dashboard }37.7, 34.4 kB36.39 → 37.6, 33.86 → 34.32 kBFilterBar on one line: ScrollArea, Tooltip, Separator
2026-10-11ui analytics, { Dashboard }37.9, 34.5 kB37.87, 34.44 kBevery part reading a relation over one page selection
2026-10-11ui analytics, { Dashboard }38, 34.6 kB37.99, 34.56 kBthe timeline's range from the brush; a chart withdrawing its last clause
2026-10-11ai ./data8.75 → 9 kB8.58 → 8.94 kBan answer's verbs as icon buttons
2026-10-11mosaic root3.5 → 3.8 → 3.85 kB3.32 → 3.77 → 3.82 kBone crossfilter across relations; clauseAntiJoin (#175)
2026-10-12ai ./data9 → 10.2 → 9 → 9.5 → 9.75 kB10.14 → 8.85 → 9.46 → 9.57 kBAsk as a Task; dataAgent gone; AskChips; verify and a grammar
2026-10-12ai ./task850 B → 1 → 1.2 kB799 B → 979 B → 1.1 kBpropose; verify and a grammar
2026-10-12mosaic root3.85 → 4.1 kB3.80 → 4.09 kBresolveRelation
2026-10-13ui analytics, { Dashboard }38.3, 34.9 kB37.84 → 38.22, 34.53 → 34.86 kBa clause outlives the part that published it
2026-10-13mosaic root4.1 → 4.15 kB4.09 → 4.11 kBthe json extension
2026-10-13ai ./data9.75 → 9.9 kB, approved by the owner9.57 → 9.85 kBan answer's read became a query on the host's QueryClient — the statement split from the read so it can key it, the key on the engine, Stop cancelling the query (/docs/design/reads); @tanstack/react-query left out like every other peer, and the root did not move (12.67 kB)
2026-10-13ui { Dashboard }34.9 → 35 kB, approved by the owner34.86 → 34.99 kBuseFieldStats became a TanStack Query query on the host's client — the key on the coordinator, the exclude applied as select, the cancel of a queued summary — less queryFieldStats (/docs/design/reads); analytics went 38.22 → 38.24 kB under its 38.3 kB, and the root (50.99 kB) and { Button } (1.29 kB) did not move
2026-10-14ui root, then measured whole51 → 51.1 kB, approved by the owner (standing approval, 2026-10-14)50.99 → 51.016 kB#200's Eyebrow landed with the unused names of cleanup PR 1 leaving the barrel (KanzoTheme stays in the bundle, drawn by ThemePreview); { Button } stayed at 1.29 kB, analytics measured 38.29 kB and { Dashboard } 34.99 kB, under their limits
2026-10-14auth ./next, over the root7.1 → 7.75 kB, approved by the owner (standing approval, 2026-10-14)7.03 → 7.72 kBcleanup PR 6: every refusal the routes, the forwarder and the proxy send is an RFC 9457 problem body under one status table, the refresh route answers the session with its tenant, and a required session's outage redirects to the problem page; the root went 2.46 → 2.38 kB (the session a query on the host's client, claims, readSession and singleFlight off it) and ./server 5.14 → 5.18 kB (claims on it, organizationGroups deleted), under their limits
2026-10-14llm ./local, ./testing1.6 → 2.7 kB, approved by the owner (standing approval, 2026-10-14); 1.2 kB (new)1.55 → 2.66 kB; 1.15 kBcleanup PR 4: localModel answering from a script a test registered on the hook. The gauge follows /local's lazy import("./testing.js"), so the rise is the double, which a production page never fetches, more than the branch that reaches it; ./testing is the double itself, over ai/test, left out like every peer. The root went 415 → 413 B with the AI SDK's names gone, under its 450 B; testing's root 5.14 → 5.27 kB and ./dom 914 → 929 B with CDK's names and the model scripts, under their 10 kB
2026-10-13every entryone gauge each, over the rootsee belowa gauge per entry (this section)
2026-10-14ui root, ai root51 kB and 20 kB, not raised50.56 → 50.69 kB, 12.84 → 12.56 kBone question part (cleanup PR 7): Question and its parts came into the ui root from Questionnaire's file with translations, and ai's Question left; { Button } stayed at 1.29 kB
2026-10-14auth ./next, over the root7.75 → 7.8 kB, approved by the owner (standing approval, 2026-10-14)7.73 → 7.78 kBthe forwarder answers an API that does not answer as api/unreachable, a 502 problem, rather than Next's bare 500 (keasy, migrating to v0.46); the root (2.38 kB) and ./server (5.18 kB) did not move
2026-10-14llm ./local and ./testing, testing root and ./dom2.7 kB, 1.2 kB, 10 kB and 10 kB, not raised2.66 → 2.14 kB, 1.15 kB → 237 B, 5.27 → 6.37 kB, 925 → 924 Ba model resolved through one provider registry: /local lost its lazy import of the double and its branch for a test, and gained the registry and localProvider's app-config.json; the double's answering moved from llm/testing into testing's root as scripted, sent into a page as source text, so ./testing is now the SDK's mock over it. The limits stay where they were, with headroom no change here asked for
2026-10-14mosaic root4.15 → 4.6 kB, approved by the owner (standing approval, 2026-10-14)4.11 → 4.53 kBjoins and answers (cleanup PR 9): JoinResolver became the one door to the joins — steps, paths, relation, field, at and crossing's at — and resolve.ts went
2026-10-14ui analytics, { Dashboard }27.6 → 28.6 kB, 35 → 36.1 kB, approved by the owner (standing approval, 2026-10-14)27.3 → 28.51 kB, 34.81 → 36.02 kBjoins and answers (cleanup PR 9): a tile's where and top and tileQuery, the one compiler every tile view reads through; a count of the measured type's keys; #199's whole-number count axis and wider bar labels. ai ./data went 8.97 → 8.61 kB, the answer schema and its second compiler gone
2026-10-14mosaic root, ui analytics, { Dashboard }, graph root4.6 → 5.2 kB, 28.6 → 29.2 kB, 36.1 → 36.75 kB, 25.25 → 26 kB, approved by the owner (standing approval, 2026-10-14)4.53 → 5.14 kB, 28.5 → 29.11 kB, 36.02 → 36.68 kB, 25.15 → 25.97 kBwhat a field is (cleanup PR 10, /docs/design/fields): JoinColumn descriptions and relationColumns, a multi-valued property as a step joined once, labelOf and namingColumn; fieldStats over descriptions, useFieldStats reading a counted graph with no query and keeping the joins' facts over a summary, titles from labels; readCatalog, the one read of fossil's four catalog relations and the engine's column types. ai ./data went 8.61 → 8.2 kB, readAnswerRelations and the card's own read gone — 9.39 → 8.97 kB once merged with PR 11's kept chat, under its 9.5 kB, not raised; ui root (50.73 kB), ./editor, ./table and { Button } did not move. auth ./next measures 7.81 kB against its 7.8 kB on this machine on main too, untouched here
2026-10-14ai ./data9.1 → 9.5 kB, approved by the owner (standing approval, 2026-10-14)8.61 → 9.39 kBa chat kept (cleanup PR 11): Ask's answer and ask parts read back through schemas of their own, each rebuilt from its fields with its tile through parseTile. The root went 12.53 → 13.29 kB with load, save, keep and useKeptChat, under its 20 kB
2026-10-14graph root26 → 26.5 kB, approved by the owner (standing approval, 2026-10-14)25.97 → 26.35 kBthe graph on GraphAr (U1): the id arithmetic and the footers' counts — two more statements in readCatalog, the folding of row groups' nulls and extents, and the one-chunk rule (the catalog's own reading came out 0.6 kB smaller than the manifest's)
2026-10-14llm ./local2.7 → 3.1 kB, approved by the owner (standing approval, 2026-10-14)2.14 → 2.83 kBan honest finish, a turn per call and a warm load: the finish check, the queue, the stream wrapper, the warm-up and its performance spans. The warm-up and its spans are the separable part (its own commit)
2026-10-14mosaic root5.2 → 6 kB, approved by the owner (2026-10-15)5.14 → 5.90 kBa graph described by its mapping (/docs/design/fields): withMapping (with the maps fossil writes over a query), typeWords and edgeWords, and splitWords as the one split the picker and Ask both read. ui analytics (29.17 → 28.99 kB) and { Dashboard } (36.68 → 36.65 kB) went down with the picker's own split and Ask's own label gone; ai ./data 8.97 → 8.94 kB; ui root and graph did not move
2026-10-15mosaic root6 → 7 kB, approved by the owner (2026-10-15)5.97 → 6.82 kBthe engine's lifecycle (/docs/design/engine): the statement is interrupted when its signal aborts, engine/lost and memory_limit, and one scanner that redacts every literal from a refused statement before it reaches onFailure
2026-10-15graph root26 → 26.5 kB, approved by the owner (standing approval, 2026-10-14)25.97 → 26.29 kBthe reference grid drawn on a canvas (/docs/design/graph): the 1-2-5 ladder, the lattice and its draw, the ink and the canvas's size, against the CSS layer and its per-frame style writes gone. auth ./next measures 7802 B against its 7.8 kB on main here too, untouched
2026-10-15graph root26.5 → 28 kB, approved by the owner (2026-10-15)26.34 → 27.99 kBa graph that survives (/docs/design/graph-hardening): graph/too-large, a restored WebGL context, reads that abort and retry, and a canvas a keyboard and a screen reader can use (about 1.35 kB of the 1.65)
2026-10-15graph root28 → 28.5 kB, approved by the owner (2026-10-15)27.99 → 28.4 kBthe graph reads a GraphAr graph (/docs/design/graph): the catalog from graphar_*, the global id computed from the per-type index, the footer reads for counts, on top of the hardening
2026-10-15auth ./server, ./next, ./testing5.25 → 5.45 kB, 7.8 → 8.71 kB, approved by the owner (standing approval, 2026-10-14); 0.2 kB (new)5.18 → 5.41 kB, 7.78 → 8.69 kB; 172 Bthe hardening layer (/docs/design/auth-hardening): the CSP nonce, the forwarder's bound and api/silent, a closed same-site check, the static-extension list, the logout token's cap, typ and jti replay check, and the one session reader; ./next includes ./server, so the two rise together; the root went 2.38 → 2.41 kB under its 2.5; ./testing is the new fakeAuth door
2026-10-15ui analytics, { Dashboard }29.2 → 30 kB, 36.75 → 37.5 kB, approved by the owner (2026-10-15)29.09 → 29.82 kB, 36.64 → 37.43 kBevery chart's text alternative (/docs/design/analytics): a visually hidden table of the answers the marks hold, or a summary past 50 rows; the chart probe went and took about 0.13 kB with it
2026-10-15mosaic root, ./engine7 kB, not lowered; 2.25 kB (new)6.78 → 4.88 kB; 2.17 kB over the rootthe engine behind its own subpath (/docs/design/engine): engine() and its DuckDB-WASM boot left the root, which is the @uwdata door; the root's limit waits for the rest of the split
2026-10-15mosaic ./engine2.25 → 2.3 kB, approved by the owner (standing approval, 2026-10-15)2.17 → 2.26 kBthe engine keeps the coordinator private (/docs/design/engine): internalCoordinatorOf and internalEngineOver, so GraphRoot, useGraph and MosaicProvider take an engine and the graph's reads stop their statement
2026-10-15auth root, ./server, ./next; ./testing gone2.5 → 0.8, 5.45 → 1.25, 8.71 → 3.35 kB (lowered)2.3 kB → 785 B, 5.44 → 1.24 kB, 8.48 → 3.32 kBthe relying party moved to the platform's edge (/docs/design/auth, The session is the platform's edge): the routes, the stores, the sealed cookie, the issuer, renewal, back-channel logout and the exchange went; ./server is jose's verification of the forwarded token, the root a provider handed the session as a prop
2026-10-15auth ./server1.25 → 1.3 kB, approved by the owner (standing approval, 2026-10-15)1.24 → 1.26 kBthe forwarded token must be an access token (/docs/design/auth): typ: "Bearer" is checked, so an ID token signed by the same realm is refused as token/refused; ./next stayed under its 3.35 kB at 3.34
2026-10-16relations root5.9 → 6.2 kB, approved by the owner (standing approval, 2026-10-15)→ 6.1 kBa JoinGraph refused where it is made (/docs/design/relations): checked() at the end of readJoinGraph and withMapping — duplicate type or edge names, a key not among its columns, an edge end or a union member that is not a type, as relations/invalid — on top of readCounts reading fossil:stats as fossil writes it

A difference of a few bytes on a package the change did not touch is the measurement, not the code.

The components index is generated, and keeps no second list

The docs' component grid is derived from the page tree, in docs/components/components-list.tsx. Shark's version keeps a hand-maintained thumbnail map beside it; that half is deliberately not copied. A hand-kept map is a second list of every component, and this repository's dominant failure is a second way to say one thing — it goes stale on the first rename and nothing fails.

What would reverse it. A thumbnail that cannot be derived and is worth more than the drift. None has been asked for.

Held by docs/components/components-list.tsx.

One changeset per change a consumer can see

A changeset says what a consumer must do differently, in the consumer's words; it is release notes waiting for a release and nothing else. The git tag decides the version, so a changeset carries no bump level. The reason a decision was taken belongs on these pages, not in a changeset.

What would reverse it. A release process in which the changeset, not the tag, decides the version.

Held by .changeset/README.md, which states it beside the changesets themselves.

On this page