@abide/abide 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -1,496 +1,617 @@
1
1
  # AGENTS.md — abide complete surface map
2
2
 
3
- > This file is the exhaustive public-surface map of `@abide/abide`: every
4
- > `exports` key, grouped by namespace, with its import specifier and a one-line
5
- > spec. The README is the curated three-primitive intro; `CONTEXT.md` is the
6
- > domain glossary; `docs/adr/` holds the rationale behind decisions. Ground
7
- > rules: there are **no barrels** — every public name has its own module path,
8
- > and the namespace marks the side it runs on (`abide/server/*` server-only,
9
- > `abide/ui/*` client-only, `abide/shared/*` isomorphic same callable, same
10
- > behavior on both sides). Package `@abide/abide`, runtime Bun 1.3, one
11
- > direct dependency (TypeScript). Import specifiers below are `exports`-map
12
- > keys (`@abide/abide/server/GET`), not source file paths.
3
+ > This file is the exhaustive map of abide's public surface: every `exports` key
4
+ > grouped by namespace, with its import specifier and a one-line spec, so an
5
+ > agent can grasp the whole API in one read. For the curated three-primitive
6
+ > intro read `README.md`; for the domain glossary read `CONTEXT.md`; for the
7
+ > rationale behind a decision read `docs/adr/`.
8
+ >
9
+ > Ground rule **no barrels**. Every public name is its own module path; there
10
+ > is no umbrella `index.ts`, so importing one name never drags side-effecting
11
+ > siblings into the bundle. The namespace marks the side a name runs on:
12
+ > `abide/server/*` server-side, `abide/ui/*` client-side, `abide/shared/*`
13
+ > isomorphic (same callable, same behaviour on both sides). The package is
14
+ > `@abide/abide` (Bun ≥ 1.3.0, one direct dependency — TypeScript). Every import
15
+ > specifier below is `@abide/abide<exports-key>`; the file path after it is the
16
+ > source, not an import target.
13
17
 
14
18
  ## The premise
15
19
 
16
- One typed declaration fans out to every surface:
20
+ One declared RPC fans out to five surfaces:
17
21
 
18
22
  ```text
19
- src/server/rpc/getMessages.ts
20
-
21
- ├─ SSR / server await getMessages({ room }) in-process, no HTTP
22
- ├─ browser await getMessages({ room }) typed fetch proxy
23
- ├─ HTTP GET /rpc/getMessages?room=…
24
- ├─ CLI my-app get-messages --room …
25
- ├─ MCP tool: get-messages
26
- └─ OpenAPI operation in /openapi.json
23
+ export const getMessages = GET(fn, { schemas })
24
+
25
+ ┌───────────┬──────────────┼──────────────┬─────────────┐
26
+ ▼ ▼ ▼ ▼ ▼
27
+ SSR call browser fetch MCP tool CLI subcmd OpenAPI op
28
+ (bare, (same call, (read-only abide-cli /openapi.json
29
+ in-proc) swap to fetch) from type) getMessages
27
30
  ```
28
31
 
29
- A `schemas.input` (any Standard Schema library zod, valibot, arktype,
30
- unadapted) is the gate: it unlocks the CLI, and for read-only methods
31
- (GET/HEAD) the MCP tool. A mutating method (POST/PUT/PATCH/DELETE) never
32
- auto-exposes to MCP it requires explicit `clients: { mcp: true }`.
32
+ A typed input unlocks the **CLI** on any RPC and **MCP** for read-only methods
33
+ (`GET`/`HEAD`): the handler's input type is projected to JSON Schema at build
34
+ (ADR-0030), so a plainly-typed handler auto-exposes with no hand-written
35
+ `schemas.input` (a declared `schemas.input` adds runtime validation on top, it
36
+ isn't what flips the surfaces on). A mutating method
37
+ (`POST`/`PUT`/`PATCH`/`DELETE`) never auto-exposes to MCP — it needs an explicit
38
+ `clients: { mcp: true }`. Explicit `clients` values always win; `browser`
39
+ defaults on. A socket with a `schema` auto-exposes to MCP and CLI regardless of
40
+ direction.
33
41
 
34
42
  ## File-based conventions
35
43
 
36
- | Path | Meaning |
37
- | --- | --- |
38
- | `src/server/rpc/<name>.ts` | One RPC per file; the export name must match the file stem; the file path becomes the URL `/rpc/<name>` (subdirectories nest into the path) |
39
- | `src/server/sockets/<name>.ts` | One broadcast socket per file; export name = file stem = topic name |
40
- | `src/mcp/prompts/<name>.md` | An MCP prompt template; `{{arg}}` placeholders become the prompt's arguments |
41
- | `src/mcp/resources/**` | Files served as MCP resources (gzip-embedded into builds) |
42
- | `src/server/config.ts` | Boot-time `env()` validation; eager-imported so a bad environment fails the boot |
43
- | `src/app.ts` | Optional `AppModule` hooks: `init`, `handle`, `handleError`, `health`, `forwardHeaders` |
44
- | `src/bundle/window.ts` | Optional `BundleWindow` default export configuring the desktop bundle's window and menus |
45
- | `src/ui/pages/**/page.abide` | A routed page; the directory path is the route — a `[id]` folder is a path param, `[[id]]` an optional param, `[...rest]` a catch-all |
46
- | `src/ui/pages/**/layout.abide` | A layout wrapping the pages below it; its `{children()}` is the router outlet |
47
- | `src/ui/public/` | Static assets served as-is |
48
- | `src/.abide/*.d.ts` | Generated typing (rpc args for `url()`, page routes, health fields, test rpc/socket clients, public asset paths) |
49
- | `dist/` | Build output — `dist/_app` client bundle, `dist/cli-thin/<platform>/` CLI tarballs |
50
-
51
- Project import aliases resolve to the five top-level source dirs: `$server`,
52
- `$ui`, `$shared`, `$mcp`, `$cli` (e.g. `$server/rpc/getMessages`,
53
- `$ui/pages/...`). `$server/rpc/*` and `$server/sockets/*` are proxied into
54
- client bundles; any other `$server/*` import from client code is a
55
- side-crossing error.
44
+ The bundler and route resolver read these paths by convention (dir aliases
45
+ `$server`, `$ui`, `$shared`, `$mcp`, `$cli` point at the matching `src/` dirs):
46
+
47
+ | Path | Meaning |
48
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49
+ | `src/server/rpc/<name>.ts` | One RPC per file; filename = export name = URL under `/rpc/`. The method helper picks the verb. Rewritten to `defineRpc` (server) / `remoteProxy` (client). |
50
+ | `src/server/sockets/<name>.ts` | One socket per file (`export const <name> = socket(...)`); path socket name. Rewritten to `defineSocket` (server) / `socketProxy` (client). |
51
+ | `src/mcp/prompts/<name>.md` | Markdown MCP prompt: frontmatter (description + arguments) + `{{arg}}` template body, compiled to `definePrompt`. |
52
+ | `src/mcp/resources/*` | MCP resource files served by the generated MCP server. |
53
+ | `src/server/config.ts` | Optional typed-env module: `export const config = env(schema)` validates `Bun.env` at boot (or the floor `export const config = Bun.env`). Eager-imported; deletable. |
54
+ | `src/app.ts` | Optional app hooks (`AppModule` shape): `init` / `handle` / `handleError` / `health` / `forwardHeaders`. Deletable. |
55
+ | `src/ui/pages/**/page.abide` | Folder-based route: a folder's `page.abide` mounts at that folder's URL. `[name]` / `[[name]]` (optional) / `[...rest]` (catch-all) are dynamic segments → `page.params`. |
56
+ | `src/ui/pages/**/layout.abide` | Wraps every page at/below its folder; renders the page where it calls `{children()}`; kept mounted across navigation. |
57
+ | `src/ui/app.html`, `src/ui/app.css` | Custom document shell and root stylesheet. |
58
+ | `src/ui/public/` | Static assets, served at the site root (`/<file>`). |
59
+ | `src/bundle/window.ts` | Optional default-exported `BundleWindow` for the desktop bundle (plus optional `src/bundle/disconnected.abide` connect-screen override). |
60
+ | `src/cli/banner.txt`, `src/cli/footer.txt` | CLI help chrome. |
61
+ | `src/.abide/*.d.ts` | Generated ambient types (`rpc.d.ts`, `routes.d.ts`, `health.d.ts`, `publicAssets.d.ts`, `testRpc.d.ts`, `testSockets.d.ts`). Do not hand-edit. |
62
+ | `dist/` | Build output: `dist/_app/` (prod client) or `dist/_app.gen-<id>/` (dev), `dist/app` (compiled binary), `dist/cli-*`. |
56
63
 
57
64
  ## CLI
58
65
 
59
- | Command | Does |
60
- | --- | --- |
61
- | `bunx abide scaffold <name>` | Scaffolds the bundled template, installs it, and (interactive TTY only) starts the dev server; `--no-install` / `--no-dev` opt out |
62
- | `abide dev` | Dev orchestrator: builds the client, runs the server as a child, watches `src/`, rebuilds + restarts on change, live-reloads the browser |
63
- | `abide build` | One-shot client build into `dist/_app` (CI / static deploys) |
64
- | `abide start` | Runs the production server against an already-built `dist/` |
65
- | `abide run <file> [args...]` | Runs any script under the abide preload — same runtime as the server (`.abide` compilation, `abide/*` + `$` alias resolution) |
66
- | `abide compile [--target=…] [--out=…]` | Compiles a standalone server executable (client assets embedded) |
67
- | `abide cli [--target=…] [--out=…] [--platforms=a,b,c]` | Builds the thin CLI binary (rpc manifest baked in) that talks to a remote server or starts a local one; `--platforms` cross-compiles into `dist/cli-thin/<platform>/` |
68
- | `abide bundle` | Assembles a movable, self-contained desktop app bundle (server binary + launcher + webview) for the host platform; unsigned |
69
- | `abide check` | Type-checks every `.abide` component's template + props through its shadow; non-zero exit on errors |
70
- | `abide lsp` | Runs the `.abide` language server over stdio (JSON-RPC) for editor diagnostics |
71
- | `abide init-agent` | Writes/refreshes the CLAUDE.md pointer to this surface map for non-scaffolded projects |
66
+ `abide <command>` (the `abide` bin):
67
+
68
+ | Command | Does |
69
+ | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
70
+ | `abide scaffold <name> [--no-install] [--no-dev]` | Scaffold a project from the bundled template, install it, and (TTY only) start dev. |
71
+ | `abide dev` | Dev orchestrator: build client, spawn the server child, watch `src/`, rebuild + restart on change, browser live-reload. |
72
+ | `abide build` | Single client build into `dist/_app/`, no server (CI / static deploys). |
73
+ | `abide start` | Run the production server against an already-built `dist/`. |
74
+ | `abide run <file> [args...]` | Run an arbitrary script under the abide preload (same runtime as the server); argv after the file is forwarded verbatim. |
75
+ | `abide compile [--target=<bun-…>] [--out=<path>]` | Build a standalone server executable. |
76
+ | `abide cli [--target=…] [--out=…] [--platforms=<a,b,c>]` | Build the thin CLI binary (manifest baked in, ships the compiled server beside it); `--platforms` cross-compiles into `dist/cli-thin/<platform>/`. |
77
+ | `abide bundle` | Assemble a self-contained desktop app bundle for the host platform (`.app` on macOS), unsigned. |
78
+ | `abide check` | Type-check every `.abide` component's template + props through its shadow program; non-zero on error. |
79
+ | `abide lsp` | Run the `.abide` language server over stdio (JSON-RPC) for editor diagnostics. |
80
+ | `abide init-agent` | Write/refresh the abide agent-guide pointer in the project root `CLAUDE.md`. |
72
81
 
73
82
  For tests, add `preload = ["@abide/abide/preload"]` under `[test]` in
74
- `bunfig.toml` and use `bun test`.
83
+ `bunfig.toml` and run `bun test`.
75
84
 
76
85
  ## Authoring contracts
77
86
 
78
- **RPC** the handler receives the schema-validated args
79
- (`InferOutput<schemas.input>`); typed generics on the helper are a compile error
80
- type the parameter, let the body infer. Inside it, `request()` / `cookies()`
81
- / `server()` read the request scope. Return `json(data)` (or `jsonl` / `sse`
82
- for streams, `error` / `redirect`, or a raw `Response`). Options are namespaced
83
- (ADR-0020): `schemas: { input, output, files }` (`files` validates uploaded
84
- `File` parts, kept out of the JSON-Schema projection); `clients: { browser, mcp,
85
- cli }`; `crossOrigin` (exempts a mutating rpc from the same-origin CSRF gate);
86
- `timeout` (handler deadline in ms 504 on every surface, composed into
87
- `request().signal`); `maxBodySize` (per-rpc 413 cap); on read helpers (GET/HEAD)
88
- `cache: { ttl, tags, throttle, debounce, shared }` (the endpoint's
89
- retention/refetch policy) and `stream: { n }` (replay depth). Kind-scoped by type:
90
- `cache`/`stream` on a write is a compile error. Query args on GET/HEAD/DELETE travel as
91
- strings coerce in the schema (`z.coerce.number()`). Beyond scalars, a
92
- type-directed wire codec (ADR-0028/0029) revives a top-level arg field into the
93
- runtime value its declared type names a numeric string → `number`/`bigint`, a
94
- `Date` from an ISO string, a `Set` from a JSON array, a `Map` from an entries
95
- array/object resolved through the warm server program; reviving is fail-open
96
- (an unrevivable value passes through as its JSON form). A body rpc also accepts
97
- a `FormData` in place of typed args (the upload escape hatch): text fields
98
- validate as args, `File` parts validate against `schemas.files`.
99
-
100
- **Consuming an rpc** the bare call `fn(args)` IS the smart read: cached,
101
- coalesced, reactive, stale-while-revalidate for replayable (GET/HEAD) reads.
102
- There are no call-site options (ADR-0020) — all retention/refetch policy is
103
- declared once on the endpoint's `cache`/`stream`. `ttl` defaults to `Infinity`:
104
- an entry is retained for its store's lifetime — the request on the server (a
105
- non-shared read dies with the request), the tab on the client (until
106
- invalidate/refresh); a write coalesces only (ttl 0, the mutation idiom).
107
- `shared` selects the process-level store instead of the request-scoped default
108
- (server); with the default `Infinity` ttl it memoises across requests (an
109
- explicit `ttl` bounds it) for an external endpoint, never per-user data; on
110
- the client it is a no-op (one tab store). A read with no request in flight (e.g.
111
- a background job) resolves against the process-level store and coalesces only
112
- (so it can't leak forever). During SSR the same call any method — resolves
113
- in-process and its value is baked into the HTML so hydration starts warm without
114
- a re-fetch (an inline write seeds too, ADR-0036; only unprompted refetch stays
115
- GET/HEAD) there is no `cache()` wrapper; the bare call carries the caching. Around it:
116
- `fn.raw(args, init?)` returns the raw `Response` (per-call transport options
117
- `signal`, `headers`, `keepalive`, live here); `fn.refresh(args?)`
118
- refetches keeping the stale value visible; `fn.patch(args?, updater)` mutates
119
- the retained value locally (absent on streaming rpcs); `fn.peek(args?)` reads
120
- it synchronously; `fn.pending(args?)` / `fn.refreshing(args?)` are reactive
121
- probes; `fn.error(args?)` is the rpc's last typed error; `fn.watch(args?,
122
- handler)` pipes each resolved value to a handler (client-only; SSR-inert);
123
- `fn.isError(e, kind?)` type-guards a caught error against the rpc's declared
124
- error kinds. A
125
- handler that returns `jsonl()`/`sse()` makes the bare call return a
126
- `NamedAsyncIterable` (`for await` it) — detected at build, nothing to declare;
127
- awaiting a streaming call is a compile error.
128
-
129
- **Typed errors** declare a constructor with
130
- `error.typed(name, status, schema?)` and `return` it from the handler; the
131
- client's `HttpError` then carries `kind` (the name) and `data` (the schema's
132
- payload), narrowed via `rpc.isError`. The framework reserves
133
- `kind: 'validation'` (422, `data: ValidationErrorData`).
134
-
135
- **Socket** `socket<T>(opts)` or `socket({ schema, … })` (with a schema, `T`
136
- infers and publishes validate). Options: `tail` (retained frames, default 1 —
137
- `tail: 0` opts out), `ttl` (retained frames expire lazily after N ms),
138
- `clientPublish` (accept browser/HTTP publishes, off by default), `clients`
139
- (mcp/cli exposure; a schema flips both on by default). The socket IS the
140
- `AsyncIterable` iterating is the live stream, no replay. Members:
141
- `publish(msg)` (isomorphic mirrors Bun's `server.publish()`; server fans out
142
- in-process + to remote subscribers, client sends a validated `pub` frame),
143
- `tail(count?)` (a
144
- subscription seeded with retained frames), `peek()` (latest retained frame),
145
- `refresh()` (drop local frames and re-pull the server tail; server-side
146
- no-op), `watch(handler)` ≡ `watch(socket, handler)` (client-only, SSR-inert),
147
- `pending()` / `refreshing()` / `done()` / `error()` (reactive stream probes),
148
- plus `name` and `clients`.
149
-
150
- **Pages and layouts** `src/ui/pages/blog/[id]/page.abide` serves
151
- `/blog/<id>`; the param arrives as a prop (`const { id } = props()`) and on the
152
- reactive `page.params`. A `[[name]]` folder is an optional segment (the route
153
- matches with or without it; the param is absent when unmatched), `[...rest]`
154
- a catch-all capturing the remaining path (last segment only). One matcher
155
- resolves routes on both sidesat the first position where two matching
156
- patterns differ, literal beats `[name]` beats `[[name]]` beats `[...rest]`. A `layout.abide` wraps every page below its directory
157
- and renders the page at its `{children()}` outlet. Links are plain `<a href>`
158
- (the router intercepts in-app hrefs); build paths with `url()` and navigate
159
- programmatically with `navigate()`.
160
-
161
- **app.ts / config.ts** — `src/app.ts` optionally exports the `AppModule`
162
- hooks: `init({ server })` (boot; may return a cleanup run on SIGINT/SIGTERM),
163
- `handle(request, next)` (single middleware), `handleError(error, request)`,
164
- `health(request)` (fields merged into the `/__abide/health` payload — public
165
- and unauthenticated, keep it cheap), and `forwardHeaders` (extra inbound
166
- header names forwarded onto in-process rpc requests beyond the built-in
167
- auth/identity set). `src/server/config.ts` holds the `env(schema)` call so a
168
- bad environment fails at boot.
169
-
170
- ## `.abide` template grammar
171
-
172
- A component file is: an optional leading `<script>` (imports + author scope),
173
- markup, optional `<style>` blocks. The compiler emits a client build and an
174
- SSR render from the same parse; `abide check` / the LSP type-check the
175
- template through a generated shadow. HTML comments are dropped; a bare
176
- `<template>` is an inert element.
177
-
178
- Reactive state is reached through **imported primitives**, resolved by import
179
- binding (alias-safe) and lowered by the compiler inside a component you read
180
- and write the declared names as plain variables (`{count}`,
181
- `onclick={() => (count += 1)}`); there is no `.value` in `.abide` authoring
182
- and no `$state` sigils. In plain `.ts` modules the same imports are runtime
183
- cells read/written through `.value`.
184
-
185
- | Primitive | Import | Spec |
186
- | --- | --- | --- |
187
- | `state(initial, transform?)` | `@abide/abide/ui/state` | Writable cell. Plain `state(v)` lowers to a serializable doc slot (SSR-resumable); with `transform` the gate runs on every write (`(next, previous) => stored`) |
188
- | `state.computed(fn)` | member of `state` | Read-only derived value, lazy, never serialized |
189
- | `state.linked(fn, transform?)` | member of `state` | Writable cell re-seeded whenever the thunk's dependencies change |
190
- | `state.share(key, value)` / `state.shared(key)` | members of `state` | Put a named value on the ambient scope / read the closest ancestor's |
191
- | `watch(source, handler)` | `@abide/abide/ui/watch` | The single reaction primitive (client-only, stripped from SSR). Sources: a bare thunk `watch(() => …)` (auto-tracked effect), a state cell, a cell array, a socket/stream (`handler(frame)` per frame with reconnect replay), an rpc (`watch(fn, args?, handler)` — runs the smart read, `handler(value)` on each change). Returns a scope-tied disposer |
192
- | `html(str)` / `` html`…` `` | `@abide/abide/ui/html` | Brands trusted raw HTML so `{expr}` inserts nodes instead of escaped text; plain `{value}` always escapes |
193
- | `props()` | `@abide/abide/ui/props` | The prop reader, resolved by import binding (alias-safe) like `state`: `const { name = fallback, ...rest } = props()`; a page/layout's declared props are additive with its route-param shape. `children` is an ordinary declared prop (`const { children } = props<{ children: Snippet }>()`), not ambient |
194
-
195
- Bindings and directives (the attribute kinds `readAttributes` parses):
196
-
197
- | Form | Spec |
198
- | --- | --- |
199
- | `{expr}` | Text interpolation, escaped; a snippet or `html`-branded value mounts as nodes. Type-directed (ADR-0032): a `Promise`/`AsyncIterable`-typed `{expr}` (or an async sub-expression) lifts to a peek-cell — `undefined` while pending (composes with `??`/`?.`), then the resolved value / latest frame; a plain value binds directly |
200
- | `{await expr}` | Explicit **blocking** await renders the awaited value inline during SSR. Valid in every position (ADR-0032): content, an attribute, an `{#if}`/`{#switch}` subject, a `{#for}` source |
201
- | `name={expr}` | Attribute/prop bound to an expression |
202
- | `name="a {expr} b"` | Interpolated attribute a literal `{` in a quoted value always interpolates (write `&lbrace;` for a literal brace) |
203
- | `{...expr}` | Spread props onto a component, attributes onto a native element (rejected on `<template>`) |
204
- | `on<event>={fn}` | Event listener (`onclick`, `onsubmit`, …); on a component it is a checked callback prop |
205
- | `bind:value={cell}` | Two-way input/select/textarea binding. `<input type="number"/"range">` writes back a number; `<select>` re-applies against late-mounting options and `<select multiple>` binds an array of selected values |
206
- | `bind:value={{ get, set }}` | Writable-computed binding: read via `get()`, write via `set(next)` |
207
- | `bind:checked={cell}` / `bind:group={cell}` | Checkbox boolean / radio-group value (SSR emits boolean attributes bare — `checked`, `open`, `selected` on the matching option) |
208
- | `bind:prop={target}` (on a component) | Two-way prop binding — the same `target` forms as an element bind (an lvalue or `{ get, set }`). The child reads `prop` normally; if it writes `prop` or forwards it to another `bind:`, those writes flow back to `target`. Bindability is usage-inferred (no child-side marker): a prop only read stays read-only, and a `bind:prop` whose child never writes is simply one-way |
209
- | `class:name={cond}` | Toggles a class; merges with a reactive `class` base in one effect |
210
- | `style:property={value}` | Sets one style property; merges with a reactive `style` base |
211
- | `attach={fn}` | Runs `fn(element)` at build time; an optional returned teardown runs on dispose |
212
-
213
- Control flow is mustache blocks (`{#…}` open, `{:…}` branch, `{/…}` close —
214
- the close must name its block, and a branch outside its block is a parse
215
- error):
216
-
217
- | Block | Spec |
218
- | --- | --- |
219
- | `{#if cond}…{:else if cond}…{:else}…{/if}` | Conditional chain (the branch keyword is `{:else if}`, with a space) |
220
- | `{#for item, i of list by key}…{:catch e}…{/for}` | Keyed list; `, i` index and `by` key optional; `{#for await item of asyncIterable}` renders rows as they arrive (its `{:catch}` shows the stream error) |
221
- | `{#await p}…{:then v}…{:catch e}…{:finally}…{/await}` | Async block. The branch form streams: SSR flushes the shell and streams the fragment out of order. The head form `{#await p then v}` is blocking — rendered inline (depth-first, serial) during the SSR pass |
222
- | `{#switch subject}{:case match}…{:default}…{/switch}` | Multi-branch on a subject; only branches render — stray content is a compile error |
223
- | `{#try}…{:catch e}…{:finally}…{/try}` | Synchronous error boundary around a build/reactive throw |
224
- | `{#snippet name(args)}…{/snippet}` | Declares a reusable builder, called as an interpolation: `{name(args)}`; a snippet value passes through props like any other value |
225
-
226
- A `Promise`/`AsyncIterable` (or an async sub-expression) lifts to a peek-cell in
227
- **every** position (ADR-0032) — content, an attribute, an `{#if}`/`{#switch}`
228
- subject, a plain `{#for}` source reading `undefined` while pending, so
229
- `{getFoo() ?? 'Loading…'}` shows the fallback, `{#if getFoo()}` takes the else
230
- branch, and a pending `{#for}` renders empty. A leading `await` makes it
231
- SSR-blocking (resolved inline); no `await` streams (pending shell, resolves on the
232
- client). The one rejection is a raw `AsyncIterable` driving a plain `{#for}`
233
- iterate its frames with `{#for await}`.
234
-
235
- Components are capitalised tags (`<Panel prop={x}>…</Panel>`); nested content
236
- becomes the component's `children` prop an ordinary declared prop of type
237
- `Snippet`, read with `const { children } = props<{ children: Snippet }>()`
238
- and called as `{children()}` — the single fill point (`{#if children}
239
- {children()}{:else}…{/if}` for a fallback; there are no named slots and no
240
- `<slot>` element). Slotted content (`<Panel>…</Panel>`) and an explicit
241
- `children={aSnippet}` attribute set the same prop slotted content rides in
242
- as the trailing prop layer, so it wins over an explicit `children` attribute
243
- on the same tag (`mergeProps`, last layer wins per key). A layout's
244
- `{children()}` is the route outlet.
245
-
246
- `<script>` and `<style>` are **not component-root-only**: either may sit
247
- inside a control-flow branch, scoped to that branch. A nested `<script>`
248
- declares branch-local `state` / `state.computed` / `state.linked` the same
249
- imported way (re-seeded per mount; static `import` statements are illegal
250
- thereimports live in the leading `<script>`). A **root** `<style>` is
251
- component-scoped; a nested `<style>` scopes to its sibling subtree only.
252
-
253
- Removed forms throw migration errors at parse time: the `<slot>` element (use
254
- a declared `children: Snippet` prop, called `{children()}`), `<template
255
- name>` snippets (use `{#snippet}`), and all `<template
256
- if/each/await/switch/…>` control flow (use `{#…}` blocks).
257
-
258
- ## Server surface `abide/server/*`
259
-
260
- ### RPC — `@documentation rpc`
261
-
262
- - `@abide/abide/server/GET` — GET rpc helper: `export const x = GET(handler, opts?)` inside `src/server/rpc/`; the bundler rewrites it to the server dispatcher or the browser proxy — calling it outside an rpc module throws.
263
- - `@abide/abide/server/POST` — POST rpc helper (mutating: JSON/FormData body).
264
- - `@abide/abide/server/PUT` PUT rpc helper (mutating).
265
- - `@abide/abide/server/PATCH` PATCH rpc helper (mutating).
266
- - `@abide/abide/server/DELETE` — DELETE rpc helper (mutating; args travel in the query string).
267
- - `@abide/abide/server/HEAD`HEAD rpc helper (read-only).
268
-
269
- ### Responses`@documentation response`
270
-
271
- - `@abide/abide/server/json` — `json(data, init?)`: JSON response with `Cache-Control: no-store` default; `json(undefined)` emits 204 and round-trips back to `undefined`; carries the value type so the rpc's `Return` infers.
272
- - `@abide/abide/server/jsonl` `jsonl(asyncIterable, init?)`: JSON Lines streaming response; consumer cancel flows into the generator's `return`; a generator throw becomes a final `{"$error": message}` line.
273
- - `@abide/abide/server/sse` — `sse(asyncIterable, init?)`: Server-Sent Events response with a 15s keepalive comment; errors emit an `event: error` frame carrying only the message.
274
- - `@abide/abide/server/error``error(status, message?, init?)`: plain-text error response (message defaults to the reason phrase); the caller's await throws `HttpError`. Member `error.typed(name, status, schema?)` declares a reusable typed-error constructor the handler returns (see Authoring contracts).
275
- - `@abide/abide/server/redirect` — `redirect(url, status = 302, init?)`: redirect response accepting relative URLs; 301/302/303/307/308.
276
-
277
- ### Request scope — `@documentation request-scope`
278
-
279
- - `@abide/abide/server/request` `request()`: the inbound `Request` for the in-flight SSR/rpc pass (AsyncLocalStorage); throws outside a request scope.
280
- - `@abide/abide/server/cookies` — `cookies()`: the request's cookie jar (Bun `CookieMap`) — reads parse the inbound header; `set`/`delete` flush as `Set-Cookie` when the handler returns.
281
- - `@abide/abide/server/server` `server()`: the active `Bun.serve` instance; a no-op stand-in during in-process dispatch (CLI/MCP/tests); throws before boot.
282
-
283
- ### Configuration `@documentation configuration`
284
-
285
- - `@abide/abide/server/env` — `env(schema)`: validates `Bun.env` against a Standard Schema at module top level (synchronous; every issue reported at once) and returns the typed config; the schema also projects the bundle's first-run setup form.
286
-
287
- ### Sockets`@documentation sockets`
288
-
289
- - `@abide/abide/server/socket` — `socket<T>(opts?)` / `socket({ schema, tail, ttl, clientPublish, clients })`: declares the broadcast topic inside `src/server/sockets/<name>.ts`; see Authoring contracts for the full `Socket<T>` member surface.
290
-
291
- ### Agent — `@documentation agent`
292
-
293
- - `@abide/abide/server/agent` — `agent(engine, messages)`: runs a provider engine (an `@abide/<provider>` package) against the app's own MCP surface inside an rpc's request scope and returns its `AgentFrame` stream; the handler picks the transport (`jsonl(agent(…))` / `sse(agent(…))`). The module also exports the neutral contract types: `NeutralMessage` (user/assistant/tool turns), `AgentFrame` (`text` deltas, `tool_use`, `tool_result`, `done` with a stop reason), `AgentSurface` (the gated tool/prompt/resource surface), and `AgentEngine` (surface + messages + origin in, frames out).
294
-
295
- ### Server plumbing `@documentation plumbing`
296
-
297
- - `@abide/abide/server/AppModule` — the type of `src/app.ts`'s optional hooks (`init`, `handle`, `handleError`, `health`, `forwardHeaders`).
298
- - `@abide/abide/server/InspectorContext`the capability object core injects into `@abide/inspector` (`loadSurface`, `cacheSnapshot`, `inFlightSnapshot`, `onRecord`, app identity); keeps the inspector a pure consumer.
299
- - `@abide/abide/server/rpc/defineRpc` — `defineRpc(method, url, handler, opts?)`: the server-side construction the bundler rewrites rpc helper calls into — validation, timeout composition, client-flag resolution, registry entry.
300
- - `@abide/abide/server/sockets/defineSocket` — `defineSocket(name, opts?)`: server-side socket construction (retained-tail buffer with lazy TTL eviction, per-subscriber queues, `server.publish` fan-out).
301
- - `@abide/abide/server/prompts/definePrompt` `definePrompt(name, opts)`: registers an MCP prompt; the resolver plugin generates one call per `src/mcp/prompts/<name>.md`.
302
- - `@abide/abide/server/prompts/renderPromptTemplate` `renderPromptTemplate(template, args)`: substitutes `{{name}}` placeholders in a prompt body (missing args collapse to empty).
303
-
304
- ## Isomorphic surface `abide/shared/*`
305
-
306
- ### Cache mutators — `@documentation cache`
307
-
308
- - `@abide/abide/shared/refresh` — `refresh(selector?, args?)`: refetch every cached read matching the selector, keeping the stale value visible until the fresh one swaps in. Selector grammar: `(fn, args)` exact call, `(fn)` every args-variant, `({ tags })` a tagged group, `()` everything. `fn.refresh(args?)` is the pre-bound sugar.
309
- - `@abide/abide/shared/patch` — `patch(fn, args?, updater)` / `patch({ tags }, updater)`: mutate the retained value(s) in place — reactive, no network; the optimistic-update / socket-frame primitive. `fn.patch(…)` is the sugar.
310
-
311
- ### Probes`@documentation probes`
312
-
313
- Probes report, never act reading one opens no fetch and no stream.
314
-
315
- - `@abide/abide/shared/pending` — `pending(selector?, args?)`: reactive "no value yet" probe over calls and streams (global, per-rpc, per-call, tagged, per-subscribable).
316
- - `@abide/abide/shared/refreshing``refreshing(selector?, args?)`: "holding a value while a fresher one is in flight" — the SWR reload / stream-reconnect badge.
317
- - `@abide/abide/shared/peek` — `peek(fn, args?)` / `peek(socket)`: the retained value (or latest frame), synchronously, `T | undefined`; reactive inside a tracking scope.
318
- - `@abide/abide/shared/done` — `done(subscribable)`: true once a stream closed (stream-only; a cache read's "done" is `!pending && !refreshing`).
319
- - `@abide/abide/shared/online` — `online()`: reactive connectivity probe — browser `online`/`offline` events; server-side it reflects the *calling client's* reported connectivity (always true during SSR and outside a scope).
320
-
321
- ### Errors — `@documentation response`
322
-
323
- - `@abide/abide/shared/HttpError` — thrown by rpc calls on non-2xx; carries `status`, `statusText`, the raw `response`, and — for typed/validation errors — `kind` + `data`.
324
- - `@abide/abide/shared/ValidationErrorData` — the `data` shape of a `kind: 'validation'` failure: the raw Standard Schema `issues` plus a `fields` (field → first message) map.
325
-
326
- ### Schema projection`@documentation rpc`
327
-
328
- - `@abide/abide/shared/withJsonSchema` — `withJsonSchema(schema, toJsonSchema)`: attaches the `toJSONSchema()` projection to a Standard Schema whose library lacks one, feeding OpenAPI, MCP, CLI help, and the bundle setup form.
329
-
330
- ### Observability`@documentation observability`
331
-
332
- - `@abide/abide/shared/health` — `health()`: reactive backend health — `{ reachable, abide, name, version, …app health-hook fields }`, polled from `/__abide/health` only while a tracking scope reads it; SSR-seeded so hydration starts warm; constant `{ reachable: true }` on the server. The `AppHealth`/`AppHealthMap` types augment from the generated `health.d.ts`.
333
- - `@abide/abide/shared/reachable` — `await reachable(host?)`: outbound reachability, same callable both sides. The first call probes (HEAD) and starts a TTL background poll; later calls answer instantly off the warm value. Any completed HTTP response counts as reachable. No host asks about the app's own backend: constant true on the server and on a loopback origin (dev, desktop bundle — works offline); a deployed origin probes like any host. The browser probes no-cors and composes `navigator.onLine` in at read time (loopback exempt). Tuned by `ABIDE_REACHABLE_TTL` / `ABIDE_REACHABLE_TIMEOUT` (server env; the browser runs the defaults).
334
- - `@abide/abide/shared/log` — the unified logger: `log(...)` / `.warn` / `.error` / `.trace` on the app's always-on channel, every record carrying request-scope context (short trace id, +elapsed, method+path); member `log.channel(name)` returns the same shape on a DEBUG-gated diagnostic channel. Renders tsv (default) or JSON per `ABIDE_LOG_FORMAT`.
335
- - `@abide/abide/shared/trace``trace()`: the current request's W3C `traceparent` (client-side: the trace of the request that rendered the page), or undefined outside any scope.
336
-
337
- ### Page`@documentation page`
338
-
339
- - `@abide/abide/shared/page` — the reactive page proxy: `page.route`, `page.params`, `page.url` (browser-space on both sides, mount base included), `page.navigating`; isomorphic, re-runs readers across navigations.
340
-
341
- ### URL`@documentation url`
342
-
343
- - `@abide/abide/shared/url` — `url(path, params?/args?)`: resolves any in-app URL to its base-correct form — a page route literal interpolates its `[name]` / `[[name]]` / `[...rest]` params (typed via `PathParams`; an absent optional drops its segment), a GET rpc path serializes typed args to the query, anything else is base-prefixed. Also exports the augmentable `RpcRoutes` / `PageRoutes` / `PublicAssets` maps and the `PathParams<P>` type.
344
-
345
- ### Templating `@documentation templating`
346
-
347
- - `@abide/abide/shared/snippet``snippet(payload)`: brands a snippet payload so a `{expr}` interpolation mounts it (client: a DOM builder; server: the rendered string); the compiler wraps `{#snippet}` bodies in this. Also exports the `Snippet<Args>` type — a callable `(...args: Args) => SnippetValue`, generic over its call arguments (`children` is `Snippet`, invoked `children()`; a row snippet is `Snippet<[Item]>`, invoked `row(item)`) — plus `SnippetValue` (the internal payload brand) and `snippetPayload(value)` (a branded value's payload, or undefined for plain values).
348
-
349
- ### Shared plumbing`@documentation plumbing`
350
-
351
- - `@abide/abide/shared/createSubscriber` `createSubscriber(start)`: open-on-first-tracked-read / close-on-last-reader resource lifecycle grounded in the signal core; the substrate under `health()`, `online()`, and the tail probes.
352
-
353
- ## UI surface `abide/ui/*` (client-only)
354
-
355
- ### Reactive state `@documentation reactive-state`
356
-
357
- - `@abide/abide/ui/state` — the `state` primitive: `state(initial, transform?)` writable cell, `state.computed(fn)` read-only derived, `state.linked(fn, transform?)` writable-reseeded, `state.share(key, value)` / `state.shared(key)` ambient context. In `.abide` files the compiler lowers reads/writes to plain variable syntax; in `.ts` the cell is read/written through `.value`.
358
- - `@abide/abide/ui/watch` — `watch(source, handler)`: the single reaction primitive over a thunk, cell, cell array, socket/stream, or rpc (see the grammar table). Client-only; the compiler strips author calls from SSR, and the `socket.watch` / `fn.watch` instance sugar is SSR-inert.
359
- - `@abide/abide/ui/props` — `props<T>()`: the prop reader, resolved by import binding (alias-safe) like `state`; a required import — there is no ambient `props()`. Destructure declared props off it (`const { name, ...rest } = props<T>()`); a page/layout's declared `T` is additive with its auto-typed route-param shape. `children` is an ordinary declared prop, not ambient: `const { children } = props<{ children: Snippet }>()` (`Snippet` from `@abide/abide/shared/snippet`).
360
-
361
- ### Templating`@documentation templating`
362
-
363
- - `@abide/abide/ui/html` — `html(string)` / `` html`…` ``: brands trusted raw HTML for unescaped interpolation; the tag does not escape its interpolations — only feed it values you trust.
364
-
365
- ### Navigate — `@documentation navigate`
366
-
367
- - `@abide/abide/ui/navigate` — `navigate(path, params?, options?)`: typed programmatic navigation off the route map; params interpolate through `url()` (base-correct). Options `{ replace, keepScroll }`. The module also exports `navigatePath(path, options?)` (already-resolved paths — the router's own entry, no re-basing) and the `NavigateOptions` type.
368
-
369
- ### UI plumbing`@documentation plumbing`
370
-
371
- Compiler/runtime machinery published so generated code, the type shadow, and
372
- tests can import it, not for app code.
373
-
374
- - `@abide/abide/ui/effect` — `effect(fn)`: the raw auto-tracked effect the compiler emits for bindings; authors use `watch`. Returns a disposer; SSR strips author calls.
375
- - `@abide/abide/ui/currentScope` — `scope()`: the ambient lexical scope — the internal lowering host for `state`/`effect` (`derive`/`linked`/`effect`/`share` land here).
376
- - `@abide/abide/ui/enterRenderScope` `enterScope()`: opens an isolated scope for an SSR render; returns the previous scope to restore.
377
- - `@abide/abide/ui/exitRenderScope` `exitScope(previous)`: restores the scope `enterScope` saved.
378
- - `@abide/abide/ui/router` — `router(...)`: the client router fills layout/page chains into comment-marker outlet boundaries, intercepts in-app links, buckets/restores scroll per history entry.
379
- - `@abide/abide/ui/startClient` — `startClient(...)`: the client entry reads every `__SSR__` field into its shared slot (cache seed, health seed, client timeout, resume manifest), hydrates the chain, starts the router.
380
- - `@abide/abide/ui/renderToStream` — `renderToStream(render)`: out-of-order SSR streaming — shell first, then one `<abide-resolve>` fragment per streaming await block in completion order; blocking (`then`-head) awaits render inline.
381
- - `@abide/abide/ui/remoteProxy``remoteProxy(method, url, opts?)`: the browser-side rpc stub the bundler emits (fetch, decode, HttpError, streaming); the `RemoteProxyOptions` type rides along.
382
- - `@abide/abide/ui/socketProxy` — `socketProxy(name)`: the browser-side socket stub — the identical `Socket<T>` shape over the page's lazily-opened multiplexed ws channel.
383
- - `@abide/abide/ui/runtime/escapeKey` — JSON-Pointer-escapes one reactive-doc path key (`~`→`~0`, `/`→`~1`).
384
- - `@abide/abide/ui/runtime/withPath` pushes one `escapeKey`-escaped render-path segment for the duration of a synchronous `build`, relative to the ambient path (the render-path identity a layout layer / child mount composes); restores after. A reactively-rebuilt block uses `withPathFrom` with a captured base instead.
385
- - `@abide/abide/ui/runtime/renderPath` the render-path a `<Child/>` mounts under: composes a child's ordinal onto the ambient path (`withPath(ordinal, …)`) to produce the `abide:await:CHILDPATH` boundary id, computed identically on both sides so the streamed-child adopter never drifts. Server-emit-only.
386
- - `@abide/abide/ui/runtime/blockId` — allocates an await/try block id namespaced by the ambient render-path (`${path}:${n}`, per-path document-order counter), so sibling child renders can run concurrently during SSR without their block ids interleaving; the bare (`path === ''`) case keeps the plain `0,1,2…` form.
387
- - `@abide/abide/ui/runtime/nextBlockId`the next await/try block id in the current render pass (document order, shared across inlined children).
388
- - `@abide/abide/ui/runtime/enterRenderPass` — marks entry into a render/mount; the outermost resets the block-id counter.
389
- - `@abide/abide/ui/runtime/exitRenderPass` — unwinds `enterRenderPass`'s depth.
390
- - `@abide/abide/ui/dom/mount` mounts a top-level page/layout into a host under an ownership scope; returns the unmount.
391
- - `@abide/abide/ui/dom/mountChild` — mounts a nested child component as a comment-marker range (dev builds also register it with the hot bridge).
392
- - `@abide/abide/ui/dom/mountStreamedChild` — the client mount for a HOISTABLE child (ADR-0039): a dual-mode adopter that probes the hydration cursor to tell whether the server inlined the child (settled) or streamed it (`abide:await:CHILDPATH` boundary, already swapped in), then adopts the range in place — falling back to a create-mount on a client navigation or an unfilled boundary. Registers with the hot bridge like `mountChild`.
393
- - `@abide/abide/ui/dom/mountSlot`mounts a component's passed-children content as a marker-bounded range.
394
- - `@abide/abide/ui/dom/outlet` — a layout's outlet: an empty `<!--abide:outlet-->…<!--/abide:outlet-->` boundary the router fills.
395
- - `@abide/abide/ui/dom/hydrate` — adopts server-rendered DOM instead of rebuilding: runs the build with a claim cursor over the existing nodes.
396
- - `@abide/abide/ui/dom/skeleton` the parsed-once static-structure clone path every bound element builds through; element holes by path, blocks by anchor comments.
397
- - `@abide/abide/ui/dom/anchorCursor` — positions a skeleton-anchored block/slot at its `<!--a-->` anchor, in clone and hydrate modes alike.
398
- - `@abide/abide/ui/dom/cloneStatic` appends a fully-static subtree (no bindings, control flow, or listeners) by cloning.
399
- - `@abide/abide/ui/dom/appendStatic` — a static text node: created (create mode) or claimed from server-rendered text (hydrate mode).
400
- - `@abide/abide/ui/dom/appendText` a reactive `{expr}` text node under a parent.
401
- - `@abide/abide/ui/dom/appendTextAt` — a reactive text node mounted at a skeleton anchor (text interleaved with element siblings).
402
- - `@abide/abide/ui/dom/appendSnippet` — mounts a `{snippet(args)}` interpolation's builder into a marker-bounded range.
403
- - `@abide/abide/ui/dom/attr` — binds an element attribute to a read (boolean true → bare attribute, false/nullish removed).
404
- - `@abide/abide/ui/dom/on` attaches an event listener whose removal is registered with the ownership scope.
405
- - `@abide/abide/ui/dom/attach` runs an `attach={fn}` attachment and registers its optional teardown.
406
- - `@abide/abide/ui/dom/bindSelectValue` — two-way `<select>` binding that re-applies the selection when the option set changes (late-mounting `{#for}`/async options; `multiple` binds an array).
407
- - `@abide/abide/ui/dom/each` keyed `{#for}` runtime: marker-bounded rows reconciled by key.
408
- - `@abide/abide/ui/dom/eachAsync` `{#for await}` runtime: rows append/reconcile as the AsyncIterable yields.
409
- - `@abide/abide/ui/dom/when` — `{#if}` runtime (single-branch swap in a marker-bounded range).
410
- - `@abide/abide/ui/dom/switchBlock` `{#switch}` runtime (also `{#if}` chains with `{:else if}` branches).
411
- - `@abide/abide/ui/dom/awaitBlock` — `{#await}` runtime: pending → resolved/error branch swap, teardown-generation guarded.
412
- - `@abide/abide/ui/dom/tryBlock` — `{#try}` runtime: synchronous error boundary around a subtree build.
413
- - `@abide/abide/ui/dom/mergeProps` composes a child's props from explicit thunk runs, spread layers, and the trailing children layer.
414
- - `@abide/abide/ui/dom/spreadProps` wraps a `{...source}` spread layer so every key resolves to a live value thunk.
415
- - `@abide/abide/ui/dom/restProps` — the live unconsumed-props object behind `const { …, ...rest } = props()`.
416
- - `@abide/abide/ui/dom/bindProp` — the parent half of a component `bind:prop`: annotates a prop's value thunk with a `set` write-back channel.
417
- - `@abide/abide/ui/dom/bindableProp` — the child half: the writable cell a component gets for a prop it writes or forwards (pass-through to the parent when bound, a local reseeding cell when not).
418
- - `@abide/abide/ui/dom/spreadAttrs` spreads an object's keys onto a native element (`<div {...rest}>`), keys enumerated once.
419
- - `@abide/abide/ui/dom/mutateDocContainer` — the lowering for an in-place mutating container method on a reactive doc (`model.items.splice(…)`, `.sort()`, a Set `.add()`, a Map `.set()`, …): clones the array/Map/Set, applies the mutation to the copy, and writes it back through `replace` so a real patch fires (readers wake, the render tree re-derives); returns the native method's result unchanged.
420
- - `@abide/abide/ui/dom/readCall` guarded method call on a reactive-doc read (the `model.draft.trim()` lowering).
421
- - `@abide/abide/ui/dom/readCell` — unified read for a `linked`/async-`computed` reference (the `$$readCell(NAME)` lowering): peeks an async cell, reads `.value` off a sync one.
422
- - `@abide/abide/ui/dom/cellPending` — whether a control-flow subject (`{#if}`/`{#switch}`) is a still-loading async cell (no value, no error) so the block renders no branch while pending instead of flashing its `{:else}`; a plain/settled value is never pending.
423
- - `@abide/abide/ui/settleAsyncCells` — the SSR Tier-2 await-barrier (the `await $$settleAsyncCells()` lowering emitted between a component's cell declarations and its template): drains + awaits the request-scoped in-flight async-cell promises so their resolved values bake into the first-pass HTML.
424
- - `@abide/abide/ui/flight` the SSR flight-starter (`$$flight(() => expr)`): hoists a hoistable await's promise into the synchronous render prefix so independent flights overlap instead of serializing; normalises a synchronous loader throw to a rejected promise and carries a synchronous `.settled` snapshot for `finalizeStreamedChildren`. Server-emit-only.
425
- - `@abide/abide/ui/isolateCellBarrier` — runs a hoisted child render (`$$isolateCellBarrier`) under its own async-cell barrier list (ALS-backed on the server) so its cell registrations and `$$settleAsyncCells` drain stay isolated from concurrent siblings and the page; an inert passthrough on the client. Server-emit-only.
426
- - `@abide/abide/ui/finalizeStreamedChildren` — the ADR-0039 when-to-stream decision run once after a component's body walk (`await $$finalizeStreamedChildren(...)`): fills each hoistable child's reserved output slot — inlining a settled flight byte-identically to the pre-ADR path, rethrowing a rejected one, or emitting an `abide:await:CHILDPATH` boundary + streaming `SsrAwait` for a still-pending one. Server-emit-only.
87
+ **RPC** (`src/server/rpc/<name>.ts`). `export const x = METHOD(handler, opts?)`.
88
+ The handler receives the validated args `StandardSchemaV1.InferOutput<schemas.input>`
89
+ when a schema is present, otherwise its own declared first parameter (or `undefined`
90
+ for a nullary handler); you never pass `<Args, Return>` call generics. It reaches
91
+ request context via `request()` (the inbound `Request`) and `cookies()` (the jar),
92
+ and returns a `Response` canonically `json(...)` (success body → the caller's
93
+ `Return`), `jsonl(...)`/`sse(...)` (streaming), `error(...)` / `error.typed(...)()`
94
+ (non-2xx, body typed `never`), `redirect(...)`, or a hand-built `Response`
95
+ (`Return` falls back to `unknown`). Typed errors are inferred from the
96
+ `error.typed(...)` branches a handler returns there is no `errors:` option.
97
+
98
+ `opts` (`RpcSharedOpts`, all optional): `schemas: { input?, output?, files? }`
99
+ (the ADR-0020 namespace `input` validates args and drives their type, `output`
100
+ is the success-body schema for OpenAPI 200 / MCP `outputSchema` and never drives
101
+ arg inference, `files` validates multipart File parts and merges them into the
102
+ args bag); `clients: { browser?, mcp?, cli? }` (surface-exposure flags);
103
+ `crossOrigin` (exempt a mutating RPC from the same-origin CSRF gate); `maxBodySize`
104
+ (pre-parse body-byte cap, 413 past it); `timeout` (per-RPC handler deadline in ms,
105
+ a 504 on every surface, composed into `request().signal`). Read helpers (GET/HEAD)
106
+ additionally accept `cache` (`ttl`/`tags`/`throttle`/`debounce`/`shared`) and
107
+ `stream` (replay depth); these are a compile error on the mutating helpers. There
108
+ is **no** `outbox` option. Query/path/form args auto-coerce from the endpoint's
109
+ typed shape (ADR-0028 build-time plan) no `z.coerce` needed; a value that will
110
+ not parse stays a string so the schema raises an honest 422.
111
+
112
+ Consume forms (`RemoteFunction`): the bare `fn(args)` **is** the smart read —
113
+ cached, coalesced, SWR-reactive; decodes by Content-Type, throws `HttpError` on
114
+ non-2xx. There is no call-site options argument on the bare call. Members:
115
+ `fn.raw(args, opts?)` (raw `Response`, no decode/throw), `fn.refresh(args?)`
116
+ (refetch keeping the stale value visible), `fn.patch(...)` (in-place cache
117
+ mutation; fetch-only, absent on streaming RPCs), `fn.peek(args?)` (retained value,
118
+ sync), `fn.pending(args?)`, `fn.refreshing(args?)`, `fn.error(args?)` (this RPC's
119
+ last typed error), `fn.isError(caught, 'name')` (typed guard), and client-only
120
+ `fn.watch(handler)` / `fn.watch(args, handler)`. A streaming handler
121
+ (`jsonl`/`sse`) makes the bare call return a `NamedAsyncIterable<Frame>`
122
+ synchronously `for await (… of fn(args))`, never `await`.
123
+
124
+ **Socket** (`src/server/sockets/<name>.ts`). `export const x = socket(opts?)`.
125
+ `opts` (`SocketOptions`): `tail` (retention count kept frames for late joiners
126
+ / reconnects; server default 1), `ttl` (evict retained frames older than N ms,
127
+ lazy), `clientPublish` (allow publishes over the wire; off by default), `schema`
128
+ (validate publish payloads; flips mcp/cli `clients` on), `clients`. A `Socket<T>`
129
+ extends `AsyncIterable<T>` (bare `for await` is the live stream, no replay) with
130
+ `publish(frame)`, `tail(count?)` (subscription seeded from the retained tail),
131
+ `peek()`, `pending()`/`refreshing()`/`done()`/`error()`, `refresh()`, and
132
+ `watch(handler)`. HTTP face at `/__abide/sockets/<name>`: `GET` reads the retained
133
+ tail, `POST` publishes (only when `clientPublish`).
134
+
135
+ **Page / layout** (`src/ui/pages/**`). `page` (isomorphic `PageSnapshot` proxy)
136
+ exposes `route`, `params`, `url` (browser-space `URL`, base-prefixed), and
137
+ `navigating`; read a field inside an effect/derived and it re-runs on navigation.
138
+ `url(path, params?, query?)` builds base-correct links; `navigate(path, …)`
139
+ performs typed in-app SPA navigation (params first for `[name]` routes, then
140
+ `{ replace?, keepScroll? }`). A layout renders the active page via `{children()}`.
141
+
142
+ **`app.ts` / `config.ts`.** `app.ts` default- or named-exports the `AppModule`
143
+ hooks (all optional; `init({ server })` may return a cleanup run on
144
+ SIGINT/SIGTERM; `handle(request, next)` is single middleware; `health(request)`
145
+ merges into `/__abide/health`, runs before `handle`, and is public). `config.ts`
146
+ exports `config` `env(schema)` (validated, typed, throws on bad config at boot)
147
+ or the unvalidated `Bun.env` floor.
148
+
149
+ **The isomorphism move.** There is no `cache()` wrapper. A bare smart RPC call
150
+ read inline during SSR is captured in the per-request cache; the runtime
151
+ snapshots each settled entry into a wire-safe form serialized into the HTML, and
152
+ the client seeds its store from it on hydration — the same call hydrates warm
153
+ instead of re-firing. Streaming reads are snapshotted again after the stream
154
+ drains and seeded over the wire.
155
+
156
+ ## .abide template grammar
157
+
158
+ A `.abide` component is HTML with a leading `<script>` (its component script);
159
+ `<script>` and `<style>` may also sit **inside a control-flow branch**, scoped to
160
+ that branch (a nested `<script>` declares branch-local `state`/`state.computed`/
161
+ `state.linked`, re-seeded per mount, and takes **no** module imports — imports
162
+ live only in the leading script; a nested `<style>` scopes to its sibling
163
+ subtree). A _root_ `<style>` is component-scoped. Reactive primitives are reached
164
+ through their own imported bindings (alias-safe) `state` from `abide/ui/state`,
165
+ `watch` from `abide/ui/watch`, `html` from `abide/ui/html`, `snippet` from
166
+ `abide/shared/snippet`, `props` from `abide/ui/props` never through `scope()`
167
+ (internal plumbing). Every one of these, `props()` included, is a required import:
168
+ a missing one surfaces as `Cannot find name '…'`.
169
+
170
+ Reactive state:
171
+
172
+ | Form | Meaning |
173
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
174
+ | `state(initial, transform?)` | Writable cell; read/write via `.value`; `transform(next, prev)` gates writes. |
175
+ | `state.computed(fn)` | Read-only cell derived from other cells (lazy, never serialized). |
176
+ | `state.linked(fn, transform?)` | Writable cell reseeded when the thunk's deps change. |
177
+ | `watch(source, handler)` | The single reaction primitive: over a cell, a cell array, a socket/stream, or an RPC; bare `watch(thunk)` is an auto-tracked effect. Client-only. |
178
+ | `props()` | Ambient prop reader: `const { name = fallback, ...rest } = props()`. |
179
+
180
+ Bindings and directives (attribute kinds `event` / `bind` / `class` / `style` /
181
+ `attach` / spread, plus plain `expression` / static):
182
+
183
+ | Form | Meaning |
184
+ | -------------------------------------------- | ---------------------------------------------------------------------------- |
185
+ | `{expr}` | Reactive text (escaped); an `html`-branded value inserts unescaped raw HTML. |
186
+ | `name={expr}` | Reactive attribute. |
187
+ | `on<event>={fn}` | Event listener (`onclick`, `oninput`, `onsubmit`, …). |
188
+ | `bind:value` / `bind:checked` / `bind:group` | Two-way form binds. |
189
+ | `bind:value={{ get, set }}` | Derived two-way binding. |
190
+ | `class:name={cond}` | Toggle a class. |
191
+ | `style:property={value}` | Set one style property. |
192
+ | `attach={fn}` | Run `fn(element)` at mount; its return is the teardown. |
193
+ | `{...spread}` | Spread an object's keys as attributes (element) or props (component). |
194
+
195
+ Control flow mustache `{#…}` blocks (NOT `<template>`):
196
+
197
+ | Block | Form |
198
+ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
199
+ | Conditional | `{#if}` / `{:else if}` / `{:else}` / `{/if}` |
200
+ | Keyed list | `{#for item, i of list by key}` / `{/for}` |
201
+ | Async list | `{#for await item of source}` / `{/for}` (over an `AsyncIterable`) |
202
+ | Promise | bare peek `{fn()}` is the default (below); `{#await p}` / `{:then v}` / `{:catch e}` / `{:finally}` / `{/await}` is the explicit opt-in |
203
+ | Switch | `{#switch subject}` / `{:case v}` / `{:default}` / `{/switch}` |
204
+ | Error boundary | `{#try}` / `{:catch}` / `{:finally}` / `{/try}` |
205
+ | Snippet | `{#snippet name(args)}…{/snippet}`, called `{name(args)}` |
206
+
207
+ Reading async data does **not** require `{#await}`. A promise/async-cell-typed
208
+ `{expr}` is a **peek** — `undefined` while pending (auto-streamed on SSR), composing
209
+ with `?? fallback` / `?.` / `{#if}` / attributes in every position (ADR-0032), paired
210
+ with the `.pending()` / `.error()` probes for affordances; `{await expr}` blocks SSR
211
+ inline (the shorthand for a `{#await then}` head). The `{#await}` block above is the
212
+ opt-in reserved for a distinct pending branch, a local `{:catch}`, or `{:then}`
213
+ type-narrowing ADR-0019 narrowed its authored role to exactly that.
214
+
215
+ Components are capitalised tags; content nested in them renders where the
216
+ component calls `{children()}` (`{#if children}{children()}{:else}…{/if}` is the
217
+ fallback). The `<slot>` element, the `<template name>` snippet form, and
218
+ `<template if>` / `<template each>` / control flow were **removed** a bare
219
+ `<template>` is now an inert element, and any removed form throws a migration
220
+ error. The branch keyword is `{:else if}` (a space).
221
+
222
+ ## Server surface abide/server/*
223
+
224
+ ### RPC helpers — @documentation rpc
225
+
226
+ - `@abide/abide/server/GET` declares a read (GET) RPC; accepts `RpcReadOpts`
227
+ (shared opts + `cache`/`stream`); query args. Bundler-rewritten; calling the
228
+ bare helper throws.
229
+ - `@abide/abide/server/POST` declares a mutating (POST) RPC; `RpcSharedOpts`
230
+ only (`cache`/`stream` are a compile error); JSON-body (or FormData) args.
231
+ - `@abide/abide/server/PUT` mutating PUT RPC; body args; `RpcSharedOpts`.
232
+ - `@abide/abide/server/PATCH` mutating PATCH RPC; body args; `RpcSharedOpts`.
233
+ - `@abide/abide/server/DELETE` mutating DELETE RPC; query args; `RpcSharedOpts`.
234
+ - `@abide/abide/server/HEAD` — read HEAD RPC alongside GET; query args;
235
+ `RpcReadOpts`.
236
+
237
+ ### Responses@documentation response
238
+
239
+ - `@abide/abide/server/json` `json(data, init?)`: JSON `TypedResponse<T>` with
240
+ RPC defaults (`no-store`), wire-encoding Set/Map/bigint/Date; `json(undefined)`
241
+ → 204. `T` drives `Return` inference.
242
+ - `@abide/abide/server/jsonl` `jsonl(iterable, init?)`: wraps an
243
+ `AsyncIterable<Frame>` as `application/jsonl` (one JSON value per line); a
244
+ generator error emits a final `{"$error":…}` line.
245
+ - `@abide/abide/server/sse` `sse(iterable, init?)`: wraps an
246
+ `AsyncIterable<Frame>` as `text/event-stream` with 15s keepalive comments;
247
+ errors emit an `event: error` frame.
248
+ - `@abide/abide/server/error` `error(status, message?, init?)`:
249
+ `text/plain` `TypedResponse<never>`. `error.typed(name, status, schema?)`
250
+ declares a reusable typed-error constructor driving `fn.isError(e, 'name')`.
251
+ - `@abide/abide/server/redirect` `redirect(url, status=302, init?)`:
252
+ `TypedResponse<never>`, accepts relative URLs, `no-store`, status restricted to
253
+ 301/302/303/307/308.
254
+
255
+ ### Request scope @documentation request-scope
256
+
257
+ - `@abide/abide/server/request` `request(): Request` the in-flight inbound
258
+ request (ALS-scoped); throws outside a request scope.
259
+ - `@abide/abide/server/cookies` `cookies(): Bun.CookieMap` the request's
260
+ cookie jar; reads parse `Cookie`, writes flush as `Set-Cookie` on return.
261
+ - `@abide/abide/server/server` — `server(): Bun.Server` — the active server; a
262
+ no-op in-process server for CLI/MCP/test dispatch; throws before init.
263
+
264
+ ### Configuration @documentation configuration
265
+
266
+ - `@abide/abide/server/env` — `env(schema)`: validate `Bun.env` against a Standard
267
+ Schema at module top level (synchronous; all issues at once) and return the
268
+ typed config; also registers the schema for the launcher setup form.
269
+
270
+ ### Sockets — @documentation sockets
271
+
272
+ - `@abide/abide/server/socket` — `socket(opts?)` / `socket({ schema })` declares a
273
+ broadcast topic returning `Socket<T>`; opts `tail`/`ttl`/`clientPublish`/
274
+ `schema`/`clients` (server-only; the client stub discards them).
275
+
276
+ ### Agent@documentation agent
277
+
278
+ - `@abide/abide/server/agent``agent(engine, messages): AsyncIterable<AgentFrame>`
279
+ runs a provider `AgentEngine` against the current request's MCP surface
280
+ (forwarding caller auth); the handler picks transport via `jsonl`/`sse`. Exports
281
+ `NeutralMessage`, `AgentFrame`, `AgentSurface`, `AgentEngine` types.
282
+
283
+ ### Server plumbing @documentation plumbing
284
+
285
+ - `@abide/abide/server/AppModule` — type of the optional `src/app.ts` hooks
286
+ (`forwardHeaders`/`init`/`handle`/`handleError`/`health`).
287
+ - `@abide/abide/server/InspectorContext` — type of the capability object core
288
+ injects into `@abide/inspector` when `ABIDE_ENABLE_INSPECTOR=true`.
289
+ - `@abide/abide/server/rpc/defineRpc` — bundler-emitted RPC builder: resolves
290
+ `clients`, validates input/files, applies `timeout`, registers the entry.
291
+ - `@abide/abide/server/sockets/defineSocket` — bundler-emitted socket builder:
292
+ per-subscriber queue + retained tail, optional `ttl`/`schema`, Bun-native
293
+ fan-out.
294
+ - `@abide/abide/server/prompts/definePrompt` — resolver-emitted prompt builder
295
+ from `src/mcp/prompts/<name>.md`; registers with the MCP dispatcher.
296
+ - `@abide/abide/server/prompts/renderPromptTemplate`substitutes `{{name}}`
297
+ placeholders in a prompt template body (missing args → empty string).
298
+
299
+ ## Isomorphic surface — abide/shared/*
300
+
301
+ ### RPC schema projection — @documentation rpc
302
+
303
+ - `@abide/abide/shared/withJsonSchema` — `withJsonSchema(schema, toJsonSchema)`
304
+ attaches a `toJSONSchema()` projection to a Standard Schema whose library lacks
305
+ one native (feeds OpenAPI / MCP / CLI / setup form).
306
+
307
+ ### Error responses @documentation response
308
+
309
+ - `@abide/abide/shared/HttpError` — the error class thrown by a remote call on
310
+ non-2xx: `status`, `statusText`, raw `response`, optional `kind`/`data` (set for
311
+ a typed error or a 422 validation failure).
312
+ - `@abide/abide/shared/ValidationErrorData` — type of `HttpError.data` when
313
+ `kind === 'validation'`: `{ issues, fields }` (raw Standard Schema issues + a
314
+ field→first-message map).
315
+
316
+ ### Cache mutation — @documentation cache
317
+
318
+ - `@abide/abide/shared/patch` — `patch(fn, args?, updater)` / `patch({tags},
319
+ updater)`: reactively mutate the retained value of matching cached reads in
320
+ place, no network the optimistic-update / real-time primitive.
321
+ - `@abide/abide/shared/refresh` — `refresh(selector?, args?)`: refetch every
322
+ matching cached read, keeping the stale value visible (`refreshing()` true)
323
+ until fresh swaps in.
324
+
325
+ ### Page@documentation page
326
+
327
+ - `@abide/abide/shared/page` — `page`: isomorphic reactive `PageSnapshot` proxy
328
+ (`route`/`params`/`url`/`navigating`); reading a field in a tracking scope
329
+ re-runs on navigation.
330
+
331
+ ### Probes — @documentation probes
332
+
333
+ - `@abide/abide/shared/pending` — `pending(source?, args?): boolean` reactive
334
+ "no value yet" over cached calls and tail streams.
335
+ - `@abide/abide/shared/peek` `peek(source, args?)` the currently-retained
336
+ value synchronously, triggering nothing; `undefined` when nothing retained.
337
+ - `@abide/abide/shared/refreshing` — `refreshing(source?, args?): boolean`
338
+ reactive "holding a value while a fresher source is in flight".
339
+ - `@abide/abide/shared/done``done(subscribable): boolean` — reactive terminal
340
+ read: true once a stream closed.
341
+ - `@abide/abide/shared/online` — `online(): boolean` reactive connectivity
342
+ probe (browser online/offline; server reflects the caller's reported state).
343
+
344
+ ### URL@documentation url
345
+
346
+ - `@abide/abide/shared/url``url(path, ...args): string` resolves any in-app URL
347
+ base-correctly (RPC query, page params, or asset); external paths pass through.
348
+ Exports `PathParams` and augmentable `RpcRoutes`/`PageRoutes`/`PublicAssets`.
349
+
350
+ ### Templating@documentation templating
351
+
352
+ - `@abide/abide/shared/snippet` — `snippet(payload)` brands a snippet payload so a
353
+ `{expr}` interpolation mounts it (the compiler wraps a `{#snippet}` body); also
354
+ exports `SnippetValue` and `Snippet<Args>` types.
355
+
356
+ ### Observability@documentation observability
357
+
358
+ - `@abide/abide/shared/health` `health(): HealthState` reactive backend-health
359
+ read (reachability + the app's `health()` fields), reader-driven poll of
360
+ `/__abide/health`; composes `navigator.onLine`.
361
+ - `@abide/abide/shared/log` — `log`: the unified request-scope-aware logger
362
+ (`log(...)`, `.warn`/`.error`/`.trace`, `.channel(name)` for a DEBUG-gated
363
+ channel); TSV by default, JSON under `ABIDE_LOG_FORMAT=json`.
364
+ - `@abide/abide/shared/reachable``reachable(host?): Promise<boolean>` —
365
+ isomorphic outbound reachability HEAD probe, cached per TTL; any response counts
366
+ reachable, only connection failure/timeout is not.
367
+ - `@abide/abide/shared/trace` — `trace(): string | undefined` the current
368
+ request's W3C `traceparent` (server from ALS, browser from `__SSR__`).
369
+
370
+ ### Isomorphic plumbing @documentation plumbing
371
+
372
+ - `@abide/abide/shared/createSubscriber` — `createSubscriber(start)`: abide-ui
373
+ subscriber grounded in the signal core (open-on-first-tracked-read,
374
+ close-on-last-reader).
375
+
376
+ ## UI surface — abide/ui/* (client-only)
377
+
378
+ ### Reactive state@documentation reactive-state
379
+
380
+ - `@abide/abide/ui/state``state(initial, transform?)` writable cell (read/reassigned as a plain variable; compiler desugars to `.value`);
381
+ members `state.computed(fn)` (read-only derived), `state.linked(fn, transform?)`
382
+ (writable, reseeded from a thunk), `state.share(key, value)` / `state.shared(key)`
383
+ (ambient scope context).
384
+ - `@abide/abide/ui/watch` — `watch(source, handler)` the single reaction primitive
385
+ (cell / cell array / socket-stream / RPC); bare `watch(thunk)` is an auto-tracked
386
+ effect; returns a scope-tied disposer; SSR-inert.
387
+ - `@abide/abide/ui/props` — `props()` prop reader (a required import, like the other
388
+ reactive names); compiler-lowered inside a component; throws if called directly.
389
+
390
+ ### Templating@documentation templating
391
+
392
+ - `@abide/abide/ui/html` — `html\`…\``(or`html(string)`) returns branded
393
+ **unescaped** raw HTML for `{expr}` insertion; interpolations are not
394
+ auto-escaped; nullish empty.
395
+
396
+ ### Navigate@documentation navigate
397
+
398
+ - `@abide/abide/ui/navigate` — `navigate(path, ...rest)` typed in-app SPA
399
+ navigation (params first for `[name]` routes, then `{ replace?, keepScroll? }`);
400
+ builds through `url()`.
401
+
402
+ ### UI plumbing @documentation plumbing
403
+
404
+ - `@abide/abide/ui/effect` — `effect(fn)` internal reactive effect (tracks reads,
405
+ re-runs, returns a disposer); compiler-emitted authors use `watch`.
406
+ - `@abide/abide/ui/currentScope` — `scope()` resolves the current lexical scope;
407
+ the internal lowering host the compiler targets.
408
+ - `@abide/abide/ui/enterRenderScope` — `enterScope()` establishes a fresh isolated
409
+ SSR-render scope, returning the previous.
410
+ - `@abide/abide/ui/exitRenderScope` — `exitScope(previous)` restores the scope
411
+ `enterScope` saved.
412
+ - `@abide/abide/ui/router` — `router(host, loaders, layoutLoaders?, probe?)` the
413
+ History-API client router: match, code-split, mount a diffed outlet chain, drive
414
+ SPA nav with scroll restoration.
415
+ - `@abide/abide/ui/startClient` — `startClient(routes, layoutRoutes?, target)` the
416
+ client entry: read `window.__SSR__`, seed cache/streamed/warm state, start the
417
+ router; returns a disposer.
418
+ - `@abide/abide/ui/renderToStream` — `renderToStream(render)` out-of-order SSR
419
+ streaming generator: shell first, then one `<abide-resolve>` fragment per
420
+ streaming `{#await}` in completion order.
421
+ - `@abide/abide/ui/remoteProxy` — `remoteProxy(method, url, options?)`
422
+ bundler-target client substitute for a server RPC (fetch over the network; does
423
+ the client pre-flight input validation; attaches the real `.watch`).
424
+ - `@abide/abide/ui/socketProxy` — `socketProxy(name)` bundler-target client
425
+ substitute for a server socket (subscribes over the multiplexed ws).
426
+ - `@abide/abide/ui/settleAsyncCells` — the SSR await-barrier draining the
427
+ request-scoped pending-cell list; client no-op.
428
+ - `@abide/abide/ui/flight` — server-only flight-starter hoisting a hoistable
429
+ await's promise into the sync prefix so independent flights overlap.
430
+ - `@abide/abide/ui/isolateCellBarrier` — runs a hoisted child render under its own
431
+ async-cell barrier so its cells isolate from siblings; client passthrough.
432
+ - `@abide/abide/ui/finalizeStreamedChildren` — the when-to-stream decision run
433
+ after a component body walk; fills each hoistable child's reserved output slot.
434
+ - `@abide/abide/ui/runtime/withPath` — pushes one escaped render-path segment for
435
+ the duration of a synchronous build.
436
+ - `@abide/abide/ui/runtime/renderPath` — composes a streamed child's ordinal
437
+ segment onto the ambient render path and returns the boundary id.
438
+ - `@abide/abide/ui/runtime/escapeKey` — escapes one key to an RFC 6901
439
+ JSON-Pointer token so a `/`-bearing key survives a `/`-joined render path.
440
+ - `@abide/abide/ui/runtime/nextBlockId` — the next await/try block id in the
441
+ current render pass, namespaced by render path.
442
+ - `@abide/abide/ui/runtime/blockId` — allocates a render-path-namespaced await/try
443
+ block id with a per-path document-order counter.
444
+ - `@abide/abide/ui/runtime/enterRenderPass` — marks entry into a render/mount;
445
+ depth 0 clears the per-path block-id counters.
446
+ - `@abide/abide/ui/runtime/exitRenderPass` — marks exit, unwinding the render-pass
447
+ depth.
448
+ - `@abide/abide/ui/dom/mount` — mounts a top-level page/layout into a host under an
449
+ ownership scope; returns a disposer.
450
+ - `@abide/abide/ui/dom/mountChild` — mounts a `<Child/>` as a marker-bounded range
451
+ (no wrapper element).
452
+ - `@abide/abide/ui/dom/mountStreamedChild` — client adopter for a hoistable child:
453
+ adopts an inlined range or a streamed boundary.
454
+ - `@abide/abide/ui/dom/mergeProps` — composes a child's props from ordered layers
455
+ (explicit runs, spreads, trailing `children`), last-wins per key.
456
+ - `@abide/abide/ui/dom/spreadProps` — wraps a `{...source}` spread layer so each
457
+ key resolves to a live value thunk.
458
+ - `@abide/abide/ui/dom/restProps` — the live `...rest` of a `props()` destructure.
459
+ - `@abide/abide/ui/dom/bindProp` — parent half of a component `bind:prop`
460
+ (annotates the prop thunk with a write-back channel).
461
+ - `@abide/abide/ui/dom/bindableProp` — child half of a two-way prop (the writable
462
+ cell the child writes/forwards).
463
+ - `@abide/abide/ui/dom/spreadAttrs` — spreads an object's keys onto a native
464
+ element (`on<event>` keys attach listeners, others bind as reactive attrs).
465
+ - `@abide/abide/ui/dom/readCall` — guarded method call on a reactive-document read
466
+ so throws name the authored scope path.
467
+ - `@abide/abide/ui/dom/readCell` — unified read for a `computed`/`linked`
468
+ reference (async peek / derive call / sync `.value`).
469
+ - `@abide/abide/ui/dom/cellPending` — whether a `{#if}`/`{#switch}` async subject
470
+ is still loading (render no branch while pending).
471
+ - `@abide/abide/ui/dom/mutateDocContainer` — in-place container mutation lowered to
472
+ clone-mutate-replace so a patch emits and readers wake.
473
+ - `@abide/abide/ui/dom/hydrate` — adopts server-rendered DOM in place with a claim
474
+ cursor (attach listeners/effects, no re-render); returns a disposer.
475
+ - `@abide/abide/ui/dom/appendText` — reactive `{expr}` interpolation (escaped text
476
+ / snippet builder / `html\`\`` raw).
477
+ - `@abide/abide/ui/dom/appendTextAt` — reactive `{expr}` mounted at a skeleton
478
+ anchor comment, interleaved with element siblings.
479
+ - `@abide/abide/ui/dom/appendSnippet` — mounts a `{snippet(args)}` builder's nodes
480
+ in a marker-bounded range.
481
+ - `@abide/abide/ui/dom/appendStatic` — a static text node, created or claimed from
482
+ SSR text.
483
+ - `@abide/abide/ui/dom/cloneStatic` — appends a fully-static bindingless subtree via
484
+ one cached-template deep clone.
485
+ - `@abide/abide/ui/dom/skeleton` — clones a template and locates its bound
486
+ holes/anchors for a subtree carrying bindings/control-flow.
487
+ - `@abide/abide/ui/dom/anchorCursor` — positions a skeleton-anchored control-flow
488
+ block or slot at its `<!--a-->` anchor.
489
+ - `@abide/abide/ui/dom/mountSlot` — mounts a component's `{children()}` content
490
+ (parent children or fallback) as a marker-bounded range.
491
+ - `@abide/abide/ui/dom/outlet` — a layout's outlet boundary the router fills with
492
+ the next chain layer.
493
+ - `@abide/abide/ui/dom/attr` — binds an element attribute to `read()` via one
494
+ effect (`name={expr}`).
495
+ - `@abide/abide/ui/dom/on` — attaches an event listener pinned to the owning scope
496
+ (`on<event>`).
497
+ - `@abide/abide/ui/dom/attach` — runs an `attach={fn}` against an element and
498
+ registers its teardown.
499
+ - `@abide/abide/ui/dom/bindSelectValue` — two-way `bind:value` for `<select>`
500
+ (reactive selection + change write-back; `multiple` = array membership).
501
+ - `@abide/abide/ui/dom/each` — keyed list binding (`{#for … by key}`),
502
+ marker-range rows reconciled by key with minimal DOM moves.
503
+ - `@abide/abide/ui/dom/eachAsync` — async keyed list over an AsyncIterable
504
+ (`{#for await}`); rows append as it yields; SSR renders none.
505
+ - `@abide/abide/ui/dom/when` — conditional swappable range (`{#if}`/`{:else}`) with
506
+ an optional pending state.
507
+ - `@abide/abide/ui/dom/awaitBlock` — await-block runtime across
508
+ pending/resolved/error branches with SSR resume adoption (`{#await}`).
509
+ - `@abide/abide/ui/dom/tryBlock` — error-boundary block catching thrown/async-cell
510
+ errors into a catch branch (`{#try}`).
511
+ - `@abide/abide/ui/dom/switchBlock` — multi-branch swappable range (first matching
512
+ `{:case}` else `{:default}`; also backs `{:else if}` chains).
427
513
 
428
514
  ## Build / tooling
429
515
 
430
- ### Building — `@documentation building`
431
-
432
- - `@abide/abide/build` — `build({ cwd, … })`: builds the client bundle into `dist/_app` (`.abide` loader, virtual-module resolver, optional Tailwind); production builds also emit `.gz` siblings; staged and atomically swapped so a live dev server never sees a half-built dist.
433
- - `@abide/abide/compile` `compile({ cwd, target?, outfile? })`: produces a standalone server executable (runs the client build first and embeds the compressed assets); returns the binary path.
516
+ ### Building — @documentation building
517
+
518
+ - `@abide/abide/build` — `build(opts?)` builds the client bundle into `dist/` and
519
+ concurrently writes `src/.abide/*.d.ts`; never throws. Options `cwd`/`minify`/
520
+ `compress`/`clean`/`exitOnFailure`/`dev`.
521
+ - `@abide/abide/compile` — `compile(opts?)` produces a standalone Bun server
522
+ executable (runs the client build first to embed assets); returns the binary
523
+ path.
524
+
525
+ ### Tooling — @documentation plumbing
526
+
527
+ - `@abide/abide/ui-plugin` — the `abide-ui` `BunPlugin` that loads and compiles
528
+ `.abide` components (and pulls scoped `<style>` into browser bundles).
529
+ - `@abide/abide/preload` — the Bun preload module registering the UI plugin, the
530
+ resolver plugin, and the `.css` no-op loader (used by `bunfig` `[test]` and the
531
+ CLI `--preload`).
532
+ - `@abide/abide/resolver-plugin` — `abideResolverPlugin({ cwd?, embedAssets?,
533
+ target? })` wiring every `abide:*` virtual module, the `$server`/`$ui`/`$shared`/
534
+ `$mcp`/`$cli` aliases, and the per-target RPC/socket rewrite (with the
535
+ client-side server-code-leak guard).
536
+ - `@abide/abide/tsconfig` — the base `tsconfig.app.json` for consuming apps to
537
+ `extends`.
538
+
539
+ ## Desktop bundle — @documentation bundle
540
+
541
+ - `@abide/abide/server/appDataDir` — `appDataDir()` returns the bundle's per-user
542
+ data dir keyed by the injected program name; cwd-independent, pure.
543
+ - `@abide/abide/bundle/BundleWindow` — type of the default export from
544
+ `src/bundle/window.ts`: `{ title?, width?, height?, menu?, config? }`.
545
+ - `@abide/abide/bundle/BundleMenu` — `{ label, items: BundleMenuItem[] }`, a
546
+ top-level bundle menu.
547
+ - `@abide/abide/bundle/BundleMenuItem` — a menu entry: separator, or `emit` (a
548
+ page `abide:menu` event), or `navigate`.
549
+ - `@abide/abide/bundle/onMenu` — `onMenu(handler)` / `onMenu(name, handler)`
550
+ subscribes to bundle menu-click events; returns an unsubscribe; inert in a
551
+ plain tab / SSR.
552
+ - `@abide/abide/bundle/bundled` — `bundled(): boolean` — am I part of the abide
553
+ desktop bundle (client reads `window.__ABIDE_BUNDLE__`, server the parent-pid
554
+ env).
555
+
556
+ ## MCP — @documentation mcp
557
+
558
+ - `@abide/abide/mcp/createMcpServer` — `createMcpServer(opts?)` constructs the
559
+ framework-generated MCP server bound to the RPC/socket registries; returns
560
+ `{ handle(request) }` (the `/__abide/mcp` handler). Tools derive from surfaces
561
+ with `clients.mcp`.
434
562
 
435
- ### Tooling plumbing — `@documentation plumbing`
436
-
437
- - `@abide/abide/preload` — the Bun preload installing the `.abide` loader, the virtual-module resolver, and a `.css` no-op loader — the same runtime for the server, scripts (`abide run`), and `bun test`.
438
- - `@abide/abide/resolver-plugin` — the resolver plugin itself: `$`-alias + virtual-module (`abide:*`) resolution, rpc/socket module rewriting, side-crossing guards.
439
- - `@abide/abide/ui-plugin` — the Bun plugin that compiles `.abide` single-file components to ES modules (layouts flagged by filename; scoped styles bundled into the entry stylesheet).
440
- - `@abide/abide/tsconfig` — the base tsconfig apps extend (`bundler` resolution, strict, `types: ["bun"]`, erasable syntax only).
441
-
442
- ## Desktop bundle — `@documentation bundle`
443
-
444
- - `@abide/abide/bundle/BundleWindow` — the type of `src/bundle/window.ts`'s default export: window title/size plus custom `menu` entries inserted between the standard Edit and Window menus.
445
- - `@abide/abide/bundle/BundleMenu` — one top-level custom menu (`label` + `items`).
446
- - `@abide/abide/bundle/BundleMenuItem` — one menu entry: a divider, an `emit` item dispatching an `abide:menu` CustomEvent into the page (optional Cmd `shortcut`), or a `navigate` item repointing the window itself.
447
- - `@abide/abide/bundle/onMenu` — `onMenu(handler)` / `onMenu(name, handler)`: subscribes to bundle menu clicks; returns an unsubscribe; inert during SSR and in plain browser tabs.
448
- - `@abide/abide/bundle/bundled` — `bundled()`: true inside the desktop bundle (client: webview init flag; server: launcher-spawned process), false in a plain browser tab or on a remote server.
449
- - `@abide/abide/server/appDataDir` — `appDataDir()`: the running bundle's per-user data dir, keyed by the bundler-injected program name; pure path computation, cwd-independent (`ABIDE_DATA_DIR` overrides).
563
+ ## Testing
450
564
 
451
- ## MCP`@documentation mcp`
565
+ ### Testing@documentation testing
452
566
 
453
- - `@abide/abide/mcp/createMcpServer` — `createMcpServer(opts?)`: the MCP server behind `/__abide/mcp` tools derived from every `clients.mcp` rpc and socket (a `<name>-tail` read tool, plus `<name>-publish` under `clientPublish`), prompts from `src/mcp/prompts/`, auth inherited from the inbound request, optional `authorize` hook. Framework-constructed; there is no user-authored server module.
567
+ - `@abide/abide/test/createTestApp` — `createTestApp()` boots the real app on an
568
+ ephemeral port; returns `{ origin, fetch, rpc, sockets, health, stop,
569
+ [Symbol.asyncDispose] }` (`rpc`/`sockets` typed by generated d.ts). Use
570
+ `await using`.
454
571
 
455
- ## Testing
572
+ ### Testing plumbing — @documentation plumbing
456
573
 
457
- - `@abide/abide/test/createTestApp` — `@documentation testing` — boots the app in-process for `bun test`: typed `app.rpc.<name>` / `app.sockets.<name>` clients (typed via the generated `testRpc.d.ts` / `testSockets.d.ts`), request scope included, no network.
458
- - `@abide/abide/test/createScriptedSurface` — `@documentation plumbing` — a scripted `AgentSurface` for engine tests: declarative tool stubs in, an MCP surface out, every dispatched call recorded for assertions.
459
- - `@abide/abide/test/assertAgentFrameConformance` — `@documentation plumbing` — collects an engine's frame stream and asserts the neutral `AgentFrame` contract (exactly one terminal `done`, paired `tool_use`/`tool_result`, string deltas); returns the frames for provider-specific assertions.
574
+ - `@abide/abide/test/createScriptedSurface` — `createScriptedSurface(tools?)` a
575
+ scripted `AgentSurface` for engine tests; records every `call`.
576
+ - `@abide/abide/test/assertAgentFrameConformance` — collects an engine frame
577
+ stream and asserts the neutral `AgentFrame` contract (one terminal `done`; every
578
+ `tool_use` answered), throwing on violation.
460
579
 
461
580
  ## Generated machine surfaces
462
581
 
463
- | Route | Serves |
464
- | --- | --- |
465
- | `/openapi.json` | The OpenAPI document projected from every rpc's method, URL, and schemas. The 200 response body comes from `schemas.output` or, absent one, the handler's return type projected to JSON Schema (ADR-0030 D2); each `error.typed(...)` branch the handler can return surfaces as its own status response (ADR-0030) |
466
- | `/__abide/mcp` | The MCP endpoint (tools from rpcs/sockets, prompts, resources); auth flows from the inbound request |
467
- | `/__abide/health` | Liveness + identity JSON: framework version, app name/version, plus the app `health(request)` hook's fields; answered ahead of `app.handle` |
468
- | `/__abide/identity` | Compatibility alias for the same payload with the legacy `abide: true` marker |
469
- | `/__abide/sockets` | The single multiplexed WebSocket every client socket rides |
470
- | `/__abide/sockets/<name>` | A socket's HTTP face: `GET` = retained tail as JSON (SSE stream under `Accept: text/event-stream`; `?tail=N` caps/seeds), `POST` = publish gated by `clientPublish`; 404 unless the socket is exposed to mcp/cli |
471
- | `/__abide/cli` | `GET` = shell install script; `/__abide/cli/<platform>` streams the thin-CLI tarball (cli + server binaries, `.env` baked with `ABIDE_APP_URL`/`ABIDE_APP_TOKEN`) |
472
- | `/__abide/inspector` | The `@abide/inspector` UI, mounted only under `ABIDE_ENABLE_INSPECTOR=true` |
582
+ Runtime routes the framework serves:
583
+
584
+ | Route | Serves |
585
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
586
+ | `/openapi.json` | OpenAPI spec for the public `/rpc/*` surface, built lazily from the frozen RPC registry. |
587
+ | `/__abide/mcp` | MCP endpoint (POST → `mcp.handle`), through the app auth/CSRF pipeline; mounted when an MCP is configured. |
588
+ | `/__abide/health` | Health/identity probe answered ahead of app middleware (framework identity + `app.health()` fields), `no-store`. |
589
+ | `/__abide/identity` | Compatibility alias of the health payload, stamped `{ abide: true }` for legacy probers. |
590
+ | `/__abide/inspector` | Operator inspector UI + data/SSE routes, gated by `ABIDE_ENABLE_INSPECTOR` (optional `@abide/inspector`). |
591
+ | `/__abide/sockets` | WebSocket upgrade for the socket multiplex hub; `/__abide/sockets/<name>` is one socket's HTTP face (GET tail, POST publish). |
592
+ | `/__abide/cli` | GET returns the platform-detecting install script; `/__abide/cli/<platform>` streams the built CLI binary tarball. |
473
593
 
474
594
  ## Environment variables
475
595
 
476
- | Variable | Effect |
477
- | --- | --- |
478
- | `PORT` | Binds that exact port (a collision fails loudly); unset, the server finds an open port from the default |
479
- | `APP_URL` | The app's public origin and optional mount base path (a bare `/v2` is tolerated); drives `url()` base-prefixing |
480
- | `ABIDE_APP_URL` | The remote server a thin CLI binary talks to (baked into its downloaded `.env`) |
481
- | `ABIDE_APP_TOKEN` | Bearer token the thin CLI sends; baked into the downloaded `.env` when the download request was authenticated |
482
- | `ABIDE_CLIENT_TIMEOUT` | Default browser-side rpc timeout in ms — read at server boot, shipped to the client via the SSR payload |
483
- | `ABIDE_DATA_DIR` | Overrides the per-user data directory on every platform |
484
- | `ABIDE_ENABLE_INSPECTOR` | `true` mounts `@abide/inspector` at `/__abide/inspector` (the package must be installed) |
485
- | `ABIDE_IDLE_TIMEOUT` | Bun per-connection idle timeout in seconds (default 10) |
486
- | `ABIDE_INSPECT` | Enables right-click Inspect in the desktop bundle's webview |
487
- | `ABIDE_LOG_FORMAT` | `json` renders one JSON object per log line (default: tab-separated tsv) |
488
- | `ABIDE_MAX_REQUEST_BODY_SIZE` | Server-wide max request body bytes (a per-rpc `maxBodySize` refines it) |
489
- | `ABIDE_REACHABLE_TTL` | `reachable()` poll cadence / freshness in ms (default 30000) |
490
- | `ABIDE_REACHABLE_TIMEOUT` | `reachable()` per-probe bound in ms (default 3000) |
491
- | `DEBUG` | Channel-gated diagnostics (`DEBUG=abide:rpc`, `abide:sockets`, `abide:build`, …); `DEBUG=-abide` silences the framework's own channel |
596
+ | Variable | Effect |
597
+ | ----------------------------- | -------------------------------------------------------------------------------------------- |
598
+ | `PORT` | Exact TCP port to bind; unset/invalid scans from 3000. |
599
+ | `APP_URL` | Derives the server's mount base path from its pathname (e.g. `/v2`). |
600
+ | `ABIDE_APP_URL` | Default server URL the CLI connects to; its pathname sets the mount base. |
601
+ | `ABIDE_APP_TOKEN` | Sent as `Authorization: Bearer <value>` on CLI→server requests. |
602
+ | `ABIDE_APP_DIR` | Overrides the dir the server serves chunks/shell/assets from (set per dev build generation). |
603
+ | `ABIDE_DATA_DIR` | Overrides the app data directory on all platforms, used as-is (no program-name suffix). |
604
+ | `ABIDE_CLIENT_TIMEOUT` | RPC client timeout in ms (1–600000), shipped to the browser transport. |
605
+ | `ABIDE_MAX_REQUEST_BODY_SIZE` | Server-wide max request body size. |
606
+ | `ABIDE_IDLE_TIMEOUT` | Bun per-connection idle timeout in seconds (default 10). |
607
+ | `ABIDE_LOG_FORMAT` | `json` renders log records as JSON instead of TSV. |
608
+ | `ABIDE_ENABLE_INSPECTOR` | `true` mounts the opt-in operator inspector UI/routes. |
609
+ | `ABIDE_INSPECT` | Enables webview devtools/inspect for desktop bundles. |
610
+ | `ABIDE_DEV_SURFACE` | `1` forces the worker to print its surface map (set by the dev orchestrator). |
611
+ | `DEBUG` | npm-debug-style channel gate for diagnostic log channels (e.g. `abide:cache`, `-abide`). |
492
612
 
493
613
  ---
494
614
 
495
- This file mirrors `package.json`'s `exports`; after adding or renaming an
496
- export, run `bun run packages/abide/scripts/readmeSurfaces.ts` and regenerate.
615
+ Mirrors `package.json`'s `exports`; run
616
+ `bun run packages/abide/scripts/readmeSurfaces.ts` after adding or renaming an
617
+ export to keep this map honest.