# 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`, 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::render(value)`, `Slot::text(value)`, `KeyedSlot::append(key, value)`, `KeyedSlot::replace(key, value)`, `KeyedSlot::remove(key)`. Methods return `impl IntoEffect`. ### req: codegen/003 003 Generated module `handles` exports typed constants: `Handle` where `I` is `Form`, 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` type against the contract. Domain types (`Email`, `TodoId`) remain user-authored; no auto-generated structs. ### req: codegen/005 005 Generated module `atoms` exports `Atom` 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`. `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` 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` 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`. 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: ``. 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 }`. Typed wrappers (`Slot`, `KeyedSlot`, `Atom`, `Handle`, `Form`) 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` / `Query` / `Mutation`. 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 }`. [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 `