Files
hemx/REQUIREMENTS.md
T
slhx agent a787e5473a docs(requirements): split state rows
Add explicit northstar ring fields to state requirements, split atom type-hash guidance, and shorten SSR bootstrap wording without changing behavior.

req: state/001

req: state/002

req: state/003

req: state/004

req: state/005

req: state/006

req: state/007
2026-06-25 17:08:02 +02:00

1184 lines
69 KiB
Markdown

# hemx — Semantic, Laterally HX
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. hemx consumes surface. hemx owns semantics. JS applies effects.**
---
## laws
### req: law/001
0 001 A feature belongs in core only if it can be expressed as typed resources plus EffectBatch ops. [north_star]
### req: law/002
0 002 A feature belongs in an integration crate if it depends on transport, framework, auth, storage, browser capability, or deployment policy. [north_star]
### req: law/003
0 003 A feature belongs in generated API if it improves author ergonomics without adding runtime semantics. [north_star]
### req: law/004
0 004 A feature belongs in user code if it is business logic, domain validation, routing policy, authorization policy, persistence, or layout choice. [north_star]
### req: law/005
0 005 Add one primitive only if it removes at least five special cases. [north_star]
---
## pitch
### req: pitch/001
0 001 hemx is checked hypermedia for Rust: authors write `.heml`, write Rust handlers, and return generated UI commands while the compiler checks every cross-file reference. [north_star]
### req: pitch/005
0 005 For ordinary server-first apps, no app-owned JavaScript is required: the browser runtime only sees lowered ids and effect bytes. [north_star]
### req: pitch/002
0 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
0 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
0 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]
---
## canonical_authoring
### req: canonical/001
0 001 The canonical app shape is templates plus Rust, not a frontend folder: `.heml` files declare roots, slots, handles, forms, keys, and optional pending/page/island facts; handlers return generated UI commands. [north_star]
### req: canonical/010
0 010 Ordinary app code avoids selectors, numeric ids, raw effects, wire formats, manual registries/form parsing, raw `SafeHtml`, and raw render calls. [north_star]
### req: canonical/011
0 011 Plain CSS owns appearance. [north_star]
### req: canonical/002
0 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, and place them into generated targets. [north_star]
### req: canonical/014
0 014 The partial-swap primitive is generated target plus rendered partial plus swap kind. [north_star]
### req: canonical/003
0 003 Canonical keyed-row CRUD reads like ordinary Rust intent: create appends a rendered row partial, update/toggle replaces a keyed row partial, and delete removes a keyed row. [north_star]
### req: canonical/015
0 015 Summary, text, and form effects compose in tuples, arrays, or `Vec<T: IntoEffect>` for dynamic batches, and no handler chooses a target with a CSS selector. [north_star]
### req: canonical/004
0 004 Generated helpers may compose only facts uniquely known from templates and checked Rust types: template, slot, optional key, form/control, class token, explicit island/event marker, and effect kind. [north_star]
### req: canonical/016
0 016 If a handler parameter, key, form, target, raw route, or legacy target would require guessing, the user must say it explicitly and diagnostics must point to the Rust and hemplate spans. [north_star]
### req: canonical/005
0 005 Generated helpers name UI intent without mixing domain work: helpers read as UI effects such as `replace`, `append`, `remove`, `set`, `clear`, `focus`, `set_attr`, or `emit` on generated page, slot, form, class, attribute, or island handles. [north_star]
### req: canonical/009
0 009 Generated helpers must not combine persistence, routing, rendering, target selection, or domain policy into generic commands such as `refresh`, `save_and_update`, `sync_component`, or `rerender`. [north_star]
### req: canonical/006
0 006 There is no separate beginner API and expert API; the simple generated shape is canonical. [north_star]
### req: canonical/012
0 012 Generated slots, partials, forms, class constants, islands/events, and page helpers are normal authoring surfaces. [north_star]
### req: canonical/013
0 013 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
0 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)`. [north_star]
### req: canonical/017
0 017 Island JavaScript owns only high-frequency local behavior and must not introduce a component runtime, client state graph, VDOM, or second UI model. [north_star]
### req: canonical/008
0 008 Offline/PWA support is opt-in adapter territory; server-first hemx may fail interactions while offline. [north_star]
### req: canonical/018
0 018 Cached shells and local-sync queues live in crates such as `hemx-pwa` or `hemx-sync`; they reuse generated slots/effects, queue explicit patches, and reconcile with server-canonical effects. [north_star]
### req: canonical/019
0 019 Core hemx must not gain a mandatory client state graph, scheduler, CRDT, or local app runtime. [north_star]
---
## modes
### req: mode/001
0 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. [north_star]
### req: mode/002
0 002 Page Enhancer is navigation as partial swap: it updates generated page/content/title/nav targets, history, scroll, shell, and fallback behavior. Authors use `data-hemx-nav`/`data-hemx-boost` anchors; ordinary navigation needs no handler. [north_star]
### req: mode/003
0 003 Interaction Handler mode handles forms, buttons, typed params, and generated partial/text/form/island effects through Rust handlers. [north_star]
### req: mode/004
0 004 Beginner docs teach server-first typed partial swaps first, Page Enhancer around the same slot/effect model, explicit leaf-widget islands, client-local/WASM only for high-frequency local behavior, and sync/offline last as opt-in adapters. [north_star]
---
## dx
### req: dx/001
0 001 Common apps feel like HTML plus tiny Rust handlers: templates, state, hemplate partials, and generated UI swaps. Basic apps hide Surface IR, ResourceId, EffectWriter, postcard, runtime opcodes, selectors, and registries. [north_star]
### req: dx/002
0 002 Happy path: write `.heml`, write a Rust handler, return generated partial/text/form/page/island commands. Ordinary app UI uses no manual ids, registry, serialization, selector targets, raw render calls, or JavaScript. [north_star]
### req: dx/003
0 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
0 004 Common handlers fit in a small function. Advanced contexts (`EffectWriter`, raw ops, custom encoders) exist but are not part of the beginner path.
### req: dx/005
0 005 Error messages explain fixes in author language, not internal language. Say “add `h-key="todo.id"` to this `h-for`”, not “missing ScopeKey for ResourceRef”.
### req: dx/006
0 006 Generated object-like helpers are the preferred authoring API and are re-exported at the component root, including `todos.append(todo)`, `todo_row.replace(todo)`, `summary.set(text)`, `new_todo.clear()`, and `page.replace(view)`. [north_star]
### req: dx/009
0 009 Generated object-like helpers hide hemplate rendering and resource lowering in the common path. [north_star]
### req: dx/010
0 010 Namespaced `targets`/`handles`/`forms`, raw `advanced::slots`, explicit render/target/html/lower helpers, and raw effect constructors are compatibility surfaces, not beginner-prelude exports, canonical examples, or ordinary docs. [north_star]
### req: dx/007
0 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
0 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
0 001 A minimal counter app requires one `.heml` file, one Rust state struct, and one handler function. No manual registry, route table, or JS; under 50 lines of Rust plus one template.
### req: ceremony/002
0 002 Generated modules are imported through a prelude or component namespace. Normal apps do not manually include `$OUT_DIR` files.
### req: ceremony/003
0 003 `build.rs` must be a one-liner for the common case: `fn main() { hemx_build::app().run().unwrap(); }`
### req: ceremony/004
0 004 No API may require users to write numeric ids, raw ResourceIds, raw opcodes, or serialized payloads in normal code.
### req: ceremony/005
0 005 `cargo run -p hemx-xtask -- app new PATH` creates a generic scaffold with page, form, keyed row partial, notice slot, handlers, and tests using generated helpers instead of raw ids, opcodes, selector UI JavaScript, or manual registries. [north_star]
### req: ceremony/006
0 006 `cargo run -p hemx-xtask -- app new --mobile PATH` creates a phone-first starter with page/form/keyed partial/notice flow, typed host capability round trip, app-owned recovery truth, and inspectable mobile release/verify metadata. [north_star]
### req: ceremony/007
0 007 The phone-first starter adds no hemx mobile framework, client store, signing-secret owner, or store-submission bot. [north_star]
---
## progressive_disclosure
### req: pd/001
0 001 A beginner can build CRUD with only: `.heml`, Rust handlers, `Form<T>` or typed params, generated object-like UI helpers, and `impl IntoEffect`.
### req: pd/002
0 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
0 003 Sync, transitions, resources/queries, islands, capabilities, and raw EffectWriter are advanced layers. They must not appear in starter examples.
### req: pd/004
0 004 Docs present levels as adapters around the same core: server-first partial swaps; cached Page/PWA shell; leaf islands or client-local handlers for high-frequency behavior; hybrid sync/offline queues last. Each level introduces only its new primitive.
---
## page_swap
### req: page_swap/001
0 001 Page swapping specializes partial swapping: render page partials into generated targets, then apply history/title/scroll/shell behavior. `data-hemx-nav` anchors keep valid `href` and work without JS; missing/empty static `href` fails build. [north_star]
### req: page_swap/002
0 002 A `data-hemx-nav` click fetches the target URL as a hemx partial request. The response is `ui::content.replace(page)` plus optional generated nav/title targets and `Navigate`; it must not add selector targeting or a second page UI model. [north_star]
### req: page_swap/003
0 003 Page swapping uses generated targets, not CSS selectors. Default content target is generated slot `content`, not `#content`; page helpers like `ui::content.page(...)` or `request.page_html(...)` adapt the same partial-swap primitive. [north_star]
### req: page_swap/004
0 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. [north_star]
### req: page_swap/005
0 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. [north_star]
### req: page_swap/006
0 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. [north_star]
### req: page_swap/007
0 007 `data-hemx-boost` progressively enhances descendant same-origin anchors and forms as a container convention, not a replacement for anchor `data-hemx-nav` or form `data-hemx-handle`; direct static-anchor/form use is a build error. [north_star]
### req: page_swap/008
0 008 Boosted links behave like `data-hemx-nav`; boosted forms behave like hemx form submissions. External links, downloads, new-tab links, and modified-clicks preserve native browser behavior. [north_star]
---
## htmx_equivalents
### req: htmx/001
0 001 hemx replaces common HTMX use-cases through typed equivalents, not HTMX syntax. [north_star]
### req: htmx/002
0 002 Easy equivalents must exist for generated target replacement, append/prepend/remove, form submit, loading indicators, confirmation, debounce/throttle, drag/drop payloads, validation errors, and form error regions. [north_star]
### req: htmx/006
0 006 Easy component-shape equivalents must exist for modals, toasts, table rows, and SVG fragments. [north_star]
### req: htmx/004
0 004 Navigation and live-update equivalents must exist for boosted links/forms, page swap, polling, history navigation, multi-target updates, response events, and SSE/push. [north_star]
### req: htmx/005
0 005 The copy-paste HTML pattern gallery must cover core CRUD/form/search/load patterns through boring `.heml`, generated targets/forms/handles, and server-owned Rust state before adding plugin-shaped or browser-policy-heavy patterns. [north_star]
### req: htmx/003
0 003 hemx core does not clone HTMX selectors (`hx-target`, `hx-select`, `hx-include`, `closest/find/this`) or trigger mini-languages. Equivalents use generated targets, typed params, forms, explicit handlers, and page/push adapters. [north_star]
---
## component
### req: component/001
0 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. [north_star]
### req: component/002
0 002 hemx supports colocated layout: `todo_list.heml` beside `todo_list.rs`, with generated APIs namespaced by component to avoid global symbol soup. [north_star]
### req: component/003
0 003 Generated APIs are component-namespaced by default. [north_star]
`ui::todo_list::todo_row`, `ui::todo_list::create`, `ui::todo_list::new_todo`, and `ui::todo_list::COMPONENT` as a checked `ComponentRef`.
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
0 004 `#[hemx::surface]` bridges generated code into a user module. Users write `#[hemx::surface] mod ui {}` instead of direct `$OUT_DIR` includes; hemx-build emits `hemx.generated.rs` for the macro to expand in place. [north_star]
### req: component/005
0 005 Optional `#[hemx::component]` validates that each template Surface handle has a corresponding `#[hemx::handler]` within the annotated module. It is strictly module-local, with cross-handler visibility only inside that module. [north_star]
### req: component/007
0 007 Missing handlers without `#[hemx::component]` are caught at app mount or test time, not `cargo check`. [north_star]
### req: component/006
0 006 `#[derive(Hemplate)]` structs are natural component boundaries. hemx_build discovers them automatically; no additional configuration is required for most apps. [north_star]
---
## surface
### req: surface/001
0 001 `hemplate_build` scans `.heml` and emits versioned postcard Surface facts at `$OUT_DIR/hemplate.surface.postcard`. `hemx_build` may take precomputed facts or ask hemplate to extract them, but hemx owns no independent `.heml` parser. [north_star]
### req: surface/002
0 002 The Surface contains: nodes (NodeId, parent, scope, element, attrs, source span), scopes (ScopeKind: Root | If | Match | Case | For { binding, key_expr }), forms (controls with raw HTML types), and component uses. [north_star]
### req: surface/003
0 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. [north_star]
### req: surface/004
0 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. [north_star]
### req: surface/005
0 005 Loop scopes expose the binding name and optional `key_expr` such as `todo.id`. hemplate only records key usage; hemx_build enforces key presence when a hemx-addressable node appears inside the loop. [north_star]
### req: surface/006
0 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. [north_star]
### req: surface/007
0 007 `hemplate-derive` does not write Surface files. Surface generation is a `build.rs` / `hemplate_build` concern, proc-macro side-effect free. [north_star]
### req: surface/008
0 008 The Surface records hemplate structural directives as first-class facts: [north_star]
`h-for`, `h-key`, `h-if`, `h-else-if`, `h-else`, `h-match`, `h-case`,
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
0 009 Raw/pre-rendered HTML insertions are opaque Surface holes. The parent
element is present; hemx_build emits the appropriate rendering call. [north_star]
### req: surface/010
0 010 Attribute values preserve their origin: static literal, dynamic `+attr`
binding, or interpolated expression. hemx-build uses this to determine whether
a `data-*` handle param is statically known or runtime-extracted. [north_star]
---
## codegen
### req: codegen/001
0 001 `hemx_build` generates `hemx.generated.rs` resource modules, `hemx.syms` proc-macro facts, and runtime id-lowering tables from Surface IR. It interprets `data-hemx-*`, `h-for`, `h-key`, and form-control conventions from Surface. [north_star]
### req: codegen/002
0 002 Generated view modules expose ergonomic root-level target objects and commands that hide render/lower details for text, partial, and keyed collection slots. [north_star]
### req: codegen/009
0 009 Generated form targets provide `clear()`, `clear(field)`, and `focus(field)` commands. [north_star]
### req: codegen/007
0 007 String-keyed generated target objects accept displayable domain ids without caller-side `.to_string()` noise. [north_star]
### req: codegen/008
0 008 Generated commands return `impl IntoEffect`, compose in plain Rust, preserve generated lowering, and fail to generate when the template lacks facts needed to infer the slot, key, form, or renderable view type. [north_star]
### req: codegen/003
0 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. [north_star]
### req: codegen/004
0 004 Generated module `forms` exports `FormContract` metadata (field names, HTML control kinds, required). `#[hemx::handler]` compares `Form<T>` against the contract. Domain types remain user-authored; no auto-generated structs. [north_star]
### req: codegen/005
0 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. [north_star]
### req: codegen/006
0 006 `hemx-build` discovers `data-hemx-on` event names from Surface inputs and emits `hemx::EventName` constants for checked Rust authoring and diagnostics. Constants do not create a trigger mini-language or new browser semantics. [north_star]
---
## public_api
### req: public_api/001
0 001 The generated API is the primary public authoring API. User code returns generated partial, text, keyed-row, form, page, nav, or island/event commands, not raw `Effect` constructors or raw render/lower calls. [north_star]
### req: public_api/002
0 002 `Effect`, `EffectWriter`, `ResourceId`, `ResourceRef`, and raw opcodes are advanced APIs. They must not appear in beginner docs, generated examples, or common diagnostics. [north_star]
### req: public_api/003
0 003 Every generated command returns `impl IntoEffect` and composes through tuple composition. [north_star]
### req: public_api/004
0 004 If a common UI operation requires raw `EffectWriter`, the public API is incomplete. [north_star]
### req: public_api/005
0 005 Beginner-facing page/template composition uses generated render or page helpers. Direct `SafeHtml`, raw `html(...)`, `target(...)`, route fragments, `hemx::advanced::render(...)`, and explicit `ui::render(...)` are advanced escape hatches. [north_star]
### req: public_api/006
0 006 Server-rendered page boundaries may use `hemx::page(...)`; handlers and ordinary partial updates must use generated target/form/page commands. [north_star]
---
## effect_algebra
### req: effect_algebra/001
0 001 The canonical op set is minimal and closed: `Put`, `Insert`, `Remove`, `Move`, `Focus`, `Navigate`, `Emit`. [north_star]
### req: effect_algebra/002
0 002 `Put` replaces the payload of a resource. For a Slot, this means replacing its rendered contents. For an Atom, this means replacing its stored value. [north_star]
### req: effect_algebra/003
0 003 `Insert`, `Remove`, and `Move` operate on keyed collection resources. They require a key type checked by generated `KeyedSlot<K, T>` wrappers. [north_star]
### req: effect_algebra/004
0 004 `Navigate` changes browser history or represents a server redirect. Route matching remains outside hemx core. [north_star]
### req: effect_algebra/005
0 005 `Emit` dispatches a native `CustomEvent` and is the only raw JS interop primitive in core. [north_star]
### req: effect_algebra/006
0 006 DOM-specific operations such as `innerHTML`, `textContent`, class toggles, or keyed node lookup are runtime lowering details, not separate author-facing concepts. [north_star]
---
## typed_id
### req: typed_id/001
0 001 Public cross-page identifiers (`Slot`, `Atom`, `Handle`, `Form`) share one internal primitive: `ResourceId { kind: ResourceKind, id: u32 }`. Typed wrappers enforce kind safety at compile time. [north_star]
### req: typed_id/002
0 002 `ResourceKind` is an internal closed enum (Slot, Atom, Handle, Form). [north_star]
Navigation is represented by `Navigate` effects, not by route resources.
External crates may not add variants. Extensibility comes via `Effect::event`
or custom `IntoEffect` implementations, never via new `ResourceKind` variants
in core. `Effect::event` lowers to the canonical `Emit` opcode.
### req: typed_id/003
0 003 A concrete runtime target is a `ResourceRef { resource: ResourceId, scope: Option<ScopeKey> }`. Effects address resources uniformly, with no special-case opcodes per resource kind. [north_star]
---
## scope
### req: scope/001
0 001 `Scope` is a first-class primitive. Keyed loops (`h-for`) create keyed
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`
`{ resource: ResourceId, scope: Option<ScopeKey> }`. [north_star]
---
## list
### req: list/001
0 001 Any `data-hemx-slot` or `data-hemx-handle` inside a hemplate `h-for` scope requires a stable key. Use syntax such as `<template h-for="item in &self.items" h-key="item.id"> ... </template>`. [north_star]
### req: list/004
0 004 Without a key, hemx-addressable nodes inside a loop are rejected at build time. Keyed identity is `ResourceRef { resource: ResourceId, scope: Some(ScopeKey::KeyValue(...)) }`. [north_star]
### req: list/002
0 002 Slots inside a keyed loop receive a composite identity. hemplate records `key_expr` in the Surface; hemx implements keyed slot lookups. [north_star]
### req: list/003
0 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`. [north_star]
### req: list/005
0 005 Compatibility functions such as `append(keyed_slot, key, view)` may exist as explicit low-level forms. Key type mismatches are compile-time errors; missing/ambiguous keys are build errors with template spans. [north_star]
---
## form
### req: form/001
0 001 Forms are source of truth in HTML. hemplate Surface exports form shape (controls, names, required, types). hemx checks Rust `Form<T>` compatibility through user-authored `#[hemx::form("...")]` domain structs and generated form metadata. [north_star]
### req: form/007
0 007 Form support generates no domain structs. Domain types such as `Email` are user-authored and first-class; the Surface describes, Rust owns, and hemx checks. [north_star]
### req: form/002
0 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. [north_star]
### req: form/003
0 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. [north_star]
### req: form/004
0 004 Form compatibility checks validate field presence, optionality, multiplicity, and parser availability. [north_star]
### req: form/008
0 008 Parser availability means the submitted value type implements `hemx::FormValue`, via `FromStr` blanket support or explicit custom parsers. Domain validation remains Rust logic (`TryFrom`, custom validators, or handler code). [north_star]
### req: form/005
0 005 HTML control facts are lower bounds, not complete domain semantics.
`type="email"` may require a Rust `Email` parser, but hemplate never defines
what a valid business email is. [north_star]
### req: form/006
0 006 Generated diagnostics distinguish structure errors from validation errors: missing field / wrong optionality are compile-time issues; invalid submitted values are runtime form errors. [north_star]
---
## form_effects
### req: form_effects/001
0 001 Generated form APIs provide common commands: `clear()`, `clear(field)`, `reset()`, `error(field, message)`, `focus(field)`, and `disable_while_pending()`. `form.clear()` clears the generated form without raw control ids. [north_star]
### req: form_effects/002
0 002 Form effects target generated form/control ids, not CSS selectors. [north_star]
### req: form_effects/003
0 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. [north_star]
---
## wire
### req: wire/001
0 001 Authoring HTML uses symbolic `data-hemx-*` attributes. Runtime HTML lowers them to compact metadata: `data-hid`, `data-sid`, optional `data-key`, atom ids, form/control ids, and `data-hemx-st`. The browser never sees handler or slot names. [north_star]
### req: wire/006
0 006 `data-hemx-root` marks a scoped root boundary. [north_star]
### req: wire/002
0 002 POST bodies carry `application/x-www-form-urlencoded` with distinguished field `__h` (handle id). Server routes by numeric id, not by URL path. [north_star]
### req: wire/003
0 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`. [north_star]
### req: wire/004
0 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. [north_star]
### req: wire/005
0 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. [north_star]
---
## abi
### req: abi/001
0 001 Surface IR, hemx symbols, generated Rust API, EffectBatch wire schema, and JS runtime each carry explicit schema/ABI versions. [north_star]
### req: abi/002
0 002 hemx_build emits a build fingerprint derived from Surface schema version, resource id allocation, EffectBatch ABI version, and runtime ABI version. [north_star]
### req: abi/003
0 003 The server includes the hemx build fingerprint in initial roots. The runtime compares it with its own fingerprint before applying EffectBatches. [north_star]
### req: abi/004
0 004 On fingerprint mismatch, the runtime refuses partial updates and falls back to full page navigation or reload. Silent mismatch is forbidden. [north_star]
### req: abi/005
0 005 Resource ids are deterministic within a build and stable across builds when canonical symbol paths do not change. Stability is best-effort across refactors, not a persistence guarantee. [north_star]
---
## runtime
### req: runtime/001
0 001 Every hemx tree declares a root boundary via `data-hemx-root`. JS runtime lookups stay within that root, so independent hemx apps/widgets/modals can coexist without ID collision. [north_star]
### req: runtime/002
0 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. [north_star]
### req: runtime/003
0 003 The core JS runtime target is under 5KB minified+gzipped. It remains a tiny op interpreter: no selectors, VDOM, scheduler, or expressions. [north_star]
### req: runtime/006
0 006 The core JS runtime reads postcard `EffectBatch` bytes and applies them as DOM operations. Optional sync/transition/WASM helpers are separate files. [north_star]
### req: runtime/004
0 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`. [north_star]
### req: runtime/005
0 005 Failed hemx HTTP requests fail closed: non-2xx responses are not applied as effects, pending state is restored, root-scoped `data-hemx-error` outlets show transport failure, and runtime emits `hemx:error` with status when available. [north_star]
---
## failure
### req: failure/001
0 001 Missing runtime targets are non-panicking. In development, runtime emits a diagnostic event and logs the missing ResourceRef. In production, optional targets no-op; missing required targets fail the batch with a recoverable error. [north_star]
### req: failure/002
0 002 EffectBatch application is ordered and transactional per root where possible. If an op fails, later ops in the same batch are skipped unless the op is marked best-effort. [north_star]
### req: failure/003
0 003 Form parse errors do not call the handler. They produce typed form errors targeting generated control ids. [north_star]
### req: failure/004
0 004 Handler errors may map to HTTP responses, form errors, navigation effects, toast/events, or app-defined error effects. Core does not prescribe UI policy. [north_star]
### req: failure/005
0 005 Wire/schema version mismatch is a hard failure. The runtime refuses to apply unknown incompatible EffectBatch versions and falls back to full page reload when possible. [north_star]
### req: failure/006
0 006 Progressive enhancement failures preserve native browser behavior for forms and links whenever valid HTML fallback exists. [north_star]
---
## axum_integration
### req: axum/001
0 001 hemx-axum supports full-shell/partial adapters around generated partial swaps. Full-page requests wrap in a user Shell; partial requests return rendered target partials or EffectBatch output. [north_star]
### req: axum/006
0 006 Page helpers add shell/title/history/fallback behavior without changing the render/target/effect model. [north_star]
### req: axum/002
0 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. [north_star]
### req: axum/003
0 003 Interactive fragments from `/demo/...` HTMX endpoints are Rust handlers returning generated target commands. Registration reads as generated handle/page/partial helpers, not low-level registry wiring. [north_star]
### req: axum/004
0 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. [north_star]
### req: axum/005
0 005 hemx-axum exposes the shared JS runtime through a content-hashed script path and immutable response headers. Apps load the hemx-owned path instead of hand-written cache-busting strings; runtime byte changes change the exposed path. [north_star]
---
## host
### req: host/001
0 001 Host capabilities are declared as typed capability uses with one of four shapes: fire, request, stream, or schedule. This contract covers browser, PWA, WebView, and native-shell hosts without adding a second UI runtime.
### req: host/002
0 002 Host adapters may produce host events or perform explicit host side effects, but they must not mutate DOM, own application/domain state, append domain events, bypass generated hemx effects, or introduce a client app-state framework.
### req: host/003
0 003 Permission-sensitive host capabilities require an explicit user-facing reason in the app-owned manifest before standard host checks may pass.
### req: host/004
0 004 Host checks report concrete failures for undeclared capability use, unsupported host capability shape, and missing permission reasons before a host adapter executes the capability call.
### req: host/005
0 005 Host results return to app code as facts. App/domain code decides whether they become commands, events, persistence, or UI effects; hemx UI updates still happen through normal EffectBatch output.
---
## auth
### req: auth/001
0 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
0 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
0 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
0 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
0 005 hemx requests preserve standard HTTP credentials semantics. Cookies, SameSite policy, Authorization headers, and session middleware remain framework/browser concerns.
---
## push
### req: push/001
0 001 Server push streams canonical postcard EffectBatch over SSE or WebSocket. hemx core owns the EffectBatch schema, not the transport.
### req: push/002
0 002 SSE/WebSocket connections are authenticated by the server framework before stream creation. hemx does not define auth semantics for streams.
### req: push/003
0 003 Push swaps are ordinary partial swaps 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.
### req: push/004
0 004 Out-of-band updates are ordinary multi-target partial swaps/effects, not a separate response model.
### req: push/005
0 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
0 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.
### req: push/007
0 007 Cross-origin push streams belong to explicit integration code rather than the standard runtime convention.
---
## sync
### req: sync/001
0 001 `hemx-sync` is an optional crate for collaborative / multiplayer state. Provides presence tracking, patch reconciliation, server-authoritative conflict resolution, and offline queueing. Not part of core.
### req: sync/002
0 002 `SyncEffect::send_patch(atom, patch)` queues a state diff for server sync. If offline, the patch is stored in a local queue and sent when connection resumes. If online, it is sent immediately via WebSocket/SSE.
### req: sync/003
0 003 Server reconciliation of received patches produces a local `EffectBatch` only when state changes. Accepted mutations patch shared state without hard-coding specific effects.
### req: sync/004
0 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
0 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
0 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
0 007 `hemx-sync` uses a flat patch model per atom, not CRDT by default. Server is authoritative; clients apply server-canonical state on conflict. CRDT support belongs in explicit integration crates, not default sync.
### req: sync/008
0 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.
---
## local
### req: local/001
0 001 Local/offline behavior is represented as app commands, domain events, and projections. Stored DOM patches or stored EffectBatch payloads are not the source of truth.
### req: local/002
0 002 Local command logs are app or integration territory until a reusable hemx contract proves common semantics; `hemx-local` is not a crate yet. hemx core must not gain mandatory browser database, client store, sync engine, or conflict policy.
### req: local/003
0 003 Replaying local work back to a server or peer sync target is explicit app/integration policy. A local projection may render immediate feedback, but server acceptance, rejection, reconciliation, export, and deletion rules remain visible product decisions.
### req: local/004
0 004 A local-first exemplar must show a command becoming a domain event and projection before hemx UI effects are produced, so the UI effect remains output of app state rather than persisted truth.
---
## interop
### req: interop/001
0 001 `Effect::event` and generated event helpers are the single hemx-to-widget bridge. Widgets, charts, games, maps, Alpine/Svelte islands, and Web Components listen via native `CustomEvent`; hemx core does not inspect their state or lifecycle. [north_star]
### req: interop/002
0 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. [north_star]
### req: interop/009
0 009 Escape hatches are leaves, never app foundations. [north_star]
### req: interop/003
0 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. [north_star]
### req: interop/010
0 010 hemx owns generated slot/island boundaries; widgets own the inside, and events cross the boundary. Commands flow widget-to-hemx through generated handles or `hemx.send(...)`; server-to-widget through helpers like `ui::chart.emit(snapshot)`. [north_star]
### req: interop/004
0 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. [north_star]
### req: interop/005
0 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. [north_star]
### req: interop/006
0 006 `data-hemx-preserve` is an explicit preserve boundary for rare leaf-widget cases where hemx updates around a subtree without destroying it. Preserve the marked subtree identity; do not diff or hydrate inside it. [north_star]
### req: interop/011
0 011 Preserve boundaries require deliberate author marks. Preserve must not become a default lifecycle model or a workaround for unclear ownership. [north_star]
### req: interop/007
0 007 The runtime emits native lifecycle events such as `hemx:before-swap`, `hemx:after-swap`, `hemx:event`, `hemx:connect`, and `hemx:disconnect` so widgets can attach at DOM/event boundaries. hemx core must not add framework-specific adapters. [north_star]
### req: interop/008
0 008 Interop prevents selector hacks, JS reinitialization races, lost widget state, and double-owned state through explicit ownership: hemx owns generated server DOM targets, widgets own leaves, and events are the crossing point. [north_star]
### req: interop/012
0 012 Core must not add selector targeting, hydration compatibility, a client store, or a framework lifecycle to make interop easy. [north_star]
---
## nav
### req: nav/001
0 001 Navigation is an effect, not a router framework: `Effect::navigate(url, mode, scroll, title)`. Core supports `Push`, `Replace`, `Redirect`. Actual route matching, guards, loaders, and nested routes are outside core.
### req: nav/002
0 002 Navigation modes: `Push` (history.pushState), `Replace` (replaceState), `Redirect` (server-side 302). Scroll behaviour: `Preserve`, `Top`, `Element(ResourceRef)`. Title is optional.
### req: nav/003
0 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
0 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
0 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.
---
## html_safety
### req: html/001
0 001 Raw HTML insertion requires an explicit safe HTML type (`SafeHtml` or equivalent). Plain `String` renders as escaped text unless explicitly wrapped. [north_star]
### req: html/002
0 002 Hemplate-rendered output may be converted to `SafeHtml` by trusted render APIs. User input is never `SafeHtml` by default. [north_star]
### req: html/004
0 004 Full-page shell composition may pass rendered hemplate fragments through explicit `SafeHtml` fields and join already-safe fragments without downgrading to `String`. [north_star]
### req: html/005
0 005 Handlers use slot/resource render helpers for effect payloads instead of raw HTML construction. [north_star]
### req: html/003
0 003 Slot render commands distinguish text payloads from HTML payloads at the type level. [north_star]
---
## view
### req: view/001
0 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. [north_star]
### req: view/002
0 002 Generated slot types may target `Display`, `Hemplate`, or explicit view wrappers. Type errors suggest the expected renderable view type. [north_star]
---
## diagnostics
### req: diag/001
0 001 Every compile-time error must point to both sides of the mismatch when possible: the Rust handler span and the template Surface span. [north_star]
### req: diag/002
0 002 Diagnostics must include a suggested fix for common cases: missing key, unknown slot, missing form field, optionality mismatch, handler param mismatch, wrong keyed slot type. [north_star]
### req: diag/003
0 003 Internal terms (`ResourceId`, `ScopeKey`, `EffectBatch`) must not appear in beginner-facing diagnostics unless `--verbose` is enabled. [north_star]
### req: diag/004
0 004 Optional `.heml` editor overlays treat `hemx-build` diagnostics and documented `.heml` syntax as authority. They may present compiler-shaped diagnostics, completion, hover, and navigation. [north_star]
### req: diag/007
0 007 Optional `.heml` editor overlays must not own a second template language, formatter, selector model, or custom editor framework. [north_star]
### req: diag/005
0 005 `.heml` editor startup for VS Code, Cursor, and Neovim must preserve normal HTML or tree-sitter HTML highlighting while using repo-owned `hemx-build` diagnostics through `hemx-lsp` as the shared authority for hemplate-specific feedback. [north_star]
### req: diag/006
0 006 `hemx-lsp` completion and hover for `.heml` Rust-shaped expressions uses hemx-owned compiler/build facts for derive-known template context fields and simple `h-for` locals. Missing or stale facts fall back to syntax/document completions. [north_star]
### req: diag/008
0 008 `hemx-lsp` must not proxy rust-analyzer or own a second Rust type system. [north_star]
---
## test
### req: test/001
0 001 `EffectWriter` implements a test backend so handlers can be unit-tested without a browser through `hemx_test` run and inspect helpers. [north_star]
### req: test/007
0 007 `EffectInspector` exposes generated-resource assertions for target updates, HTML/text payloads, keyed insert/replace/remove, navigation, emitted events, slots, and atoms. [north_star]
### req: test/008
0 008 Canonical example tests 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. [north_star]
### req: test/009
0 009 Browser/E2E selector helpers are low-level adapters only; they are generated or named around public authoring concepts such as handles, targets, forms, nav links, roots, islands, class tokens, or keys. [north_star]
### req: test/010
0 010 Browser/E2E selector helpers must not become app authoring APIs. [north_star]
### req: test/002
0 002 `#[hemx::component]` may compile without generated Surface files for incremental module-local testing. [north_star]
### req: test/011
0 011 `#[hemx::surface]` requires `hemx.generated.rs` in `$OUT_DIR` and fails with an actionable diagnostic when generation is missing. [north_star]
### req: test/003
0 003 Generated registries are validated by compile-time tests: missing handler implementations produce test failures with actionable messages. [north_star]
### req: test/004
0 004 Repository-wide verification uses a resource-aware runner that caps Cargo build jobs and Rust test threads from available CPU and memory. User-requested concurrency cannot exceed the detected safe cap. [north_star]
### req: test/012
0 012 Browser E2E runs as an isolated step and can be skipped explicitly when browser infrastructure is unavailable. [north_star]
### req: test/005
0 005 Tests that inspect rendered HTML structure, attributes, escaping, or ordering use DOM-aware parsing such as `scraper` or existing local HTML parsing helpers. Raw string assertions are reserved for tiny literal payload checks. [north_star]
### req: test/006
0 006 Repo-owned browser smoke entry points that guard examples use `hemx-xtask` commands, start their own local example server, drive a real browser through the CDP browser tool, and clean up the example process. [north_star]
### req: test/013
0 013 Durable browser coverage must not rely on ad hoc `/tmp` scripts. [north_star]
---
## check
### req: check/001
0 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
0 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
0 003 Renaming a slot or handle breaks `cargo check` immediately with a span pointing to the Rust handler or template source.
### req: check/004
0 004 Root-scoped slot lookup: JS runtime resolves `data-sid` only within the nearest/current `data-hemx-root`.
---
## state
### req: state/001
0 001 Typed atoms with `Atom<T>` are explicit addressable state resources. [north_star]
### req: state/002
0 002 Atoms are not reactive by default. Updating an atom does not re-render anything until a handler returns an effect referencing it. [north_star]
### req: state/003
0 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. [north_star]
### req: state/007
0 007 Atom runtime values are type-erased postcard bytes; types are compile-time only. A deterministic `TypeHash` may be generated by `hemx_build` for diagnostics, but JS runtime behavior must not depend on Rust `TypeId`. [north_star]
### req: state/004
0 004 SSR roots may carry a `data-hemx-st` base64url postcard blob on `data-hemx-root`. Runtime decodes it into the client atom store. Server-computed atoms are available to client-side handlers without a round-trip. [north_star]
### req: state/006
0 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. [north_star]
### req: state/005
0 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. [north_star]
---
## client_local
### req: client_local/001
001 Client-local handlers use the same function shape as server handlers. Opting into a client-local backend changes where the handler executes, not the authoring model.
### req: client_local/002
002 Local UI state may live as ordinary fields on the app/component state. `Atom<T>` is required only when the value must be addressed by effects, bootstrapped, synced, or subscribed.
### req: client_local/003
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 hemx-core v0. `#[hemx::handler(client)]` is illustrative; final syntax belongs to `hemx-wasm` integration.
---
## async_data
### req: async_data/001
001 If introduced, async remote data helpers live in optional `Resource<T>` / `Query<K, T>` / `Mutation<I, O>` integrations, not in v0 core. v0 server-first data loading is ordinary Rust/Axum code.
### 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.
### req: async_data/003
003 If introduced, query helpers must compile to ordinary handlers and effects; they must not introduce a client-side data framework or cache as a core dependency.
---
## 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 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 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, `#[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.
### req: v0/003
003 v0 proof apps are: counter, docs-site HTMX replacement, form wizard, auth action, SSE notification stream, and keyed todo list.
### req: v0/004
004 The local-first kanban remains the north-star milestone, not a v0 blocker.
---
## examples
### req: examples/001
001 The repository must contain canonical examples that act as API tests. The v0 set covers counter, todo CRUD, form wizard, docs-site page swap, auth action, SSE notifications, and keyed todo list. [north_star]
### req: examples/007
007 `examples/html_examples` is the copy-paste HTML pattern gallery proving htmx-style CRUD/form/search/load patterns map to boring `.heml`, generated resources, and server-owned Rust state. [north_star]
### req: examples/008
008 The Workout example is the phone-first local-first product exemplar for commands/events/projections, complete session flow, recovery, host/replay failures, and host boundary results returned through app code. [north_star]
### req: examples/009
009 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. [north_star]
### req: examples/002
002 Each example must have a maximum ceremony budget. The counter example must fit in under 50 lines of user-authored Rust plus one template. Todo CRUD must fit in under 150 lines excluding model definitions.
### req: examples/003
003 If an example requires raw EffectWriter, manual ids, manual JS, or manual registry setup, the API is considered too complex.
### req: examples/004
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 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.
### req: examples/006
006 The Workout exemplar must have one boring command surface for local development, product tests, production server build, Android/iOS mobile release metadata, and mobile verification. [north_star]
### req: examples/010
010 The Workout common path uses generated helpers instead of manual registry conversion. [north_star]
### req: examples/011
011 Workout mobile commands make app identity, version, production origin, runtime asset policy, cache/offline policy, environment/secrets boundary, rollback expectation, and external store-signing/submission blockers explicit without adding a broad `hemx-mobile` framework. [north_star]
---
## 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 hemx + hemplate + hemx-sync. [north_star]
---
## ts
### req: ts/001
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 `hemx_build`) → `hemx_build``hemx.generated.rs` + `hemx.syms` + diagnostics.
### req: build/002
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 `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 `#[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 `#[hemx::component]` macro.
---
## misc
### req: misc/001
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. `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 `hemx-sync`). Modes are opt-in per handler, not global.
### req: misc/004
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 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.
### req: misc/008
008 Core design rule: add one primitive only if it deletes five special cases. `ScopeKey` deletes: loop keying, component scoping, modal instances, nested forms, portal boundaries. `ResourceId` deletes: special opcodes per kind, separate registries, separate wire formats, separate test APIs. `Effect::event` deletes: plugin API, custom JS bridges, chart adapters, map SDK wrappers.
---
## boundary
### req: boundary/001
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-hemx-*` or any other tool-prefixed attributes. It records them faithfully as generic raw attributes in the Surface.
### req: boundary/003
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 `#[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-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. 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:
```rust
fn ping() -> impl IntoEffect
async fn add(app: State<App>, form: Form<NewTodo>) -> impl IntoEffect
async fn rename(app: State<App>, todo_id: TodoId, title: Title) -> impl IntoEffect
async fn delete(app: State<App>, todo_id: TodoId) -> impl IntoEffect
```
`State<App>` is illustrative integration context; equivalent framework extractors or app references are adapter concerns. Form fields and `data-*` params parse through normal Rust `FromForm`/`FormValue`/`FromStr`-style traits, so domain newtypes remain user-authored. Handlers may return `impl IntoEffect` or `Result<impl IntoEffect, E>` for fallible database/domain work; `IntoEffect` values compose through tuples while `Result` paths preserve a typed error boundary (`IntoHandlerFailure` in the Axum adapter) for integrations to map failures to generated UI effects, toasts, events, or HTTP responses. Result-specific registry adapters are generated/integration internals; canonical app code uses the same `#[hemx::handler]` and `#[hemx::app]` authoring shape for plain and fallible handlers.
---
## derive_app
### req: derive_app/001
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 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-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. 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]
### req: style/003
003 When a hemplate dynamic class attribute needs more than one class token, Rust passes a displayable list of generated `CssClass` values as view data. Conditional state classes should compose from generated constants with boring Rust helpers such as `classes::card.with_if(selected, classes::is_selected)`. Rust should not assemble ad hoc class strings for known style tokens. [north_star]
---
## convention
### req: convention/001
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-delay`, `data-hemx-throttle`, `data-hemx-every`, `data-hemx-interval`, `data-hemx-revealed`, `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-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-hemx-debounce`, `data-hemx-delay`, and `data-hemx-throttle` support simple millisecond values. No trigger mini-language in core.
### req: convention/004
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-hemx-every` and `data-hemx-interval` dispatch a handle at a fixed interval while the element remains in the document. `data-hemx-revealed` dispatches once when the element enters view, with an immediate fallback when `IntersectionObserver` is unavailable. Duplicate timers/observers per root are avoided.
### req: convention/006
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, `aria-busy`, indicator visibility, and disabled controls around request/effect execution; handler semantics are unchanged.
### req: convention/008
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-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 hemx-axum supports `multipart/form-data` as an integration boundary for file uploads.
### req: multipart/002
002 Multipart parsing belongs to hemx-axum/axum extractors, not hemx-core.
### req: multipart/003
003 File upload forms preserve native browser fallback behavior.
---
## target_policy
### req: target/001
001 hemx does not support response-side selector retargeting. Handlers choose targets by returning generated UI commands for generated resources.
### req: target/002
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.