@abide/abide 0.33.1 → 0.34.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 (160) hide show
  1. package/AGENTS.md +39 -34
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +21 -21
  4. package/package.json +5 -5
  5. package/src/abideResolverPlugin.ts +1 -1
  6. package/src/build.ts +1 -1
  7. package/src/compile.ts +1 -1
  8. package/src/lib/bundle/BundleMenu.ts +1 -1
  9. package/src/lib/bundle/BundleMenuItem.ts +1 -1
  10. package/src/lib/bundle/BundleWindow.ts +1 -1
  11. package/src/lib/bundle/bundled.ts +1 -1
  12. package/src/lib/bundle/disconnected.abide +105 -75
  13. package/src/lib/bundle/onMenu.ts +1 -1
  14. package/src/lib/mcp/createMcpServer.ts +1 -1
  15. package/src/lib/server/AppModule.ts +1 -1
  16. package/src/lib/server/DELETE.ts +1 -1
  17. package/src/lib/server/GET.ts +1 -1
  18. package/src/lib/server/HEAD.ts +1 -1
  19. package/src/lib/server/PATCH.ts +1 -1
  20. package/src/lib/server/POST.ts +1 -1
  21. package/src/lib/server/PUT.ts +1 -1
  22. package/src/lib/server/agent.ts +1 -1
  23. package/src/lib/server/appDataDir.ts +1 -1
  24. package/src/lib/server/cookies.ts +1 -1
  25. package/src/lib/server/env.ts +1 -1
  26. package/src/lib/server/error.ts +1 -1
  27. package/src/lib/server/json.ts +1 -1
  28. package/src/lib/server/jsonl.ts +1 -1
  29. package/src/lib/server/prompts/definePrompt.ts +1 -1
  30. package/src/lib/server/prompts/renderPromptTemplate.ts +1 -1
  31. package/src/lib/server/reachable.ts +1 -1
  32. package/src/lib/server/redirect.ts +1 -1
  33. package/src/lib/server/request.ts +1 -1
  34. package/src/lib/server/rpc/defineVerb.ts +1 -1
  35. package/src/lib/server/runtime/buildCacheSnapshot.ts +1 -1
  36. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +2 -2
  37. package/src/lib/server/runtime/types/InspectorContext.ts +2 -2
  38. package/src/lib/server/server.ts +1 -1
  39. package/src/lib/server/socket.ts +1 -1
  40. package/src/lib/server/sockets/defineSocket.ts +1 -1
  41. package/src/lib/server/sse.ts +1 -1
  42. package/src/lib/shared/HttpError.ts +1 -1
  43. package/src/lib/shared/cache.ts +18 -18
  44. package/src/lib/shared/createSubscriber.ts +1 -1
  45. package/src/lib/shared/health.ts +1 -1
  46. package/src/lib/shared/html.ts +1 -1
  47. package/src/lib/shared/isSubscribable.ts +1 -1
  48. package/src/lib/shared/log.ts +1 -1
  49. package/src/lib/shared/online.ts +1 -1
  50. package/src/lib/shared/page.ts +1 -1
  51. package/src/lib/shared/pending.ts +2 -2
  52. package/src/lib/shared/probeRegistries.ts +2 -2
  53. package/src/lib/shared/refreshing.ts +2 -2
  54. package/src/lib/shared/selectorMatcher.ts +8 -8
  55. package/src/lib/shared/selectorPrefix.ts +1 -1
  56. package/src/lib/shared/snippet.ts +1 -1
  57. package/src/lib/shared/toTagSet.ts +4 -0
  58. package/src/lib/shared/trace.ts +1 -1
  59. package/src/lib/shared/types/CacheEntry.ts +3 -3
  60. package/src/lib/shared/types/CacheOptions.ts +3 -3
  61. package/src/lib/shared/types/CacheSelector.ts +2 -2
  62. package/src/lib/shared/url.ts +1 -1
  63. package/src/lib/shared/withJsonSchema.ts +1 -1
  64. package/src/lib/test/assertAgentFrameConformance.ts +1 -1
  65. package/src/lib/test/createScriptedSurface.ts +1 -1
  66. package/src/lib/test/createTestApp.ts +1 -1
  67. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +7 -2
  68. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +3 -4
  69. package/src/lib/ui/compile/abideUiPlugin.ts +1 -1
  70. package/src/lib/ui/compile/analyzeComponent.ts +2 -1
  71. package/src/lib/ui/compile/compileComponent.ts +3 -3
  72. package/src/lib/ui/compile/compileModule.ts +30 -2
  73. package/src/lib/ui/compile/compileSSR.ts +5 -5
  74. package/src/lib/ui/compile/compileShadow.ts +54 -36
  75. package/src/lib/ui/compile/desugarSignals.ts +147 -18
  76. package/src/lib/ui/compile/generateBuild.ts +15 -28
  77. package/src/lib/ui/compile/generateSSR.ts +20 -13
  78. package/src/lib/ui/compile/isControlFlow.ts +18 -0
  79. package/src/lib/ui/compile/isTextLeaf.ts +15 -0
  80. package/src/lib/ui/compile/lowerContext.ts +45 -5
  81. package/src/lib/ui/compile/prepareNestedScript.ts +12 -8
  82. package/src/lib/ui/compile/renameSignalRefs.ts +21 -4
  83. package/src/lib/ui/compile/skeletonable.ts +1 -14
  84. package/src/lib/ui/compile/types/AnalyzedComponent.ts +1 -0
  85. package/src/lib/ui/computed.ts +32 -0
  86. package/src/lib/ui/createScope.ts +91 -0
  87. package/src/lib/ui/dom/anchorCursor.ts +1 -1
  88. package/src/lib/ui/dom/appendSnippet.ts +1 -1
  89. package/src/lib/ui/dom/appendStatic.ts +1 -1
  90. package/src/lib/ui/dom/appendText.ts +1 -1
  91. package/src/lib/ui/dom/appendTextAt.ts +1 -1
  92. package/src/lib/ui/dom/applyResolved.ts +1 -1
  93. package/src/lib/ui/dom/attach.ts +7 -2
  94. package/src/lib/ui/dom/attr.ts +1 -1
  95. package/src/lib/ui/dom/awaitBlock.ts +1 -1
  96. package/src/lib/ui/dom/cloneStatic.ts +1 -1
  97. package/src/lib/ui/dom/each.ts +4 -3
  98. package/src/lib/ui/dom/eachAsync.ts +1 -1
  99. package/src/lib/ui/dom/hydrate.ts +20 -6
  100. package/src/lib/ui/dom/mount.ts +12 -1
  101. package/src/lib/ui/dom/mountChild.ts +6 -2
  102. package/src/lib/ui/dom/mountSlot.ts +1 -1
  103. package/src/lib/ui/dom/on.ts +10 -4
  104. package/src/lib/ui/dom/openMarker.ts +3 -3
  105. package/src/lib/ui/dom/skeleton.ts +13 -5
  106. package/src/lib/ui/dom/switchBlock.ts +1 -1
  107. package/src/lib/ui/dom/text.ts +1 -1
  108. package/src/lib/ui/dom/tryBlock.ts +1 -1
  109. package/src/lib/ui/dom/when.ts +1 -1
  110. package/src/lib/ui/effect.ts +7 -2
  111. package/src/lib/ui/enterScope.ts +18 -0
  112. package/src/lib/ui/exitScope.ts +8 -0
  113. package/src/lib/ui/history.ts +102 -0
  114. package/src/lib/ui/installHotBridge.ts +6 -8
  115. package/src/lib/ui/linked.ts +0 -1
  116. package/src/lib/ui/navigate.ts +1 -1
  117. package/src/lib/ui/outbox.ts +115 -0
  118. package/src/lib/ui/persist.ts +112 -0
  119. package/src/lib/ui/probeNavigation.ts +1 -1
  120. package/src/lib/ui/remoteProxy.ts +1 -1
  121. package/src/lib/ui/renderToStream.ts +1 -1
  122. package/src/lib/ui/router.ts +1 -1
  123. package/src/lib/ui/runtime/CURRENT_SCOPE.ts +10 -0
  124. package/src/lib/ui/runtime/OUTLET_TAG.ts +1 -1
  125. package/src/lib/ui/runtime/PATCH_BUS.ts +28 -0
  126. package/src/lib/ui/runtime/RESUME.ts +1 -1
  127. package/src/lib/ui/runtime/captureModelDoc.ts +29 -0
  128. package/src/lib/ui/runtime/claimExpected.ts +26 -0
  129. package/src/lib/ui/runtime/createComputedNode.ts +1 -1
  130. package/src/lib/ui/runtime/createDoc.ts +73 -3
  131. package/src/lib/ui/runtime/enterRenderPass.ts +1 -1
  132. package/src/lib/ui/runtime/escapeKey.ts +13 -0
  133. package/src/lib/ui/runtime/exitRenderPass.ts +1 -1
  134. package/src/lib/ui/runtime/hotReplace.ts +17 -8
  135. package/src/lib/ui/runtime/inScope.ts +19 -0
  136. package/src/lib/ui/runtime/localStoragePersistence.ts +34 -0
  137. package/src/lib/ui/runtime/nextBlockId.ts +1 -1
  138. package/src/lib/ui/runtime/seedModelDoc.ts +26 -0
  139. package/src/lib/ui/runtime/types/{Derived.ts → Computed.ts} +1 -1
  140. package/src/lib/ui/runtime/types/Doc.ts +3 -0
  141. package/src/lib/ui/runtime/types/HotInstance.ts +6 -2
  142. package/src/lib/ui/runtime/types/PatchEvent.ts +16 -0
  143. package/src/lib/ui/runtime/unescapeKey.ts +12 -0
  144. package/src/lib/ui/runtime/walkPath.ts +3 -1
  145. package/src/lib/ui/scope.ts +19 -0
  146. package/src/lib/ui/socketProxy.ts +1 -1
  147. package/src/lib/ui/startClient.ts +1 -1
  148. package/src/lib/ui/state.ts +1 -2
  149. package/src/lib/ui/sync.ts +42 -0
  150. package/src/lib/ui/tail.ts +1 -1
  151. package/src/lib/ui/types/History.ts +14 -0
  152. package/src/lib/ui/types/Outbox.ts +14 -0
  153. package/src/lib/ui/types/PersistHandle.ts +11 -0
  154. package/src/lib/ui/types/PersistenceStore.ts +12 -0
  155. package/src/lib/ui/types/Scope.ts +57 -0
  156. package/src/lib/ui/types/SyncTransport.ts +13 -0
  157. package/src/preload.ts +1 -1
  158. package/src/lib/shared/toScopeSet.ts +0 -4
  159. package/src/lib/ui/derived.ts +0 -49
  160. package/src/lib/ui/doc.ts +0 -15
package/AGENTS.md CHANGED
@@ -25,13 +25,13 @@
25
25
 
26
26
  One declared verb fans out to every surface, with no extra work:
27
27
 
28
- ```
28
+ ```text
29
29
  export const getMessages = GET(fn, { inputSchema })
30
30
 
31
31
  ┌───────────────┬────────────┼──────────────┬────────────────┐
32
32
  SSR call browser fetch MCP tool CLI subcommand OpenAPI op
33
- cache(fn)() fetch /rpc/... (read-only + app get-messages /openapi.json
34
- (in-process) (typed proxy) schema → tool) (schema flags) (described)
33
+ cache(fn)() fetch /rpc/... getMessages app get-messages /openapi.json
34
+ (in-process) (typed proxy) (read-only+schema) (schema→flags) (described)
35
35
  ```
36
36
 
37
37
  A Standard Schema (zod / valibot / arktype, unadapted) is the contract: it
@@ -76,7 +76,7 @@ For `bun test`, add `preload = ["@abide/abide/preload"]` under `[test]` in `bunf
76
76
 
77
77
  ## Server surface — `abide/server/*` (and authored helpers)
78
78
 
79
- ### RPC verbs — `@readme rpc`
79
+ ### RPC verbs — `@documentation rpc`
80
80
  Each is `Verb(fn, opts?)` with three overloads (see `src/lib/server/rpc/types/VerbHelper.ts`):
81
81
  `Verb(fn, { inputSchema, outputSchema?, filesSchema?, clients?, timeout?, maxBodySize?, crossOrigin? })`,
82
82
  `Verb(fn, { clients })`, and bare `Verb(fn)`. Return type usually infers from the
@@ -94,7 +94,7 @@ plain call decodes the body + throws `HttpError` on non-2xx; `.raw` resolves to
94
94
  `Response` undecoded; `.stream(args)` returns a `Subscribable` (SSE/JSONL frames, or
95
95
  the decoded body once) for `tail()`; `.fetch(req)` is framework-internal dispatch.
96
96
 
97
- ### Responses — `@readme response`
97
+ ### Responses — `@documentation response`
98
98
  - `abide/server/json(data, init?)` — JSON with `Cache-Control: no-store`; `json(undefined)` → 204 that round-trips to `undefined`.
99
99
  - `abide/server/error(status, message?, init?)` — text/plain error; message defaults to the status reason phrase.
100
100
  - `abide/server/redirect(url, status=302, init?)` — accepts relative URLs (platform `Response.redirect` doesn't); 301/302/303/307/308.
@@ -102,29 +102,29 @@ the decoded body once) for `tail()`; `.fetch(req)` is framework-internal dispatc
102
102
  - `abide/server/sse(iterable, init?)` — wrap an `AsyncIterable` as Server-Sent Events; 15s keepalive; errors → `event: error`.
103
103
  - `abide/shared/HttpError` — thrown by remote calls on non-2xx; carries `status`, `statusText`, raw `response`.
104
104
 
105
- ### Sockets — `@readme sockets`
105
+ ### Sockets — `@documentation sockets`
106
106
  - `abide/server/socket(opts?)` — declare a `Socket<T>` (an isomorphic `AsyncIterable<T>`) inside `src/server/sockets/<name>.ts`. Opts: `{ schema?, tail?, ttl?, clientPublish?, clients? }`. `tail: n` retains last n frames; `ttl` evicts older. HTTP face at `/__abide/sockets/<name>`: GET = retained tail, POST = publish (gated by `clientPublish`).
107
107
 
108
- ### Agents — `@readme agent`
108
+ ### Agents — `@documentation agent`
109
109
  - `abide/server/agent(engine, messages)` — run a model engine against the app's own (already-gated) MCP surface; returns the engine's `AgentFrame` stream. Wrap in `jsonl()`/`sse()` to pick the transport. Types exported: `NeutralMessage`, `AgentFrame` (`text`/`tool_use`/`tool_result`/`done`), `AgentSurface`, `AgentEngine`. Engines live in `@abide/<provider>` packages, never in core.
110
110
 
111
- ### Request scope — `@readme request-scope`
111
+ ### Request scope — `@documentation request-scope`
112
112
  Resolve only during an in-flight SSR render or RPC handler (throw outside one):
113
113
  - `abide/server/request()` → the inbound `Request`.
114
114
  - `abide/server/cookies()` → Bun `CookieMap`; writes flush as `Set-Cookie` on return.
115
115
  - `abide/server/server()` → active `Bun.serve` (or a no-op in-process server under CLI/MCP/test).
116
116
 
117
- ### Config / data — `@readme configuration` / `reference`
117
+ ### Config / data — `@documentation configuration` / `reference`
118
118
  - `abide/server/env(schema, opts?)` — validate `Bun.env` against a Standard Schema at module top level; fails boot loudly. Registered so the bundle launcher derives its setup form.
119
119
  - `abide/server/appDataDir()` → the running bundle's per-user data dir (cwd-independent, pure).
120
120
 
121
- ### Observability — `@readme observability`
121
+ ### Observability — `@documentation observability`
122
122
  - `abide/server/reachable(host)` — server-only outbound reachability; first call probes (HEAD), then warm-polls every TTL. Tuned by `ABIDE_REACHABLE_TTL` / `ABIDE_REACHABLE_TIMEOUT`.
123
123
  - `abide/shared/health()` → `HealthState` (framework + app `health()` hook payload, typed via generated `health.d.ts`); also at `/__abide/health`.
124
124
  - `abide/shared/log` — unified logger carrying request-scope context; `log()/warn/error/trace` on the app channel, `log.channel(name)` on a `DEBUG`-gated channel. `ABIDE_LOG_FORMAT=json` for JSON lines.
125
125
  - `abide/shared/trace()` → current W3C `traceparent` (isomorphic; client reads the SSR-stamped trace).
126
126
 
127
- ### App / inspector types — `@readme plumbing`
127
+ ### App / inspector types — `@documentation plumbing`
128
128
  - `abide/server/AppModule` — shape of `src/app.ts` (error handler, health hook, lifecycle).
129
129
  - `abide/server/InspectorContext` — inspector wiring type.
130
130
  - `abide/server/rpc/defineVerb`, `abide/server/sockets/defineSocket`, `abide/server/prompts/definePrompt`, `abide/server/prompts/renderPromptTemplate` — bundler-emitted runtime bindings; you don't call these by hand (the `GET`/`socket`/`.md` files compile down to them).
@@ -133,24 +133,24 @@ Resolve only during an in-flight SSR render or RPC handler (throw outside one):
133
133
 
134
134
  ## Isomorphic surface — `abide/shared/*`
135
135
 
136
- ### Cache — `@readme cache`
137
- - `abide/shared/cache(fnOrProducer)` → a memoised callable. Key auto-derives (method+url+args for a remote fn; producer-reference+args for a producer). Options `{ ttl?, scope?, global?, swr? }`: `ttl` ms-past-resolve (omit = forever, `0` = dedupe-only / the **mutation idiom**); `scope` tags for group invalidation; `global` = process-level store (server); `swr` = stale-while-revalidate — on invalidate, keep the stale value (reader sees `refreshing()`, not `pending()`) and refetch in the background. `swr: true` refetches immediately; `swr: { throttle | debounce }` adds a coalescing window. GET-only, throws on a write.
138
- - `cache.invalidate(selector?, args?)` — end retention early (by fn, fn+args, `{ scope }`, or all).
136
+ ### Cache — `@documentation cache`
137
+ - `abide/shared/cache(fnOrProducer)` → a memoised callable. Key auto-derives (method+url+args for a remote fn; producer-reference+args for a producer). Options `{ ttl?, tags?, global?, swr? }`: `ttl` ms-past-resolve (omit = forever, `0` = dedupe-only / the **mutation idiom**); `tags` for group invalidation; `global` = process-level store (server); `swr` = stale-while-revalidate — on invalidate, keep the stale value (reader sees `refreshing()`, not `pending()`) and refetch in the background. `swr: true` refetches immediately; `swr: { throttle | debounce }` adds a coalescing window. GET-only, throws on a write.
138
+ - `cache.invalidate(selector?, args?)` — end retention early (by fn, fn+args, `{ tags }`, or all).
139
139
  - `cache.patch(selector, updater, args?)` — optimistic local fold over matching entries.
140
140
  - `cache.on(subscribable, (frame, ctx) => …)` — event-driven cache maintenance: run a handler per socket/stream frame; `ctx.invalidate` / `ctx.patch` are coverage-tracked and resync on reconnect. Client-only (no-op on server).
141
141
 
142
- ### Probes — `@readme probes` (read-only; reading never triggers work)
142
+ ### Probes — `@documentation probes` (read-only; reading never triggers work)
143
143
  - `abide/shared/pending(selector?, args?)` — "no value yet" (in-flight call, or stream awaiting first frame).
144
144
  - `abide/shared/refreshing(selector?, args?)` — "holding a value while a fresher source is in flight".
145
145
  - `abide/shared/online()` — reactive connectivity probe (server: the calling client's reported state via offline header).
146
146
 
147
147
  Same selector grammar as `cache.invalidate`; also accept a `Subscribable`. See CONTEXT.md → Probe.
148
148
 
149
- ### Routing / page — `@readme page` / `url`
149
+ ### Routing / page — `@documentation page` / `url`
150
150
  - `abide/shared/page` — reactive proxy: `route`, `params`, `url` (browser-space), `navigating`. Isomorphic; re-runs reactive readers on navigation.
151
151
  - `abide/shared/url(path, query?)` — type-safe URL builder; augmented by generated `RpcRoutes`/`PageRoutes`/`PublicAssets` for autocomplete. Applies mount base.
152
152
 
153
- ### Templating values — `@readme plumbing`
153
+ ### Templating values — `@documentation plumbing`
154
154
  - `abide/shared/html` — `html\`...\`` tagged template → `RawHtml` (trusted markup); `rawHtmlString(v)`.
155
155
  - `abide/shared/snippet(payload)` → `Snippet` — a reusable template builder (the `<template name=…>` form).
156
156
  - `abide/shared/createSubscriber(start)` — the subscription primitive behind sockets/streams/probes.
@@ -159,53 +159,58 @@ Same selector grammar as `cache.invalidate`; also accept a `Subscribable`. See C
159
159
 
160
160
  ## UI surface — `abide/ui/*` (client-only)
161
161
 
162
- ### Reactive primitives — `@readme plumbing` (in scope inside `.abide`, no import)
163
- - `abide/ui/state(initial, transform?)` writable `State<T>` (`.value` getter/setter). Local truth. `transform(next, prev) => T` is a write-coercion gate: each `.value=` stores what it returns (`return prev` rejects via the `Object.is` no-op); construction `initial` is verbatim. Plain `state(x)` is a serializable doc slot; `state(x, transform)` is a non-serializing `.value` cell.
164
- - `abide/ui/linked(seed, transform?)` writable `State<T>` seeded reactively from upstream (Angular's `linkedSignal`): owns a local value, reseeds when the `seed` thunk's deps change, edits stay local. `transform` gates reseeds and writes alike. Thunk seed is required (it is the reactivity); seed captured by reference (clone in the thunk for isolation). Non-serializing — reseeds on resume.
165
- - `abide/ui/derived(compute)` lazy read-only computed (`.value`); re-derived on resume, never serialized. `derived(compute, set)` writable lens: `.value` derives from upstream, assigning runs imperative `set(next)` to write *through* to the sources (no local store).
166
- - `abide/ui/effect(fn)` → run now, re-run on dependency change; `fn` may return teardown / be async; returns dispose.
167
- - `abide/ui/doc(initial?)` → reactive document: immutable tree addressed by path, every change a patch (the substrate under all reactivity / resumability / sync).
162
+ ### Reactive primitives — `@documentation plumbing` (declared through `scope()` in a `.abide` `<script>`; the runtime forms are NOT public exports)
163
+ Reactive state is owned by a scope and declared explicitly through it: `const count = scope().state(0)` (or a captured handle: `const s = scope(); const count = s.state(0)`). A **bare** `state(...)` / `linked(...)` / `computed(...)` call is a compile error the surface always shows the scope interaction. The declared binding is then read/written by its bare name (`{count}`, `count = count + 1`); the compiler lowers that to the doc API.
164
+ - `scope().state(initial, transform?)` local truth. Plain `state(x)` lowers to a serializable `model` (= `scope()`) doc slot. `state(x, transform)` is a write-coercion gate (`.value` cell).
165
+ - `scope().linked(seed, transform?)` a `.value` cell seeded reactively from upstream (Angular's `linkedSignal`): owns a local value, reseeds when the `seed` thunk's deps change, edits stay local. Non-serializing reseeds on resume.
166
+ - `scope().computed(compute)` → a read-only computed **doc slot** (`scope().derive("name", compute)`), referenced as `name()` (string-free reader): re-computed on resume, never serialized/journalled. Always read-only (no `set` overload) a writable computed is expressed at the binding site (`bind:value={{ get, set }}`), where the write actually originates.
167
+ - `prop('name')` → a read-only computed doc slot over the parent thunk (`scope().derive`), referenced as `name()`.
168
+ - `abide/ui/effect(fn)` → run now, re-run on dependency change; `fn` may return teardown / be async; returns dispose. (`effect` IS still a public export.)
169
+ - `abide/ui/scope(address?)` → `Scope` — **the data + capability seam; the only public way to reach reactive state.** The reactive document (immutable path-addressed tree, every change a patch — JSON Pointer keys via `escapeKey`, so a key may contain `/`) is now *internal* (`doc`/`createDoc`): a scope wraps one and mirrors its interface — `read`/`replace`/`add`/`remove`/`cell`/`derive`/`snapshot` (data; `derive(path, compute)` is a computed slot, never stored/serialized/journalled), `child`/`root`/`parent` (tree), `dispose`. Every applied patch is announced on an internal process-global patch bus (`PATCH_BUS`) carrying `{ doc, patch, inverse }` — the single tap undo / persistence / sync consume; computed recomputes never reach it. Capabilities route where the scope's changes go — `record({ limit? })` to an undo journal (then `undo`/`redo`/`canUndo`/`canRedo`), `persist(key?)` to durable storage (key defaults to the scope `id`), `broadcast(transport)` to peers. A passable value (`<Child parentScope={scope}/>`). `scope()` = the ambient current scope — `mount`/`hydrate` (client) and `enterScope`/`exitScope` (SSR, per render) establish one per component; the lowering emits `const model = scope()`, so `scope()`'s data IS the component's `state`; `scope('/')` = the root. Resolves correctly inside event handlers, effects, and attach teardowns (they pin the scope they were registered under).
170
+
171
+ ### Local-first queue — `@documentation ui`
172
+ - `abide/ui/outbox({ key, send, store?, online?, onDrop? })` → `Outbox<T>` — durable FIFO queue for local-first writes; distinct from `scope().broadcast()` (which replicates *state*, not outbound mutations). `enqueue(payload)` records a serializable mutation (persisted via `persist`, so it survives a reload) and drains FIFO while `online()`; success dequeues, offline-mid-send keeps the head and retries on reconnect (auto-drains on the online edge), an online rejection drops the entry and reports via `onDrop` (roll back the optimistic change there). At-least-once → `send` should be idempotent. `pending()` is a reactive read for a sync indicator. Built on `doc` + `persist` + `online` (the queue is itself a persisted doc).
168
173
 
169
174
  ### Streams
170
175
  - `abide/ui/tail(subscribable)` → latest frame (`T | undefined`); `tail(x, { last: n })` → window `T[]`. Reactive latest-wins read of a socket/stream. `tail.status` / `tail.error` expose richer stream state. See CONTEXT.md → Tail.
171
176
 
172
- ### Navigation / lifecycle — `@readme plumbing`
177
+ ### Navigation / lifecycle — `@documentation plumbing`
173
178
  - `abide/ui/navigate(path, replace?)` — client navigation.
174
179
  - `abide/ui/router(...)`, `abide/ui/startClient(...)`, `abide/ui/renderToStream(render)` — bootstrap/render runtime (compiler/launcher uses these).
175
180
 
176
- ### DOM + render runtime — `@readme plumbing` (compiler-emitted; you don't hand-write these)
181
+ ### DOM + render runtime — `@documentation plumbing` (compiler-emitted; you don't hand-write these)
177
182
  `abide/ui/dom/{mount,mountChild,hydrate,text,appendText,appendTextAt,appendSnippet,appendStatic,cloneStatic,skeleton,anchorCursor,mountSlot,attr,on,attach,each,eachAsync,when,awaitBlock,tryBlock,switchBlock,applyResolved}` and `abide/ui/runtime/{nextBlockId,enterRenderPass,exitRenderPass}`. These are what `analyzeComponent → generateBuild/generateSSR` lower a `.abide` file into — every bound element builds through the parser-backed `skeleton` (one clone + located holes / `<!--a-->` anchors); there is no imperative element builder. Read them only to understand compiler output.
178
183
  - `abide/ui/remoteProxy`, `abide/ui/socketProxy` — the browser-side implementations the bundler swaps in for `GET(...)` / `socket(...)`.
179
184
 
180
185
  ### `.abide` component format (see `src/lib/ui/README.md`)
181
186
  Valid HTML with `<script>` + native `<template>` control flow + scoped `<style>`.
182
- - **Bindings:** `{expr}` text, `name={expr}` attr, `onclick={fn}`, `bind:value={…}` / `bind:checked` / `bind:group`, `attach={fn}` (node-lifetime attachment — the dual of `on`; the `use:`-action / `{@attach}` equivalent, lowered to `ui/dom/attach`).
187
+ - **Bindings:** `{expr}` text, `name={expr}` attr, `onclick={fn}`, `bind:value={…}` / `bind:checked` / `bind:group`, `attach={fn}` (node-lifetime attachment — the dual of `on`; the `use:`-action / `{@attach}` equivalent, lowered to `ui/dom/attach`). A two-way `bind:` target is an **lvalue** (`count`, `model.lines[i]`) or an **accessor object** `bind:value={{ get, set }}` (reads via `get()`, writes via `set(v)`) — the only way to two-way-bind derived state; binding a read-only `computed` is a compile error.
183
188
  - **Control flow (native `<template>`):** `if` with a nested `else` (the `<template else>` is a CHILD of its `<template if>`, not a sibling), `each={list} as="x" key="x.id"`, `await={p}`/`then`/`catch`, `switch`/`case`/`default`.
184
- - **A branch is a lexical scope:** any control-flow branch may host its own nested `<script>` and `<style>`. The nested `<script>` declares branch-local **plain** signals (`state`/`derived`/`linked`/`prop`) — owned by the branch's render scope, re-seeded each mount (not the serializable top-level `doc`) — with the branch's binding in scope (`then`/`catch`'s `as` value, the `each` `as`/`key` row), so it can derive from the awaited/iterated value; its bindings cover the branch subtree and later siblings auto-deref them. The nested `<style>` is scoped to that branch alone (its own `data-a-<hash>`), not the whole component.
189
+ - **A branch is a lexical scope:** any control-flow branch may host its own nested `<script>` and `<style>`. The nested `<script>` declares branch-local **plain** signals (`state`/`computed`/`linked`/`prop`) — owned by the branch's render scope, re-seeded each mount (not the serializable top-level `doc`) — with the branch's binding in scope (`then`/`catch`'s `as` value, the `each` `as`/`key` row), so it can derive from the awaited/iterated value; its bindings cover the branch subtree and later siblings auto-deref them. The nested `<style>` is scoped to that branch alone (its own `data-a-<hash>`), not the whole component.
185
190
  - **Components:** capitalised tags (`<Layout title=…>`); children fill `<slot/>`; props are reactive (passed as thunks). A component has no directives — every attribute is a prop under its written name (so `onclick=`/`bind:open=`/`attach=` pass through as props, e.g. callbacks, not the DOM-element directives those are on a lowercase tag) and is type-checked against the child's declared props. `prop('name')` reads a typed component prop (the parent-supplied thunk, reactive + read-only); route params come from the `page` proxy (`page.params.name`), not `prop()`.
186
191
  - **Snippets / named slots:** `<template name="x" args={…}>` declares a reusable named builder (the `snippet()` form), rendered like a function — covers named slots / `{@render}`.
187
192
  - **Reactivity:** write plain assignment (`count += 1`, `items.push(x)`); the compiler lowers it to patches. Deep-field edits wake only that field.
188
- - **SSR:** byte-identical HTML string; `renderToStream` ships the shell then streams `<template await>` fragments out of order; `hydrate` adopts static structure in place (control-flow blocks + child components fall back to `mount`/re-render — known gap).
193
+ - **SSR:** byte-identical HTML string; `renderToStream` ships the shell then streams `<template await>` fragments out of order; `hydrate` adopts the server DOM in place static structure, `if`/`else`, keyed `each`, `switch`, `try`, and child components/slots all claim existing nodes (no re-render; focus/scroll/input preserved). The one cold-rebuild case left is a genuinely-pending `await` that is neither resumable (`RESUME[id]`) nor cache-warm it discards the boundary and builds the pending branch fresh; an `await` over a `cache`d read is warm on resume and adopts without a flash.
189
194
 
190
195
  ---
191
196
 
192
- ## Build / tooling — `@readme plumbing`
197
+ ## Build / tooling — `@documentation plumbing`
193
198
  - `abide/ui-plugin` — Bun loader plugin for `.abide` files.
194
199
  - `abide/resolver-plugin` — resolves the `$server`/`$ui`/virtual modules and swaps server↔client runtime per target.
195
200
  - `abide/preload` — Bun preload registering both plugins (use in `bunfig.toml` `[test]`).
196
201
  - `abide/build`, `abide/compile` — programmatic build / standalone-executable compile.
197
202
  - `abide/tsconfig` — base tsconfig for consumer apps.
198
203
 
199
- ## Desktop bundle — `@readme bundle`
204
+ ## Desktop bundle — `@documentation bundle`
200
205
  - `abide/bundle/BundleWindow` — `src/bundle/window.ts` default-export shape (`title`, `width`, `height`, `menu`, `config` schema for the first-run setup form).
201
206
  - `abide/bundle/BundleMenu`, `abide/bundle/BundleMenuItem` — custom menu types.
202
207
  - `abide/bundle/onMenu(handler)` / `onMenu(name, handler)` — subscribe to native menu clicks (returns unsubscribe; drop into an `effect`). Inert outside the bundle.
203
208
  - `abide/bundle/bundled()` — true inside the desktop webview (isomorphic detection).
204
209
 
205
- ## MCP — `@readme plumbing`
210
+ ## MCP — `@documentation plumbing`
206
211
  - `abide/mcp/createMcpServer(opts)` — framework-internal; the `abide:mcp` virtual constructs it. Tools derive from verbs/sockets flagged `clients.mcp: true`; auth inherits from the inbound request; optional `authorize` hook. Served at `/__abide/mcp`.
207
212
 
208
- ## Testing — `@readme testing`
213
+ ## Testing — `@documentation testing`
209
214
  - `abide/test/createTestApp()` → `TestApp` with typed `RpcClient` / `SocketClient` — in-process dispatch, no network.
210
215
  - `abide/test/createScriptedSurface(...)` — records tool dispatches for engine tests.
211
216
  - `abide/test/assertAgentFrameConformance(...)` — the frame contract every engine must satisfy (exactly one `done`, last; every `tool_use` answered by a same-id/name `tool_result`).
@@ -240,5 +245,5 @@ Valid HTML with `<script>` + native `<template>` control flow + scoped `<style>`
240
245
 
241
246
  *Maintenance (abide repo only): this file mirrors `package.json` `exports`. After
242
247
  adding/renaming an export, run `bun run scripts/readmeSurfaces.ts` from
243
- `packages/abide/` (it lists every export by `@readme` slug and fails on any untagged
244
- one) and reflect the change here.*
248
+ `packages/abide/` (it lists every export by `@documentation` slug and fails on any
249
+ untagged one) and reflect the change here.*
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # abide
2
2
 
3
+ ## 0.34.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`3697394`](https://github.com/briancray/abide/commit/3697394b693f82266dadd227033d08cacd9df0bf) - make scope() the sole reactive surface ([`7484b44`](https://github.com/briancray/abide/commit/7484b44034542a9bdbdaeedd6c8b9bb08c71f2cf))
8
+
9
+ ### Patch Changes
10
+
11
+ - [`1593504`](https://github.com/briancray/abide/commit/159350461d6b1070bd0981c7a8ae049655eb1c7d) - drop unused Doc import from Scope type ([`383669b`](https://github.com/briancray/abide/commit/383669b957648e4393e4df341dc9e1b01303fab5))
12
+
13
+ - [`3697394`](https://github.com/briancray/abide/commit/3697394b693f82266dadd227033d08cacd9df0bf) - rename selector "scope" to "tags" ([`38c41c8`](https://github.com/briancray/abide/commit/38c41c838091448c1477f588e6fafc4e566f9830))
14
+
15
+ - [`3697394`](https://github.com/briancray/abide/commit/3697394b693f82266dadd227033d08cacd9df0bf) - rename @readme export tag to @documentation ([`7045cfb`](https://github.com/briancray/abide/commit/7045cfbe5a9d3f69d165cb91fd01355f76162495))
16
+
17
+ - [`3697394`](https://github.com/briancray/abide/commit/3697394b693f82266dadd227033d08cacd9df0bf) - preserve template-literal whitespace when indenting module bodies ([`89d975a`](https://github.com/briancray/abide/commit/89d975ae58dc4459c85909f414e0e46431924103))
18
+
19
+ - [`3697394`](https://github.com/briancray/abide/commit/3697394b693f82266dadd227033d08cacd9df0bf) - resolve scope() in the shadow program and map binding names ([`a06c38a`](https://github.com/briancray/abide/commit/a06c38a08d417bd45509c58d661caa5f45b34ae3))
20
+
21
+ - [`3697394`](https://github.com/briancray/abide/commit/3697394b693f82266dadd227033d08cacd9df0bf) - migrate to the scope() authoring surface ([`b0691fc`](https://github.com/briancray/abide/commit/b0691fc56746e516b7196eec008c063c0dda5e6d))
22
+
23
+ - [`3697394`](https://github.com/briancray/abide/commit/3697394b693f82266dadd227033d08cacd9df0bf) - rewrite scope() to avoid assignment-in-expression ([`c6e021b`](https://github.com/briancray/abide/commit/c6e021b68401bb30e55f11cf587a4b1bdf6f41c9))
24
+
25
+ - [`3697394`](https://github.com/briancray/abide/commit/3697394b693f82266dadd227033d08cacd9df0bf) - regenerate README/AGENTS and refocus the write-readme skill ([`d40a3fe`](https://github.com/briancray/abide/commit/d40a3fec152c339f267327a95348af51c1f62027))
26
+
3
27
  ## 0.33.1
4
28
 
5
29
  ### Patch Changes
package/README.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # abide
2
2
 
3
- **A type-safe isomorphic framework built on web standards and Bun. Zero runtime dependencies, single runtime.**
3
+ **Write one function. Get a typed HTTP endpoint, a CLI, an MCP tool, and an OpenAPI operation — from the same line of code.**
4
4
 
5
- abide is not just a web framework. Strongly-typed RPCs surface through HTTP, a CLI, and an MCP, with a full OpenAPI spec. Everything is generated for you with zero additional work - but you control surface exposure. Its a framework built for humans *and* machines.
5
+ abide is a type-safe isomorphic framework built on web standards and Bun. You declare a verb once; the bundler swaps the runtime per side and projects that one declaration onto every surface humans hit it over HTTP or the CLI, machines hit it over MCP, and you control which surfaces each verb exposes. It's a framework built for humans *and* machines.
6
+
7
+ - Zero runtime dependencies
8
+ - A single runtime (Bun) in every mode — dev, build, and compiled binary
6
9
 
7
10
  ## Quick start
8
11
 
@@ -10,7 +13,7 @@ abide is not just a web framework. Strongly-typed RPCs surface through HTTP, a C
10
13
  bunx abide scaffold my-app # scaffolds, installs, and starts the dev server
11
14
  ```
12
15
 
13
- To see a full demo, clone the repo and run the kitchen-sink example — every surface is a runnable, documented page:
16
+ To see every surface live, clone the repo and run the kitchen-sink example — each page is runnable and documented:
14
17
 
15
18
  ```sh
16
19
  git clone https://github.com/briancray/abide
@@ -18,9 +21,7 @@ cd abide && bun install
18
21
  cd examples/kitchen-sink && bun run dev
19
22
  ```
20
23
 
21
- ## Three foundational primitives
22
-
23
- ### RPCs
24
+ ## RPCs
24
25
 
25
26
  An RPC is a handler wrapped by its HTTP method — one export per file under `src/server/rpc/`. The file path is the URL; the schema validates args and projects the MCP tool, CLI flags, and OpenAPI operation. Standard Schema is the contract: zod, valibot, and arktype work unadapted.
26
27
 
@@ -50,13 +51,13 @@ cache(fn)() fetch /rpc/... getMessages app get-messages /openapi.json
50
51
  (in-process) (typed proxy) (read-only+schema) (schema→flags) (described)
51
52
  ```
52
53
 
53
- The same callable is consumed differently per side — `cache(getMessages)({ room })` in-process, the swapped `fetch` in the browser, `.raw(args)` for a `Response`, `.stream(args)` for `tail()`. A schema gates the machine surfaces: it unlocks CLI and (for read-only verbs) MCP; a mutating verb never auto-exposes to MCP — it needs an explicit `clients: { mcp: true }`. Per-verb `timeout` (504, on every surface) is distinct from the client-side `ABIDE_CLIENT_TIMEOUT`.
54
+ The same callable is consumed differently per side — `cache(getMessages)({ room })` in-process, the swapped `fetch` in the browser, `.raw(args)` for a `Response`, `.stream(args)` for `tail()`. A schema gates the machine surfaces: it unlocks the CLI and (for read-only verbs) MCP; a mutating verb never auto-exposes to MCP — it needs an explicit `clients: { mcp: true }`. Per-verb `timeout` (504, on every surface) is distinct from the client-side `ABIDE_CLIENT_TIMEOUT`.
54
55
 
55
56
  > Query args travel as strings — use `z.coerce.*` so numbers and booleans validate.
56
57
 
57
- ### Sockets
58
+ ## Sockets
58
59
 
59
- One broadcast topic per file under `src/server/sockets/`. A `Socket<T>` is an isomorphic `AsyncIterable<T>` — every socket multiplexes onto one ws at `/__abide/sockets`.
60
+ One broadcast topic per file under `src/server/sockets/`. A `Socket<T>` is an isomorphic `AsyncIterable<T>` — every socket multiplexes onto one ws at `/__abide/sockets`. A `schema` validates publishes, infers the frame type, and flips on the MCP/CLI read faces.
60
61
 
61
62
  ```ts
62
63
  // src/server/sockets/chat.ts
@@ -70,11 +71,11 @@ export const chat = socket({ schema, tail: 100, ttl: 3_600_000 })
70
71
  export type ChatMessage = z.infer<typeof schema>
71
72
  ```
72
73
 
73
- For clients that can't speak the ws multiplex, each socket has an HTTP face at `/__abide/sockets/<name>` — `GET` returns the retained tail, `POST` publishes (gated by `clientPublish`).
74
+ For clients that can't speak the ws multiplex, each socket has an HTTP face at `/__abide/sockets/<name>` — `GET` returns the retained tail, `POST` publishes (gated by `clientPublish`, off by default).
74
75
 
75
- ### Components
76
+ ## Components
76
77
 
77
- Components are `.abide` files — valid HTML with `<script>`, native `<template>` control flow, `{expr}` bindings, and a component-scoped `<style>`. The reactive primitives (`state`, `derived`, `effect`, `prop`) are in scope without import. This page reads the verb above through `cache()`, tails the socket live, and exercises most of the template grammar:
78
+ Components are `.abide` files — valid HTML with `<script>`, native `<template>` control flow, `{expr}` bindings, and a component-scoped `<style>`. Reactive state is reached through `scope()` (`scope().state(v)`, `scope().computed(fn)`); `prop` and `effect` are in scope without import. This page reads the verb above through `cache()`, tails the socket live, and exercises most of the template grammar:
78
79
 
79
80
  ```html
80
81
  <script>
@@ -85,19 +86,19 @@ import { publishChat } from '$server/rpc/publishChat.ts'
85
86
  import { chat } from '$server/sockets/chat.ts'
86
87
  import Avatar from '$ui/Avatar.abide'
87
88
 
88
- let room = prop('room') // typed via src/.abide/routes.d.ts
89
- const history = derived(() => cache(getMessages)({ room })) // SSR snapshot + reactive refetch
90
- const latest = derived(() => tail(chat)) // re-renders on every new frame
89
+ let room = prop('room') // typed via src/.abide/routes.d.ts
90
+ const history = scope().computed(() => cache(getMessages)({ room })) // SSR snapshot + reactive refetch
91
+ const latest = scope().computed(() => tail(chat)) // re-renders on every new frame
91
92
 
92
- let from = state('alice')
93
- let text = state('')
94
- let filter = state('all') // all | mine | others
95
- let onlyUnread = state(false)
93
+ let from = scope().state('alice')
94
+ let text = scope().state('')
95
+ let filter = scope().state('all') // all | mine | others
96
+ let onlyUnread = scope().state(false)
96
97
 
97
98
  async function send(event: SubmitEvent) {
98
99
  event.preventDefault()
99
100
  if (text.trim() === '') return
100
- await publishChat({ room, from, text }) // server validates, then broadcasts
101
+ await publishChat({ from, text }) // server validates, then broadcasts
101
102
  text = ''
102
103
  }
103
104
  </script>
@@ -150,5 +151,4 @@ async function send(event: SubmitEvent) {
150
151
  </style>
151
152
  ```
152
153
 
153
-
154
154
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.33.1",
3
+ "version": "0.34.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Isomorphic multimodal HTTP framework built for humans and machines in a single Bun runtime",
@@ -72,11 +72,11 @@
72
72
  "./shared/trace": "./src/lib/shared/trace.ts",
73
73
  "./shared/url": "./src/lib/shared/url.ts",
74
74
  "./shared/createSubscriber": "./src/lib/shared/createSubscriber.ts",
75
- "./ui/state": "./src/lib/ui/state.ts",
76
- "./ui/linked": "./src/lib/ui/linked.ts",
77
- "./ui/derived": "./src/lib/ui/derived.ts",
78
75
  "./ui/effect": "./src/lib/ui/effect.ts",
79
- "./ui/doc": "./src/lib/ui/doc.ts",
76
+ "./ui/scope": "./src/lib/ui/scope.ts",
77
+ "./ui/enterScope": "./src/lib/ui/enterScope.ts",
78
+ "./ui/exitScope": "./src/lib/ui/exitScope.ts",
79
+ "./ui/outbox": "./src/lib/ui/outbox.ts",
80
80
  "./ui/dom/mount": "./src/lib/ui/dom/mount.ts",
81
81
  "./ui/dom/mountChild": "./src/lib/ui/dom/mountChild.ts",
82
82
  "./ui/dom/hydrate": "./src/lib/ui/dom/hydrate.ts",
@@ -99,7 +99,7 @@ Also rewrites modules under src/server/rpc and src/server/sockets:
99
99
  defineSocket on the server (with the socket name + opts) or
100
100
  socketProxy on the client (name only — opts are server-side).
101
101
  */
102
- // @readme plumbing
102
+ // @documentation plumbing
103
103
  export function abideResolverPlugin({
104
104
  cwd = process.cwd(),
105
105
  embedAssets = false,
package/src/build.ts CHANGED
@@ -32,7 +32,7 @@ last-good server) survives instead of crashing and orphaning the child. By
32
32
  default a failure exits the process (one-shot `abide build` / `compile`);
33
33
  the dev orchestrator passes `exitOnFailure: false`.
34
34
  */
35
- // @readme plumbing
35
+ // @documentation building
36
36
  export async function build({
37
37
  cwd = process.cwd(),
38
38
  minify = true,
package/src/compile.ts CHANGED
@@ -16,7 +16,7 @@ entry. Defaults
16
16
  the target to the host platform and appends `.exe` for windows targets.
17
17
  Returns the path of the emitted binary; exits the process on build failure.
18
18
  */
19
- // @readme plumbing
19
+ // @documentation building
20
20
  export async function compile({
21
21
  cwd = process.cwd(),
22
22
  target = detectTarget(),
@@ -5,7 +5,7 @@ A top-level bundle menu, inserted into the macOS menu bar between the standard
5
5
  Edit and Window menus. `label` titles the menu; `items` are its entries top to
6
6
  bottom.
7
7
  */
8
- // @readme bundle
8
+ // @documentation bundle
9
9
  export type BundleMenu = {
10
10
  label: string
11
11
  items: BundleMenuItem[]
@@ -18,7 +18,7 @@ calls `webview_navigate` with the given URL (the native side, on the UI thread).
18
18
  That's how the built-in Server menu drives the connect screen — `emit` reaches
19
19
  the loaded page, `navigate` repoints the window itself.
20
20
  */
21
- // @readme bundle
21
+ // @documentation bundle
22
22
  export type BundleMenuItem =
23
23
  | { separator: true }
24
24
  | { label: string; shortcut?: string; emit: string }
@@ -13,7 +13,7 @@ built-in File menu (Start server / Connect / Disconnect) are always installed.
13
13
  `menu` adds custom top-level menus between the Edit and Window menus; their items
14
14
  emit `abide:menu` events the app handles. See BundleMenuItem.
15
15
  */
16
- // @readme bundle
16
+ // @documentation bundle
17
17
  export type BundleWindow = {
18
18
  title?: string
19
19
  width?: number
@@ -26,7 +26,7 @@ its *rendering context* (the webview) is inside it.
26
26
  Distinct from runningAsStandaloneBinary(), which is also true for a plain compiled
27
27
  (install-tarball) server binary — still a web app, not a bundle.
28
28
  */
29
- // @readme bundle
29
+ // @documentation bundle
30
30
  export function bundled(): boolean {
31
31
  if (typeof window === 'undefined') {
32
32
  return Boolean(Bun.env.ABIDE_PARENT_PID)