The kernel
The one dependency-free package every other package may import, and what it holds, what it refuses to hold, and what each of its five pieces replaced; with the rule that holds the direction.
Status: to be built. @kanzo-tech/kernel does not exist at the tree this page describes; the
duplicates in What it replaces do, and the migration is the Breaking list at
the end.
Five ideas were written more than once across the packages, each time by the package that happened to need it first: what a failure is, how a wait ends, what an engine is to a reader, how a handle is given back, and how a span is marked. None of them is a package's own business, and none can live in a package that has a dependency of its own, because every other package would then depend on that dependency. So they live in a package that depends on nobody.
The rule
@kanzo-tech/kernel is layer 0: it imports nothing, declares no dependency and no peer, touches
no DOM and no window, and does nothing at import. Every other package may import it; it imports
none of them. It holds five things and no others:
| Piece | What it is | Hand-written budget |
|---|---|---|
Problem | the one failure class, in RFC 9457's shape, with isCode and isProblem | 150 lines |
within, until | the one bounded wait, and the abandon it is built on | 60 lines |
Queryable, Table, Vector | the read side of an engine, as types | 40 lines |
dispose, asyncDispose | the symbols a handle that must be given back is disposed through | 30 lines |
mark | one span in the Performance timeline, prefixed kanzo: | 40 lines |
The package is 230 hand-written lines as built (claude/kernel), and 350 is its ceiling; the
reference-correct version of Problem (the derived type, the typed data, the cycle guard, the
foreign-data filter, the ignore-a-wrong-member rebuild) fits in it, at 142 of its 150. The registry of codes
(How a failure leaves the library) is generated and
not counted: it is a table, one row a code, and a table does not add a way of doing anything. The
ceiling is held by the size gate with the figure measured at the first build; raising it is a
decision with its changeset, never a quiet edit (the repository's standing rule on size).
Layers, downward only. The kernel is the only package every layer imports, which is what makes it L0 and not a convenience: a package at L1 that cannot import it would have to define its own, and that is the state this page ends. The architecture page owns the layer table and the guard that holds it.
Why a package, and not a subpath of theme or a type in mosaic. theme is CSS and token
tables, and a failure class does not belong in it. mosaic is the heaviest package at the bottom of
the graph, and the engine rule keeps its optional peers out of every root barrel, so a package that
wants only Problem would import a DuckDB-WASM dependency to get it. The reference is OpenTelemetry,
which separates the API a library codes against, a few types and no-op functions with no
dependencies, from the SDK a host installs; a library never depends on the implementation, and a
host never installs the API on purpose (READ: the OpenTelemetry overview). The kernel is the
family's API in that sense, and mosaic/engine is one SDK of its Queryable.
What it does not hold, on purpose. A result type: throwing the failure whole is the rule and it
holds (the thrown value). A dependency
container, an event bus, a store, a plugin system. A telemetry sink or a tracer: mark writes to the
browser's own timeline and nothing else. Translations: each component declares its own, and the
naming rule keeps it so. A helper two packages happen to share but that no
rule needs does not enter, whatever its size: the admission test for the kernel is the one in
admission, with the extra condition that it is already written twice.
What would reverse it: the owner moving the shared layer into @fossil-lang/types, which already
owns Host, FossilError and within. That reverses the direction "kanzo imports nothing of
fossil's" that packages/graph/src/layers.test.ts holds, and it makes fossil's release cadence the
UI's; it is the alternative the architecture review weighed and this page declines. Or the kernel
passing 350 lines for a sixth piece: then the piece is argued for here before it is written, and the
first thing the argument must show is two existing copies.
Problem
One class, Problem, in the shape of RFC 9457 Problem Details for HTTP APIs. A failure that
leaves any package is a Problem or it is an AbortError, and nothing else.
class Problem<C extends string = string> extends Error {
readonly name: "Problem";
readonly type: string; // RFC 9457's identifier: the registry's base + `code`, derived, never set
readonly code: C; // `area/kind`, the extension member a host branches on
readonly title: string; // the registry's, one per type; never the HTTP phrase
readonly status?: number;
readonly data: ProblemData<C>; // plain JSON; the registry's fields for a registered code
readonly help?: string;
readonly related?: readonly Problem[];
readonly severity: "error" | "warning" | "info";
// `message` is the RFC's `detail`; `cause` is whole, in process
toJSON(): ProblemBody; // application/problem+json
static from(body: ProblemBody): Problem;
}
function isCode(value: unknown): value is string; // the `area/kind` grammar
function isProblem(value: unknown, code?: string): value is Problem;The reference is RFC 9457, and each member is read from the clause that defines it.
| Member | Clause | What the kernel does |
|---|---|---|
type | §3.1.1: a URI reference identifying the problem type, the consumer's "primary identifier"; "about:blank" means no semantics beyond the status (§4.2.1) | tag:kanzo.tech,2026:problem/ followed by the code, derived from code and never independent. A coded problem has semantics beyond its status, so about:blank would be the wrong type |
title | §3.1.3: a short summary of the type, which "SHOULD NOT change from occurrence to occurrence" | the registry's title for the code, so never the status phrase (that is about:blank's title) and never the occurrence's text |
status | §3.1.2: advisory; the response must carry the same status | the registry's status where the code crosses a wire; the server must also set it on the response |
detail | §3.1.4: specific to this occurrence; consumers "SHOULD NOT parse" it | the error's message |
code, data, help, severity, related, cause | §3.2: extension members, which a client "MUST ignore" if unrecognised; §4 asks for names of three or more characters from letters, digits and _ | all six meet that. code is what a host branches on; data is typed per code |
Where the sources disagreed with what this page first said. The page named code the
discriminator and left type out. §3.1.1 makes type the primary identifier a consumer must use, so
type is now written. It is derived one-to-one from code, so the two cannot disagree and a client
that follows the RFC to the letter and branches on type gets the same answer as one that branches on
code; code stays because a short string is what a switch reads. The page also said §4.2.1 asked
for the status phrase as title: it does, but only under about:blank, which a coded problem does
not use. packages/auth/src/problem.ts still writes the about:blank form; it moves with the
migration, and fossil's FossilError has no type on purpose (its site prefix is a deployment's to
choose; /docs/design/errors in that repository), which is why the kernel mints a tag URI and not a
URL.
Why a tag URI. §3.1.1 allows a non-resolvable type and names the tag scheme (RFC 4151) as its own
example, so tag:kanzo.tech,2026:problem/engine/lost needs no site at a stable address and no prefix a
deployment chooses. It cannot be dereferenced, and the RFC asks consumers not to. The RFC says
resolvable URIs are "encouraged", and that adopting one later is "a new identity for the problem type":
a breaking change for every code at once, which is the cost of this choice, taken knowing it. The
kanzo.tech authority and the year 2026 are the owner's to confirm (RFC 4151 asks that the authority
held the name on the date given; UNVERIFIED here). An unregistered urn: namespace was the other
candidate; RFC 8141 asks that a namespace identifier be registered, which has not been done
(UNVERIFIED: the RFC was not read for this page). The base lives in problems.bnf, so the Rust
projection derives the same type.
The page a person opens is the host's, and is not the type. A link a reader follows to learn what
engine/lost means is a deployment's documentation, at a deployment's address, and it may change
without the problem's identity changing: so it is not derived from the tag, and the kernel holds no
base for it. The one place it is set is describe(code, data)'s page, the host's wording of a
code, which every coded Problem the library draws already takes (Problem):
a base is (code) => ({ page: base + code }). A second way — a provider, a base on the class, a
help URL on the wire — would be a second place to set the same link. RFC 9457's instance and
type are left as they are: neither is a help page. What would reverse it: a problem drawn by the
library with no describe in reach — a part that renders a coded failure and does not forward the
host's function — which would need a base that reaches it another way; or the owner choosing a
resolvable type, which would make the type its own help page.
cause and related on the wire. RFC 9457 is silent on both, and JavaScript's Error.cause
(any value, cycles possible) is the only reference. toJSON follows cause and related only when
the member is a Problem, at most 8 deep, and cuts a member that is already on the path from the root,
so a cycle or a very long chain serialises and never throws or loops. A foreign Problem-shaped
cause (a FossilError, a second copy of this package, anything that is not this copy's instanceof Problem) keeps only the data fields the registry declares for its code, of the declared type, and
none for a code the registry lacks: it is the author of those members, not us, and unknown members are
where a token or a path would be. Its detail is kept, because a Problem's message is the
contract isProblem tests. The safer choice is taken and its cost is that a host's own cause loses
its data on the wire; the host's own Problem at the root keeps it.
The failure that motivated it. Four classes with the identical constructor (code, message, data, options) were written by copy: EngineError (packages/mosaic/src/engine.ts), GraphError
(packages/graph/src/core/error.ts), LlmError (packages/llm/src/error.ts) and AuthError
(packages/auth/src/types.ts). Beside them fossil has FossilError, graphar has GraphArError,
and one UI function, the Problem composite, read them all by their shape with a regex of its own
(packages/ui/src/composites/problem.tsx). keasy's server answered application/json with a
bespoke {code, title, detail, data} and no status, 9457-shaped but not 9457, while the BFF
answered application/problem+json, so every client branched on the content type. And the same
event had two codes: the BFF's session/absent and the server's auth/session-required, both a 401
for "nobody is signed in". A host registry keyed on both, and a third on fossil's.
Rules.
- A test is structural, never
instanceof.isProblem(e, code?)is true of anything with a stringmessageand acodein the grammar, so aFossilError, a Problem rebuilt from a worker'spostMessageand a second copy of the kernel in one bundle all pass. Two copies of a class are two classes; fossil'sisFossilErrormakes the same call for the same reason (packages/types/src/error.ts, its doc comment). TheProblemcomposite's regex becomesisCode. - The thrown value is whole in process, and thin on the wire. In process
causeis whatever was caught, kept as it came.toJSONemitscauseonly when it is itself aProblem, and never a stack or a foreign error's message: a foreigncauseis what carries a statement's text, a URL with a credential, a file path. This is whydetailnever names a statement or a token either, and why the wire reads as a statement of what failed and not with what. datais plain JSON and typed per code, from the generated registry.ProblemDataMapis generated fromproblems.bnf, and the constructor's rest parameters are a conditional type over it:datais a required argument exactly when the registry declares a required field, its fields are the declared ones, and a mismatch is a compile error (new Problem("graph/too-large", "x", { count: 2 })fails; the test holds it with@ts-expect-error). This is the typed-error pattern of a mapped type from a code table, not an overload per code. A code a host owns is a free string anddataisRecord<string, unknown>. The host's registry (keasy'srbac/*) is generated by the same generator from its own source, so two registries share a grammar and not a list.- A programming or deployment fault is not a
Problem.useSessionoutside its provider throws a plainErroron purpose (the auth layer): a code invites acatchthat routes it to a sign-in page, which is the wrong answer to "you wired this up wrong". The guard (specified on the architecture page) ratchets the count of plainnew Error(sites per file, so a new one is a visible decision. - An interruption is not a failure. A call stopped from outside rejects with the signal's reason,
an
AbortError, as an abortedfetchdoes;isProblemis false of it, and a host tells "my Stop" from a failure bysignal.aborted. TodayuseStream,use-task,ask-chat/read.tsand keasy'suse-assistanteach re-implement "was this my cancel"; the rule is stated once, here, and the code follows.
Two names that collide
@kanzo-tech/ui has a Problem composite (the component that draws a failure,
admission) and a Table
component; the kernel has a Problem class and a Table type. The rule, stated here and nowhere
else: the kernel's name is the reference's word and stays plain; a file that needs both imports the
component under <Name>View.
Problem is RFC 9457's own word for its object, and Table is Apache Arrow's for the thing the
Queryable returns (Table and Vector are the part of apache-arrow's a reader reads), so the
kernel's names are the reference's, and /docs/design/naming settles a collision the one way: the
reference's name stays and ours moves. The component is the one that is renamed at an import:
import { Problem as ProblemView, Table as TableView } from "@kanzo-tech/ui"; the docs still call it
Problem and Table, which is the name a reader looks for. keasy already uses ProblemView. It is
a spelling at one import site, not a second name in a barrel. A file that needs only one imports it
plain.
Where the sources disagreed with what this page first said: it aliased the class (ProblemError)
and kept the component plain. That was one rule for Problem alone, and Table would have needed
the opposite to keep Arrow's word; one rule for both is this one.
What would reverse it: more than a handful of files needing both, in which case the component is
renamed outright and the alias rule goes with the clash. Until then the
guard documented-exports.test.ts ("gives each name one meaning across every entry") is the reason
the kernel is not among its entries: two meanings for Problem and Table would grow its
UNSETTLED list, which may only shrink, so the entry waits for the rename.
The production axes. Failure and recovery: one shape means one registry row per code says what a
host does next, and Problem.from rebuilds the error across a worker or a response, so recovery code
is written once. Security and privacy: rule 2. Performance and size: the class and its predicates
are the 150 lines, with no dependency. Stable API: a code is stable once released, never reworded and
never reused; a rename is a breaking change a changeset states, as everywhere here. Tests: the
class has a table-driven test over the registry (every row builds, serialises and rebuilds) and a test
that isProblem accepts a FossilError.
What would reverse it: a host that wants failures in a vocabulary that is not area/kind and will
not adopt it, or fossil's error class adopted as the base so the kernel's Problem becomes a
re-use of it. Either is one file to change, because the only thing outside the kernel that names the
class is a type annotation.
Held by packages/ui/src/failures-reach-the-host.test.ts, "bites on onFailure, onError or error
typed string" (that a surface hands the host the thrown value, which the kernel does not change).
The guards that hold the single class are not yet written.
within
One helper bounds a wait on something outside the process, and it is the only place a deadline's clock lives.
within<T>(ms, wait: (signal) => Promise<T>, { signal, silent: (after) => Problem }): Promise<T>
until<T>(answer: Promise<T>, signal?: AbortSignal): Promise<T>within hands wait a signal that aborts when the deadline passes or the caller's signal does
(AbortSignal.any: "the reason will be set to the reason of the first signal that is aborted", MDN),
and waits through until on that one signal, so the deadline's reason is silent(ms), a Problem
coded by who did not answer and carrying after, and the caller's abort rejects with the caller's
reason. until is the abandon underneath: the promise, or the signal's reason the moment it aborts,
with the work behind it left to finish unread. This is fossil's helper,
packages/types/src/deadline.ts, with one mechanism and not two: the first version of the kernel's
also raced a second rejection against the signal, which fossil does not, and the duplicate is gone.
Two differences from fossil's remain on purpose: silent returns a Problem and not any Error
(no bare TimeoutError can leave), and until's signal is optional.
Why not AbortSignal.timeout. MDN: it aborts with a TimeoutError DOMException, which cannot
carry who did not answer or after, and its clock is active time, paused while a worker is suspended
or the page is in the back-forward cache, where a test's fake timers cannot drive it at all. The
clock is setTimeout. AbortSignal.any is Chrome 116, Firefox 124, Safari 17.4, Node 20.3 and Deno
1.39 (MDN browser-compat-data, read 2026-10-15).
The failure that motivated it. The wait was bounded four times, differently each time. DuckDB's
boot: abandon plus a setTimeout (packages/mosaic/src/engine.ts). Auth: deadline(),
which aborts a controller with the error and rejects with it (packages/auth/src/deadline.ts).
The graph's device: a bare setTimeout that calls fail (packages/graph/src/render/webgl.ts).
And fossil's within. They differ in whether the work is aborted or abandoned, in whether the caller's
signal composes, and in what the rejection is, and where two of them bound one wait, the one that fires first names the failure
(one deadline per wait).
Rules.
- Every wait on the outside goes through
within, and nothing else callssetTimeoutto reject or abort. Debounces, linger timers and animation settle times are not waits on anything and stay where they are (ai/src/reasoning.tsx,ui/src/lib/use-debounced-commit.ts); the guard reads the callback, not the call. - A wait that can be cancelled is, and one that cannot is abandoned.
waitis given the composed signal; an API that takes none (a call into wasm) is wrapped inuntiland its late answer ignored. This is the DuckDB boot, and it is the only place an abandoned answer is acceptable: a query is not abandoned, it is interrupted (the engine). - Work inside the process has a signal and no deadline. A query, a layout, a plan can legitimately take minutes; the only thing that waits forever is the person's patience, and they have Stop. The line is fossil's (Every wait ends, G1) and it is drawn the same way here.
silentis mandatory. There is no default, so no bareTimeoutErrorcan leave the library; the caller says who did not answer.
The production axes. Failure: a deadline that fires names its party, and the composition makes
an outer Stop reach every inner wait, which is what the forwarder lacked
(the auth layer). Size: 60 lines replace four. Tests: one test file in the
kernel runs within under fake timers for the four outcomes (answers, deadline, caller abort,
already-aborted) and every package's own test asserts only its figure and its code.
What would reverse it: the three waits turning out to need different semantics, abort for one and
abandon for another, in a way a flag on the one helper does not express; then until and a deadline
helper are separate again and the guard moves with them.
Held by packages/mosaic/src/engine-boot.test.ts, "rejects with data.after when the worker never
answers within the 60 s deadline", which keeps its title when the boot moves onto within.
Queryable
The read side of an engine is one interface, in the kernel, as types. The kernel holds the shape; the contract that goes with it, that aborting a query interrupts the statement, and the conformance kit that asserts it, are the engine's.
interface Queryable { query(sql: string, options: { readonly signal: AbortSignal }): Promise<Table> }
interface Table { readonly numRows: number;
readonly schema: { readonly fields: readonly { readonly name: string }[] };
getChild(name: string): Vector | null }
interface Vector { readonly length: number; get(index: number): unknown; toArray(): ArrayLike<unknown> }Table and Vector are the part of apache-arrow's a reader reads, so an Arrow table is one without a
conversion and the kernel does not depend on Arrow. The file registry (registerFiles,
registerFileBuffer, dropFiles) is DuckDB-WASM-specific and stays with the engine that has it. A
reader that only reads (ai/data, the graph's catalog, the relations) takes a Queryable.
The failure that motivated it. Four hand-written copies of the type had drifted: ui's Table and
Vector (packages/mosaic/src/engine.ts), the graph's Answer (packages/graph/src/core/source.ts),
graphar's one-member Engine (packages/graphar-duckdb/src/attach.ts) and fossil's
(packages/types/src/engine.ts), plus Pick<Engine, "query"> in ai at three sites
(ai/src/data/read.ts, ask-chat.ts). They agreed on query and disagreed on whether its signal
reaches the statement.
Rules.
- The signal is required and always passed. TypeScript accepts an implementation that declares fewer parameters, so honouring it is the engine's contract and not the compiler's, which is why the contract has a test, in the engine.
- graphar and fossil keep their own structural types and say so. graphar-duckdb links no kanzo
package because its code is meant to go upstream; its one-member
Engineis a deliberate subset ofQueryable, and a comment says so. Nothing in the kernel is imported by either.
What would reverse it: a consumer needing what query cannot do (streaming Arrow batches, an
ingest). Then a second port is added beside this one, and this one is not widened.
Disposable
A handle that must be given back is disposed through Symbol.asyncDispose (Symbol.dispose where
the release is synchronous), and the kernel provides the two symbols.
The reference is the TC39 explicit resource management proposal, using and await using, which
graphar's GraphAttachment and fossil's mount already follow. The kernel's part is three lines:
dispose is Symbol.dispose where it exists and Symbol.for("Symbol.dispose") where it does not, and
the same for asyncDispose; nothing is written to Symbol.
Where the sources disagreed with the brief. The fallback was to be what TypeScript does. It is
not: TypeScript's emitted helper __addDisposableResource has no fallback and throws
TypeError("Symbol.dispose is not defined."), and its documented polyfill (the 5.2 release notes) is
Symbol.dispose ??= Symbol("Symbol.dispose"), a private symbol. The proposal's README is silent on a
fallback. Symbol.for is therefore this package's choice, and it is the safer one: a private symbol
made by one copy of a package is not the one another copy made, and TypeScript's recipe, run after
this module loads, would leave the kernel holding a different symbol from the host's. So the
requirement is written down: a host that downlevels using runs Symbol.dispose ??= Symbol.for("Symbol.dispose") (and the same for asyncDispose), in any order relative to the kernel.
What a consumer needs. TypeScript 5.2 or later and "lib": ["esnext.disposable"] (or
"esnext"), because the declarations say typeof Symbol.dispose; a target of ES2022 or below if it
lets TypeScript downlevel using. Native symbols (MDN browser-compat-data, read 2026-10-15):
Symbol.dispose in Chrome 125, Firefox 141, Node 24, Deno 1.37; Symbol.asyncDispose in Chrome 127,
the others the same; Safari has not shipped them, which is the case the fallback is for.
The failure that motivated it. Three conventions for "give back what I attached" and none shared:
the Mosaic engine's serial and release over a name (packages/mosaic/src/engine.ts), graphar's
[Symbol.asyncDispose] on an attachment, keasy's releaseCorpora. The engine had no disposal at all,
so a page could not release a 36 MB module and its worker, and a worker that died left the singleton
resolved to a dead engine.
Rules.
- The handles are the engine, an attachment, a loaded model and the graph's store. Each
implements
[asyncDispose], and a host that holds one holds it withawait usingor disposes it in an effect cleanup. - A closure remains the right shape for a subscription. A subscribe returns its unsubscribe
because React's effect cleanup is a closure; making it a
Disposablewould put a wrapper between two shapes that already fit. - A refcount a caller must observe keeps its own API.
@fossil-lang/storage'smountrefcounts credentials, a different resource, and stays.
The production axes. Failure and recovery: a disposed engine settles pending reads with
engine/lost and the next engine() boots again; hosts re-attach. Size: 30 lines. Tests: each
handle's test disposes it twice and asserts it is idempotent.
What would reverse it: a target that cannot carry the symbols and rejects the Symbol.for
fallback; or a handle whose release must be observable and returns a count, which is mount's case
and keeps its own API.
mark
One function writes a span to the Performance timeline, named kanzo:<area>:<name>, and a failure
to write it never reaches the caller.
mark("area:name", start, detail?) calls performance.measure(name, { start, detail }) with the
kanzo: prefix, and swallows what the browser throws: a measurement is never worth a failed call. The
name is typed area:name, so the kernel's span is kanzo:<area>:<name>. It is what
packages/llm/src/local.ts already does for kanzo:llm:load, kanzo:llm:warm-up and
kanzo:llm:first-call, generalised by its argument.
The reference is W3C User Timing Level 3, §3.1.3 measure(), read in full. Two things differ from the
first version of this page. end is optional: "otherwise, let end time be the value that would be
returned by the Performance object's now() method", so mark leaves it out and the call is the
API's own. And detail is passed through StructuredSerialize, so a detail that cannot be cloned (a
function, a Problem with a stack) makes measure throw and the span is lost; the caller passes plain
data. The spec names no prefix convention (its recommended names are mark_*); kanzo: is the
family's own. There is no now() helper: performance.now() is the reference clock, start is a
reading of it, and a second clock would be a second way to say the same thing. The shape, a name, a
start, a detail, is the OpenTelemetry span's without a tracer, and the kernel does not build one.
The failure that motivated it. There was one instrumentation (kanzo:llm:*) and no way for the
other waits to be seen: how long DuckDB took to boot, how long the device took to come up, how long
a renewal took. A deadline's figure is the slowest honest answer, and that is read off spans.
Rules.
- A wait that has a deadline also has a span.
withindoes not mark; the caller marks, because only the caller knows the name. - No sink. There is no
onEventregistry in the kernel. Assist'sAssistEventis a separate product channel and stays. - A page without the API loses the span and nothing else.
What would reverse it: a host that wants real OpenTelemetry. Then mark gains an adapter, and the
tracer is still not built here.
What it replaces
| Piece | Written before at | Deleted |
|---|---|---|
Problem | EngineError packages/mosaic/src/engine.ts; GraphError packages/graph/src/core/error.ts; LlmError packages/llm/src/error.ts; AuthError packages/auth/src/types.ts; the area/kind regex packages/ui/src/composites/problem.tsx; keasy's server ErrorBody | all four classes; the regex; ErrorBody |
within, until | abandon and the boot timer packages/mosaic/src/engine.ts; deadline() packages/auth/src/deadline.ts; the device timer packages/graph/src/render/webgl.ts | abandon on the query path; deadline.ts; the graph timer |
Queryable, Table, Vector | Table, Vector packages/mosaic/src/engine.ts; Answer packages/graph/src/core/source.ts; Pick<Engine, "query"> packages/ai/src/data/read.ts, ask-chat.ts | the three copies |
dispose, asyncDispose | serial/release packages/mosaic/src/engine.ts; keasy releaseCorpora | the ad hoc release |
mark | the private span packages/llm/src/local.ts | the private copy |
The guards
The guards that hold the kernel's pieces (the single class, the single deadline, no stray global, no stale citation, the layer direction) are specified once, on the architecture page. None exists yet, so none is cited as Held by here.
Breaking
In the consumer's words, one changeset for each of the packages named.
EngineError,GraphError,LlmErrorandAuthErrorare gone; catch aProblemwithisProblem(error, "engine/unavailable")instead oferror instanceof EngineError. The codes are the same unless the failure page lists a rename.TableandVectorare no longer part of@kanzo-tech/mosaic; import them from@kanzo-tech/kernel.Engine.querystops the running statement when its signal aborts (the engine).
What would reverse the whole page: nothing short of the owner choosing a single shared package across all three repositories; each piece above has its own reversal, and none depends on the others.
The layers, and the one rule at each seam
Five layers with imports pointing down, a dependency-free kernel at the bottom, one rule for each place two packages meet, and the guards that make the direction a test instead of a sentence.
The engine
Why every package reaches DuckDB through one read-only port and one door, why aborting a query interrupts the statement, why a dying engine is a supported state, and why the library redacts what it throws; and what would reverse each rule.