Files
hemx/REQUIREMENTS.md
T
slhx agent e0484f8eb6 refactor(REQUIREMENTS): integrate DX ergonomics, component scoping, and build-pipeline fixes
- add golden_path, progressive_disclosure, ceremony, diagnostics,
  view_model, interop, client_local, component, codegen, derive sections
- introduce ResourceRef/ResourceId split, ScopeKey, atom mutability
  and pub/sub distinction, form control ids, 5KB target runtime
- fix build typo, macro policy, htmx boundary, version singleton,
  runtime delegation scope
- strengthen Surface with hemplate-real directives (h-for, h-key, +attr)
- preserve no-!-macros policy, north_star feel: Svelte at call site,
  Rust at boundary

Eval: architecture 9/10, dx 9/10, orthogonality 9/10, suckless 8.5/10
2026-05-10 12:16:02 +02:00

595 lines
32 KiB
Markdown

# slhx — Semantic, Laterally HX
slhx does not compete with React by becoming a better frontend framework.
slhx competes with React by making frontend frameworks unnecessary for most apps.
> **hemplate owns syntax. hemplate emits surface. slhx consumes surface. slhx owns semantics. JS executes bytecode.**
---
## pitch
### req: pitch/001
001 slhx is checked hypermedia for Rust. Write `.heml`, write `#[slhx::handler]`, return commands. The compiler checks every cross-file reference. The browser runtime only sees ids and effect bytes. No JS app code required. [north_star]
### req: pitch/002
002 No CSS selectors. No hx-* strings. No virtual DOM. No client framework. No hidden global proxy magic. No hydration. No user-authored JavaScript for standard forms, lists, navigation, or server push. [north_star]
### req: pitch/003
003 slhx replaces React/Vue not with a UI framework, but with a compiler contract: hemplate knows the surface, Rust knows the types, slhx knows the effects, the browser only executes commands. [north_star]
### req: pitch/004
004 The north-star feel: **Svelte at the call site, Rust at the boundary.** Short handler bodies, compile-checked HTML contracts, one language for server and client logic. [north_star]
---
## dx
### req: dx/001
001 The common case must feel like writing a Svelte/Vue component: template, state, handlers, and targeted updates. Users should not need to understand Surface IR, ResourceId, EffectWriter, postcard, or runtime opcodes for basic apps. [north_star]
### req: dx/002
002 The happy path is: write `.heml`, write `#[slhx::handler]`, return generated slot/atom commands. No manual ids, no manual registry, no manual serialization, no manual JavaScript. [north_star]
### req: dx/003
003 Public APIs are generated around the user's names. If the template declares `data-slhx-slot="todo_list"`, the user gets `slots::todo_list`, not `SlotId(12)`.
### req: dx/004
004 Common handlers must fit in a small function. Advanced contexts (`EffectWriter`, raw ops, custom encoders) exist but are not part of the beginner path.
### req: dx/005
005 Error messages must explain fixes in author language, not internal language. Say “add `key todo.id` to this loop”, not “missing ScopeKey for ResourceRef”.
### req: dx/006
006 Generated resource methods are the preferred authoring API: `slots::todo_list.render(view)`, `slots::card.replace(key, view)`, `slots::count.text(42)`, `atoms::user.set(user)`. These return `impl IntoEffect`. Raw `Effect::render`, `Effect::set`, and `EffectWriter` remain low-level. [north_star]
### req: dx/007
007 Tuple composition of `IntoEffect` is the canonical batch syntax: `(a, b, c)` implements `IntoEffect` up to arity 12. `Effect::batch((...))` is available but not required for the happy path.
### req: dx/008
008 User-authored JavaScript is never required for standard forms, lists, navigation, optimistic actions, or server push. Custom JS is only needed at opaque leaf boundaries such as charts, maps, editors, and Web Components.
---
## ceremony
### req: ceremony/001
001 A minimal counter app requires one `.heml` file, one Rust state struct, and one handler function. No manual registry, no manual route table, no manual JS. Under 50 lines of user-authored Rust plus one template.
### req: ceremony/002
002 Generated modules are imported through a prelude or component namespace. Users should not manually include `$OUT_DIR` files in normal apps.
### req: ceremony/003
003 `build.rs` must be a one-liner for the common case: `fn main() { slhx_build::app().run().unwrap(); }`
### req: ceremony/004
004 No API may require users to write numeric ids, raw ResourceIds, raw opcodes, or serialized payloads in normal code.
---
## progressive_disclosure
### req: pd/001
001 A beginner can build CRUD with only: `.heml`, `#[slhx::handler]`, `Form<T>`, generated `slots::*` methods, and `impl IntoEffect`.
### req: pd/002
002 Atoms are not required for basic server-first apps. They appear only when client-local state, SSR bootstrapped state, or WASM handlers are used.
### req: pd/003
003 Sync, transitions, resources/queries, islands, capabilities, and raw EffectWriter are advanced layers. They must not appear in starter examples.
### req: pd/004
004 Documentation must present three levels: server-first, client-local, hybrid-sync. Each level introduces only the new primitive it needs.
---
## component
### req: component/001
001 The primary authoring unit is a hemplate component plus adjacent Rust handlers. A component owns a template root, generated slots, generated handles, generated form checks, and source spans.
### req: component/002
002 slhx supports colocated layout: `todo_list.heml` beside `todo_list.rs`, with generated APIs namespaced by component to avoid global symbol soup.
### req: component/003
003 Generated APIs are component-namespaced by default:
`ui::slots::todo_row`, `ui::handles::create`, `ui::forms::create`.
Global exports are opt-in only.
### req: component/004
004 `#[slhx::surface]` bridges generated code into a user module. Users write `#[slhx::surface] mod ui {}` instead of `include!(concat!(env!("OUT_DIR"), ...))`. slhx-build emits `slhx.generated.rs` which the macro expands in place. No direct `$OUT_DIR` includes in user-authored source.
### req: component/005
005 An optional `#[slhx::component]` macro may validate that every handle declared in the template Surface has a corresponding `#[slhx::handler]` within the annotated module. This is the only macro with cross-handler visibility inside a single module; it remains strictly local. Missing handlers without `#[slhx::component]` are caught at app mount or test time, not `cargo check`.
### req: component/006
006 `#[derive(Hemplate)]` structs are natural component boundaries. slhx_build
generates APIs per component/template namespace and may merge component
Surfaces only at explicit app/root boundaries.
---
## codegen
### req: codegen/001
001 `slhx_build` generates three artifacts from the generic Surface IR: (a) `slhx.generated.rs` containing ergonomic resource modules (`slots`, `handles`, `forms`, `atoms`), (b) `slhx.syms` for proc-macro validation, (c) runtime id-lowering tables. `slhx_build` interprets tool-specific conventions (`data-slhx-*`, `h-for`, `h-key`, form controls) from the Surface. [north_star]
### req: codegen/002
002 Generated module `slots` exposes ergonomic methods: `Slot<T>::render(value)`, `Slot<T>::text(value)`, `KeyedSlot<K,T>::append(key, value)`, `KeyedSlot<K,T>::replace(key, value)`, `KeyedSlot<K,T>::remove(key)`. Methods return `impl IntoEffect`.
### req: codegen/003
003 Generated module `handles` exports typed constants: `Handle<I>` where `I` is `Form<T>`, a param type, or `()`. Users rarely reference handles directly; they are consumed by `#[slhx::handler]` for validation.
### req: codegen/004
004 Generated module `forms` exports `FormContract` metadata (field names, HTML control kinds, required). `#[slhx::handler]` compares the `Form<T>` type against the contract. Domain types (`Email`, `TodoId`) remain user-authored; no auto-generated structs.
### req: codegen/005
005 Generated module `atoms` exports `Atom<T>` for values that must be addressable, hydrated, or synced. Ordinary Rust fields on app/components are not automatically atoms.
---
## invariant
### req: invariant/001
001 User-authored references are symbolic at author time and numeric at runtime.
### req: invariant/002
002 The JS runtime never parses CSS selectors, expressions, or handler names.
### req: invariant/003
003 Rust handlers return effects; they do not imperatively mutate DOM.
### req: invariant/004
004 Cross-file references fail at `cargo check` with a precise span.
### req: invariant/005
005 slhx core owns effects, typed ids, and registries only. Routing, auth, sessions, transport, transitions, and sync are integration concerns.
---
## boundary
### req: boundary/001
001 hemplate does not expose a slhx API. It exposes a stable, generic Template Surface IR. slhx is one consumer; a11y tools, test generators, and documentation generators are others.
### req: boundary/002
002 hemplate never interprets `data-slhx-*` or any other tool-prefixed
attribute. It records them as raw `name: value` pairs in the Surface.
### req: boundary/003
003 slhx never parses `.heml` directly. It consumes `hemplate.surface.postcard` emitted by `hemplate_build`. slhx interprets tool-specific conventions (`data-slhx-handle`, `data-slhx-slot`, etc.) from the generic Surface.
---
## surface
### req: surface/001
001 `hemplate_build` scans `.heml` files and emits `$OUT_DIR/hemplate.surface.postcard` (postcard-encoded, deterministic, versioned).
### req: surface/002
002 The Surface contains: nodes (NodeId, parent, scope, element, attrs, source span), scopes (ScopeKind: Root | If | For { binding, key_expr }), forms (form controls with raw HTML types), and component uses.
### req: surface/003
003 Node identity is `NodeId` in a parent/scope graph. No `css_path` is used as a primary identifier. An optional `debug_path` string may exist for diagnostics only.
### req: surface/004
004 Form controls in the Surface carry raw HTML facts: `ControlKind::Text`, `ControlKind::Number { min, max, step }`, `ControlKind::Checkbox`, `ControlKind::Select { multiple, options }`, etc. No Rust type mapping lives in hemplate.
### req: surface/005
005 Loop scopes expose the binding name and an optional `key_expr` (e.g. `todo.id`). hemplate does not enforce key usage; it only records it for consumers.
### req: surface/006
006 Surface schema is versioned (`schema_version: u32`). Postcard encoding, no JSON. `no_std`-compatible schema definition so any tool can read it without heavy dependencies.
### req: surface/007
007 `hemplate-derive` does not write Surface files. Surface generation is a `build.rs` / `hemplate_build` concern, proc-macro side-effect free.
### req: surface/008
008 The Surface records hemplate structural directives as first-class facts:
`h-for`, `h-key`, `h-if`, `h-else-if`, `h-else`, dynamic `+attr` bindings,
and interpolated attr/text expressions. slhx consumes these facts; it never
parses `.heml` source directly.
### req: surface/009
009 Raw/pre-rendered HTML insertions are opaque Surface holes. The parent
Surface records the insertion point and source span, but nodes inside inserted
HTML belong to the child component Surface or remain invisible to tools.
### req: surface/010
010 Attribute values preserve their origin: static literal, dynamic `+attr`
expression, or interpolated template string. slhx param inference consumes
these from Surface and never reparses `.heml`.
---
## build
### req: build/001
001 Build order: `.heml``hemplate_build``hemplate.surface.postcard``slhx_build``slhx.generated.rs` + `slhx.syms` + diagnostics.
### req: build/002
002 Proc-macros (`#[slhx::handler]`, `#[slhx::surface]`) are side-effect free. They read generated artifacts (`slhx.syms`, `slhx.generated.rs`) but never parse `.heml`, never process generic Surface IR, and never write files. Global codegen lives only in `build.rs` invoked by `slhx_build`. [north_star]
### req: build/003
003 `slhx-derive` (`#[slhx::handler]`) reads `slhx.syms` at expansion time to validate handle names, slot names, and form signatures. It generates only local glue (static fn-table entry) plus compile errors.
### req: build/004
004 `#[slhx::surface]` reads `slhx.generated.rs` from `$OUT_DIR` and expands it into the annotated module. It is a pure include/bridge macro with no semantic analysis of its own.
### req: build/005
005 A `build.rs` failure (missing Surface, version mismatch, stale hash) is a hard error before proc-macro expansion.
### req: build/006
006 Proc-macros are considered local: they have knowledge of the item they annotate, plus pre-generated symbol tables. They do not have global knowledge of all handlers across the crate. Global checks (e.g. every declared handle has an implementation) are either deferred to app-mount tests or enabled by an optional `#[slhx::component]` macro.
---
## effect
### req: effect/001
001 Rust handlers return `impl IntoEffect`, not a concrete `Vec<Effect>`. `IntoEffect::encode(self, &mut EffectWriter)` may write directly into a response buffer, a test collector, or an event stream. Zero-allocation encoding is possible; allocation is not required.
### req: effect/002
002 `EffectWriter` has a fixed canonical op set: `Set`, `Patch`, `Insert`, `Remove`, `Move`, `Focus`, `Navigate`, `Emit`. The DOM is one backend; core does not hardcode DOM operations. Wire format is canonical postcard opcodes; Rust API is flexible.
### req: effect/003
003 Multiple effects are combined with tuple composition or a method chain on `EffectWriter`. No `!` call-syntax macros.
### req: effect/004
004 `Effect::render(slot, value)` is sugar for `Effect::set` on a Slot resource. `Effect::text(slot, value)` is sugar for `Effect::set` with a text payload.
### req: effect/005
005 effects may carry an opaque transition token, but core never interprets or implements transitions. `slhx-transition` provides transition semantics as an orthogonal integration.
### req: effect/006
006 `Effect::event(name, payload)` dispatches a native `CustomEvent` on the root element. Core interprets the payload as opaque bytes. Web Components, charts, editors, or legacy JS may listen without slhx knowing about them. [north_star]
### req: effect/007
007 `Effect::event` is the raw escape hatch. Common UI events such as toast, dialog close, focus, and clipboard may have typed helper wrappers (`toast("Saved")`, `nav::push("/dashboard")`) in optional crates. Helpers compile to `Effect::event` or canonical ops.
### req: effect/008
008 Small semantic helper types may implement `IntoEffect`: `Toast`, `Nav`, `FormError`, generated slot commands, generated atom commands. Users compose them by returning tuples.
---
## state
### req: state/001
001 Typed atoms with `Atom<T>` are explicit addressable state resources.
Read via `atom.get()`, mutate via `atom.set()` / `atom.update()`, publish to
the runtime via generated atom commands or `Effect::set(atom, value)`.
No hidden global proxy / reactive graph. Atoms are explicit values in `struct App`.
### req: state/002
002 Atoms are not reactive by default. Updating an atom does not re-render
anything unless the handler returns an effect targeting consumers.
Subscriptions are explicit runtime/store APIs. No automatic component re-render graph.
### req: state/003
003 The JS runtime maintains a client-side atom store keyed by `AtomId`. Runtime values are type-erased postcard bytes. Types are compile-time only. A deterministic `TypeHash` may be generated by `slhx_build` for diagnostics, but the JS runtime does not depend on Rust `TypeId`.
### req: state/004
004 SSR pages carry a `data-slhx-st` base64url-encoded postcard blob on the document root. Runtime decodes it into the client atom store. Atoms computed from server state are immediately available to client-side handlers without a round-trip.
### req: state/005
005 Not all state is an Atom. Ordinary Rust fields are preferred unless the value must be independently addressed, bootstrapped, synced, or subscribed. Atoms are explicit resources, not the default state container.
---
## form
### req: form/001
001 Forms are source of truth in HTML. hemplate Surface exports form shape (controls, names, required, types). slhx checks compatibility with the Rust handler's `Form<T>` type. No auto-generated structs; domain types (e.g. `Email`) are first-class. The Surface describes; Rust owns; slhx checks.
### req: form/002
002 The handle id is carried as `__h` in POST `application/x-www-form-urlencoded`. A JSON body is allowed at the integration boundary (`application/json`) only if the handler accepts it; core uses form encoding.
### req: form/003
003 Handler receives `form: Form<CreateTodo>`. Validation errors target `(FormId, field_name)` or generated control ids. The runtime maps them to originating form controls via control ids derived from Surface `NodeId`, not via slot ids.
---
## list
### req: list/001
001 Any `data-slhx-slot` or `data-slhx-handle` inside a hemplate `h-for` scope requires a stable key. Preferred syntax: `<template h-for="item in &self.items" h-key="item.id"> ... </template>`. Without a key, slhx-addressable nodes inside the loop are rejected at build time. Keyed identity is `(SlotId, KeyValue)`.
### req: list/002
002 Slots inside a keyed loop receive a composite identity: `(SlotId, KeyValue)`, not a flat id. hemplate records `key_expr` in the Surface; slhx implements keyed slot lookups.
### req: list/003
003 Effects on keyed slots: `replace_keyed(slot, key, value)`, `remove_keyed(slot, key)`, `append_keyed(slot, key, value)`. Mismatch between key type and slot key type is compile-time error.
---
## typed_id
### req: typed_id/001
001 All public cross-page identifiers (`Slot`, `Atom`, `Handle`, `Form`) share a single internal primitive `ResourceId { kind: ResourceKind, id: u32 }`. A concrete runtime target is a `ResourceRef { resource: ResourceId, scope: Option<ScopeKey> }`. Typed wrappers (`Slot<T>`, `KeyedSlot<K, T>`, `Atom<T>`, `Handle<I>`, `Form<T>`) enforce kind safety at compile time. No special-case opcodes per resource kind; effects address resources uniformly. [north_star]
### req: typed_id/002
002 `ResourceKind` is an internal closed enum (Slot, Atom, Handle, Form, Route). External crates may not add variants. Extensibility comes via `Effect::event`, `Effect::emit`, or custom `IntoEffect` implementations, never via new `ResourceKind` variants in core.
---
## async_data
### req: async_data/001
001 Async remote data lives in `Resource<T>` / `Query<K, T>` / `Mutation<I, O>`. These are optional, not core primitives. They provide loading/error/refresh semantics without client-side data libraries.
### req: async_data/002
002 `QueryEffect::reload(res)` triggers a re-fetch and re-render. The server sends a new `EffectBatch` when data is ready. Query/Resource effects live in a separate API surface to keep core small.
---
## scope
### req: scope/001
001 `Scope` is a first-class primitive. Keyed loops (`h-for`), conditional
branches (`h-if`), component instances, modals, tabs, nested forms — all are
scopes. A slhx-addressable node inside any dynamic scope must carry a stable
`ScopeKey`. Concrete runtime targets are addressed through `ResourceRef`
`{ resource: ResourceId, scope: Option<ScopeKey> }`. [north_star]
---
## wire
### req: wire/001
001 Authoring HTML uses symbolic `data-slhx-*` attributes. Rendered runtime
HTML lowers these to compact numeric metadata: `data-hid`, `data-sid`,
optional `data-key`, optional atom ids, optional form/control ids, and
`data-slhx-st` for state bootstrap. The browser never sees handler or slot
names. `data-slhx-root` marks a scoped root boundary.
### req: wire/002
002 POST bodies carry `application/x-www-form-urlencoded` with distinguished field `__h` (handle id). Server routes by numeric id, not by URL path.
### req: wire/003
003 Responses are `text/html` fragments (or `application/slhx` for push streams). Fragments may contain `<template data-slhx>` elements whose text content is a base64url-encoded postcard `EffectBatch`. JS decodes and applies.
### req: wire/004
004 Server push is orthogonal: integration crates stream postcard `EffectBatch` over SSE or WebSocket connections. slhx core owns the effect bytes; transport and connection management are integration concerns.
### req: wire/005
005 No JSON anywhere in slhx-internal artifacts. Public request/response envelopes use `application/x-www-form-urlencoded`; effects and symbols use postcard. `application/json` is acceptable only at integration boundaries.
---
## runtime
### req: runtime/001
001 Every slhx tree must declare a root boundary via `data-slhx-root` on an ancestor element. The JS runtime resolves lookups within that root only. Multiple independent slhx apps/widgets/modals may coexist on the same document without ID collision. [north_star]
### req: runtime/002
002 JS runtime attaches a single delegated listener per event type on each `data-slhx-root`. No per-node listeners. Dispatch resolves target via `data-hid` / `data-sid` attributes on the event path scoped to its root.
### req: runtime/003
003 The core JS runtime target is under 5KB minified+gzipped. It remains a tiny op interpreter (no selectors, no VDOM, no scheduler, no expressions). It reads postcard `EffectBatch` bytes and applies them as DOM operations. Optional sync/transition/WASM helpers are separate files.
### req: runtime/004
004 `RuntimeCaps { binary, wasm, sync, transitions }` is exchanged once at init. Handlers may query capabilities and degrade gracefully (e.g. fall back to server request if WASM unavailable). Core does not know about these features; caps are opaque to the wire protocol and only consulted by integrations.
---
## js
### req: js/001
001 Runtime reads attributes `data-hid` and `data-sid`, delegates events on each `data-slhx-root`, and applies effects by direct DOM mutation scoped to that root.
### req: js/002
002 No build step, no virtual DOM, no diffing, no scheduler. Receiving an effect = apply ops immediately in declared order.
### req: js/003
003 The runtime implements the canonical `EffectBatch` opcode schema from
`slhx-core`. DOM-specific operations such as `ReplaceHtml`, `SetText`,
`AddClass`, or `RemoveKeyed` are lowering details generated from canonical
resource ops.
---
## check
### req: check/001
001 All cross-file references verified at `cargo check`. Unknown handle → hard error. Unknown slot → hard error. Type mismatch between slot and atom → hard error.
### req: check/002
002 Dead/missing handle diagnostics are best-effort by default. With `#[slhx::component]`, dead/missing handlers inside the component are checked at `cargo check`. Without it, missing implementations are caught at app mount or in generated registry tests.
### req: check/003
003 Renaming a slot or handle breaks `cargo check` immediately with a span pointing to the Rust handler or template source.
### req: check/004
004 Page-scoped slot lookup: JS runtime resolves `data-sid` only within the current Page root element.
---
## sync
### req: sync/001
001 `slhx-sync` is an optional crate for collaborative / multiplayer state. Provides presence tracking, patch reconciliation, conflict resolution (server-authoritative), and offline queueing. Not part of core.
### req: sync/002
002 `SyncEffect::send_patch(atom, patch)` queues a state diff for server sync. If offline, the patch is stored in a local queue and sent when connection resumes. If online, it is sent immediately via WebSocket/SSE.
### req: sync/003
003 Server reconciliation of received patches produces a local `EffectBatch` only when state changes. Accepted mutations patch shared state without hard-coding specific effects.
### req: sync/004
004 `SyncEffect::broadcast(channel, effect_batch)` sends an `EffectBatch` to all subscribers of a named channel. Used for presence updates and live collaboration. The server framework manages the transport (WS/SSE).
### req: sync/005
005 `#[slhx_sync::presence]` is an attribute macro on functions that return `impl IntoEffect` when a user joins or leaves a shared session. Emits `SyncEffect::broadcast` over a presence channel scoped to the session.
### req: sync/006
006 `SyncEffect::ack(atom)` acknowledges a successful server-side mutation, allowing the client to clear its local optimistic queue for that atom.
### req: sync/007
007 `slhx-sync` uses a flat patch model per atom, not CRDT by default. Server is authoritative; clients apply server-canonical state on conflict. Optional CRDT backend may be provided by a future `slhx-crdt` crate.
---
## interop
### req: interop/001
001 `Effect::event` is the single bridge between slhx and third-party JS. External widgets, charts, and maps listen via native `CustomEvent`. slhx core does not inspect or manage them. [north_star]
### req: interop/002
002 Web Components and custom elements are valid opaque leaf nodes. slhx does not inspect shadow DOM. Escape hatches are leaves, never app foundations.
### req: interop/003
003 WASM islands (`#[slhx::island]`) compile handler code to WASM for client-local execution. The island is a leaf in the DOM; slhx core is unaware of WASM except via the same `EffectBatch` contract.
### req: interop/004
004 Existing `hx-*` attributes are treated as ordinary raw attributes in the hemplate Surface without slhx semantics. An optional `slhx-htmx-migrate` tool may read Surface `hx-*` attrs and suggest equivalent `data-slhx-*` handlers/effects.
---
## test
### req: test/001
001 `EffectWriter` implements a test backend so handlers can be unit-tested without a browser: `slhx_test::run(handler, input)` returns an `EffectInspector` with `contains(op)`, `has_slot(slot)`, `has_atom(atom)`, etc.
---
## navigation
### req: nav/001
001 Navigation is an effect, not a router framework: `Effect::navigate(url, mode, scroll, title)`. Core supports `Push`, `Replace`, `Redirect`. Actual route matching, guards, loaders, and nested routes are outside core.
### req: nav/002
002 Navigation modes: `Push` (history.pushState), `Replace` (replaceState), `Redirect` (server-side 302). Scroll behaviour: `Preserve`, `Top`, `Element(ResourceId)`. Title is optional.
### req: nav/003
003 Navigation enhancement preserves real anchors. Links keep valid `href`. slhx may intercept enhanced links through `data-slhx-handle` or `data-slhx-nav`, but without JS the browser performs normal navigation.
---
## client_local
### req: client_local/001
001 Client-local handlers use the same function shape as server handlers. Adding `client` changes the execution backend, not the authoring model.
### req: client_local/002
002 Local UI state may live as ordinary fields on the app/component state. `Atom<T>` is required only when the value must be addressed by effects, synced, hydrated, or subscribed.
### req: client_local/003
003 High-frequency UI handlers (drag, pointermove, animation tick) must not require server round-trips or handwritten JS.
---
## escape_hatch
### req: escape_hatch/001
001 Three sanctioned escape hatches exist: (1) Web Components as opaque leaf nodes, (2) `Effect::event` for imperative JS interop, (3) WASM islands for CPU-intensive client logic. All three are leaves in the slhx tree, never the app foundation.
---
## view_model
### req: view/001
001 Slots render view types, not necessarily domain types. Domain-to-view conversion is explicit Rust (`From`, `Into`, or constructor). slhx never assumes a domain object is its own view.
### req: view/002
002 Generated slot types are allowed to target `Display`, `Hemplate`, or explicit view wrappers. Type errors should suggest the expected renderable view type.
---
## diagnostics
### req: diag/001
001 Every compile-time error must point to both sides of the mismatch when possible: the Rust handler span and the template Surface span.
### req: diag/002
002 Diagnostics must include a suggested fix for common cases: missing key, unknown slot, missing form field, optionality mismatch, handler param mismatch, wrong keyed slot type.
### req: diag/003
003 Internal terms (`ResourceId`, `ScopeKey`, `EffectBatch`) must not appear in beginner-facing diagnostics unless `--verbose` is enabled.
---
## derive_handler
### req: derive_handler/001
001 `#[slhx::handler]` validates: handle name exists in symbol table, params match form surface or `data-*` attributes, return type implements `IntoEffect`. Generate code registers the function in a static lookup table keyed by numeric handle id.
### req: derive_handler/002
002 Handler param inference from `data-*` attributes: when a template declares `data-card-id="{card.id}"` on a node with `data-slhx-handle`, the handler may declare `card_id: CardId` as a parameter. slhx-build checks attribute → param name and type mapping.
### req: derive_handler/003
003 Handler parameters are inferred from four sources: `Form<T>`, `data-*` attributes on the triggering node, route params supplied by integration crates, and explicit app/context parameters. Missing or incompatible params are compile-time errors with source spans.
### req: derive_handler/004
004 The common handler signature forms are:
`fn h(form: Form<T>, app: &mut App) -> impl IntoEffect`
`fn h(id: Id, app: &mut App) -> impl IntoEffect`
`fn h(event: Event<T>, app: &mut App) -> impl IntoEffect`
`fn h(ctx: Ctx, ...) -> Result<impl IntoEffect, Error>`
---
## derive_app
### req: derive_app/001
001 `#[slhx::app]` marks an application/root state type and registry entry point. There may be one registry per app/root type, and multiple runtime instances may exist per process. slhx does not require a process-global singleton.
---
## ts
### req: ts/001
001 TypeScript definitions for `slhx-js` runtime are shipped as a single `.d.ts` file. Types mirror the postcard `EffectBatch` schema for advanced consumers. Tooling must not depend on these types for core functionality; they are developer convenience only.
---
## examples
### req: examples/001
001 The repository must contain canonical examples that act as API tests: counter, todo CRUD, form wizard, realtime dashboard, local-first kanban.
### req: examples/002
002 Each example must have a maximum ceremony budget. The counter example must fit in under 50 lines of user-authored Rust plus one template. Todo CRUD must fit in under 150 lines excluding model definitions.
### req: examples/003
003 If an example requires raw EffectWriter, manual ids, manual JS, or manual registry setup, the API is considered too complex.
---
## milestone
### req: ms/001
001 **Milestone app: Local-first Multiplayer Kanban.** A board with drag-and-drop cards, 60fps pointer-follow, optimistic updates, offline queue, conflict reconciliation, live presence, and SSR-first rendering — all without React/Vue/VDOM, in a single typed Rust codebase. This acts as the north-star integration test for slhx + hemplate + slhx-sync. [north_star]
---
## misc
### req: misc/001
001 Workspace layout: `slhx-core` (types + postcard schema, no_std), `slhx-derive` (proc-macros), `slhx-build` (surface consumer + code generation), `slhx-axum` (integration), `slhx-js` (runtime single file), `slhx-transition` (optional), `slhx-sync` (optional), `slhx-wasm` (optional). No kitchen-sink crate.
### req: misc/002
002 All crates compile on stable Rust. MSRV 1.80. `slhx-core` has zero proc-macro dependencies.
### req: misc/003
003 No auth, no routing, no session storage inside slhx core. `slhx-axum` provides typed route mounting; actual routing is axum/tower.
### req: misc/004
004 Three execution modes supported: server-first (request/response), client-local WASM (requestAnimationFrame, no round-trip), and hybrid sync (local + remote via `slhx-sync`). Modes are opt-in per handler, not global.
### req: misc/005
005 The only required user-facing proc-macro in slhx core is `#[slhx::handler]`.
Optional ergonomic macros may exist: `#[slhx::surface]`, `#[slhx::component]`,
`#[slhx::app]`, and integration-crate macros such as `#[slhx::island]` or
`#[slhx_sync::presence]`. No `!` call-syntax macros.
### req: misc/006
006 Source spans are present on every Surface node, attribute, and scope. Error messages cite file, line, and column. This is non-negotiable for DX.
### req: misc/007
007 All slhx HTML attributes use `data-` prefix (`data-slhx-handle`, `data-slhx-slot`, `data-slhx-atom`, `data-slhx-root`). No unprefixed custom attributes. Valid HTML, tool-friendly, zero custom syntax.
### req: misc/008
008 Id allocation is deterministic from canonical symbol paths. Ids are stable across builds unless the symbol path changes. Deploy mismatch between server and client is caught by a build-schema version check, not silent failure.
### req: misc/009
009 Core design rule: add one primitive only if it deletes five special cases. `ScopeKey` deletes: loop keying, component scoping, modal instances, nested forms, portal boundaries. `ResourceId` deletes: special opcodes per kind, separate registries, separate wire formats, separate test APIs. `Effect::event` deletes: plugin API, custom JS bridges, chart adapters, map SDK wrappers.