refactor!: rename slhx to hemx

Rename the tracked product identity, crate/package names, Rust paths/macros, generated artifacts, runtime files, public attributes, examples, docs, requirements, and tests from slhx to hemx without compatibility shims.

Verified with cargo run -p hemx-xtask -- test, cargo test -p hemx-derive --test compile_fail, cargo test -p hemx-js, cargo test -p hemx-axum, cargo test -p hemx-v0-examples, cargo check --workspace, redgate list, redgate refs, redgate health --strict, git diff --check, and git grep/ls-files legacy-name audits.

req: misc/001

req: codegen/001

req: component/004

req: runtime/001
This commit is contained in:
slhx agent
2026-06-05 06:52:37 +02:00
parent d4e865ef92
commit c33500440e
69 changed files with 1415 additions and 1415 deletions
+126 -126
View File
@@ -1,11 +1,11 @@
# slhx — Semantic, Laterally HX
# hemx — Semantic, Laterally HX
slhx makes server-rendered HTML feel like it grew just enough interactivity.
hemx makes server-rendered HTML feel like it grew just enough interactivity.
App authors change state in Rust, render hemplate partials, and return generated
UI intent; the runtime swaps generated slots without selectors, a VDOM, or a
client app state framework.
> **hemplate owns syntax. hemplate emits surface. slhx consumes surface. slhx owns semantics. JS applies effects.**
> **hemplate owns syntax. hemplate emits surface. hemx consumes surface. hemx owns semantics. JS applies effects.**
---
@@ -31,13 +31,13 @@ client app state framework.
## pitch
### req: pitch/001
001 slhx is checked hypermedia for Rust. Write `.heml`, write Rust handlers, return generated UI commands. The compiler checks every cross-file reference. The browser runtime only sees lowered ids and effect bytes. No JS app code is required for ordinary server-first apps. [north_star]
001 hemx is checked hypermedia for Rust. Write `.heml`, write Rust handlers, return generated UI commands. The compiler checks every cross-file reference. The browser runtime only sees lowered ids and effect bytes. No JS app code is required for ordinary server-first apps. [north_star]
### req: pitch/002
002 No CSS selectors. No hx-* strings. No virtual DOM. No client framework. No hidden global proxy magic. No component hydration. SSR state bootstrap via `data-slhx-st` is allowed, but the browser never reconstructs a component tree. [north_star]
002 No CSS selectors. No hx-* strings. No virtual DOM. No client framework. No hidden global proxy magic. No component hydration. SSR state bootstrap via `data-hemx-st` is allowed, but the browser never reconstructs a component tree. [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]
003 hemx replaces React/Vue not with a UI framework, but with a compiler contract: hemplate knows the surface, Rust knows the types, hemx knows the effects, the browser only executes commands. [north_star]
### req: pitch/004
004 The north-star feel: **boring server-rendered HTML with typed, selectorless partial swaps.** Short handler bodies, compile-checked HTML contracts, plain Rust state changes, hemplate-rendered partials, and a tiny runtime that applies effects. [north_star]
@@ -47,7 +47,7 @@ client app state framework.
## canonical_authoring
### req: canonical/001
001 The canonical app shape is templates plus Rust, not a frontend folder: `.heml` files declare `data-slhx-root`, `data-slhx-slot`, `data-slhx-handle`, `data-slhx-form`, `h-key`, optional pending/page/island facts, and handlers return generated UI commands. Ordinary app code avoids selectors, numeric ids, raw effects, wire formats, manual registries/form parsing, raw `SafeHtml`, and raw render calls. Plain CSS owns appearance. [north_star]
001 The canonical app shape is templates plus Rust, not a frontend folder: `.heml` files declare `data-hemx-root`, `data-hemx-slot`, `data-hemx-handle`, `data-hemx-form`, `h-key`, optional pending/page/island facts, and handlers return generated UI commands. Ordinary app code avoids selectors, numeric ids, raw effects, wire formats, manual registries/form parsing, raw `SafeHtml`, and raw render calls. Plain CSS owns appearance. [north_star]
### req: canonical/002
002 Typed partial swaps are the primary UX, not an advanced feature: handlers change domain state in Rust, convert domain values into view values, render hemplate partials through generated helpers, and place them into generated targets. The real primitive is generated target + rendered partial + swap kind. [north_star]
@@ -65,20 +65,20 @@ client app state framework.
006 There is no separate beginner API and expert API. The simple generated shape is canonical and should scale: generated slots, partials, forms, class constants, islands/events, and page helpers are normal. Explicit primitives, raw targets, raw HTML, raw effects, manual registries/form parsing, low-level ids/opcodes, wire formats, and raw routes remain named escape hatches or internals around the same render/target/effect/transport model. [north_star]
### req: canonical/007
007 Opaque islands are explicit leaf adapters: templates declare `data-slhx-island` and optional generated handles/events; server code may emit snapshots/events such as `ui::game.emit(event)`, while island JS owns only high-frequency local behavior. Islands do not introduce a component runtime, client state graph, VDOM, or second UI model. [north_star]
007 Opaque islands are explicit leaf adapters: templates declare `data-hemx-island` and optional generated handles/events; server code may emit snapshots/events such as `ui::game.emit(event)`, while island JS owns only high-frequency local behavior. Islands do not introduce a component runtime, client state graph, VDOM, or second UI model. [north_star]
### req: canonical/008
008 Offline/PWA support is opt-in adapter territory. Server-first slhx may fail interactions while offline; cached shells and local-sync queues live in crates such as `slhx-pwa` or `slhx-sync`, reuse generated slots/effects, queue explicit patches, and reconcile with server-canonical effects. Core slhx must not gain a mandatory client state graph, scheduler, CRDT, or local app runtime. [north_star]
008 Offline/PWA support is opt-in adapter territory. Server-first hemx may fail interactions while offline; cached shells and local-sync queues live in crates such as `hemx-pwa` or `hemx-sync`, reuse generated slots/effects, queue explicit patches, and reconcile with server-canonical effects. Core hemx must not gain a mandatory client state graph, scheduler, CRDT, or local app runtime. [north_star]
---
## modes
### req: mode/001
001 slhx has one core authoring loop: render a partial and place it into a generated target with a swap kind. HTTP handlers, page navigation, push streams, and island events are transport/adapters around that loop.
001 hemx has one core authoring loop: render a partial and place it into a generated target with a swap kind. HTTP handlers, page navigation, push streams, and island events are transport/adapters around that loop.
### req: mode/002
002 Page Enhancer mode is a specialized partial swap for navigation: it updates generated page/content/title/nav targets and adds history, scroll, shell, and fallback behavior. Authors use real anchors with `data-slhx-nav` or `data-slhx-boost`; no user-authored handler is required for ordinary navigation.
002 Page Enhancer mode is a specialized partial swap for navigation: it updates generated page/content/title/nav targets and adds history, scroll, shell, and fallback behavior. Authors use real anchors with `data-hemx-nav` or `data-hemx-boost`; no user-authored handler is required for ordinary navigation.
### req: mode/003
003 Interaction Handler mode handles forms, buttons, typed params, and generated partial/text/form/island effects through Rust handlers.
@@ -97,7 +97,7 @@ client app state framework.
002 The happy path is: write `.heml`, write a Rust handler, return generated partial/text/form/page/island commands. No manual ids, no manual registry, no manual serialization, no CSS selector targets, no raw render calls, and no manual JavaScript for ordinary app UI. [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)`.
003 Public APIs are generated around the user's names. If the template declares `data-hemx-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.
@@ -125,7 +125,7 @@ client app state framework.
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(); }`
003 `build.rs` must be a one-liner for the common case: `fn main() { hemx_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.
@@ -151,10 +151,10 @@ client app state framework.
## page_swap
### req: page_swap/001
001 Page swapping is a first-class specialization of partial swapping: render a page partial, place it into generated page targets, then apply history/title/scroll/shell behavior. Authors mark real anchors with `data-slhx-nav`; links keep valid `href` and work without JS. Missing or empty static `href` on a `data-slhx-nav` anchor is a build error.
001 Page swapping is a first-class specialization of partial swapping: render a page partial, place it into generated page targets, then apply history/title/scroll/shell behavior. Authors mark real anchors with `data-hemx-nav`; links keep valid `href` and work without JS. Missing or empty static `href` on a `data-hemx-nav` anchor is a build error.
### req: page_swap/002
002 A `data-slhx-nav` click fetches the target URL as a slhx partial request. The response is conceptually `ui::content.replace(page)` plus optional generated nav/title targets and a `Navigate` effect; it must not introduce selector targeting or a second page-specific UI model.
002 A `data-hemx-nav` click fetches the target URL as a hemx partial request. The response is conceptually `ui::content.replace(page)` plus optional generated nav/title targets and a `Navigate` effect; it must not introduce selector targeting or a second page-specific UI model.
### req: page_swap/003
003 Page swapping uses generated targets, not CSS selectors. The default content target is the generated slot named `content`, not `#content`; explicit page helpers such as `ui::content.page(req, view)` or `request.page_html(ui::content.render(view), shell)` are adapters around the same partial-swap primitive.
@@ -163,26 +163,26 @@ client app state framework.
004 Ordinary page navigation must not require user-authored handlers. Explicit navigation handlers are available only when custom application logic is needed, and they still return generated target/page commands.
### req: page_swap/005
005 Browser back/forward is supported. On `popstate`, slhx fetches the URL as a partial request and applies the same page-swap update without pushing a new history entry.
005 Browser back/forward is supported. On `popstate`, hemx fetches the URL as a partial request and applies the same page-swap update without pushing a new history entry.
### req: page_swap/006
006 If a page lacks the expected `content` slot, slhx-axum falls back to normal browser navigation in production and emits a diagnostic in development.
006 If a page lacks the expected `content` slot, hemx-axum falls back to normal browser navigation in production and emits a diagnostic in development.
### req: page_swap/007
007 `data-slhx-boost` progressively enhances descendant same-origin anchors and forms. It is a container convention, not a replacement for `data-slhx-nav` on an anchor or `data-slhx-handle` on a form; placing it directly on static anchors or forms is a build error. Links behave like `data-slhx-nav`; forms behave like slhx form submissions. External links, downloads, new-tab links, and modified-clicks preserve native browser behavior.
007 `data-hemx-boost` progressively enhances descendant same-origin anchors and forms. It is a container convention, not a replacement for `data-hemx-nav` on an anchor or `data-hemx-handle` on a form; placing it directly on static anchors or forms is a build error. Links behave like `data-hemx-nav`; forms behave like hemx form submissions. External links, downloads, new-tab links, and modified-clicks preserve native browser behavior.
---
## htmx_equivalents
### req: htmx/001
001 slhx replaces common HTMX use-cases through typed equivalents, not HTMX syntax.
001 hemx replaces common HTMX use-cases through typed equivalents, not HTMX syntax.
### req: htmx/002
002 Easy equivalents must exist for: generated target replacement, append/prepend/remove, boosted links/forms, page swap, form submit, loading indicators, confirmation, debounce/throttle, polling, history navigation, multi-target updates, response events, SSE/push, validation errors, modals, toasts, table rows, SVG fragments, and form error regions.
### req: htmx/003
003 slhx core deliberately does not clone selector-based HTMX features: `hx-target` selectors, `hx-select`, `hx-include` selectors, `closest/find/this` target strings, or trigger mini-languages. Equivalent patterns use generated targets, typed params, forms, explicit handlers, and page/push adapters around partial swaps.
003 hemx core deliberately does not clone selector-based HTMX features: `hx-target` selectors, `hx-select`, `hx-include` selectors, `closest/find/this` target strings, or trigger mini-languages. Equivalent patterns use generated targets, typed params, forms, explicit handlers, and page/push adapters around partial swaps.
---
@@ -192,7 +192,7 @@ client app state framework.
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.
002 hemx 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:
@@ -200,20 +200,20 @@ client app state framework.
Category modules such as `targets`, `handles`, and `forms` remain available for organization/compatibility, while raw slot constants live under `advanced::slots`; global exports (`ui::slots::*`, `ui::handles::*`, `ui::components::*`) 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.
004 `#[hemx::surface]` bridges generated code into a user module. Users write `#[hemx::surface] mod ui {}` instead of `include!(concat!(env!("OUT_DIR"), ...))`. hemx-build emits `hemx.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`.
005 An optional `#[hemx::component]` macro may validate that every handle declared in the template Surface has a corresponding `#[hemx::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 `#[hemx::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 discovers them automatically; no additional configuration is required for most apps.
006 `#[derive(Hemplate)]` structs are natural component boundaries. hemx_build discovers them automatically; no additional configuration is required for most apps.
---
## surface
### req: surface/001
001 `hemplate_build` may scan `.heml` files and emit `$OUT_DIR/hemplate.surface.postcard` (postcard-encoded, deterministic, versioned). `slhx_build` may also be called in-process with precomputed hemplate Surface facts or, for simple build scripts, ask hemplate to parse/extract the Surface before slhx interprets it. slhx owns no independent `.heml` parser.
001 `hemplate_build` may scan `.heml` files and emit `$OUT_DIR/hemplate.surface.postcard` (postcard-encoded, deterministic, versioned). `hemx_build` may also be called in-process with precomputed hemplate Surface facts or, for simple build scripts, ask hemplate to parse/extract the Surface before hemx interprets it. hemx owns no independent `.heml` parser.
### req: surface/002
002 The Surface contains: nodes (NodeId, parent, scope, element, attrs, source span), scopes (ScopeKind: Root | If | Match | Case | For { binding, key_expr }), forms (form controls with raw HTML types), and component uses.
@@ -225,7 +225,7 @@ Category modules such as `targets`, `handles`, and `forms` remain available for
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. slhx_build enforces key presence only when a slhx-addressable node appears inside the loop.
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. hemx_build enforces key presence only when a hemx-addressable node appears inside the loop.
### 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.
@@ -236,16 +236,16 @@ Category modules such as `targets`, `handles`, and `forms` remain available for
### 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`, `h-match`, `h-case`,
dynamic `+attr` bindings, and interpolated attr/text expressions. slhx consumes
these facts; if a build script points slhx_build at `.heml` files, hemplate still performs parsing and Surface extraction.
dynamic `+attr` bindings, and interpolated attr/text expressions. hemx consumes
these facts; if a build script points hemx_build at `.heml` files, hemplate still performs parsing and Surface extraction.
### req: surface/009
009 Raw/pre-rendered HTML insertions are opaque Surface holes. The parent
element is present; slhx_build emits the appropriate rendering call.
element is present; hemx_build emits the appropriate rendering call.
### req: surface/010
010 Attribute values preserve their origin: static literal, dynamic `+attr`
binding, or interpolated expression. slhx-build uses this to determine whether
binding, or interpolated expression. hemx-build uses this to determine whether
a `data-*` handle param is statically known or runtime-extracted.
---
@@ -253,22 +253,22 @@ a `data-*` handle param is statically known or runtime-extracted.
## codegen
### req: codegen/001
001 `slhx_build` generates three artifacts from the generic Surface IR: (a) `slhx.generated.rs` containing ergonomic resource modules (`slots`, `targets`, `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]
001 `hemx_build` generates three artifacts from the generic Surface IR: (a) `hemx.generated.rs` containing ergonomic resource modules (`slots`, `targets`, `handles`, `forms`, `atoms`), (b) `hemx.syms` for proc-macro validation, (c) runtime id-lowering tables. `hemx_build` interprets tool-specific conventions (`data-hemx-*`, `h-for`, `h-key`, form controls) from the Surface. [north_star]
### req: codegen/002
002 Generated view modules expose ergonomic root-level target objects and commands that hide render/lower details: text slots provide `set(text)`, singleton partial slots provide `replace(view)`/`clear()`, keyed collection slots provide `append(view)`, `prepend(view)`, `replace(view)`, `remove(key_or_view)`, and forms provide `clear()`/`clear(field)`/`focus(field)`. String-keyed target objects accept displayable domain ids without caller-side `.to_string()` noise. Commands return `impl IntoEffect`, compose in plain Rust, preserve generated lowering, and fail to generate when the template lacks enough facts to infer the slot, key, form, or renderable view type.
### 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.
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 `#[hemx::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.
004 Generated module `forms` exports `FormContract` metadata (field names, HTML control kinds, required). `#[hemx::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, bootstrapped, or synced. Ordinary Rust fields on app/components are not automatically atoms.
### req: codegen/006
006 `slhx-build` discovers `data-slhx-on` event names from hemplate Surface inputs and emits generated `slhx::EventName` constants. Event constants are metadata for checked Rust authoring and diagnostics; they do not create a trigger mini-language or new browser runtime semantics. [north_star]
006 `hemx-build` discovers `data-hemx-on` event names from hemplate Surface inputs and emits generated `hemx::EventName` constants. Event constants are metadata for checked Rust authoring and diagnostics; they do not create a trigger mini-language or new browser runtime semantics. [north_star]
---
@@ -303,7 +303,7 @@ a `data-*` handle param is statically known or runtime-extracted.
003 `Insert`, `Remove`, and `Move` operate on keyed collection resources. They require a key type checked by generated `KeyedSlot<K, T>` wrappers.
### req: effect_algebra/004
004 `Navigate` changes browser history or represents a server redirect. Route matching remains outside slhx core.
004 `Navigate` changes browser history or represents a server redirect. Route matching remains outside hemx core.
### req: effect_algebra/005
005 `Emit` dispatches a native `CustomEvent` and is the only raw JS interop primitive in core.
@@ -331,7 +331,7 @@ in core. `Effect::event` lowers to the canonical `Emit` opcode.
### req: scope/001
001 `Scope` is a first-class primitive. Keyed loops (`h-for`) create keyed
dynamic scopes and require `h-key` for slhx-addressable nodes. Conditional
dynamic scopes and require `h-key` for hemx-addressable nodes. Conditional
branches (`h-if`, `h-else-if`, `h-else`, `h-match`, `h-case`) create optional
presence scopes. Component instances, modals, tabs, and nested forms are scoped
resources. Concrete runtime targets are addressed through `ResourceRef`
@@ -342,10 +342,10 @@ resources. Concrete runtime targets are addressed through `ResourceRef`
## 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 `ResourceRef { resource: ResourceId, scope: Some(ScopeKey::KeyValue(...)) }`.
001 Any `data-hemx-slot` or `data-hemx-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, hemx-addressable nodes inside the loop are rejected at build time. Keyed identity is `ResourceRef { resource: ResourceId, scope: Some(ScopeKey::KeyValue(...)) }`.
### req: list/002
002 Slots inside a keyed loop receive a composite identity. hemplate records `key_expr` in the Surface; slhx implements keyed slot lookups.
002 Slots inside a keyed loop receive a composite identity. hemplate records `key_expr` in the Surface; hemx implements keyed slot lookups.
### req: list/003
003 Generated helpers for keyed slots are `append(view)`, `prepend(view)`, `replace(view)`, and `remove(key_or_view)` when the template and view type provide an unambiguous `h-key`. Compatibility functions such as `append(keyed_slot, key, view)` may exist as explicit low-level forms. Mismatch between key type and slot key type is a compile-time error, and missing/ambiguous keys are build errors with template spans.
@@ -355,7 +355,7 @@ resources. Concrete runtime targets are addressed through `ResourceRef`
## 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 Rust `Form<T>` types through user-authored `#[slhx::form("...")]` domain structs and generated form metadata. No auto-generated structs; domain types (e.g. `Email`) are first-class. The Surface describes; Rust owns; slhx checks.
001 Forms are source of truth in HTML. hemplate Surface exports form shape (controls, names, required, types). hemx checks compatibility with Rust `Form<T>` types through user-authored `#[hemx::form("...")]` domain structs and generated form metadata. No auto-generated structs; domain types (e.g. `Email`) are first-class. The Surface describes; Rust owns; hemx 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.
@@ -364,7 +364,7 @@ resources. Concrete runtime targets are addressed through `ResourceRef`
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.
### req: form/004
004 Form compatibility checks validate field presence, optionality, multiplicity, and parser availability. Parser availability means the submitted value type implements `slhx::FormValue` (blanket-provided for `FromStr`, or explicitly implemented for custom parsers). Domain validation remains Rust logic (`TryFrom`, custom validators, or handler code).
004 Form compatibility checks validate field presence, optionality, multiplicity, and parser availability. Parser availability means the submitted value type implements `hemx::FormValue` (blanket-provided for `FromStr`, or explicitly implemented for custom parsers). Domain validation remains Rust logic (`TryFrom`, custom validators, or handler code).
### req: form/005
005 HTML control facts are lower bounds, not complete domain semantics.
@@ -385,39 +385,39 @@ what a valid business email is.
002 Form effects target generated form/control ids, not CSS selectors.
### req: form_effects/003
003 Templates may declare error display targets with `data-slhx-error-for="field"`. Generated form error effects render into those targets when present and fall back to control validity APIs otherwise.
003 Templates may declare error display targets with `data-hemx-error-for="field"`. Generated form error effects render into those targets when present and fall back to control validity APIs otherwise.
---
## 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.
001 Authoring HTML uses symbolic `data-hemx-*` 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-hemx-st` for state bootstrap. The browser never sees handler or slot names. `data-hemx-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 HTTP interaction responses may be `text/html` fragments containing `<template data-slhx>...`. Push streams use `application/slhx` or transport-specific event frames carrying serialized `EffectBatch`.
003 HTTP interaction responses may be `text/html` fragments containing `<template data-hemx>...`. Push streams use `application/hemx` or transport-specific event frames carrying serialized `EffectBatch`.
### 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.
004 Server push is orthogonal: integration crates stream postcard `EffectBatch` over SSE or WebSocket connections. hemx 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.
005 No JSON anywhere in hemx-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.
---
## abi
### req: abi/001
001 Surface IR, slhx symbols, generated Rust API, EffectBatch wire schema, and JS runtime each carry explicit schema/ABI versions.
001 Surface IR, hemx symbols, generated Rust API, EffectBatch wire schema, and JS runtime each carry explicit schema/ABI versions.
### req: abi/002
002 slhx_build emits a build fingerprint derived from Surface schema version, resource id allocation, EffectBatch ABI version, and runtime ABI version.
002 hemx_build emits a build fingerprint derived from Surface schema version, resource id allocation, EffectBatch ABI version, and runtime ABI version.
### req: abi/003
003 The server includes the slhx build fingerprint in initial roots. The runtime compares it with its own fingerprint before applying EffectBatches.
003 The server includes the hemx build fingerprint in initial roots. The runtime compares it with its own fingerprint before applying EffectBatches.
### req: abi/004
004 On fingerprint mismatch, the runtime refuses partial updates and falls back to full page navigation or reload. Silent mismatch is forbidden.
@@ -430,16 +430,16 @@ what a valid business email is.
## 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]
001 Every hemx tree must declare a root boundary via `data-hemx-root` on an ancestor element. The JS runtime resolves lookups within that root only. Multiple independent hemx 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.
002 JS runtime attaches a single delegated listener per event type on each `data-hemx-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 Core runtime exposes a minimal version/fingerprint handshake only. Capability negotiation belongs to integration crates such as `slhx-wasm`, `slhx-sync`, and `slhx-transition`.
004 Core runtime exposes a minimal version/fingerprint handshake only. Capability negotiation belongs to integration crates such as `hemx-wasm`, `hemx-sync`, and `hemx-transition`.
---
@@ -468,45 +468,45 @@ what a valid business email is.
## axum_integration
### req: axum/001
001 slhx-axum supports the common full-shell/partial pattern as an adapter around generated partial swaps. Full-page requests are wrapped in a user-provided Shell; slhx partial requests may return a rendered partial for a generated target or an EffectBatch. Page helpers add shell/title/history/fallback behavior without changing the render/target/effect model.
001 hemx-axum supports the common full-shell/partial pattern as an adapter around generated partial swaps. Full-page requests are wrapped in a user-provided Shell; hemx partial requests may return a rendered partial for a generated target or an EffectBatch. Page helpers add shell/title/history/fallback behavior without changing the render/target/effect model.
### req: axum/002
002 Existing Axum routes remain normal Axum routes. slhx does not own routing. slhx-axum only mounts handler dispatch, runtime assets, and optional push endpoints.
002 Existing Axum routes remain normal Axum routes. hemx does not own routing. hemx-axum only mounts handler dispatch, runtime assets, and optional push endpoints.
### req: axum/003
003 Interactive fragments that would traditionally be implemented as `/demo/...` HTMX endpoints should be expressible as Rust handlers returning generated target commands. Integration registration should read as interactions over generated handles and page/partial helpers, not low-level registry wiring.
### req: axum/004
004 Query-string demo endpoints may be migrated to typed handler params from `data-*` attributes or forms. `Query<T>` remains available in normal Axum routes but is not the slhx happy path.
004 Query-string demo endpoints may be migrated to typed handler params from `data-*` attributes or forms. `Query<T>` remains available in normal Axum routes but is not the hemx happy path.
---
## auth
### req: auth/001
001 Auth is not part of slhx core. Authentication, authorization, sessions, cookies, CSRF, and permissions are handled by axum/tower extractors and middleware. slhx handlers may accept typed auth/context extractors.
001 Auth is not part of hemx core. Authentication, authorization, sessions, cookies, CSRF, and permissions are handled by axum/tower extractors and middleware. hemx handlers may accept typed auth/context extractors.
### req: auth/002
002 slhx-axum preserves normal HTTP auth semantics. Unauthorized handlers may return normal HTTP 401/403, a navigation effect, or an application-defined auth failure effect.
002 hemx-axum preserves normal HTTP auth semantics. Unauthorized handlers may return normal HTTP 401/403, a navigation effect, or an application-defined auth failure effect.
### req: auth/003
003 Progressive enhancement is preserved: login/logout forms remain valid HTML forms. With JS disabled, the server performs normal redirects; with slhx enabled, handlers may return EffectBatch responses.
003 Progressive enhancement is preserved: login/logout forms remain valid HTML forms. With JS disabled, the server performs normal redirects; with hemx enabled, handlers may return EffectBatch responses.
### req: auth/004
004 CSRF is integration-level. slhx-axum must allow normal hidden form fields, cookies, and extractor-based CSRF validation. slhx core does not define CSRF policy.
004 CSRF is integration-level. hemx-axum must allow normal hidden form fields, cookies, and extractor-based CSRF validation. hemx core does not define CSRF policy.
### req: auth/005
005 slhx requests preserve standard HTTP credentials semantics. Cookies, SameSite policy, Authorization headers, and session middleware remain framework/browser concerns.
005 hemx requests preserve standard HTTP credentials semantics. Cookies, SameSite policy, Authorization headers, and session middleware remain framework/browser concerns.
---
## push
### req: push/001
001 Server push streams canonical postcard EffectBatch over SSE or WebSocket. slhx core owns the EffectBatch schema, not the transport.
001 Server push streams canonical postcard EffectBatch over SSE or WebSocket. hemx core owns the EffectBatch schema, not the transport.
### req: push/002
002 SSE/WebSocket connections are authenticated by the server framework before stream creation. slhx does not define auth semantics for streams.
002 SSE/WebSocket connections are authenticated by the server framework before stream creation. hemx does not define auth semantics for streams.
### req: push/003
003 Push swaps are ordinary partial swaps carried over SSE/WebSocket: streamed effects target generated slots, atoms, or island events such as `ui::feed.prepend(event)` or `ui::game.emit(snapshot)`. No selector-based `sse-swap` semantics in core.
@@ -518,14 +518,14 @@ what a valid business email is.
005 Push is one-way server-to-client delivery of EffectBatch. It does not define client mutation, optimistic queues, reconciliation, or conflict handling.
### req: push/006
006 `data-slhx-sse` is declared on `data-slhx-root` and opens only non-empty same-origin SSE URLs by default. Empty static URLs and non-root placement are build errors; cross-origin streams belong to explicit integration code rather than the standard runtime convention.
006 `data-hemx-sse` is declared on `data-hemx-root` and opens only non-empty same-origin SSE URLs by default. Empty static URLs and non-root placement are build errors; cross-origin streams belong to explicit integration code rather than the standard runtime convention.
---
## 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.
001 `hemx-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.
@@ -537,13 +537,13 @@ what a valid business email is.
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.
005 `#[hemx_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.
007 `hemx-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 `hemx-crdt` crate.
### req: sync/008
008 Sync is bidirectional state reconciliation built on top of push/transport. It is not required for server-sent dashboards, notifications, or live status updates.
@@ -553,28 +553,28 @@ what a valid business email is.
## interop
### req: interop/001
001 `Effect::event` and generated event helpers are the single slhx-to-widget bridge. External widgets, charts, games, maps, Alpine/Svelte islands, and Web Components listen via native `CustomEvent`; slhx core does not inspect their state, rendering internals, or framework lifecycle. [north_star]
001 `Effect::event` and generated event helpers are the single hemx-to-widget bridge. External widgets, charts, games, maps, Alpine/Svelte islands, and Web Components listen via native `CustomEvent`; hemx core does not inspect their state, rendering internals, or framework lifecycle. [north_star]
### req: interop/002
002 Web Components and custom elements are valid opaque leaf nodes. slhx does not inspect shadow DOM or mutate inside custom elements unless the author explicitly exposes slhx-owned slots/handles at the boundary. Escape hatches are leaves, never app foundations.
002 Web Components and custom elements are valid opaque leaf nodes. hemx does not inspect shadow DOM or mutate inside custom elements unless the author explicitly exposes hemx-owned slots/handles at the boundary. Escape hatches are leaves, never app foundations.
### req: interop/003
003 WASM islands and third-party framework islands are explicit leaf boundaries. slhx may replace the island root as a generated target, but it does not manage inside it; slhx owns the generated slot/island boundary, Alpine/Svelte/Web Components/hand-written widgets own the inside, and events cross the boundary. Commands flow widget-to-slhx through explicit generated handles or `slhx.send(...)`, and server-to-widget through generated event helpers such as `ui::chart.emit(snapshot)`.
003 WASM islands and third-party framework islands are explicit leaf boundaries. hemx may replace the island root as a generated target, but it does not manage inside it; hemx owns the generated slot/island boundary, Alpine/Svelte/Web Components/hand-written widgets own the inside, and events cross the boundary. Commands flow widget-to-hemx through explicit generated handles or `hemx.send(...)`, and server-to-widget through generated event helpers such as `ui::chart.emit(snapshot)`.
### 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.
004 Existing `hx-*` attributes are treated as ordinary raw attributes in the hemplate Surface without hemx semantics. An optional `hemx-htmx-migrate` tool may read Surface `hx-*` attrs and suggest equivalent `data-hemx-*` handlers/effects.
### req: interop/005
005 HTMX-style response triggers and widget notifications are represented by `Effect::event` or generated event helpers. Events are native `CustomEvent`s scoped to the slhx root.
005 HTMX-style response triggers and widget notifications are represented by `Effect::event` or generated event helpers. Events are native `CustomEvent`s scoped to the hemx root.
### req: interop/006
006 `data-slhx-preserve` is an explicit preserve boundary for rare leaf-widget cases where slhx updates around a subtree without destroying it. Preserve semantics must be simple: preserve the marked subtree identity, do not diff or hydrate inside it, and require authors to mark the boundary deliberately. Preserve must not become a default lifecycle model or a workaround for unclear ownership.
006 `data-hemx-preserve` is an explicit preserve boundary for rare leaf-widget cases where hemx updates around a subtree without destroying it. Preserve semantics must be simple: preserve the marked subtree identity, do not diff or hydrate inside it, and require authors to mark the boundary deliberately. Preserve must not become a default lifecycle model or a workaround for unclear ownership.
### req: interop/007
007 The runtime emits native lifecycle events such as `slhx:before-swap`, `slhx:after-swap`, `slhx:event`, `slhx:connect`, and `slhx:disconnect` so Alpine, Svelte, Web Components, and hand-written widgets can attach at DOM/event boundaries. slhx core must not add framework-specific adapters.
007 The runtime emits native lifecycle events such as `hemx:before-swap`, `hemx:after-swap`, `hemx:event`, `hemx:connect`, and `hemx:disconnect` so Alpine, Svelte, Web Components, and hand-written widgets can attach at DOM/event boundaries. hemx core must not add framework-specific adapters.
### req: interop/008
008 Interop must prevent selector hacks, manual JS reinitialization races, lost widget state after swaps, and double-owned state by making ownership explicit: slhx owns generated server DOM targets, the external widget owns explicit leaves, and events are the supported crossing point. Core must not add selector targeting, hydration compatibility, a client store, or a framework lifecycle to make interop easy.
008 Interop must prevent selector hacks, manual JS reinitialization races, lost widget state after swaps, and double-owned state by making ownership explicit: hemx owns generated server DOM targets, the external widget owns explicit leaves, and events are the supported crossing point. Core must not add selector targeting, hydration compatibility, a client store, or a framework lifecycle to make interop easy.
---
@@ -587,10 +587,10 @@ what a valid business email is.
002 Navigation modes: `Push` (history.pushState), `Replace` (replaceState), `Redirect` (server-side 302). Scroll behaviour: `Preserve`, `Top`, `Element(ResourceRef)`. 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.
003 Navigation enhancement preserves real anchors. Links keep valid `href`. hemx may intercept enhanced links through `data-hemx-handle` or `data-hemx-nav`, but without JS the browser performs normal navigation.
### req: nav/004
004 slhx supports both normal HTTP redirects and navigation effects. HTTP redirects are preferred for full-page/non-enhanced flows; navigation effects are preferred for enhanced interaction responses.
004 hemx supports both normal HTTP redirects and navigation effects. HTTP redirects are preferred for full-page/non-enhanced flows; navigation effects are preferred for enhanced interaction responses.
### req: nav/005
005 Page swap preserves browser history semantics: push, replace, popstate, scroll behavior, and normal modified-click behavior. Back/forward may re-fetch partial content or restore from a bounded cache; correctness must not depend on the cache.
@@ -613,7 +613,7 @@ what a valid business email is.
## view
### 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.
001 Slots render view types, not necessarily domain types. Domain-to-view conversion is explicit Rust (`From`, `Into`, or constructor). hemx 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.
@@ -636,10 +636,10 @@ what a valid business email is.
## test
### req: test/001
001 `EffectWriter` implements a test backend so handlers can be unit-tested without a browser: `slhx_test::run(handler, input)`, `slhx_test::inspect_batch(dispatched_batch)`, and `slhx_test::inspect_wire(bytes)` return an `EffectInspector` with generated-resource assertions such as `has_target(generated_target)`, `updates_text(generated_target)`, `updates_html(generated_target)`, `target_html_containing(generated_target, text)`, `inserts_html_containing(generated_target, key, text)`, `replaces_keyed_html_containing(generated_target, key, text)`, `removes_key(generated_target, key)`, `pushes_to(url)`, `emits(name, payload)`, `emits_containing(name, text)`, `has_slot(slot)`, `has_atom(atom)`, etc. Canonical example tests should prefer generated-target assertions so tests use the same generated target objects as handlers instead of importing raw slot constants or matching raw effects/payloads. Browser/E2E selector helpers are allowed only as low-level test adapters for driving rendered HTML and must be generated or named around public authoring concepts such as handles, targets, forms, nav links, roots, islands, class tokens, or keys; they must not become app authoring APIs or teach selector targeting.
001 `EffectWriter` implements a test backend so handlers can be unit-tested without a browser: `hemx_test::run(handler, input)`, `hemx_test::inspect_batch(dispatched_batch)`, and `hemx_test::inspect_wire(bytes)` return an `EffectInspector` with generated-resource assertions such as `has_target(generated_target)`, `updates_text(generated_target)`, `updates_html(generated_target)`, `target_html_containing(generated_target, text)`, `inserts_html_containing(generated_target, key, text)`, `replaces_keyed_html_containing(generated_target, key, text)`, `removes_key(generated_target, key)`, `pushes_to(url)`, `emits(name, payload)`, `emits_containing(name, text)`, `has_slot(slot)`, `has_atom(atom)`, etc. Canonical example tests should prefer generated-target assertions so tests use the same generated target objects as handlers instead of importing raw slot constants or matching raw effects/payloads. Browser/E2E selector helpers are allowed only as low-level test adapters for driving rendered HTML and must be generated or named around public authoring concepts such as handles, targets, forms, nav links, roots, islands, class tokens, or keys; they must not become app authoring APIs or teach selector targeting.
### req: test/002
002 `#[slhx::component]` may compile without generated Surface files for incremental module-local testing. `#[slhx::surface]` requires `slhx.generated.rs` in `$OUT_DIR` and fails with an actionable diagnostic when generation is missing, because it is the public generated API bridge.
002 `#[hemx::component]` may compile without generated Surface files for incremental module-local testing. `#[hemx::surface]` requires `hemx.generated.rs` in `$OUT_DIR` and fails with an actionable diagnostic when generation is missing, because it is the public generated API bridge.
### req: test/003
003 Generated registries are validated by compile-time tests: missing handler implementations produce test failures with actionable messages.
@@ -658,13 +658,13 @@ what a valid business email is.
001 All symbolic cross-file references visible to build/proc-macro validation are 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.
002 Dead/missing handle diagnostics are best-effort by default. With `#[hemx::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 Root-scoped slot lookup: JS runtime resolves `data-sid` only within the nearest/current `data-slhx-root`.
004 Root-scoped slot lookup: JS runtime resolves `data-sid` only within the nearest/current `data-hemx-root`.
---
@@ -677,13 +677,13 @@ what a valid business email is.
002 Atoms are not reactive by default. Updating an atom does not re-render anything until a handler returns an effect referencing it.
### req: state/003
003 The JS runtime may maintain a narrow atom value table keyed by `AtomId` only for explicit `Atom<T>` resources and SSR bootstrap. This is not an app state framework, component store, cache, or reactive graph. 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`.
003 The JS runtime may maintain a narrow atom value table keyed by `AtomId` only for explicit `Atom<T>` resources and SSR bootstrap. This is not an app state framework, component store, cache, or reactive graph. Runtime values are type-erased postcard bytes; types are compile-time only. A deterministic `TypeHash` may be generated by `hemx_build` for diagnostics, but the JS runtime does not depend on Rust `TypeId`.
### req: state/004
004 SSR roots may carry a `data-slhx-st` base64url-encoded postcard blob on the `data-slhx-root` element. Runtime decodes it into the client atom store. Atoms computed from server state are immediately available to client-side handlers without a round-trip.
004 SSR roots may carry a `data-hemx-st` base64url-encoded postcard blob on the `data-hemx-root` element. 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/006
006 Malformed `data-slhx-st` bootstrap state must not stop the standard runtime from binding roots, handlers, navigation, or push. The runtime reports `slhx:state-error` and continues with an empty atom store for that root.
006 Malformed `data-hemx-st` bootstrap state must not stop the standard runtime from binding roots, handlers, navigation, or push. The runtime reports `hemx:state-error` and continues with an empty atom store for that root.
### 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.
@@ -702,7 +702,7 @@ what a valid business email is.
003 High-frequency UI handlers (drag, pointermove, animation tick) must not require server round-trips or handwritten JS.
### req: client_local/004
004 The exact opt-in syntax for client-local handlers is not part of slhx-core v0. `#[slhx::handler(client)]` is illustrative; final syntax belongs to `slhx-wasm` integration.
004 The exact opt-in syntax for client-local handlers is not part of hemx-core v0. `#[hemx::handler(client)]` is illustrative; final syntax belongs to `hemx-wasm` integration.
---
@@ -731,17 +731,17 @@ what a valid business email is.
003 Rust handlers return effects; they do not imperatively mutate DOM.
### req: invariant/004
004 Cross-file references that are visible to build/proc-macro validation fail at `cargo check` with a precise span. Global completeness checks, such as missing handler implementations across a crate, are `cargo check` errors only inside `#[slhx::component]`; otherwise they are caught at app mount or generated registry tests.
004 Cross-file references that are visible to build/proc-macro validation fail at `cargo check` with a precise span. Global completeness checks, such as missing handler implementations across a crate, are `cargo check` errors only inside `#[hemx::component]`; otherwise they are caught at app mount or generated registry tests.
### req: invariant/005
005 slhx core owns effects, typed ids, and registries only. Routing, auth, sessions, transport, transitions, and sync are integration concerns.
005 hemx core owns effects, typed ids, and registries only. Routing, auth, sessions, transport, transitions, and sync are integration concerns.
---
## v0_scope
### req: v0/001
001 v0 stable release includes: Surface consumption, generated slots/forms/handles, `#[slhx::handler]`, tuple `IntoEffect`, form dispatch, typed data-* params, keyed slots, page swap, root-scoped runtime, EffectBatch wire schema, diagnostics, tests, and `slhx-axum` integration.
001 v0 stable release includes: Surface consumption, generated slots/forms/handles, `#[hemx::handler]`, tuple `IntoEffect`, form dispatch, typed data-* params, keyed slots, page swap, root-scoped runtime, EffectBatch wire schema, diagnostics, tests, and `hemx-axum` integration.
### req: v0/002
002 v0 excludes: sync, wasm/client-local handlers, transitions, query/cache helpers, CRDT, custom component lifecycle, built-in auth, built-in router, and HTMX compatibility mode.
@@ -757,7 +757,7 @@ what a valid business email is.
## examples
### req: examples/001
001 The repository must contain canonical examples that act as API tests. v0 examples are counter, todo CRUD, form wizard, docs-site page swap, auth action, SSE notifications, and keyed todo list; local-first kanban is a north-star milestone example. The full techdemo may include an opaque leaf-widget island that communicates through `Effect::event`, without moving island mechanics into slhx core.
001 The repository must contain canonical examples that act as API tests. v0 examples are counter, todo CRUD, form wizard, docs-site page swap, auth action, SSE notifications, and keyed todo list; local-first kanban is a north-star milestone example. The full techdemo may include an opaque leaf-widget island that communicates through `Effect::event`, without moving island mechanics into hemx core.
### 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.
@@ -769,68 +769,68 @@ what a valid business email is.
004 Canonical examples are compile-tested golden API contracts. Changing generated API shape requires updating the examples deliberately.
### req: examples/005
005 Canonical examples must not contain user-authored browser JavaScript. They may load the shared slhx runtime (`/slhx.js`) and may use declarative `data-slhx-*` attributes; inline `<script>`, `on*=` event handlers, and `javascript:` URLs are forbidden outside opaque leaf-widget examples.
005 Canonical examples must not contain user-authored browser JavaScript. They may load the shared hemx runtime (`/hemx.js`) and may use declarative `data-hemx-*` attributes; inline `<script>`, `on*=` event handlers, and `javascript:` URLs are forbidden outside opaque leaf-widget examples.
---
## 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]
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 hemx + hemplate + hemx-sync. [north_star]
---
## 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.
001 TypeScript definitions for `hemx-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.
---
## build
### req: build/001
001 Build order: `.heml` → hemplate Surface facts (precomputed by `hemplate_build` or extracted in-process by hemplate for `slhx_build`) → `slhx_build``slhx.generated.rs` + `slhx.syms` + diagnostics.
001 Build order: `.heml` → hemplate Surface facts (precomputed by `hemplate_build` or extracted in-process by hemplate for `hemx_build`) → `hemx_build``hemx.generated.rs` + `hemx.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]
002 Proc-macros (`#[hemx::handler]`, `#[hemx::surface]`) are side-effect free. They read generated artifacts (`hemx.syms`, `hemx.generated.rs`) but never parse `.heml`, never process generic Surface IR, and never write files. Global codegen lives only in `build.rs` invoked by `hemx_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.
003 `hemx-derive` (`#[hemx::handler]`) reads `hemx.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.
004 `#[hemx::surface]` reads `hemx.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.
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 `#[hemx::component]` macro.
---
## 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.
001 Workspace layout: `hemx-core` (types + postcard schema, no_std), `hemx-derive` (proc-macros), `hemx-build` (surface consumer + code generation), `hemx-axum` (integration), `hemx-js` (runtime single file), `hemx-transition` (optional), `hemx-sync` (optional), `hemx-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.
002 All crates compile on stable Rust. MSRV 1.80. `hemx-core` has zero proc-macro dependencies.
### req: misc/003
003 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.
003 Three execution modes supported: server-first (request/response), client-local WASM (requestAnimationFrame, no round-trip), and hybrid sync (local + remote via `hemx-sync`). Modes are opt-in per handler, not global.
### req: misc/004
004 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.
004 The only required user-facing proc-macro in hemx core is `#[hemx::handler]`.
Optional ergonomic macros may exist: `#[hemx::surface]`, `#[hemx::component]`,
`#[hemx::app]`, and integration-crate macros such as `#[hemx::island]` or
`#[hemx_sync::presence]`. No `!` call-syntax macros.
### req: misc/005
005 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/006
006 Authoring slhx attributes use the `data-slhx-*` prefix. Runtime lowering may emit compact `data-hid`, `data-sid`, `data-key`, atom ids, and control ids. No unprefixed custom attributes.
006 Authoring hemx attributes use the `data-hemx-*` prefix. Runtime lowering may emit compact `data-hid`, `data-sid`, `data-key`, atom ids, and control ids. No unprefixed custom attributes.
### req: misc/007
007 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.
@@ -843,26 +843,26 @@ Optional ergonomic macros may exist: `#[slhx::surface]`, `#[slhx::component]`,
## 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.
001 hemplate does not expose a hemx API. It exposes a stable, generic Template Surface IR. hemx 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 attributes. It records them faithfully as generic raw attributes in the Surface.
002 hemplate never interprets `data-hemx-*` or any other tool-prefixed attributes. It records them faithfully as generic raw attributes in the Surface.
### req: boundary/003
003 slhx never owns `.heml` parsing semantics directly. It consumes hemplate Surface facts, either from `hemplate.surface.postcard` emitted by `hemplate_build` or from in-process hemplate Surface extraction requested by `slhx_build`. slhx interprets tool-specific conventions (`data-slhx-handle`, `data-slhx-slot`, etc.) from the generic Surface.
003 hemx never owns `.heml` parsing semantics directly. It consumes hemplate Surface facts, either from `hemplate.surface.postcard` emitted by `hemplate_build` or from in-process hemplate Surface extraction requested by `hemx_build`. hemx interprets tool-specific conventions (`data-hemx-handle`, `data-hemx-slot`, etc.) from the generic Surface.
---
## 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.
001 `#[hemx::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.
002 Handler param inference from `data-*` attributes: when a template declares `data-card-id="{card.id}"` on a node with `data-hemx-handle`, the handler may declare `card_id: CardId` as a parameter. hemx-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. slhx does not implement selector-based `hx-include`; shared params are represented by forms, hidden inputs, scoped context, or explicit `data-*` attributes.
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. hemx does not implement selector-based `hx-include`; shared params are represented by forms, hidden inputs, scoped context, or explicit `data-*` attributes.
### req: derive_handler/004
004 The common handler signature forms are plain Rust functions, synchronous or async:
@@ -879,24 +879,24 @@ async fn delete(app: State<App>, todo_id: TodoId) -> impl IntoEffect
## derive_app
### req: derive_app/001
001 `#[slhx::app(...)]` marks an application/root registry entry point and composes generated component handler modules into one app registry from the app state. 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 or handwritten chains of per-component registration calls in canonical app code.
001 `#[hemx::app(...)]` marks an application/root registry entry point and composes generated component handler modules into one app registry from the app state. There may be one registry per app/root type, and multiple runtime instances may exist per process. hemx does not require a process-global singleton or handwritten chains of per-component registration calls in canonical app code.
---
## locality
### req: locality/001
001 slhx does not support selector targets such as `closest`, `find`, or `this` in core. The equivalent pattern is a named generated target on the local component, keyed row, modal, toast, SVG fragment, form error region, or page content area.
001 hemx does not support selector targets such as `closest`, `find`, or `this` in core. The equivalent pattern is a named generated target on the local component, keyed row, modal, toast, SVG fragment, form error region, or page content area.
### req: locality/002
002 Diagnostics should suggest adding `data-slhx-slot` or `h-key` to the local element when users attempt a self/row update pattern, and should explain which generated target helper would become available.
002 Diagnostics should suggest adding `data-hemx-slot` or `h-key` to the local element when users attempt a self/row update pattern, and should explain which generated target helper would become available.
---
## style
### req: style/001
001 Plain CSS and SCSS own appearance. slhx-build discovers static class tokens from `.heml`, `.css`, and `.scss` build inputs and generates `CssClass` constants so Rust can reference known classes without raw strings. slhx does not parse selectors for behavior, cascade policy, or layout semantics. [north_star]
001 Plain CSS and SCSS own appearance. hemx-build discovers static class tokens from `.heml`, `.css`, and `.scss` build inputs and generates `CssClass` constants so Rust can reference known classes without raw strings. hemx does not parse selectors for behavior, cascade policy, or layout semantics. [north_star]
### req: style/002
002 Generated class constants are ergonomic references only: they do not create a CSS framework, require a framework project structure, or make dynamic class expressions compile-time facts. Unknown Rust class references fail by normal Rust name resolution when the generated constant is absent. [north_star]
@@ -909,41 +909,41 @@ async fn delete(app: State<App>, todo_id: TodoId) -> impl IntoEffect
## convention
### req: convention/001
001 slhx-axum and the JS runtime support common UX conventions as attributes, not core effects: `data-slhx-pending-class`, `data-slhx-indicator`, `data-slhx-confirm`, `data-slhx-debounce`, `data-slhx-throttle`, `data-slhx-every`, `data-slhx-disable-while-pending`, `data-slhx-policy`, and `data-slhx-on`. These are orthogonal to the core effect algebra.
001 hemx-axum and the JS runtime support common UX conventions as attributes, not core effects: `data-hemx-pending-class`, `data-hemx-indicator`, `data-hemx-confirm`, `data-hemx-debounce`, `data-hemx-throttle`, `data-hemx-every`, `data-hemx-disable-while-pending`, `data-hemx-policy`, and `data-hemx-on`. These are orthogonal to the core effect algebra.
### req: convention/002
002 Default event triggers: `submit` for forms, `click` for buttons and links. `data-slhx-on` overrides the default for the runtime-supported delegated events: `click`, `submit`, `input`, `change`, `dragstart`, `dragover`, and `drop`. Unsupported static event names are build errors.
002 Default event triggers: `submit` for forms, `click` for buttons and links. `data-hemx-on` overrides the default for the runtime-supported delegated events: `click`, `submit`, `input`, `change`, `dragstart`, `dragover`, and `drop`. Unsupported static event names are build errors.
### req: convention/003
003 `data-slhx-debounce` and `data-slhx-throttle` support simple millisecond values. No trigger mini-language in core.
003 `data-hemx-debounce` and `data-hemx-throttle` support simple millisecond values. No trigger mini-language in core.
### req: convention/004
004 `data-slhx-confirm` dispatches a native `confirm()` before handler dispatch. Static empty confirmation messages are build errors because they silently disable the guard in browsers. Custom confirm UI belongs to integration crates.
004 `data-hemx-confirm` dispatches a native `confirm()` before handler dispatch. Static empty confirmation messages are build errors because they silently disable the guard in browsers. Custom confirm UI belongs to integration crates.
### req: convention/005
005 `data-slhx-every` dispatches a handle at a fixed interval while the element remains in the document. Duplicate timers per root are avoided.
005 `data-hemx-every` dispatches a handle at a fixed interval while the element remains in the document. Duplicate timers per root are avoided.
### req: convention/006
006 Request concurrency policy (`latest`, `queue`, `drop`, `parallel`) may be declared per handle with `data-slhx-policy`. Default for debounced/input handlers is `latest`; default for form submit is `drop` while pending. Stale EffectBatches from superseded requests must not be applied.
006 Request concurrency policy (`latest`, `queue`, `drop`, `parallel`) may be declared per handle with `data-hemx-policy`. Default for debounced/input handlers is `latest`; default for form submit is `drop` while pending. Stale EffectBatches from superseded requests must not be applied.
### req: convention/007
007 Pending indicators are cosmetic only. The runtime toggles pending classes, indicator visibility, and disabled controls around request/effect execution; handler semantics are unchanged.
### req: convention/008
008 `data-slhx-disable-while-pending` disables the triggering form controls or button while the request is active and restores them afterward.
008 `data-hemx-disable-while-pending` disables the triggering form controls or button while the request is active and restores them afterward.
### req: convention/009
009 Unknown `data-slhx-*` authoring attributes are build errors with a suggested fix. slhx-owned attributes are a checked contract, not a silent extension namespace; opaque/integration islands should use explicit allowed attributes or their own non-slhx `data-*` names.
009 Unknown `data-hemx-*` authoring attributes are build errors with a suggested fix. hemx-owned attributes are a checked contract, not a silent extension namespace; opaque/integration islands should use explicit allowed attributes or their own non-hemx `data-*` names.
---
## multipart
### req: multipart/001
001 slhx-axum supports `multipart/form-data` as an integration boundary for file uploads.
001 hemx-axum supports `multipart/form-data` as an integration boundary for file uploads.
### req: multipart/002
002 Multipart parsing belongs to slhx-axum/axum extractors, not slhx-core.
002 Multipart parsing belongs to hemx-axum/axum extractors, not hemx-core.
### req: multipart/003
003 File upload forms preserve native browser fallback behavior.
@@ -953,7 +953,7 @@ async fn delete(app: State<App>, todo_id: TodoId) -> impl IntoEffect
## target_policy
### req: target/001
001 slhx does not support response-side selector retargeting. Handlers choose targets by returning generated UI commands for generated resources.
001 hemx does not support response-side selector retargeting. Handlers choose targets by returning generated UI commands for generated resources.
### req: target/002
002 slhx does not implement response-side CSS fragment selection in core. Servers return explicit hemplate partials for generated targets or EffectBatches containing generated target effects.
002 hemx does not implement response-side CSS fragment selection in core. Servers return explicit hemplate partials for generated targets or EffectBatches containing generated target effects.