One read per datum
Why every read the library makes for a host — a relation's fields, an answer's rows — is a TanStack Query query inside the library's own hook, on the host's QueryClient, with no queryOptions surface; and what would reverse it.
The same datum was read by several parts of one page, each in its own way. A relation's fields
were read by the dashboard's useFieldStats, again by AnswerCard over the same relation, and a
third time by readAnswerRelations for Ask's prompt, so a page drew one relation's summary two or
three times and the model and the card could be told different distinct counts. Over a corpus none
of those reads is left: what a field holds is the corpus's profile, on the join graph
(What a field is). A host that read
anything else of its own wrote a promise memo that forgot failures — keasy wrote three, and a guard
to stop a fourth. Each was a cache, and none of them knew about the others.
The library reads through TanStack Query, on the host's client
Every read the library makes on a host's behalf is a TanStack Query query, made inside the
library's own hook, on the QueryClient the host's QueryClientProvider puts in context. The
hook is the surface — useFieldStats, Ask's useAskChat, useSession — and the library's surface has no
queryOptions, no keys and no client of its own. @tanstack/react-query is a peer of the entries
that read, and only of them: @kanzo-tech/ui/analytics and @kanzo-tech/ai/data, each an optional
peer reached only from that subpath through the engine rule
(decided 13 October 2026). @kanzo-tech/ai's root — Chat, Assist, the chat hooks — reads
nothing through it, so a host that only draws a field the model helps fill, as metadata-form does,
installs nothing new. @kanzo-tech/auth is the third reader: AuthProvider reads the session
useSession hands out as a query, and since the provider is on its root, there the peer is required
(the auth layer).
The reference is the React integrations of tRPC and of Supabase. @trpc/react-query's hooks run
useQuery inside the procedure's own hook, on the client the host's QueryClientProvider holds;
Supabase Cache Helpers' useQuery does the same over a PostgREST query, deriving the key from the
query. What both settle is the split this page takes: the library owns what is read and how it is
keyed; the host owns the client — its defaults, its devtools, its persistence, its garbage
collection — and has exactly one of it.
What it bought, each a defect before:
- One read, deduplicated. The dashboard and an answer's card ask for a relation's fields under
one key, and the second asker gets the first's answer, never a second
SUMMARIZE. The key is the relation's: what a part hides (exclude) is applied to the read afterwards, so two parts that hide different columns still read once. - An abort is the query's. A query function is handed a signal. An answer's read hands it to
Engine.query, and Stop cancels the query, so the wait ends at once; aSUMMARIZEstill queued on the coordinator when every part that asked has gone is taken off its queue. - A failure is kept out of the cache. A rejected read is an error state with the thrown value whole, and the next asker reads again; the memo keasy wrote three times is the cache's default.
Discarded: queryOptions from each package, read by the host's own useQuery. It was the
review's first proposal, and it is TanStack's own shape for a library of query definitions. It makes
two ways to read one datum — the hook a part uses inside, and the options a host wires itself — and
the host then writes the hook, the error handling and the abort the library already wrote. Back if
a host needs a read outside React or ahead of render — a server component prefetching a session, a
loader — and then that one read gets its options beside its hook, and no other does.
Discarded: a private QueryClient inside the library. Two clients on one page, a cache the host
cannot see in its devtools or clear on sign-out, and defaults it cannot set. Back if a host cannot
render a provider at all — and every reading package is a React package.
What would reverse it: a consumer whose application caches with something else — SWR, Apollo — and refuses a second cache library. Then the reads become descriptions a small adapter per cache library runs, and TanStack's is the first adapter, not the only path.
Held by packages/ui/src/charts/field-stats.test.tsx, "reads a relation once for every part that
asks, whatever each hides"; "reads on the host's client, and throws without one".
packages/ai/src/data/ask-chat.test.ts, "ends the read with the chat's signal, keeps nothing of it,
and reads again when asked again". The peer stays off both root entries by
packages/ui/src/optional-peers.test.ts, "are reached from no package's root entry", which walks the
import graph from each package's root module rather than asserting names — the peer arrived with no
exported name at all, inside hooks.
A read is made once, and kept as long as the datum holds
A library query never retries, and is kept for as long as what it read cannot change. A retry is
a second wait on one failure, which the failure rule forbids; so every query
the library makes passes retry: false, whatever the host's client defaults to. A datum that cannot
change passes staleTime and gcTime of Infinity: what it read is the page's for the page's life.
A session changes under its key — renewed, ended, signed out in another tab — so it passes
staleTime: 0, and is read again on the events the table names.
| Datum | Read by | Kept | Read again |
|---|---|---|---|
a relation's SUMMARIZE, for one whose columns no corpus counted (What a field is) | useFieldStats, and through it Dashboard | the page's life | when the relation or the provider's coordinator changes, which is another key |
| an answer's rows | useAskChat, under the page's chips | the page's life, by engine and statement | when a chip or the answer changes, which changes the statement |
| a session | AuthProvider, and through it useSession and Gate | until it is stale, which is at once | when the tab becomes visible, after a renewal, and when another tab says so |
The keys are the library's, and they are not an API. Each says what was read and from what — the coordinator or engine it was read on, and the relation or the compiled statement, which holds the page's clauses — so a new input is a new read, and nothing asks the host to invalidate by hand. A host that signs out clears its client, as it would for any other data of the person's.
Restoring a stored chat is not a query. A chat's messages are loaded once, before its first turn,
by the chat hooks on @kanzo-tech/ai's root, which takes no TanStack peer; caching them would keep a
second copy of a conversation the chat itself holds. When the chat hooks take load and save,
load is a plain call.
What would reverse it: a datum the library caches that changes under the same key — a table a host rewrites in place under its name, a corpus rewritten under its attachment. Then that key gains the datum's own version — a corpus's from its manifest — and the rest stay as they are.
Held by packages/ui/src/charts/field-stats.test.tsx, "keeps a failure out of the cache, whole,
and reads again for the next part that asks". packages/ai/src/data/ask-chat.test.ts, "reads an
answer asked again under the same chips once, on the host's client".
Crossfilter
Why everything that narrows the page is a filter — one clause on one page selection, the timeline and the canvas included — why "kept" is decided once and only drawn differently per view, why a position is an encoding and "no position" a state of its own, and what that changes in the graph, the timeline and the gestures.
What a field is
Why a field's facts come from the corpus — what it is from fossil's manifest, what it holds from the counts fossil's writer stores beside it — and reach every reader through JoinGraph, so nothing in the library guesses them from a column's name or a threshold; and what would reverse each part.