@abide/abide 0.50.1 → 0.52.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.
Files changed (58) hide show
  1. package/AGENTS.md +486 -490
  2. package/CHANGELOG.md +39 -0
  3. package/README.md +130 -149
  4. package/package.json +4 -1
  5. package/src/abideResolverPlugin.ts +12 -0
  6. package/src/checkAbide.ts +60 -7
  7. package/src/lib/cli/completeCli.ts +38 -0
  8. package/src/lib/cli/printTopLevelHelp.ts +1 -0
  9. package/src/lib/cli/renderCliCompletions.ts +56 -0
  10. package/src/lib/cli/runCli.ts +27 -0
  11. package/src/lib/server/render.ts +70 -0
  12. package/src/lib/server/runtime/cacheStalenessBroadcaster.ts +33 -0
  13. package/src/lib/server/runtime/createServer.ts +43 -0
  14. package/src/lib/server/runtime/createUiPageRenderer.ts +37 -0
  15. package/src/lib/server/runtime/pageRenderSlot.ts +15 -0
  16. package/src/lib/server/runtime/runWithRequestScope.ts +1 -0
  17. package/src/lib/server/runtime/types/RequestStore.ts +9 -0
  18. package/src/lib/server/sockets/createSocketDispatcher.ts +9 -0
  19. package/src/lib/server/sockets/registerSocket.ts +12 -0
  20. package/src/lib/shared/CACHE_STALENESS_SOCKET.ts +8 -0
  21. package/src/lib/shared/RESERVED_SOCKET_PREFIX.ts +8 -0
  22. package/src/lib/shared/applyCacheStalenessLocally.ts +18 -0
  23. package/src/lib/shared/attachRpcSelectorMethods.ts +3 -1
  24. package/src/lib/shared/cache.ts +32 -4
  25. package/src/lib/shared/cacheStalenessSlot.ts +21 -0
  26. package/src/lib/shared/createRpcServerProgram.ts +187 -0
  27. package/src/lib/shared/docSnapshotsSlot.ts +13 -0
  28. package/src/lib/shared/invalidate.ts +39 -0
  29. package/src/lib/shared/matcherFromEnvelope.ts +31 -0
  30. package/src/lib/shared/prepareRpcModule.ts +22 -3
  31. package/src/lib/shared/refresh.ts +9 -2
  32. package/src/lib/shared/selectorMatcher.ts +2 -15
  33. package/src/lib/shared/serializeSelector.ts +69 -0
  34. package/src/lib/shared/setsIntersect.ts +15 -0
  35. package/src/lib/shared/types/CacheStalenessApply.ts +13 -0
  36. package/src/lib/shared/types/CacheStalenessFrame.ts +24 -0
  37. package/src/lib/shared/types/DocSnapshots.ts +12 -0
  38. package/src/lib/shared/types/RemoteFunction.ts +11 -8
  39. package/src/lib/shared/types/RpcBuildStamps.ts +9 -0
  40. package/src/lib/shared/types/SsrPayload.ts +5 -0
  41. package/src/lib/test/createTestApp.ts +15 -0
  42. package/src/lib/ui/compile/COMPOUND_ASSIGNMENT_OPERATORS.ts +19 -0
  43. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
  44. package/src/lib/ui/compile/compileShadow.ts +141 -25
  45. package/src/lib/ui/compile/lowerDocAccess.ts +2 -14
  46. package/src/lib/ui/compile/lowerScript.ts +23 -0
  47. package/src/lib/ui/compile/renameSignalRefs.ts +65 -0
  48. package/src/lib/ui/compile/wrapReactionCellSources.ts +117 -0
  49. package/src/lib/ui/createScope.ts +42 -1
  50. package/src/lib/ui/dom/assertClaimedText.ts +14 -4
  51. package/src/lib/ui/dom/attr.ts +25 -2
  52. package/src/lib/ui/dom/writeCell.ts +22 -0
  53. package/src/lib/ui/runtime/DOC_SEED.ts +11 -0
  54. package/src/lib/ui/runtime/claimExpected.ts +6 -1
  55. package/src/lib/ui/runtime/reportHydrationDivergence.ts +30 -0
  56. package/src/lib/ui/startClient.ts +22 -1
  57. package/src/lib/ui/subscribeCacheStaleness.ts +85 -0
  58. package/src/serverEntry.ts +12 -0
package/AGENTS.md CHANGED
@@ -1,605 +1,601 @@
1
1
  # AGENTS.md — abide complete surface map
2
2
 
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/`.
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. Where the
5
+ > `README.md` teaches the three foundational primitives (RPC, socket, component),
6
+ > this maps the whole API in one read. `CONTEXT.md` is the domain glossary and
7
+ > `docs/adr/` carries the rationale behind each decision.
8
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 dependencyTypeScript). Every import
15
- > specifier below is `@abide/abide<exports-key>`; the file path after it is the
16
- > source, not an import target.
9
+ > Ground rule: **no barrels.** Every public name has its own module path
10
+ > (`abide/server/GET`, `abide/shared/cache`, `abide/ui/state`, …) there is no
11
+ > umbrella `index.ts`, so importing one name never drags side-effecting siblings
12
+ > into the bundle. The namespace marks the side a name runs on: `abide/server/*`
13
+ > is server-side, `abide/ui/*` is client-side, `abide/shared/*` is isomorphic
14
+ > (same callable, same intent both sides usually identical behaviour, e.g.
15
+ > `HttpError`; sometimes a side-swapped runtime behind one intent, e.g. `cache`,
16
+ > `invalidate`, `refresh`).
17
+ >
18
+ > Package `@abide/abide` runs on **Bun ≥ 1.3.0** with **one direct dependency**
19
+ > (TypeScript). The surface bullets below write the short `abide/…` specifier for
20
+ > readability — the installable path prefixes the package name, so `abide/server/GET`
21
+ > imports from `@abide/abide/server/GET`. These are the public `exports` keys, not
22
+ > the on-disk file paths.
17
23
 
18
24
  ## The premise
19
25
 
20
- One declared RPC fans out to five surfaces:
26
+ One typed RPC declaration branches to every surface:
21
27
 
22
28
  ```text
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 schema) getMessages
29
+ export const getMessages = GET(handler, { schemas })
30
+
31
+ ┌──────────┬──────────────┼──────────────┬──────────────┐
32
+ ▼ ▼ ▼
33
+ SSR call browser MCP tool CLI OpenAPI op
34
+ the bare fetch (read-only, subcommand in
35
+ call, typed proxy auto-exposed) abide cli /openapi.json
36
+ in-process fn(args) getMessages
30
37
  ```
31
38
 
32
- A `schemas.input` (any Standard Schema) unlocks the **CLI** on any RPC and
33
- **MCP** for read-only methods (`GET`/`HEAD`). A mutating method
34
- (`POST`/`PUT`/`PATCH`/`DELETE`) never auto-exposes to MCP it needs an explicit
35
- `clients: { mcp: true }`. Explicit `clients` values always win; `browser`
36
- defaults on. A socket with a `schema` auto-exposes to MCP and CLI regardless of
37
- direction.
39
+ A **typed handler input** flips those surfaces on: the input type is projected to
40
+ JSON Schema at build (ADR-0030), so a plainly-typed handler auto-exposes to the
41
+ CLI and — for read-only methods (`GET`/`HEAD`) MCP with no hand-written
42
+ `schemas.input`. A declared `schemas.input` adds runtime validation (422) on top.
43
+ A mutating method (`POST`/`PUT`/`PATCH`/`DELETE`) never auto-exposes to MCP
44
+ opt it in with `clients: { mcp: true }`.
38
45
 
39
46
  ## File-based conventions
40
47
 
41
- The bundler and route resolver read these paths by convention (dir aliases
42
- `$server`, `$ui`, `$shared`, `$mcp`, `$cli` point at the matching `src/` dirs):
48
+ The bundler (`abideResolverPlugin`) reads these paths. Import aliases into the
49
+ tree: `$server`, `$ui`, `$shared`, `$mcp`, `$cli`.
43
50
 
44
51
  | Path | Meaning |
45
52
  |---|---|
46
- | `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). |
47
- | `src/server/sockets/<name>.ts` | One socket per file (`export const <name> = socket(...)`); path socket name. Rewritten to `defineSocket` (server) / `socketProxy` (client). |
48
- | `src/mcp/prompts/<name>.md` | Markdown MCP prompt: frontmatter (description + arguments) + `{{arg}}` template body, compiled to `definePrompt`. |
49
- | `src/mcp/resources/*` | MCP resource files served by the generated MCP server. |
50
- | `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. |
51
- | `src/app.ts` | Optional app hooks (`AppModule` shape): `init` / `handle` / `handleError` / `health` / `forwardHeaders`. Deletable. |
52
- | `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`. |
53
- | `src/ui/pages/**/layout.abide` | Wraps every page at/below its folder; renders the page where it calls `{children()}`; kept mounted across navigation. |
54
- | `src/ui/app.html`, `src/ui/app.css` | Custom document shell and root stylesheet. |
55
- | `src/ui/public/` | Static assets, served at the site root (`/<file>`). |
56
- | `src/bundle/window.ts` | Optional default-exported `BundleWindow` for the desktop bundle (plus optional `src/bundle/disconnected.abide` connect-screen override). |
57
- | `src/cli/banner.txt`, `src/cli/footer.txt` | CLI help chrome. |
58
- | `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. |
59
- | `dist/` | Build output: `dist/_app/` (prod client) or `dist/_app.gen-<id>/` (dev), `dist/app` (compiled binary), `dist/cli-*`. |
53
+ | `src/server/rpc/<name>.ts` | One RPC endpoint per file; URL is `/rpc/<name>`. Export name = file stem. Flat dynamic `[seg]` segments are rejected here (pass ids as args). |
54
+ | `src/server/sockets/<name>.ts` | One `socket(...)` per file; socket name = path under `sockets/` (nested kept, e.g. `orders/new`). Multiplexed over `/__abide/sockets`. |
55
+ | `src/ui/pages/<route>/page.abide` | Folder-based routed page; URL = directory path. Dynamic segments keep `[name]` / `[[name]]` / `[...rest]` shape → `page.params`. |
56
+ | `src/ui/pages/<route>/layout.abide` | Nested layout wrapping the route chain below; renders it at `{children()}`. |
57
+ | `src/ui/public/` | Static assets, served at site root; embedded for standalone compile. |
58
+ | `src/ui/app.html` | Custom document shell (optional; a bundled default is used otherwise). |
59
+ | `src/mcp/prompts/<name>.md` | Markdown MCP prompts; name = path minus `.md`, `/`→`-` (`code/review.md` `code-review`). |
60
+ | `src/mcp/resources/` | MCP resources, embedded for standalone builds. |
61
+ | `src/server/config.ts` | Re-exported as `abide:config` for boot-time `env(schema)` validation (optional). |
62
+ | `src/app.ts` | Re-exported as `abide:app` the `AppModule` request hooks (optional). |
63
+ | `src/bundle/window.ts` | Default-export `BundleWindow` config baked into the desktop launcher (optional). |
64
+ | `src/.abide/*.d.ts` | Editor `.d.ts` artifacts written by `build()` (not at boot). |
65
+ | `dist/` | Build output; client bundle `dist/_app/` (dev: per-generation `dist/_app.gen-<id>`). |
60
66
 
61
67
  ## CLI
62
68
 
63
- `abide <command>` (the `abide` bin):
64
-
65
69
  | Command | Does |
66
70
  |---|---|
67
- | `abide scaffold <name> [--no-install] [--no-dev]` | Scaffold a project from the bundled template, install it, and (TTY only) start dev. |
68
- | `abide dev` | Dev orchestrator: build client, spawn the server child, watch `src/`, rebuild + restart on change, browser live-reload. |
69
- | `abide build` | Single client build into `dist/_app/`, no server (CI / static deploys). |
70
- | `abide start` | Run the production server against an already-built `dist/`. |
71
- | `abide run <file> [args...]` | Run an arbitrary script under the abide preload (same runtime as the server); argv after the file is forwarded verbatim. |
71
+ | `abide scaffold <name> [--no-install] [--no-dev]` | Scaffold the bundled template, install, and (TTY only) start dev. |
72
+ | `abide dev` | Build the client + run the server with file-watch rebuild/restart and browser live-reload. |
73
+ | `abide build` | Single client build into `dist/_app/` (no server). |
74
+ | `abide check` | Type-check every `.abide` template + props via the shadow program; non-zero on errors. |
75
+ | `abide start` | Run the production server against a built `dist/`. |
76
+ | `abide run <file> [args…]` | Run a script under the abide preload (same runtime as the server). |
72
77
  | `abide compile [--target=<bun-…>] [--out=<path>]` | Build a standalone server executable. |
73
- | `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>/`. |
74
- | `abide bundle` | Assemble a self-contained desktop app bundle for the host platform (`.app` on macOS), unsigned. |
75
- | `abide check` | Type-check every `.abide` component's template + props through its shadow program; non-zero on error. |
76
- | `abide lsp` | Run the `.abide` language server over stdio (JSON-RPC) for editor diagnostics. |
77
- | `abide init-agent` | Write/refresh the abide agent-guide pointer in the project root `CLAUDE.md`. |
78
+ | `abide cli [--target=] [--out=] [--platforms=a,b,c]` | Build the thin CLI binary (ships the server beside it); `--platforms` cross-compiles. |
79
+ | `abide bundle` | Assemble a movable, self-contained (unsigned) desktop app bundle for this platform. |
80
+ | `abide lsp` | Run the `.abide` language server over stdio (JSON-RPC) for editors. |
81
+ | `abide init-agent` | Write/refresh a `CLAUDE.md` pointer to this surface map (non-scaffolded projects). |
78
82
 
79
83
  For tests, add `preload = ["@abide/abide/preload"]` under `[test]` in
80
84
  `bunfig.toml` and run `bun test`.
81
85
 
82
86
  ## Authoring contracts
83
87
 
84
- **RPC** (`src/server/rpc/<name>.ts`). `export const x = METHOD(handler, opts?)`.
85
- The handler receives the validated args — `StandardSchemaV1.InferOutput<schemas.input>`
86
- when a schema is present, otherwise its own declared first parameter (or `undefined`
87
- for a nullary handler); you never pass `<Args, Return>` call generics. It reaches
88
- request context via `request()` (the inbound `Request`) and `cookies()` (the jar),
89
- and returns a `Response` — canonically `json(...)` (success body the caller's
90
- `Return`), `jsonl(...)`/`sse(...)` (streaming), `error(...)` / `error.typed(...)()`
91
- (non-2xx, body typed `never`), `redirect(...)`, or a hand-built `Response`
92
- (`Return` falls back to `unknown`). Typed errors are inferred from the
93
- `error.typed(...)` branches a handler returns there is no `errors:` option.
94
-
95
- `opts` (`RpcSharedOpts`, all optional): `schemas: { input?, output?, files? }`
96
- (the ADR-0020 namespace — `input` validates args and drives their type, `output`
97
- is the success-body schema for OpenAPI 200 / MCP `outputSchema` and never drives
98
- arg inference, `files` validates multipart File parts and merges them into the
99
- args bag); `clients: { browser?, mcp?, cli? }` (surface-exposure flags);
100
- `crossOrigin` (exempt a mutating RPC from the same-origin CSRF gate); `maxBodySize`
101
- (pre-parse body-byte cap, 413 past it); `timeout` (per-RPC handler deadline in ms,
102
- a 504 on every surface, composed into `request().signal`). Read helpers (GET/HEAD)
103
- additionally accept `cache` (`ttl`/`tags`/`throttle`/`debounce`/`shared`) and
104
- `stream` (replay depth); these are a compile error on the mutating helpers. There
105
- is **no** `outbox` option. Query/path/form args auto-coerce from the endpoint's
106
- typed shape (ADR-0028 build-time plan) no `z.coerce` needed; a value that will
107
- not parse stays a string so the schema raises an honest 422.
108
-
109
- Consume forms (`RemoteFunction`): the bare `fn(args)` **is** the smart read
110
- cached, coalesced, SWR-reactive; decodes by Content-Type, throws `HttpError` on
111
- non-2xx. There is no call-site options argument on the bare call. Members:
112
- `fn.raw(args, opts?)` (raw `Response`, no decode/throw), `fn.refresh(args?)`
113
- (refetch keeping the stale value visible), `fn.patch(...)` (in-place cache
114
- mutation; fetch-only, absent on streaming RPCs), `fn.peek(args?)` (retained value,
115
- sync), `fn.pending(args?)`, `fn.refreshing(args?)`, `fn.error(args?)` (this RPC's
116
- last typed error), `fn.isError(caught, 'name')` (typed guard), and client-only
117
- `fn.watch(handler)` / `fn.watch(args, handler)`. A streaming handler
118
- (`jsonl`/`sse`) makes the bare call return a `NamedAsyncIterable<Frame>`
119
- synchronously — `for await (… of fn(args))`, never `await`.
120
-
121
- **Socket** (`src/server/sockets/<name>.ts`). `export const x = socket(opts?)`.
122
- `opts` (`SocketOptions`): `tail` (retention count — kept frames for late joiners
123
- / reconnects; server default 1), `ttl` (evict retained frames older than N ms,
124
- lazy), `clientPublish` (allow publishes over the wire; off by default), `schema`
125
- (validate publish payloads; flips mcp/cli `clients` on), `clients`. A `Socket<T>`
126
- extends `AsyncIterable<T>` (bare `for await` is the live stream, no replay) with
127
- `publish(frame)`, `tail(count?)` (subscription seeded from the retained tail),
128
- `peek()`, `pending()`/`refreshing()`/`done()`/`error()`, `refresh()`, and
129
- `watch(handler)`. HTTP face at `/__abide/sockets/<name>`: `GET` reads the retained
130
- tail, `POST` publishes (only when `clientPublish`).
131
-
132
- **Page / layout** (`src/ui/pages/**`). `page` (isomorphic `PageSnapshot` proxy)
133
- exposes `route`, `params`, `url` (browser-space `URL`, base-prefixed), and
134
- `navigating`; read a field inside an effect/derived and it re-runs on navigation.
135
- `url(path, params?, query?)` builds base-correct links; `navigate(path, …)`
136
- performs typed in-app SPA navigation (params first for `[name]` routes, then
137
- `{ replace?, keepScroll? }`). A layout renders the active page via `{children()}`.
138
-
139
- **`app.ts` / `config.ts`.** `app.ts` default- or named-exports the `AppModule`
140
- hooks (all optional; `init({ server })` may return a cleanup run on
141
- SIGINT/SIGTERM; `handle(request, next)` is single middleware; `health(request)`
142
- merges into `/__abide/health`, runs before `handle`, and is public). `config.ts`
143
- exports `config` — `env(schema)` (validated, typed, throws on bad config at boot)
144
- or the unvalidated `Bun.env` floor.
145
-
146
- **The isomorphism move.** There is no `cache()` wrapper. A bare smart RPC call
147
- read inline during SSR is captured in the per-request cache; the runtime
148
- snapshots each settled entry into a wire-safe form serialized into the HTML, and
149
- the client seeds its store from it on hydration — the same call hydrates warm
150
- instead of re-firing. Streaming reads are snapshotted again after the stream
151
- drains and seeded over the wire.
88
+ - **RPC** (`src/server/rpc/<name>.ts`) `export const x = GET(handler, opts?)`.
89
+ The handler receives `InferOutput<schemas.input>` (intersected with
90
+ `InferOutput<schemas.files>` for multipart), or reads its arg shape structurally
91
+ off the function when schemaless. Inside it, `request()` / `cookies()` reach the
92
+ request scope; it returns `json` / `jsonl` / `sse` / `error` / `redirect` /
93
+ `error.typed(...)` / a raw `Response`. `opts` (ADR-0020): `schemas: { input?,
94
+ output?, files? }` (nested, **not** flat `inputSchema`/`filesSchema`; there is
95
+ **no** `outbox` field), `clients: { browser, mcp, cli }`, `crossOrigin`,
96
+ `timeout` (ms 504), `maxBodySize`; read helpers add `cache` and `stream`
97
+ (a compile error on mutating helpers). Query/path/form args auto-coerce from the
98
+ endpoint's typed shape at build (ADR-0028) — no `z.coerce`; an unparseable value
99
+ stays a string so the schema raises an honest 422.
100
+ - **Consume forms** the **bare call `fn(args)` is the smart read** (cached,
101
+ coalesced, reactive; in-process during SSR and baked into the HTML for warm
102
+ hydration there is no `cache()` wrapper). Around it: `fn.raw(args, opts?)`
103
+ (raw `Response`), `fn.refresh` / `fn.invalidate` / `fn.patch` / `fn.peek` /
104
+ `fn.pending` / `fn.refreshing` / `fn.error` / `fn.watch` / `fn.isError`. A
105
+ streaming handler (`jsonl`/`sse`) makes the bare call return a
106
+ `NamedAsyncIterable<Frame>` (the iterable IS the value; `await`-ing it is a
107
+ compile error; `patch` is dropped).
108
+ - **Socket** (`src/server/sockets/<name>.ts`) `export const x =
109
+ socket<T>(opts?)`. `opts`: `tail` (retention depth; defaults to keeping the last
110
+ frame), `ttl` (per-frame eviction ms), `clientPublish` (accept `pub` frames from
111
+ the wire; off by default), `schema` (validates publishes, projects MCP/CLI),
112
+ `clients`. `Socket<T>` is an `AsyncIterable<T>` — iterate for the live stream,
113
+ `.tail(count?)` to seed from retention first.
114
+ - **Page / layout** (`.abide`) dynamic `[id]` segments arrive on `page.params`;
115
+ a `layout.abide` renders the route chain below at `{children()}`. Reactive
116
+ navigation reads `page` (`route`/`params`/`url`/`navigating`) and writes via
117
+ `navigate`.
118
+ - **`src/app.ts` / `src/server/config.ts`** the `AppModule` hooks
119
+ (`init`/`handle`/`handleError`/`health`/`forwardHeaders`) and the boot-time
120
+ `env(schema)` config, respectively.
152
121
 
153
122
  ## .abide template grammar
154
123
 
155
- A `.abide` component is HTML with a leading `<script>` (its component script);
156
- `<script>` and `<style>` may also sit **inside a control-flow branch**, scoped to
157
- that branch (a nested `<script>` declares branch-local `state`/`state.computed`/
158
- `state.linked`, re-seeded per mount, and takes **no** module imports imports
159
- live only in the leading script; a nested `<style>` scopes to its sibling
160
- subtree). A *root* `<style>` is component-scoped. Reactive primitives are reached
161
- through their own imported bindings (alias-safe) — `state` from `abide/ui/state`,
162
- `watch` from `abide/ui/watch`, `html` from `abide/ui/html`, `snippet` from
163
- `abide/shared/snippet` — never through `scope()` (internal plumbing). The one
164
- ambient reader is `props()` (no import).
124
+ A component is valid HTML: a leading `<script>` (module scope — imports hoist
125
+ here for the whole template), markup with `{expr}` interpolation and directive
126
+ attributes, `{#…}` control-flow blocks, and an optional root `<style>`. Reactive
127
+ primitives are **imported by their own module paths** and resolved by import
128
+ binding (alias-safe); the one ambient reader is `props()`.
165
129
 
166
- Reactive state:
130
+ A `<script>` and a `<style>` are **not** component-root-only: either may sit
131
+ inside a control-flow branch. A nested `<script>` declares branch-local
132
+ `state` / `state.computed` / `state.linked` (re-seeded per mount, no module
133
+ imports); a nested `<style>` scopes to its sibling subtree, not the whole
134
+ component. So a *root* `<style>` is component-scoped, a nested one is branch-scoped.
135
+
136
+ ### Reactive state
167
137
 
168
138
  | Form | Meaning |
169
139
  |---|---|
170
- | `state(initial, transform?)` | Writable cell; read/write via `.value`; `transform(next, prev)` gates writes. |
171
- | `state.computed(fn)` | Read-only cell derived from other cells (lazy, never serialized). |
172
- | `state.linked(fn, transform?)` | Writable cell reseeded when the thunk's deps change. |
173
- | `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. |
174
- | `props()` | Ambient prop reader: `const { name = fallback, ...rest } = props()`. |
140
+ | `import { state } from '@abide/abide/ui/state'` | The reactive-cell primitive; called bare (`let count = state(0)`). Read and reassigned as a plain variable — the compiler desugars to the cell. `state(v, transform?)` gates writes through `transform(next, previous)`. |
141
+ | `state.computed(() => …)` | Read-only derived cell (lazy, never serialized). |
142
+ | `state.linked(() => src, transform?)` | Writable cell reseeded when the thunk's deps change. |
143
+ | `import { watch } from '@abide/abide/ui/watch'` | The single reaction primitive — `watch(source, handler)` over a cell, a cell array, a socket/stream, or an rpc; bare `watch(thunk)` is an auto-tracked effect. Client-only, stripped from SSR. |
144
+ | `import { props } from '@abide/abide/ui/props'` | The prop reader `const { name = fallback, ...rest } = props()`. A required import (not ambient, not `scope()`). |
175
145
 
176
- Bindings and directives (attribute kinds `event` / `bind` / `class` / `style` /
177
- `attach` / spread, plus plain `expression` / static):
146
+ A writable computed is expressed at the binding: `bind:value={{ get, set }}`.
147
+ `scope()` is internal plumbing now, never authored; `effect` is compiler-emitted —
148
+ authors write `watch`, not a bare `effect(...)`.
178
149
 
179
- | Form | Meaning |
150
+ ### Bindings & directives
151
+
152
+ | Syntax | Meaning |
180
153
  |---|---|
181
- | `{expr}` | Reactive text (escaped); an `html`-branded value inserts unescaped raw HTML. |
182
- | `name={expr}` | Reactive attribute. |
183
- | `on<event>={fn}` | Event listener (`onclick`, `oninput`, `onsubmit`, …). |
154
+ | `{expr}` | Escaped text interpolation. |
155
+ | `{html\`…\`}` / `{html(str)}` | `html`-branded raw markup, inserted unescaped. |
156
+ | `name={expr}` | Attribute (or component prop) bound to an expression. |
157
+ | `on<event>={fn}` | Event listener (`onclick={…}`). |
184
158
  | `bind:value` / `bind:checked` / `bind:group` | Two-way form binds. |
185
- | `bind:value={{ get, set }}` | Derived two-way binding. |
159
+ | `bind:value={{ get, set }}` | Derived two-way bind. |
186
160
  | `class:name={cond}` | Toggle a class. |
187
161
  | `style:property={value}` | Set one style property. |
188
- | `attach={fn}` | Run `fn(element)` at mount; its return is the teardown. |
189
- | `{...spread}` | Spread an object's keys as attributes (element) or props (component). |
162
+ | `attach={fn}` | Run a node-lifetime attachment against the element. |
163
+ | `{...spread}` | Spread an object's keys props on a component, attributes on an element. |
164
+
165
+ ### Control flow
190
166
 
191
- Control flow — mustache `{#…}` blocks (NOT `<template>`):
167
+ Mustache `{#…}` blocks, not `<template>`. The rendered blocks are
168
+ `if`/`for`/`await`/`switch`/`try`; `snippet` is a declaration block.
192
169
 
193
170
  | Block | Form |
194
171
  |---|---|
195
- | Conditional | `{#if}` / `{:else if}` / `{:else}` / `{/if}` |
196
- | Keyed list | `{#for item, i of list by key}` / `{/for}` |
197
- | Async list | `{#for await item of source}` / `{/for}` (over an `AsyncIterable`) |
198
- | Promise | `{#await p}` / `{:then v}` / `{:catch e}` / `{:finally}` / `{/await}` |
199
- | Switch | `{#switch subject}` / `{:case v}` / `{:default}` / `{/switch}` |
200
- | Error boundary | `{#try}` / `{:catch}` / `{:finally}` / `{/try}` |
201
- | Snippet | `{#snippet name(args)}…{/snippet}`, called `{name(args)}` |
202
-
203
- Components are capitalised tags; content nested in them renders where the
204
- component calls `{children()}` (`{#if children}{children()}{:else}…{/if}` is the
205
- fallback). The `<slot>` element, the `<template name>` snippet form, and
206
- `<template if>` / `<template each>` / control flow were **removed** a bare
207
- `<template>` is now an inert element, and any removed form throws a migration
208
- error. The branch keyword is `{:else if}` (a space).
172
+ | `{#if}` / `{:else if}` / `{:else}` / `{/if}` | Conditional chain (`{:else if}` has a space). |
173
+ | `{#for item, i of list by key}` / `{/for}` | Keyed list; `{#for await x of source}` iterates an AsyncIterable. |
174
+ | `{#await p}` / `{:then v}` / `{:catch e}` / `{:finally}` / `{/await}` | The explicit async opt-in (pending branch + `{:then}` narrowing). |
175
+ | `{#switch}` / `{:case}` / `{:default}` / `{/switch}` | Value switch. |
176
+ | `{#try}` / `{:catch}` / `{:finally}` / `{/try}` | Render error boundary. |
177
+ | `{#snippet name(args)}…{/snippet}` | Reusable builder; called `{name(args)}`. |
178
+
179
+ Async reads have no ceremony: the **bare call in an interpolation**
180
+ (`{getMessages({ limit })}`) is the documented default a peek that reads
181
+ `undefined` while pending (auto-streaming on SSR), composing with `?? fallback`,
182
+ `?.`, `{#if}`, and attributes; pair it with `.pending(...)` / `.error(...)`
183
+ probes. `{await fn()}` is an inline read that blocks SSR until the value is in the
184
+ HTML. `{#await}` is the opt-in for branch structure and `{:then}` narrowing not
185
+ the default.
186
+
187
+ Components are capitalised tags; the content nested in a component renders where
188
+ it calls `{children()}` (`{#if children}{children()}{:else}…{/if}` is the
189
+ fallback — no `<slot>` element, no named slots). The `<slot>` element, the
190
+ `<template name>` snippet form, and `<template if>`/`<template each>`/… control
191
+ flow were **removed** — a bare `<template>` is now an inert element, and using a
192
+ removed form throws a migration error.
209
193
 
210
194
  ## Server surface — abide/server/*
211
195
 
212
196
  ### RPC helpers — @documentation rpc
213
197
 
214
- - `@abide/abide/server/GET` — declares a read (GET) RPC; accepts `RpcReadOpts`
215
- (shared opts + `cache`/`stream`); query args. Bundler-rewritten; calling the
216
- bare helper throws.
217
- - `@abide/abide/server/POST` — declares a mutating (POST) RPC; `RpcSharedOpts`
218
- only (`cache`/`stream` are a compile error); JSON-body (or FormData) args.
219
- - `@abide/abide/server/PUT` — mutating PUT RPC; body args; `RpcSharedOpts`.
220
- - `@abide/abide/server/PATCH` — mutating PATCH RPC; body args; `RpcSharedOpts`.
221
- - `@abide/abide/server/DELETE` — mutating DELETE RPC; query args; `RpcSharedOpts`.
222
- - `@abide/abide/server/HEAD` — read HEAD RPC alongside GET; query args;
223
- `RpcReadOpts`.
224
-
225
- ### Responses@documentation response
226
-
227
- - `@abide/abide/server/json` — `json(data, init?)`: JSON `TypedResponse<T>` with
228
- RPC defaults (`no-store`), wire-encoding Set/Map/bigint/Date; `json(undefined)`
229
- → 204. `T` drives `Return` inference.
230
- - `@abide/abide/server/jsonl` `jsonl(iterable, init?)`: wraps an
231
- `AsyncIterable<Frame>` as `application/jsonl` (one JSON value per line); a
232
- generator error emits a final `{"$error":…}` line.
233
- - `@abide/abide/server/sse` — `sse(iterable, init?)`: wraps an
234
- `AsyncIterable<Frame>` as `text/event-stream` with 15s keepalive comments;
235
- errors emit an `event: error` frame.
236
- - `@abide/abide/server/error` — `error(status, message?, init?)`:
237
- `text/plain` `TypedResponse<never>`. `error.typed(name, status, schema?)`
238
- declares a reusable typed-error constructor driving `fn.isError(e, 'name')`.
239
- - `@abide/abide/server/redirect` — `redirect(url, status=302, init?)`:
240
- `TypedResponse<never>`, accepts relative URLs, `no-store`, status restricted to
241
- 301/302/303/307/308.
198
+ - `abide/server/GET` — read helper; `export const x = GET(fn, opts?)`. Adds
199
+ `cache`/`stream` opts. Read-only, so a typed input auto-exposes it to MCP + CLI.
200
+ - `abide/server/HEAD` — read helper, same shape as `GET` (headers only, no body).
201
+ - `abide/server/POST` — mutating helper; `RpcSharedOpts` only (`cache`/`stream`
202
+ are a compile error). Never auto-exposes to MCP.
203
+ - `abide/server/PUT` — mutating helper (idempotent replace); as `POST`.
204
+ - `abide/server/PATCH` — mutating helper (partial update); as `POST`.
205
+ - `abide/server/DELETE` — mutating helper (removal); as `POST`.
206
+
207
+ ### Response helpers — @documentation response
208
+
209
+ - `abide/server/json``json<T>(data, init?)` → `TypedResponse<T>`; default
210
+ `no-store`, honest-JSON encodes Set/Map/bigint/Date (ADR-0029); `undefined` body
211
+ 204.
212
+ - `abide/server/jsonl` `jsonl<Frame>(iterable, init?)`; JSON Lines stream
213
+ (`application/jsonl`), cancellation calls `iter.return()`, a generator error
214
+ emits a final `{"$error":…}` line.
215
+ - `abide/server/sse` — `sse<Frame>(iterable, init?)`; `text/event-stream` with a
216
+ 15 s keepalive comment; an error emits an `event: error` frame.
217
+ - `abide/server/error` — `error(status, message?, init?)` `TypedResponse<never>`
218
+ (positional status wins). `error.typed(name, status, schema?)` returns a typed
219
+ error constructor driving `rpc.isError`; `schema` types `.data`, never validates.
220
+ - `abide/server/redirect` — `redirect(url, status = 302, init?)`; accepts relative
221
+ URLs, sets `Location` + `no-store`.
242
222
 
243
223
  ### Request scope — @documentation request-scope
244
224
 
245
- - `@abide/abide/server/request` — `request(): Request` the in-flight inbound
246
- request (ALS-scoped); throws outside a request scope.
247
- - `@abide/abide/server/cookies` — `cookies(): Bun.CookieMap` the request's
248
- cookie jar; reads parse `Cookie`, writes flush as `Set-Cookie` on return.
249
- - `@abide/abide/server/server` — `server(): Bun.Server` the active server; a
250
- no-op in-process server for CLI/MCP/test dispatch; throws before init.
225
+ - `abide/server/request` — `request(): Request`; the inbound request from the ALS
226
+ scope, throws outside one.
227
+ - `abide/server/cookies` — `cookies(): Bun.CookieMap`; live read + `.set` / `.delete`,
228
+ flushed as `Set-Cookie` on return.
229
+ - `abide/server/server` — `server(): Server`; the active `Bun.serve`, an in-process
230
+ no-op server under CLI/MCP/test, else throws.
251
231
 
252
- ### Configuration — @documentation configuration
232
+ ### Server rendering — @documentation render
253
233
 
254
- - `@abide/abide/server/env` — `env(schema)`: validate `Bun.env` against a Standard
255
- Schema at module top level (synchronous; all issues at once) and return the
256
- typed config; also registers the schema for the launcher setup form.
234
+ - `abide/server/render` — `render(path, params?, query?)`: SSR a page route to an
235
+ HTML string from within a request scope; args mirror `url()`/`navigate`.
257
236
 
258
237
  ### Sockets — @documentation sockets
259
238
 
260
- - `@abide/abide/server/socket` — `socket(opts?)` / `socket({ schema })` declares a
261
- broadcast topic returning `Socket<T>`; opts `tail`/`ttl`/`clientPublish`/
262
- `schema`/`clients` (server-only; the client stub discards them).
239
+ - `abide/server/socket` — `socket<T>(opts?)`: declare one broadcast topic per file
240
+ under `src/server/sockets/`. Returns `Socket<T>` (isomorphic `AsyncIterable<T>`).
263
241
 
264
- ### Agent — @documentation agent
242
+ ### Configuration — @documentation configuration
265
243
 
266
- - `@abide/abide/server/agent` — `agent(engine, messages): AsyncIterable<AgentFrame>`
267
- runs a provider `AgentEngine` against the current request's MCP surface
268
- (forwarding caller auth); the handler picks transport via `jsonl`/`sse`. Exports
269
- `NeutralMessage`, `AgentFrame`, `AgentSurface`, `AgentEngine` types.
244
+ - `abide/server/env` — `env(schema)`: validate `Bun.env` against a Standard Schema
245
+ at module top-level, returning typed config; fails boot loudly, sync-only.
270
246
 
271
- ### Server plumbing — @documentation plumbing
247
+ ### App hooks & inspector — @documentation plumbing
272
248
 
273
- - `@abide/abide/server/AppModule` — type of the optional `src/app.ts` hooks
274
- (`forwardHeaders`/`init`/`handle`/`handleError`/`health`).
275
- - `@abide/abide/server/InspectorContext` — type of the capability object core
249
+ - `abide/server/AppModule` — `type AppModule`: the optional hooks exported from
250
+ `src/app.ts` (`init`/`handle`/`handleError`/`health`/`forwardHeaders`).
251
+ - `abide/server/InspectorContext` — `type InspectorContext`: the capabilities core
276
252
  injects into `@abide/inspector` when `ABIDE_ENABLE_INSPECTOR=true`.
277
- - `@abide/abide/server/rpc/defineRpc` — bundler-emitted RPC builder: resolves
278
- `clients`, validates input/files, applies `timeout`, registers the entry.
279
- - `@abide/abide/server/sockets/defineSocket` — bundler-emitted socket builder:
280
- per-subscriber queue + retained tail, optional `ttl`/`schema`, Bun-native
281
- fan-out.
282
- - `@abide/abide/server/prompts/definePrompt` — resolver-emitted prompt builder
283
- from `src/mcp/prompts/<name>.md`; registers with the MCP dispatcher.
284
- - `@abide/abide/server/prompts/renderPromptTemplate` — substitutes `{{name}}`
285
- placeholders in a prompt template body (missing args → empty string).
286
253
 
287
- ## Isomorphic surfaceabide/shared/*
254
+ ### RPC / socket registrars @documentation plumbing
288
255
 
289
- ### RPC schema projection @documentation rpc
256
+ - `abide/server/rpc/defineRpc` `defineRpc(method, url, handler, opts?)`: the real
257
+ server-side RPC builder the bundler emits; runs validation → 422, timeout → 504,
258
+ and registers the registry entry.
259
+ - `abide/server/sockets/defineSocket` — `defineSocket<T>(name, opts?)`: the server
260
+ runtime `socket()` binds to; retention defaults to `tail ?? 1`, `ttl` evicts
261
+ lazily, schema validates synchronously on publish.
290
262
 
291
- - `@abide/abide/shared/withJsonSchema` — `withJsonSchema(schema, toJsonSchema)`
292
- attaches a `toJSONSchema()` projection to a Standard Schema whose library lacks
293
- one native (feeds OpenAPI / MCP / CLI / setup form).
263
+ ## Isomorphic surface — abide/shared/*
294
264
 
295
- ### Error responses — @documentation response
265
+ ### RPC schema helper — @documentation rpc
296
266
 
297
- - `@abide/abide/shared/HttpError` — the error class thrown by a remote call on
298
- non-2xx: `status`, `statusText`, raw `response`, optional `kind`/`data` (set for
299
- a typed error or a 422 validation failure).
300
- - `@abide/abide/shared/ValidationErrorData` — type of `HttpError.data` when
301
- `kind === 'validation'`: `{ issues, fields }` (raw Standard Schema issues + a
302
- field→first-message map).
267
+ - `abide/shared/withJsonSchema` — `withJsonSchema(schema, toJsonSchema)`: attach a
268
+ `toJSONSchema()` projection to a Standard Schema whose library lacks one; feeds
269
+ OpenAPI / MCP / CLI.
303
270
 
304
- ### Cache mutation — @documentation cache
271
+ ### Response errors — @documentation response
305
272
 
306
- - `@abide/abide/shared/patch` — `patch(fn, args?, updater)` / `patch({tags},
307
- updater)`: reactively mutate the retained value of matching cached reads in
308
- place, no network the optimistic-update / real-time primitive.
309
- - `@abide/abide/shared/refresh` — `refresh(selector?, args?)`: refetch every
310
- matching cached read, keeping the stale value visible (`refreshing()` true)
311
- until fresh swaps in.
273
+ - `abide/shared/HttpError` — `class HttpError extends Error`; thrown by remote
274
+ calls on non-2xx. Carries `status`, `statusText`, `response`, optional `kind`
275
+ (declared error name or `'validation'`) and `data`.
276
+ - `abide/shared/ValidationErrorData` — `type ValidationErrorData`: the
277
+ `HttpError.data` shape when `kind === 'validation'` (422) `{ issues, fields }`,
278
+ `fields` mapping top-level field → first message.
312
279
 
313
- ### Page — @documentation page
280
+ ### Cache — @documentation cache
314
281
 
315
- - `@abide/abide/shared/page` `page`: isomorphic reactive `PageSnapshot` proxy
316
- (`route`/`params`/`url`/`navigating`); reading a field in a tracking scope
317
- re-runs on navigation.
282
+ Isomorphic per ADR-0041: local on the client, broadcast to every connected client
283
+ from the server.
318
284
 
319
- ### Probes@documentation probes
285
+ - `abide/shared/invalidate``invalidate(selector?, args?)`: the drop verb — drop
286
+ matching cached reads so the next read reloads lazily. Selector grammar:
287
+ `(fn, args)` / `(fn)` / `({ tags })` / `()`.
288
+ - `abide/shared/patch` — `patch(fn, args?, updater)` / `patch({ tags }, updater)`:
289
+ mutate the retained value of matching reads in place, reactive, no network (the
290
+ optimistic-update / socket-frame primitive).
291
+ - `abide/shared/refresh` — `refresh(selector?, args?)`: refetch matching reads now,
292
+ keeping the stale value visible (`refreshing()` true) until the fresh one swaps in.
320
293
 
321
- - `@abide/abide/shared/pending``pending(source?, args?): boolean` — reactive
322
- "no value yet" over cached calls and tail streams.
323
- - `@abide/abide/shared/peek` — `peek(source, args?)` — the currently-retained
324
- value synchronously, triggering nothing; `undefined` when nothing retained.
325
- - `@abide/abide/shared/refreshing` — `refreshing(source?, args?): boolean` —
326
- reactive "holding a value while a fresher source is in flight".
327
- - `@abide/abide/shared/done` — `done(subscribable): boolean` — reactive terminal
328
- read: true once a stream closed.
329
- - `@abide/abide/shared/online` — `online(): boolean` — reactive connectivity
330
- probe (browser online/offline; server reflects the caller's reported state).
294
+ ### Probes@documentation probes
331
295
 
332
- ### URL@documentation url
296
+ Reactive, report-never-actreading opens no fetch/stream.
333
297
 
334
- - `@abide/abide/shared/url` — `url(path, ...args): string` resolves any in-app URL
335
- base-correctly (RPC query, page params, or asset); external paths pass through.
336
- Exports `PathParams` and augmentable `RpcRoutes`/`PageRoutes`/`PublicAssets`.
298
+ - `abide/shared/pending` — `pending(selector?/subscribable?, args?): boolean` "no
299
+ value yet"; `()` = anything in flight.
300
+ - `abide/shared/peek` `peek(fn, args?)` / `peek(socket)` / `peek(cell)`: the
301
+ retained value synchronously, no trigger; null-tolerant.
302
+ - `abide/shared/refreshing` — `refreshing(selector?, args?): boolean` — holding a
303
+ prior value while a fresher source is in flight (distinct from `pending`).
304
+ - `abide/shared/done` — `done(subscribable): boolean` — stream-only, true once the
305
+ source has closed.
306
+ - `abide/shared/online` — `online(): boolean` — reactive connectivity; browser
307
+ reads `navigator.onLine`, server reflects the calling client's reported state.
337
308
 
338
- ### Templating — @documentation templating
309
+ ### Observability — @documentation observability
339
310
 
340
- - `@abide/abide/shared/snippet` — `snippet(payload)` brands a snippet payload so a
341
- `{expr}` interpolation mounts it (the compiler wraps a `{#snippet}` body); also
342
- exports `SnippetValue` and `Snippet<Args>` types.
311
+ - `abide/shared/health` — `health(): HealthState` reactive backend-health read;
312
+ polls `/__abide/health` only while a tracking scope reads it. Also exports
313
+ `AppHealth` / `AppHealthMap` / `HealthState` types.
314
+ - `abide/shared/log` — `log: Log`, callable (`log(message, data?)` is the info-level
315
+ speaker) with members `log.warn` / `log.error` / `log.trace(name, work)` /
316
+ `log.channel(name)` (DEBUG-gated); records carry request-scope context, tsv or
317
+ JSON under `ABIDE_LOG_FORMAT=json`. (No `log.info` — the base call is info.)
318
+ - `abide/shared/reachable` — `reachable(host?): Promise<boolean>`: isomorphic
319
+ outbound reachability, HEADs the origin, caches per TTL; no host = the app's own
320
+ backend (constant `true` on server).
321
+ - `abide/shared/trace` — `trace(): string | undefined`: the current request's W3C
322
+ `traceparent` (server from the ALS scope, browser from `__SSR__`).
323
+
324
+ ### Page & URL — @documentation page / url
325
+
326
+ - `abide/shared/page` — `page: PageSnapshot`: reactive proxy with `route` /
327
+ `params` / `url` / `navigating` for the active page; reading a field in a tracked
328
+ scope re-runs on navigation. (`@documentation page`)
329
+ - `abide/shared/url` — `url(path, ...args): string`: resolve any in-app URL to its
330
+ base-correct form (RPC → query, page route → typed params, asset → bare path);
331
+ throws on a missing required param. Also exports `RpcRoutes` / `PageRoutes` /
332
+ `PublicAssets` / `PathParams`. (`@documentation url`)
343
333
 
344
- ### Observability — @documentation observability
334
+ ### Templating — @documentation templating
345
335
 
346
- - `@abide/abide/shared/health` — `health(): HealthState` reactive backend-health
347
- read (reachability + the app's `health()` fields), reader-driven poll of
348
- `/__abide/health`; composes `navigator.onLine`.
349
- - `@abide/abide/shared/log` `log`: the unified request-scope-aware logger
350
- (`log(...)`, `.warn`/`.error`/`.trace`, `.channel(name)` for a DEBUG-gated
351
- channel); TSV by default, JSON under `ABIDE_LOG_FORMAT=json`.
352
- - `@abide/abide/shared/reachable` — `reachable(host?): Promise<boolean>` —
353
- isomorphic outbound reachability HEAD probe, cached per TTL; any response counts
354
- reachable, only connection failure/timeout is not.
355
- - `@abide/abide/shared/trace` — `trace(): string | undefined` — the current
356
- request's W3C `traceparent` (server from ALS, browser from `__SSR__`).
336
+ - `abide/shared/snippet` — `snippet<Payload>(payload): SnippetValue`: brand a
337
+ payload so a `{expr}` interpolation mounts it instead of escaping (client builder
338
+ / server string). Also exports `snippetPayload` and the `Snippet` / `SnippetValue`
339
+ types. (The `html` primitive lives under the UI surface.)
357
340
 
358
- ### Isomorphic plumbing — @documentation plumbing
341
+ ### Subscriber — @documentation plumbing
359
342
 
360
- - `@abide/abide/shared/createSubscriber` — `createSubscriber(start)`: abide-ui
361
- subscriber grounded in the signal core (open-on-first-tracked-read,
362
- close-on-last-reader).
343
+ - `abide/shared/createSubscriber` — `createSubscriber(start)`: an
344
+ open-on-first-read / close-on-last-reader primitive over abide's signal core, for
345
+ building custom reactive sources.
363
346
 
364
347
  ## UI surface — abide/ui/* (client-only)
365
348
 
366
349
  ### Reactive state — @documentation reactive-state
367
350
 
368
- - `@abide/abide/ui/state` — `state(initial, transform?)` writable cell (`.value`);
369
- members `state.computed(fn)` (read-only derived), `state.linked(fn, transform?)`
370
- (writable, reseeded from a thunk), `state.share(key, value)` / `state.shared(key)`
371
- (ambient scope context).
372
- - `@abide/abide/ui/watch` — `watch(source, handler)` the single reaction primitive
373
- (cell / cell array / socket-stream / RPC); bare `watch(thunk)` is an auto-tracked
374
- effect; returns a scope-tied disposer; SSR-inert.
375
- - `@abide/abide/ui/props` `props()` ambient prop reader; compiler-lowered inside
376
- a component; throws if called directly.
351
+ - `abide/ui/state` — the `state(v, transform?)` writable cell plus members
352
+ `state.computed` (read-only derived), `state.linked` (writable, reseeded from a
353
+ thunk), and the context seam `state.share` / `state.shared`.
354
+ - `abide/ui/watch` — the single reaction primitive `watch(source, handler)` (and
355
+ bare `watch(thunk)` auto-tracked effect); unifies effect / socket.on / cache.on /
356
+ rpc-selector; client-only, returns a scope-tied disposer.
357
+ - `abide/ui/props` `props()` prop reader; compiler-lowered inside a `.abide`
358
+ component, throws if called directly.
377
359
 
378
360
  ### Templating — @documentation templating
379
361
 
380
- - `@abide/abide/ui/html` — `html\`…\`` (or `html(string)`) returns branded
381
- **unescaped** raw HTML for `{expr}` insertion; interpolations are not
382
- auto-escaped; nullish → empty.
383
-
384
- ### Navigate — @documentation navigate
385
-
386
- - `@abide/abide/ui/navigate` `navigate(path, ...rest)` typed in-app SPA
387
- navigation (params first for `[name]` routes, then `{ replace?, keepScroll? }`);
388
- builds through `url()`.
389
-
390
- ### UI plumbing — @documentation plumbing
391
-
392
- - `@abide/abide/ui/effect` `effect(fn)` internal reactive effect (tracks reads,
393
- re-runs, returns a disposer); compiler-emitted — authors use `watch`.
394
- - `@abide/abide/ui/currentScope` `scope()` resolves the current lexical scope;
395
- the internal lowering host the compiler targets.
396
- - `@abide/abide/ui/enterRenderScope` — `enterScope()` establishes a fresh isolated
397
- SSR-render scope, returning the previous.
398
- - `@abide/abide/ui/exitRenderScope` — `exitScope(previous)` restores the scope
399
- `enterScope` saved.
400
- - `@abide/abide/ui/router` — `router(host, loaders, layoutLoaders?, probe?)` the
401
- History-API client router: match, code-split, mount a diffed outlet chain, drive
402
- SPA nav with scroll restoration.
403
- - `@abide/abide/ui/startClient` `startClient(routes, layoutRoutes?, target)` the
404
- client entry: read `window.__SSR__`, seed cache/streamed/warm state, start the
405
- router; returns a disposer.
406
- - `@abide/abide/ui/renderToStream` `renderToStream(render)` out-of-order SSR
407
- streaming generator: shell first, then one `<abide-resolve>` fragment per
408
- streaming `{#await}` in completion order.
409
- - `@abide/abide/ui/remoteProxy` — `remoteProxy(method, url, options?)`
410
- bundler-target client substitute for a server RPC (fetch over the network; does
411
- the client pre-flight input validation; attaches the real `.watch`).
412
- - `@abide/abide/ui/socketProxy` — `socketProxy(name)` bundler-target client
413
- substitute for a server socket (subscribes over the multiplexed ws).
414
- - `@abide/abide/ui/settleAsyncCells` — the SSR await-barrier draining the
415
- request-scoped pending-cell list; client no-op.
416
- - `@abide/abide/ui/flight` — server-only flight-starter hoisting a hoistable
417
- await's promise into the sync prefix so independent flights overlap.
418
- - `@abide/abide/ui/isolateCellBarrier` runs a hoisted child render under its own
419
- async-cell barrier so its cells isolate from siblings; client passthrough.
420
- - `@abide/abide/ui/finalizeStreamedChildren` the when-to-stream decision run
421
- after a component body walk; fills each hoistable child's reserved output slot.
422
- - `@abide/abide/ui/runtime/withPath` pushes one escaped render-path segment for
423
- the duration of a synchronous build.
424
- - `@abide/abide/ui/runtime/renderPath` — composes a streamed child's ordinal
425
- segment onto the ambient render path and returns the boundary id.
426
- - `@abide/abide/ui/runtime/escapeKey` — escapes one key to an RFC 6901
427
- JSON-Pointer token so a `/`-bearing key survives a `/`-joined render path.
428
- - `@abide/abide/ui/runtime/nextBlockId` — the next await/try block id in the
429
- current render pass, namespaced by render path.
430
- - `@abide/abide/ui/runtime/blockId` allocates a render-path-namespaced await/try
431
- block id with a per-path document-order counter.
432
- - `@abide/abide/ui/runtime/enterRenderPass` marks entry into a render/mount;
433
- depth 0 clears the per-path block-id counters.
434
- - `@abide/abide/ui/runtime/exitRenderPass` marks exit, unwinding the render-pass
435
- depth.
436
- - `@abide/abide/ui/dom/mount` — mounts a top-level page/layout into a host under an
437
- ownership scope; returns a disposer.
438
- - `@abide/abide/ui/dom/mountChild` — mounts a `<Child/>` as a marker-bounded range
439
- (no wrapper element).
440
- - `@abide/abide/ui/dom/mountStreamedChild` — client adopter for a hoistable child:
441
- adopts an inlined range or a streamed boundary.
442
- - `@abide/abide/ui/dom/mergeProps` composes a child's props from ordered layers
443
- (explicit runs, spreads, trailing `children`), last-wins per key.
444
- - `@abide/abide/ui/dom/spreadProps` wraps a `{...source}` spread layer so each
445
- key resolves to a live value thunk.
446
- - `@abide/abide/ui/dom/restProps` the live `...rest` of a `props()` destructure.
447
- - `@abide/abide/ui/dom/bindProp` — parent half of a component `bind:prop`
448
- (annotates the prop thunk with a write-back channel).
449
- - `@abide/abide/ui/dom/bindableProp` — child half of a two-way prop (the writable
450
- cell the child writes/forwards).
451
- - `@abide/abide/ui/dom/spreadAttrs` — spreads an object's keys onto a native
452
- element (`on<event>` keys attach listeners, others bind as reactive attrs).
453
- - `@abide/abide/ui/dom/readCall` — guarded method call on a reactive-document read
454
- so throws name the authored scope path.
455
- - `@abide/abide/ui/dom/readCell` — unified read for a `computed`/`linked`
456
- reference (async peek / derive call / sync `.value`).
457
- - `@abide/abide/ui/dom/cellPending` — whether a `{#if}`/`{#switch}` async subject
458
- is still loading (render no branch while pending).
459
- - `@abide/abide/ui/dom/mutateDocContainer` — in-place container mutation lowered to
460
- clone-mutate-replace so a patch emits and readers wake.
461
- - `@abide/abide/ui/dom/hydrate` — adopts server-rendered DOM in place with a claim
462
- cursor (attach listeners/effects, no re-render); returns a disposer.
463
- - `@abide/abide/ui/dom/appendText` — reactive `{expr}` interpolation (escaped text
464
- / snippet builder / `html\`\`` raw).
465
- - `@abide/abide/ui/dom/appendTextAt` — reactive `{expr}` mounted at a skeleton
466
- anchor comment, interleaved with element siblings.
467
- - `@abide/abide/ui/dom/appendSnippet` — mounts a `{snippet(args)}` builder's nodes
468
- in a marker-bounded range.
469
- - `@abide/abide/ui/dom/appendStatic` — a static text node, created or claimed from
470
- SSR text.
471
- - `@abide/abide/ui/dom/cloneStatic` — appends a fully-static bindingless subtree via
472
- one cached-template deep clone.
473
- - `@abide/abide/ui/dom/skeleton` — clones a template and locates its bound
474
- holes/anchors for a subtree carrying bindings/control-flow.
475
- - `@abide/abide/ui/dom/anchorCursor` — positions a skeleton-anchored control-flow
476
- block or slot at its `<!--a-->` anchor.
477
- - `@abide/abide/ui/dom/mountSlot` — mounts a component's `{children()}` content
478
- (parent children or fallback) as a marker-bounded range.
479
- - `@abide/abide/ui/dom/outlet` — a layout's outlet boundary the router fills with
480
- the next chain layer.
481
- - `@abide/abide/ui/dom/attr` — binds an element attribute to `read()` via one
482
- effect (`name={expr}`).
483
- - `@abide/abide/ui/dom/on` — attaches an event listener pinned to the owning scope
484
- (`on<event>`).
485
- - `@abide/abide/ui/dom/attach` — runs an `attach={fn}` against an element and
486
- registers its teardown.
487
- - `@abide/abide/ui/dom/bindSelectValue` — two-way `bind:value` for `<select>`
488
- (reactive selection + change write-back; `multiple` = array membership).
489
- - `@abide/abide/ui/dom/each` — keyed list binding (`{#for by key}`),
490
- marker-range rows reconciled by key with minimal DOM moves.
491
- - `@abide/abide/ui/dom/eachAsync` — async keyed list over an AsyncIterable
492
- (`{#for await}`); rows append as it yields; SSR renders none.
493
- - `@abide/abide/ui/dom/when` — conditional swappable range (`{#if}`/`{:else}`) with
494
- an optional pending state.
495
- - `@abide/abide/ui/dom/awaitBlock` — await-block runtime across
496
- pending/resolved/error branches with SSR resume adoption (`{#await}`).
497
- - `@abide/abide/ui/dom/tryBlock` — error-boundary block catching thrown/async-cell
498
- errors into a catch branch (`{#try}`).
499
- - `@abide/abide/ui/dom/switchBlock` — multi-branch swappable range (first matching
500
- `{:case}` else `{:default}`; also backs `{:else if}` chains).
362
+ - `abide/ui/html` — `html` marks a string as trusted raw HTML (`html(str)` or
363
+ tagged `` html`…` ``) so a `{expr}` inserts nodes unescaped; nullish empty.
364
+
365
+ ### Navigation — @documentation navigate
366
+
367
+ - `abide/ui/navigate` — `navigate(path, params?, options?)`: typed client
368
+ navigation through `url()`, writing history + the reactive route
369
+ (`replace`/`keepScroll`). Also exports `navigatePath` and `NavigateOptions`.
370
+
371
+ ### Effect — @documentation plumbing
372
+
373
+ - `abide/ui/effect` — the internal effect primitive the compiler emits (runs now,
374
+ re-runs on change, supports teardown/async); authors use `watch`.
375
+
376
+ ### Client entry, router & proxies @documentation plumbing
377
+
378
+ - `abide/ui/startClient` — the official client entry: reads `window.__SSR__`, seeds
379
+ cache/cell/doc state, installs slots, starts the router; returns a disposer.
380
+ - `abide/ui/router` — the History-API router: matches the path, imports page +
381
+ layout chunks, mounts them as an outlet chain, diffs on navigation, restores scroll.
382
+ - `abide/ui/renderToStream` — server async generator for out-of-order SSR streaming:
383
+ yields the shell, then one `<abide-resolve>` fragment per streaming await block.
384
+ - `abide/ui/remoteProxy` the client-side RPC substitute: a `RemoteFunction` that
385
+ fetches over the network (input pre-flight validation, timeout/abort merge, output
386
+ decode). Also exports `RemoteProxyOptions`.
387
+ - `abide/ui/socketProxy` the client-side `Socket` substitute over the multiplexed
388
+ ws channel (iterate / `.tail` / `.publish` / real `.watch`).
389
+
390
+ ### Scope & render-pass plumbing — @documentation plumbing
391
+
392
+ - `abide/ui/currentScope` resolve the current lexical reactive scope (mints a
393
+ detached root outside any scope).
394
+ - `abide/ui/enterRenderScope` — establish a fresh per-render SSR scope, returning the
395
+ previous to restore.
396
+ - `abide/ui/exitRenderScope` — restore the scope `enterRenderScope` saved.
397
+ - `abide/ui/settleAsyncCells` — SSR await-barrier draining the request-scoped
398
+ pending-cell list; no-op on client.
399
+ - `abide/ui/flight` start a hoisted SSR flight from a thunk with a synchronous
400
+ `settled`/`value`/`error` snapshot so flights overlap. Also exports `FlightPromise`.
401
+ - `abide/ui/isolateCellBarrier` run a hoisted child render under its own async-cell
402
+ barrier list; inert passthrough on client.
403
+ - `abide/ui/finalizeStreamedChildren` the ADR-0039 when-to-stream decision after a
404
+ body walk (inline a settled flight, rethrow a rejected one, or stream). Also
405
+ exports `StagedChild`.
406
+ - `abide/ui/runtime/withPath` — push one escaped render-path segment for a
407
+ synchronous `build`, restoring after.
408
+ - `abide/ui/runtime/renderPath` — compose a child's ordinal segment onto the ambient
409
+ path, returning the render-path string (the streamed-child boundary id).
410
+ - `abide/ui/runtime/escapeKey` — escape one path key to an RFC-6901 JSON-Pointer token.
411
+ - `abide/ui/runtime/nextBlockId` allocate the next await/try block id in the current
412
+ render pass, namespaced by the ambient path.
413
+ - `abide/ui/runtime/blockId` allocate a render-path-namespaced block id
414
+ (`${path}:${n}`) from a counters map.
415
+ - `abide/ui/runtime/enterRenderPass` mark entry into a render/mount, clearing the
416
+ per-path block-id counters at the outermost depth.
417
+ - `abide/ui/runtime/exitRenderPass` — mark exit from a render/mount, unwinding the depth.
418
+
419
+ ### DOM runtime helpers — @documentation plumbing
420
+
421
+ Compiler-emitted; an author never imports these directly.
422
+
423
+ - `abide/ui/dom/mount` mount a top-level page/layout into a host under an ownership
424
+ scope + render pass; returns a disposer.
425
+ - `abide/ui/dom/mountChild` mount a child component as a marker-bounded range (no
426
+ wrapper), filing its dispose with the owner.
427
+ - `abide/ui/dom/mountStreamedChild` dual-mode adopter for a hoistable child: adopt an
428
+ inlined range, claim a streamed boundary, or create-mount fresh.
429
+ - `abide/ui/dom/mountSlot` — mount a component's slot content as a marker-bounded range
430
+ (create fills, hydrate adopts); runs once.
431
+ - `abide/ui/dom/mergeProps` — compose a child's props from ordered layers (runs,
432
+ spreads, slot) into one last-layer-wins Proxy bag.
433
+ - `abide/ui/dom/spreadProps` — wrap a `{...source}` spread as a live Proxy of value
434
+ thunks (keys/membership re-evaluated live; `children` hidden).
435
+ - `abide/ui/dom/restProps` — live Proxy of the unconsumed props for `{ ...rest }`,
436
+ excluding destructured keys and `children`.
437
+ - `abide/ui/dom/bindProp` — the parent half of a component `bind:prop`: annotate the
438
+ value thunk with a `.set` write-back channel.
439
+ - `abide/ui/dom/bindableProp` — the child half of a two-way prop: pass-through when the
440
+ parent bound it, else a local reseeding `linked` cell.
441
+ - `abide/ui/dom/spreadAttrs` — spread a thunk's keys onto a native element (`on*` →
442
+ listeners, others reactive attributes); `exclude` skips explicit attrs.
443
+ - `abide/ui/dom/readCall` — guarded non-optional method call on a reactive-document
444
+ read, with legible authored-scope errors.
445
+ - `abide/ui/dom/readCell` — unified read of a `computed`/`linked`/derive reference
446
+ (async throwing peek, function → call, sync → `.value`).
447
+ - `abide/ui/dom/writeCell` — unified write of a `linked` reference from an author
448
+ assignment (async `.set`, sync → `.value =`).
449
+ - `abide/ui/dom/cellPending` — whether a control-flow subject is a still-loading async
450
+ cell, so a block renders no branch while loading.
451
+ - `abide/ui/dom/mutateDocContainer` — apply an in-place container mutation
452
+ (`splice`/`sort`/`add`/…) on a reactive-document value by clone-mutate-`replace`.
453
+ - `abide/ui/dom/hydrate` — adopt existing server-rendered DOM in place with a claim
454
+ cursor active (no re-render); returns a disposer.
455
+ - `abide/ui/dom/appendText` — a reactive `{expr}` interpolation: escaped text,
456
+ `html`-branded raw, or a snippet call; create-appends or claims the SSR text node.
457
+ - `abide/ui/dom/appendTextAt` — a reactive `{expr}` mounted just after a skeleton
458
+ anchor comment (text interleaved with element siblings).
459
+ - `abide/ui/dom/appendSnippet` — mount a `{snippet(args)}` builder in a marker-bounded
460
+ range, reactive in its args.
461
+ - `abide/ui/dom/appendStatic` — a static (non-reactive) text node; create-appends or
462
+ claims the merged SSR text node.
463
+ - `abide/ui/dom/cloneStatic` — append a fully-static binding-free subtree by cloning a
464
+ cached template (create) / advancing the claim cursor (hydrate).
465
+ - `abide/ui/dom/skeleton` — realize a compiled skeleton (static subtree with located
466
+ holes) and return its element + anchor holes.
467
+ - `abide/ui/dom/anchorCursor` — position a skeleton-anchored block/slot: the create
468
+ insertion reference after the anchor and the hydrate claim cursor.
469
+ - `abide/ui/dom/outlet` — a layout outlet: insert/claim an empty `abide:outlet` comment
470
+ boundary; returns `{ open, close }`.
471
+ - `abide/ui/dom/attr` — bind an element attribute to `read()` via one effect
472
+ (boolean-present/absent + stringify semantics).
473
+ - `abide/ui/dom/on` — attach an event listener (`onclick={…}` target), scope-pinned and
474
+ batched, removal registered on the owner.
475
+ - `abide/ui/dom/attach` — run an `attach={}` attachment against an element at build
476
+ time and register its teardown (the dual of `on`).
477
+ - `abide/ui/dom/bindSelectValue` — two-way `bind:value` for `<select>` (effect drives
478
+ selection re-applied via MutationObserver; `change` writes back; `multiple` → array).
479
+ - `abide/ui/dom/each` — keyed list runtime (`{#for … by key}`): each row a
480
+ marker-bounded range, reconciled by key with minimal DOM moves.
481
+ - `abide/ui/dom/eachAsync` — async keyed list runtime (`{#for await … of …}`):
482
+ append/reconcile as the iterator yields; mid-stream rejection routes to the catch.
483
+ - `abide/ui/dom/when` — conditional runtime (`{#if}`/`{:else}`): a swappable range
484
+ tracking `condition()`, with an optional pending state for a bare async subject.
485
+ - `abide/ui/dom/awaitBlock` — `{#await}` runtime: render pending then swap to
486
+ resolved/error; reactive re-runs on cache invalidation; hydration resumes or rebuilds.
487
+ - `abide/ui/dom/tryBlock` — `{#try}` reactive error-boundary runtime: catch build /
488
+ read / re-run throws, swap to the catch branch, re-arm on recovery.
489
+ - `abide/ui/dom/switchBlock` — `{#switch}` / `{:else if}` runtime: a swappable range
490
+ selecting the first matching (`===`) case (or default), with per-case async gating.
501
491
 
502
492
  ## Build / tooling
503
493
 
504
494
  ### Building — @documentation building
505
495
 
506
- - `@abide/abide/build` — `build(opts?)` builds the client bundle into `dist/` and
507
- concurrently writes `src/.abide/*.d.ts`; never throws. Options `cwd`/`minify`/
508
- `compress`/`clean`/`exitOnFailure`/`dev`.
509
- - `@abide/abide/compile` `compile(opts?)` produces a standalone Bun server
510
- executable (runs the client build first to embed assets); returns the binary
511
- path.
512
-
513
- ### Tooling@documentation plumbing
514
-
515
- - `@abide/abide/ui-plugin` — the `abide-ui` `BunPlugin` that loads and compiles
516
- `.abide` components (and pulls scoped `<style>` into browser bundles).
517
- - `@abide/abide/preload` — the Bun preload module registering the UI plugin, the
518
- resolver plugin, and the `.css` no-op loader (used by `bunfig` `[test]` and the
519
- CLI `--preload`).
520
- - `@abide/abide/resolver-plugin` — `abideResolverPlugin({ cwd?, embedAssets?,
521
- target? })` wiring every `abide:*` virtual module, the `$server`/`$ui`/`$shared`/
522
- `$mcp`/`$cli` aliases, and the per-target RPC/socket rewrite (with the
523
- client-side server-code-leak guard).
524
- - `@abide/abide/tsconfig` — the base `tsconfig.app.json` for consuming apps to
525
- `extends`.
526
-
527
- ## Desktop bundle — @documentation bundle
528
-
529
- - `@abide/abide/server/appDataDir` — `appDataDir()` returns the bundle's per-user
530
- data dir keyed by the injected program name; cwd-independent, pure.
531
- - `@abide/abide/bundle/BundleWindow` — type of the default export from
532
- `src/bundle/window.ts`: `{ title?, width?, height?, menu?, config? }`.
533
- - `@abide/abide/bundle/BundleMenu` `{ label, items: BundleMenuItem[] }`, a
534
- top-level bundle menu.
535
- - `@abide/abide/bundle/BundleMenuItem` — a menu entry: separator, or `emit` (a
536
- page `abide:menu` event), or `navigate`.
537
- - `@abide/abide/bundle/onMenu` `onMenu(handler)` / `onMenu(name, handler)`
538
- subscribes to bundle menu-click events; returns an unsubscribe; inert in a
539
- plain tab / SSR.
540
- - `@abide/abide/bundle/bundled` — `bundled(): boolean` — am I part of the abide
541
- desktop bundle (client reads `window.__ABIDE_BUNDLE__`, server the parent-pid
542
- env).
543
-
544
- ## MCP @documentation mcp
545
-
546
- - `@abide/abide/mcp/createMcpServer` — `createMcpServer(opts?)` constructs the
547
- framework-generated MCP server bound to the RPC/socket registries; returns
548
- `{ handle(request) }` (the `/__abide/mcp` handler). Tools derive from surfaces
549
- with `clients.mcp`.
496
+ - `abide/build` — `build({ cwd, … })`: build the client bundle into `dist/_app`
497
+ (optional gzip/Tailwind), and write `src/.abide/*.d.ts`.
498
+ - `abide/compile` — `compile({ cwd, target?, outfile? })`: build the client then a
499
+ standalone Bun server executable; returns the binary path.
500
+
501
+ ### Plumbing — @documentation plumbing
502
+
503
+ - `abide/preload`the Bun preload registering `abideUiPlugin` +
504
+ `abideResolverPlugin` + a css-noop loader; `ABIDE_TARGET` selects server/client mode.
505
+ - `abide/resolver-plugin` — `abideResolverPlugin({ cwd, embedAssets, target })`: the
506
+ BunPlugin wiring the `abide:*` virtuals and rewriting rpc/socket/prompt modules.
507
+ - `abide/ui-plugin` — `abideUiPlugin`: the BunPlugin that loads `.abide` components
508
+ (compiles to an ES module; a `layout.abide` lowers its outlet to the router).
509
+ - `abide/tsconfig` — the consumer-extendable `tsconfig.app.json`.
510
+
511
+ ## Desktop bundle
512
+
513
+ ### Desktop bundle — @documentation bundle
514
+
515
+ - `abide/bundle/BundleWindow` — `type BundleWindow`: the default-export window config
516
+ (title/size/menu) from `src/bundle/window.ts`, baked into the launcher.
517
+ - `abide/bundle/BundleMenu``type BundleMenu`: a `{ label, items }` top-level menu
518
+ inserted between the Edit and Window menus.
519
+ - `abide/bundle/BundleMenuItem` — `type BundleMenuItem`: a divider or clickable item
520
+ (`emit` dispatches an `abide:menu` event, `navigate` repoints the window, `shortcut`
521
+ = Cmd-key).
522
+ - `abide/bundle/onMenu` `onMenu(handler)` / `onMenu(name, handler)`: subscribe to
523
+ `abide:menu` clicks, returns unsubscribe; inert in SSR / a plain browser tab.
524
+ - `abide/bundle/bundled` — `bundled(): boolean`: true when running inside the abide
525
+ desktop bundle (isomorphic).
526
+ - `abide/server/appDataDir` — `appDataDir()`: the running bundle's per-user data dir
527
+ keyed by program name (pure, cwd-independent). (`@documentation bundle`)
528
+
529
+ ## MCP
530
+
531
+ ### MCP server @documentation mcp
532
+
533
+ - `abide/mcp/createMcpServer` — `createMcpServer(opts?)`: construct the framework MCP
534
+ server bound to the RPC registry; `handle(request)` is what `/__abide/mcp` invokes.
535
+ Tools come from rpcs/sockets with `clients.mcp: true`; optional `authorize`.
536
+
537
+ ### Agent @documentation agent
538
+
539
+ - `abide/server/agent` — `agent(engine, messages)`: run a model engine against the
540
+ app's own MCP surface, returning the neutral `AgentFrame` stream (no transport — wrap
541
+ in `jsonl()` / `sse()`).
542
+
543
+ ### Prompts — @documentation plumbing
544
+
545
+ - `abide/server/prompts/definePrompt` — `definePrompt(name, opts)`: build + register a
546
+ `Prompt`; called by the resolver-generated module for each `src/mcp/prompts/*.md`.
547
+ - `abide/server/prompts/renderPromptTemplate` — `renderPromptTemplate(template, args)`:
548
+ substitute `{{name}}` placeholders (missing → empty string).
550
549
 
551
550
  ## Testing
552
551
 
553
552
  ### Testing — @documentation testing
554
553
 
555
- - `@abide/abide/test/createTestApp` — `createTestApp()` boots the real app on an
556
- ephemeral port; returns `{ origin, fetch, rpc, sockets, health, stop,
557
- [Symbol.asyncDispose] }` (`rpc`/`sockets` typed by generated d.ts). Use
558
- `await using`.
554
+ - `abide/test/createTestApp` — the augmentable typed test app exposing
555
+ `app.rpc.<rpc>` / `app.sockets.<name>` (types arrive from `src/.abide/` after a build).
559
556
 
560
- ### Testing plumbing — @documentation plumbing
557
+ ### Plumbing — @documentation plumbing
561
558
 
562
- - `@abide/abide/test/createScriptedSurface` — `createScriptedSurface(tools?)` a
563
- scripted `AgentSurface` for engine tests; records every `call`.
564
- - `@abide/abide/test/assertAgentFrameConformance` — collects an engine frame
565
- stream and asserts the neutral `AgentFrame` contract (one terminal `done`; every
566
- `tool_use` answered), throwing on violation.
559
+ - `abide/test/createScriptedSurface` — `createScriptedSurface(...)`: a scripted
560
+ `AgentSurface` with declarative tool stubs for engine tests; records every `call`.
561
+ - `abide/test/assertAgentFrameConformance` — `assertAgentFrameConformance(...)`: assert
562
+ the neutral `AgentFrame` contract (single terminal `done`, matched
563
+ `tool_use`/`tool_result`, string deltas).
567
564
 
568
565
  ## Generated machine surfaces
569
566
 
570
- Runtime routes the framework serves:
567
+ Runtime routes served by the framework:
571
568
 
572
569
  | Route | Serves |
573
570
  |---|---|
574
- | `/openapi.json` | OpenAPI spec for the public `/rpc/*` surface, built lazily from the frozen RPC registry. |
575
- | `/__abide/mcp` | MCP endpoint (POST → `mcp.handle`), through the app auth/CSRF pipeline; mounted when an MCP is configured. |
576
- | `/__abide/health` | Health/identity probe answered ahead of app middleware (framework identity + `app.health()` fields), `no-store`. |
577
- | `/__abide/identity` | Compatibility alias of the health payload, stamped `{ abide: true }` for legacy probers. |
578
- | `/__abide/inspector` | Operator inspector UI + data/SSE routes, gated by `ABIDE_ENABLE_INSPECTOR` (optional `@abide/inspector`). |
579
- | `/__abide/sockets` | WebSocket upgrade for the socket multiplex hub; `/__abide/sockets/<name>` is one socket's HTTP face (GET tail, POST publish). |
580
- | `/__abide/cli` | GET returns the platform-detecting install script; `/__abide/cli/<platform>` streams the built CLI binary tarball. |
571
+ | `/openapi.json` | The generated OpenAPI document. |
572
+ | `/__abide/mcp` | The generated MCP server endpoint (`createMcpServer().handle`). |
573
+ | `/__abide/sockets` | The sockets hub (ws multiplex); `/__abide/sockets/<name>` is the per-socket HTTP/SSE face (GET tail, POST publish). |
574
+ | `/__abide/health` | Health JSON, aggregating the app's optional `health(request)` hook. |
575
+ | `/__abide/identity` | Server identity (name / version). |
576
+ | `/__abide/cli` | CLI binary download; `/__abide/cli/<platform>` streams the thin cli + server. |
577
+ | `/__abide/inspector` | Inspector surface, mounted only when `ABIDE_ENABLE_INSPECTOR=true`. |
581
578
 
582
579
  ## Environment variables
583
580
 
584
- | Variable | Effect |
581
+ | Var | Effect |
585
582
  |---|---|
586
- | `PORT` | Exact TCP port to bind; unset/invalid scans from 3000. |
587
- | `APP_URL` | Derives the server's mount base path from its pathname (e.g. `/v2`). |
588
- | `ABIDE_APP_URL` | Default server URL the CLI connects to; its pathname sets the mount base. |
589
- | `ABIDE_APP_TOKEN` | Sent as `Authorization: Bearer <value>` on CLI→server requests. |
590
- | `ABIDE_APP_DIR` | Overrides the dir the server serves chunks/shell/assets from (set per dev build generation). |
591
- | `ABIDE_DATA_DIR` | Overrides the app data directory on all platforms, used as-is (no program-name suffix). |
592
- | `ABIDE_CLIENT_TIMEOUT` | RPC client timeout in ms (1–600000), shipped to the browser transport. |
593
- | `ABIDE_MAX_REQUEST_BODY_SIZE` | Server-wide max request body size. |
594
- | `ABIDE_IDLE_TIMEOUT` | Bun per-connection idle timeout in seconds (default 10). |
595
- | `ABIDE_LOG_FORMAT` | `json` renders log records as JSON instead of TSV. |
596
- | `ABIDE_ENABLE_INSPECTOR` | `true` mounts the opt-in operator inspector UI/routes. |
597
- | `ABIDE_INSPECT` | Enables webview devtools/inspect for desktop bundles. |
598
- | `ABIDE_DEV_SURFACE` | `1` forces the worker to print its surface map (set by the dev orchestrator). |
599
- | `DEBUG` | npm-debug-style channel gate for diagnostic log channels (e.g. `abide:cache`, `-abide`). |
583
+ | `PORT` | Server listen port. |
584
+ | `APP_URL` | Derives the server's mount base path / public URL. |
585
+ | `ABIDE_APP_DIR` | Overrides the client-asset dir (default `dist/_app`). |
586
+ | `ABIDE_APP_URL` | Default remote server URL the CLI targets when no saved connection. |
587
+ | `ABIDE_APP_TOKEN` | Bearer token the CLI sends to the target server. |
588
+ | `ABIDE_DATA_DIR` | Overrides the app's per-user data dir. |
589
+ | `ABIDE_CLIENT_TIMEOUT` | Client transport timeout in ms (bounded 1–600000). |
590
+ | `ABIDE_IDLE_TIMEOUT` | Server socket idle timeout in seconds (default 10). |
591
+ | `ABIDE_MAX_REQUEST_BODY_SIZE` | Caps the max request body size. |
592
+ | `ABIDE_LOG_FORMAT` | `json` emits structured JSON log records (else tsv). |
593
+ | `ABIDE_DEV_SURFACE` | `1` enables request logging even under dev. |
594
+ | `ABIDE_ENABLE_INSPECTOR` | `true` mounts the inspector at `/__abide/inspector`. |
595
+ | `ABIDE_INSPECT` | Truthy opens the bundle webview with devtools enabled. |
596
+ | `DEBUG` | Enables debug logging; the value is the namespace filter (supports negation). |
600
597
 
601
598
  ---
602
599
 
603
- Mirrors `package.json`'s `exports`; run
604
- `bun run packages/abide/scripts/readmeSurfaces.ts` after adding or renaming an
605
- export to keep this map honest.
600
+ This map mirrors `package.json`'s `exports`. After adding, renaming, or removing an
601
+ export, run `bun run packages/abide/scripts/readmeSurfaces.ts` and update this file.