@abide/abide 0.51.0 → 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 +509 -525
  2. package/CHANGELOG.md +29 -0
  3. package/README.md +128 -157
  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 +92 -14
  45. package/src/lib/ui/compile/lowerDocAccess.ts +2 -14
  46. package/src/lib/ui/compile/lowerScript.ts +6 -1
  47. package/src/lib/ui/compile/renameSignalRefs.ts +65 -0
  48. package/src/lib/ui/compile/wrapReactionCellSources.ts +32 -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,617 +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 type) 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 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.
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 }`.
41
45
 
42
46
  ## File-based conventions
43
47
 
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-*`. |
48
+ The bundler (`abideResolverPlugin`) reads these paths. Import aliases into the
49
+ tree: `$server`, `$ui`, `$shared`, `$mcp`, `$cli`.
50
+
51
+ | Path | Meaning |
52
+ |---|---|
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>`). |
63
66
 
64
67
  ## CLI
65
68
 
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`. |
69
+ | Command | Does |
70
+ |---|---|
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). |
77
+ | `abide compile [--target=<bun-…>] [--out=<path>]` | Build a standalone server executable. |
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). |
81
82
 
82
83
  For tests, add `preload = ["@abide/abide/preload"]` under `[test]` in
83
84
  `bunfig.toml` and run `bun test`.
84
85
 
85
86
  ## Authoring contracts
86
87
 
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.
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.
155
121
 
156
122
  ## .abide template grammar
157
123
 
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).
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()`.
129
+
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
137
+
138
+ | Form | Meaning |
139
+ |---|---|
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()`). |
145
+
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(...)`.
149
+
150
+ ### Bindings & directives
151
+
152
+ | Syntax | Meaning |
153
+ |---|---|
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={…}`). |
158
+ | `bind:value` / `bind:checked` / `bind:group` | Two-way form binds. |
159
+ | `bind:value={{ get, set }}` | Derived two-way bind. |
160
+ | `class:name={cond}` | Toggle a class. |
161
+ | `style:property={value}` | Set one style property. |
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
166
+
167
+ Mustache `{#…}` blocks, not `<template>`. The rendered blocks are
168
+ `if`/`for`/`await`/`switch`/`try`; `snippet` is a declaration block.
169
+
170
+ | Block | Form |
171
+ |---|---|
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}` narrowingnot
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.
221
193
 
222
194
  ## Server surface — abide/server/*
223
195
 
224
196
  ### RPC helpers — @documentation rpc
225
197
 
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.
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`.
254
222
 
255
223
  ### Request scope — @documentation request-scope
256
224
 
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.
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.
263
231
 
264
- ### Configuration — @documentation configuration
232
+ ### Server rendering — @documentation render
265
233
 
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.
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`.
269
236
 
270
237
  ### Sockets — @documentation sockets
271
238
 
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).
239
+ - `abide/server/socket` — `socket<T>(opts?)`: declare one broadcast topic per file
240
+ under `src/server/sockets/`. Returns `Socket<T>` (isomorphic `AsyncIterable<T>`).
275
241
 
276
- ### Agent — @documentation agent
242
+ ### Configuration — @documentation configuration
277
243
 
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.
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.
282
246
 
283
- ### Server plumbing — @documentation plumbing
247
+ ### App hooks & inspector — @documentation plumbing
284
248
 
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
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
288
252
  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
253
 
299
- ## Isomorphic surfaceabide/shared/*
254
+ ### RPC / socket registrars @documentation plumbing
300
255
 
301
- ### 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.
302
262
 
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).
263
+ ## Isomorphic surface — abide/shared/*
306
264
 
307
- ### Error responses — @documentation response
265
+ ### RPC schema helper — @documentation rpc
308
266
 
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).
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.
315
270
 
316
- ### Cache mutation — @documentation cache
271
+ ### Response errors — @documentation response
317
272
 
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.
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.
324
279
 
325
- ### Page — @documentation page
280
+ ### Cache — @documentation cache
326
281
 
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.
282
+ Isomorphic per ADR-0041: local on the client, broadcast to every connected client
283
+ from the server.
330
284
 
331
- ### 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.
332
293
 
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).
294
+ ### Probes@documentation probes
343
295
 
344
- ### URL@documentation url
296
+ Reactive, report-never-actreading opens no fetch/stream.
345
297
 
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`.
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.
349
308
 
350
- ### Templating — @documentation templating
309
+ ### Observability — @documentation observability
351
310
 
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.
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`)
355
333
 
356
- ### Observability — @documentation observability
334
+ ### Templating — @documentation templating
357
335
 
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__`).
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.)
369
340
 
370
- ### Isomorphic plumbing — @documentation plumbing
341
+ ### Subscriber — @documentation plumbing
371
342
 
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).
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.
375
346
 
376
347
  ## UI surface — abide/ui/* (client-only)
377
348
 
378
349
  ### Reactive state — @documentation reactive-state
379
350
 
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.
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.
389
359
 
390
360
  ### Templating — @documentation templating
391
361
 
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).
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.
513
491
 
514
492
  ## Build / tooling
515
493
 
516
494
  ### Building — @documentation building
517
495
 
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`.
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).
562
549
 
563
550
  ## Testing
564
551
 
565
552
  ### Testing — @documentation testing
566
553
 
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`.
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).
571
556
 
572
- ### Testing plumbing — @documentation plumbing
557
+ ### Plumbing — @documentation plumbing
573
558
 
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.
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).
579
564
 
580
565
  ## Generated machine surfaces
581
566
 
582
- Runtime routes the framework serves:
567
+ Runtime routes served by the framework:
583
568
 
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. |
569
+ | Route | Serves |
570
+ |---|---|
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`. |
593
578
 
594
579
  ## Environment variables
595
580
 
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`). |
581
+ | Var | Effect |
582
+ |---|---|
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). |
612
597
 
613
598
  ---
614
599
 
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.
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.