Translate one-based public shard numbers to mutest's zero-based API, retain requested labels in output paths, and grant compiler-probe tests a 120-second per-mutant floor. Corrected hemx-build shard 1/8 now passes. req: test/022 req: test/023
81 KiB
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.
law
001 A feature belongs in core only if it can be expressed as typed resources plus EffectBatch ops. [north_star]
002 A feature belongs in an integration crate if it depends on transport, framework, auth, storage, browser capability, or deployment policy. [north_star]
003 A feature belongs in generated API if it improves author ergonomics without adding runtime semantics. [north_star]
004 A feature belongs in user code if it is business logic, domain validation, routing policy, authorization policy, persistence, or layout choice. [north_star]
005 Add one primitive only if it removes at least five special cases. [north_star]
pitch
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]
005 For ordinary server-first apps, no app-owned JavaScript is required: the browser runtime only sees lowered ids and effect bytes. [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]
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]
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
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]
010 Ordinary app code avoids selectors, numeric ids, raw effects, wire formats, manual registries/form parsing, raw SafeHtml, and raw render calls. [north_star]
011 Plain CSS owns appearance. [north_star]
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]
014 The partial-swap primitive is generated target plus rendered partial plus swap kind. [north_star]
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]
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]
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]
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]
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]
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]
006 There is no separate beginner API and expert API; the simple generated shape is canonical. [north_star]
012 Generated slots, partials, forms, class constants, islands/events, and page helpers are normal authoring surfaces. [north_star]
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]
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]
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]
008 Offline/PWA support is opt-in adapter territory; server-first hemx may fail interactions while offline. [north_star]
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]
019 Core hemx must not gain a mandatory client state graph, scheduler, CRDT, or local app runtime. [north_star]
mode
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]
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]
003 Interaction Handler mode handles forms, buttons, typed params, and generated partial/text/form/island effects through Rust handlers. [north_star]
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
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]
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]
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).
004 Common handlers fit in a small function. Advanced contexts (EffectWriter, raw ops, custom encoders) exist but are not part of the beginner path.
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”.
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]
009 Generated object-like helpers hide hemplate rendering and resource lowering in the common path. [north_star]
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]
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.
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
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.
002 Generated modules are imported through a prelude or component namespace. Normal apps do not manually include $OUT_DIR files.
003 build.rs must be a one-liner for the common case: fn main() { hemx_build::app().run().unwrap(); }
004 No API may require users to write numeric ids, raw ResourceIds, raw opcodes, or serialized payloads in normal code.
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]
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]
007 The phone-first starter adds no hemx mobile framework, client store, signing-secret owner, or store-submission bot. [north_star]
pd
001 A beginner can build CRUD with only: .heml, Rust handlers, Form<T> or typed params, generated object-like UI helpers, and impl IntoEffect.
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.
003 Sync, transitions, resources/queries, islands, capabilities, and raw EffectWriter are advanced layers. They must not appear in starter examples.
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
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]
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]
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]
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]
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]
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]
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]
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]
009 Enhanced GET navigation treats the URL as shareable page state: native successful GET controls serialize into the request URL, history updates by intent, and reload/bookmark/back/forward reconstruct the same view. [north_star]
010 URL-as-state support remains a page/navigation convention, not a router, omnisearch framework, selector include system, or client state graph. [north_star]
htmx
001 hemx replaces common HTMX use-cases through typed equivalents, not HTMX syntax. [north_star]
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]
006 Easy component-shape equivalents must exist for modals, toasts, table rows, and SVG fragments. [north_star]
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]
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]
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
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]
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]
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.
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]
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; unknown scopes report the available generated component names. [north_star]
007 Missing handlers without #[hemx::component] are caught at app mount or test time, not cargo check. [north_star]
006 #[derive(Hemplate)] structs are natural component boundaries. hemx_build discovers them automatically; no additional configuration is required for most apps. [north_star]
surface
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]
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]
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]
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]
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]
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]
007 hemplate-derive does not write Surface files. Surface generation is a build.rs / hemplate_build concern, proc-macro side-effect free. [north_star]
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.
009 Raw/pre-rendered HTML insertions are opaque Surface holes. The parent element is present; hemx_build emits the appropriate rendering call. [north_star]
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
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]
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]
009 Generated form targets provide clear(), clear(field), and focus(field) commands. [north_star]
007 String-keyed generated target objects accept displayable domain ids without caller-side .to_string() noise. [north_star]
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]
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]
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]
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]
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
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]
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]
003 Every generated command returns impl IntoEffect and composes through tuple composition. [north_star]
004 If a common UI operation requires raw EffectWriter, the public API is incomplete. [north_star]
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]
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
001 The canonical op set is minimal and closed: Put, Insert, Remove, Move, Focus, Navigate, Emit. [north_star]
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]
003 Insert, Remove, and Move operate on keyed collection resources. They require a key type checked by generated KeyedSlot<K, T> wrappers. [north_star]
004 Navigate changes browser history or represents a server redirect. Route matching remains outside hemx core. [north_star]
005 Emit dispatches a native CustomEvent and is the only raw JS interop primitive in core. [north_star]
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
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]
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.
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
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
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]
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]
002 Slots inside a keyed loop receive a composite identity. hemplate records key_expr in the Surface; hemx implements keyed slot lookups. [north_star]
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]
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]
006 Filtered keyed collections reconcile by removing filtered-out keys, replacing retained keys, and appending newly visible keys rather than clearing and re-adding every row. [north_star]
form
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]
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]
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]
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]
004 Form compatibility checks validate field presence, optionality, multiplicity, and parser availability, including raw Rust identifiers for reserved HTML control names. [north_star]
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]
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]
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
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]
002 Form effects target generated form/control ids, not CSS selectors. [north_star]
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
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]
006 data-hemx-root marks a scoped root boundary. [north_star]
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]
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]
004 Server push is orthogonal: integration crates stream canonical EffectBatch bytes over SSE or WebSocket connections. hemx core owns the effect codec; transport and connection management are integration concerns. [north_star]
005 No JSON anywhere in hemx-internal artifacts. Public request/response envelopes use application/x-www-form-urlencoded; EffectBatch uses the versioned hemx codec; generated symbols and surface facts may use postcard. application/json is acceptable only at integration boundaries. [north_star]
007 EffectBatch::encoded_len reports the exact canonical wire size, and to_wire uses it to pre-size encoding to one output allocation while preserving canonical bytes. [north_star]
008 The canonical EffectBatch codec must use HEMX magic, fixed-width little-endian numbers, length-prefixed UTF-8, and one-byte closed-variant tags. [north_star]
009 EffectBatch::from_wire must reject bad magic, truncation at every byte boundary, invalid UTF-8, unknown tags, and trailing bytes without panicking. [north_star]
010 EffectBatch must expose only encoded_len, to_wire, and from_wire as its wire contract; postcard conversion is not a parallel effect-batch format. [north_star]
abi
001 Surface IR, hemx symbols, generated Rust API, EffectBatch wire schema, and JS runtime each carry explicit schema/ABI versions. [north_star]
002 hemx_build emits a build fingerprint derived from Surface schema version, resource id allocation, EffectBatch ABI version, and runtime ABI version. [north_star]
003 The server includes the hemx build fingerprint in initial roots. The runtime compares it with its own fingerprint before applying EffectBatches. [north_star]
004 On fingerprint mismatch, the runtime refuses partial updates and falls back to full page navigation or reload. Silent mismatch is forbidden. [north_star]
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
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]
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]
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]
006 The core JS runtime reads canonical versioned hemx EffectBatch bytes and applies them as DOM operations. Optional sync/transition/WASM helpers are separate files. [north_star]
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]
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
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]
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]
003 Form parse errors do not call the handler. They produce typed form errors targeting generated control ids. [north_star]
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]
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]
006 Progressive enhancement failures preserve native browser behavior for forms and links whenever valid HTML fallback exists. [north_star]
axum
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]
006 Page helpers add shell/title/history/fallback behavior without changing the render/target/effect model. [north_star]
002 Existing Axum 0.8 routes remain normal Axum routes. hemx does not own routing. hemx-axum only mounts handler dispatch, runtime assets, and optional push endpoints. [north_star]
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]
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]
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
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.
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.
003 Permission-sensitive host capabilities require an explicit user-facing reason in the app-owned manifest before standard host checks may pass.
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.
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
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.
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.
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.
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.
005 hemx requests preserve standard HTTP credentials semantics. Cookies, SameSite policy, Authorization headers, and session middleware remain framework/browser concerns.
push
001 Server push streams canonical versioned hemx EffectBatch bytes over SSE or WebSocket. hemx core owns the effect codec, not the transport.
002 SSE/WebSocket connections are authenticated by the server framework before stream creation. hemx does not define auth semantics for streams.
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.
004 Out-of-band updates are ordinary multi-target partial swaps/effects, not a separate response model.
005 Push is one-way server-to-client delivery of EffectBatch. It does not define client mutation, optimistic queues, reconciliation, or conflict handling.
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.
007 Cross-origin push streams belong to explicit integration code rather than the standard runtime convention.
008 SSE must transport canonical EffectBatch bytes as one unpadded base64url value in the hemx event data field. [north_star]
sync
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.
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.
003 Server reconciliation of received patches produces a local EffectBatch only when state changes. Accepted mutations patch shared state without hard-coding specific effects.
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).
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.
006 SyncEffect::ack(atom) acknowledges a successful server-side mutation, allowing the client to clear its local optimistic queue for that atom.
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.
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.
009 Every queued mutation has a stable client-generated command id, actor/session scope, schema version, and causal ordering token so retries and reconnects do not create ambiguous duplicates. [north_star]
010 The authoritative server processes command ids idempotently and returns an acknowledgement, canonical replacement, explicit rejection, or conflict result; transport success alone never clears local work. [north_star]
011 The client durably advances its acknowledgement cursor only after the corresponding canonical result is committed locally; crash or reload between send and acknowledgement is safe to retry. [north_star]
012 Initial sync uses a versioned snapshot plus an ordered change cursor or an equivalent bounded protocol; reconnect resumes from the last committed cursor and falls back to a fresh snapshot when history is unavailable. [north_star]
013 Server rejection and conflict are distinct outcomes. Each produces application-visible reconciliation data, compensates or replaces optimistic projection explicitly, and never silently drops the command. [north_star]
014 Queued command and projection schema upgrades are transactional and versioned. Incompatible data fails closed with export/reset recovery instead of partial replay. [north_star]
015 Durable browser storage failures, corruption, eviction, and quota exhaustion are observable and recoverable; the adapter must not claim offline durability after persistence fails. [north_star]
016 Reconnect uses bounded exponential backoff with jitter and explicit online/offline state; manual retry remains available, and reconnect storms are prevented. [north_star]
017 Queue size, payload size, in-flight commands, retry rate, and server fan-out are bounded with backpressure and actionable overflow behavior. Unbounded buffering is forbidden. [north_star]
018 Multiple tabs or workers coordinate one durable queue through an explicit lease/ownership protocol or safe idempotent parallel replay; they must not race destructive queue updates. [north_star]
019 Replay revalidates the current authenticated principal, authorization, tenancy, and command preconditions on the server; cached permission from enqueue time is not authority. [north_star]
020 Applications explicitly choose retention, encryption-at-rest, export, and deletion policy for local command data; hemx-sync exposes lifecycle hooks but does not invent product policy. [north_star]
021 Sync diagnostics expose queue depth, oldest command age, connection state, retry count, cursor, acknowledgement latency, conflicts, and rejected commands without logging sensitive payloads by default. [north_star]
022 The default reconciliation model is server-authoritative and deterministic for identical snapshot, command sequence, and server results; custom merge or CRDT policy is an explicit integration. [north_star]
023 A durable browser test proves offline mutation, reload, reconnect replay, duplicate delivery, rejection, conflict, schema mismatch, and final convergence through public hemx APIs. [north_star]
024 Sync channel names must contain 1..=128 ASCII alphanumeric, colon, underscore, hyphen, or period bytes. [north_star]
025 Flat patch identifiers must contain 1..=128 bytes from the sync-channel character set. [north_star]
026 Flat patch keys must contain 1..=64 bytes, begin with an ASCII letter, continue with ASCII alphanumeric, underscore, or hyphen bytes, and exclude reserved keys. [north_star]
027 Flat patch string values must not exceed 4,096 bytes, and integer values must remain within JavaScript's safe integer range. [north_star]
028 Flat patch serialization must produce valid JSON with escaped string values. [north_star]
029 Flat patch deserialization must reject unknown fields, unsupported schemas, and invalid identifiers, keys, or values. [north_star]
local
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.
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.
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.
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
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]
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]
009 Escape hatches are leaves, never app foundations. [north_star]
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]
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]
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]
005 HTMX-style response triggers and widget notifications are represented by Effect::event or generated event helpers. Events are native CustomEvents scoped to the hemx root. [north_star]
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]
011 Preserve boundaries require deliberate author marks. Preserve must not become a default lifecycle model or a workaround for unclear ownership. [north_star]
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]
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]
012 Core must not add selector targeting, hydration compatibility, a client store, or a framework lifecycle to make interop easy. [north_star]
nav
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.
002 Navigation modes: Push (history.pushState), Replace (replaceState), Redirect (server-side 302). Scroll behaviour: Preserve, Top, Element(ResourceRef). Title is optional.
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.
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.
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
001 Raw HTML insertion requires an explicit safe HTML type (SafeHtml or equivalent). Plain String renders as escaped text unless explicitly wrapped. [north_star]
002 Hemplate-rendered output may be converted to SafeHtml by trusted render APIs. User input is never SafeHtml by default. [north_star]
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]
005 Handlers use slot/resource render helpers for effect payloads instead of raw HTML construction. [north_star]
003 Slot render commands distinguish text payloads from HTML payloads at the type level. [north_star]
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. [north_star]
002 Generated slot types may target Display, Hemplate, or explicit view wrappers. Type errors suggest the expected renderable view type. [north_star]
003 Generated Hemplate views expose a template-derived size hint, and trusted hemx render helpers use it to pre-size output without changing rendered HTML. [north_star]
diag
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]
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]
003 Internal terms (ResourceId, ScopeKey, EffectBatch) must not appear in beginner-facing diagnostics unless --verbose is enabled. [north_star]
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]
007 Optional .heml editor overlays must not own a second template language, formatter, selector model, or custom editor framework. [north_star]
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]
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. A simple local is one Rust identifier bound directly to a self vector field; malformed bindings and non-vector fields do not produce local facts. Missing or stale facts fall back to syntax/document completions. [north_star]
008 hemx-lsp must not proxy rust-analyzer or own a second Rust type system. [north_star]
009 hemx-lsp diagnostics select the offending directive and target in the current .heml source when compiler metadata identifies them, rather than defaulting every error to line 0 column 0. [north_star]
010 Hovering a generated data-hemx-root, slot, form, or handle value reports its generated resource kind and Rust ui::<name> symbol from current hemx-build facts. [north_star]
test
001 EffectWriter implements a test backend so handlers can be unit-tested without a browser through hemx_test run and inspect helpers. [north_star]
007 EffectInspector exposes generated-resource assertions for target updates, HTML/text payloads, keyed insert/replace/remove, navigation, emitted events, slots, and atoms. [north_star]
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]
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]
010 Browser/E2E selector helpers must not become app authoring APIs. [north_star]
002 #[hemx::component] may compile without generated Surface files for incremental module-local testing. [north_star]
011 #[hemx::surface] requires hemx.generated.rs in $OUT_DIR and fails with an actionable diagnostic when generation is missing. [north_star]
003 Generated registries are validated by compile-time tests: missing handler implementations produce test failures with actionable messages. [north_star]
004 Repository-wide verification uses a resource-aware runner that runs from the hemx workspace root regardless of the caller's directory and caps Cargo build jobs and Rust test threads from available CPU and memory. User-requested concurrency cannot exceed the detected safe cap. [north_star]
012 Browser E2E runs as an isolated step and can be skipped explicitly when browser infrastructure is unavailable. [north_star]
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]
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]
013 Durable browser coverage must not rely on ad hoc /tmp scripts. [north_star]
014 html_examples browser smoke asserts dynamic interactions complete without full-page reload or navigation. [north_star]
015 Verification docs distinguish fast, focused browser, and full tiers while preserving cargo run -p hemx-xtask -- test as the full local authority. [north_star]
016 The full local verification path has an explicit timeout budget; any shard split is deterministic and preserves equivalent coverage. [north_star]
017 Test helpers for rendered runtime ids report the generated handle or slot name, not only raw data-hid or data-sid selectors. [north_star]
018 EffectInspector assertion methods fail at the caller with the expected generated target and payload condition plus the actual effect operations, avoiding opaque boolean assertion failures. [north_star]
019 Repo-owned process-backed tests use one RAII harness that waits for TCP readiness, reports early exit or timeout with the process label and address, and always reaps the child. [north_star]
020 Full local release verification must mutation-test each mutation-applicable Rust library through its package-native test targets. [north_star]
021 Unexplained missed mutants must block release; equivalent, invariant-only, and infrastructure-inapplicable mutants must be explicitly classified. [north_star]
022 cargo run -p hemx-xtask -- mutation [PACKAGE] [SHARD/TOTAL] must run capped exhaustive package-native mutation tests with enough per-mutant time for repo-owned compiler probes, and fail on unexplained survivors. [north_star]
023 A supplied mutation SHARD/TOTAL uses one-based 1..=TOTAL numbering, maps to the deterministic native shard, and rejects invalid bounds; omission preserves the full-package gate. [north_star]
check
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.
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.
003 Renaming a slot or handle breaks cargo check immediately with a span pointing to the Rust handler or template source.
004 Root-scoped slot lookup: JS runtime resolves data-sid only within the nearest/current data-hemx-root.
state
001 Typed atoms with Atom<T> are explicit addressable state resources. [north_star]
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]
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]
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]
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]
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]
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
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. [north_star]
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. [north_star]
003 High-frequency UI handlers (drag, pointermove, animation tick) must not require server round-trips or handwritten JS. [north_star]
004 Client-local handler opt-in syntax is an integration contract, not hemx core; hemx-wasm owns #[hemx::handler(client)] and its browser boundary. [north_star]
005 A client-local handler accepts generated events and explicit state and returns the same IntoEffect/EffectBatch contract as a server handler; it must not expose DOM mutation APIs. [north_star]
006 hemx-wasm exports only handlers explicitly opted into client execution and rejects parameters, captures, or return values that cannot cross its generated ABI with an actionable Rust span. [north_star]
007 Each hemx root owns its client-local state instance; initialization from defaults or versioned data-hemx-st input is explicit, and no process-global browser singleton owns application state. [north_star]
008 Invalid browser event payload or incompatible bootstrap state must not call the handler; the runtime emits a root-scoped diagnostic, restores pending UI, and preserves a native/server fallback where declared. [north_star]
009 A successfully dispatched client-local handler applies its ordinary EffectBatch through the same version/fingerprint checks and ordered root-scoped effect interpreter used for server responses. [north_star]
010 A client-local interaction performs no network request unless the handler explicitly returns an integration effect that declares remote work. [north_star]
011 Client-local handlers are cancellable or supersedable according to the generated handle policy, and stale asynchronous completions must not overwrite newer state or effects. [north_star]
012 Removing a hemx root releases its handler bindings, timers, observers, pending work, and local state; repeated mount/unmount must not leak root-owned browser resources. [north_star]
013 Direct-manipulation client handlers meet an evidence-backed interaction budget: ordinary input responds within 100 ms, and pointer-follow animation work fits the tested frame budget on the supported baseline browser. [north_star]
014 Client-local behavior has a browser-level proof using generated resources, a real WASM artifact, zero app-authored JavaScript, and network instrumentation that distinguishes local from server execution. [north_star]
015 The client-local boundary must reject an empty event kind or an event kind over 256 UTF-8 bytes before application code runs. [north_star]
016 The client-local boundary must reject an optional value over 65,536 UTF-8 bytes before application code runs. [north_star]
017 The client-local boundary must reject an optional key over 1,024 UTF-8 bytes before application code runs. [north_star]
018 The client-local boundary must reject encoded state over 1,048,576 UTF-8 bytes before application code runs. [north_star]
019 The client-local boundary must accept only event ABI version 1 and state ABI version 1 before application code runs. [north_star]
async_data
001 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. [north_star]
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. [north_star]
003 Query helpers must compile to ordinary handlers and effects; they must not introduce a client-side data framework or cache as a core dependency. [north_star]
invariant
001 User-authored references are symbolic at author time and numeric at runtime. [north_star]
002 The JS runtime never parses CSS selectors, expressions, or handler names. [north_star]
003 Rust handlers return effects; they do not imperatively mutate DOM. [north_star]
004 Cross-file references visible to build/proc-macro validation fail at cargo check with a precise span. [north_star]
006 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. [north_star]
005 hemx core owns effects, typed ids, and registries only. Routing, auth, sessions, transport, transitions, and sync are integration concerns. [north_star]
v0
001 v0 stable release includes Surface consumption, generated slots/forms/handles, #[hemx::handler], tuple IntoEffect, form dispatch, typed data-* params, and keyed slots. [north_star]
005 v0 stable release includes page swap, root-scoped runtime, EffectBatch wire schema, diagnostics, tests, and hemx-axum integration. [north_star]
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.
003 v0 proof apps are: counter, docs-site HTMX replacement, form wizard, auth action, SSE notification stream, and keyed todo list.
004 The local-first kanban remains the north-star milestone, not a v0 blocker.
examples
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]
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]
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]
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]
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. [north_star]
003 If an example requires raw EffectWriter, manual ids, manual JS, or manual registry setup, the API is considered too complex. [north_star]
004 Canonical examples are compile-tested golden API contracts. Changing generated API shape requires updating the examples deliberately. [north_star]
005 Canonical examples must not contain user-authored browser JavaScript; inline <script>, on*= handlers, and javascript: URLs are forbidden outside opaque leaf-widget examples. [north_star]
012 Canonical examples may load the shared hemx runtime (/hemx.js) and may use declarative data-hemx-* attributes. [north_star]
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]
010 The Workout common path uses generated helpers instead of manual registry conversion. [north_star]
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. [north_star]
013 Workout mobile commands must not add a broad hemx-mobile framework. [north_star]
ms
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. [north_star]
002 The Local-first Multiplayer Kanban milestone must avoid React/Vue/VDOM while staying in a single typed Rust codebase. [north_star]
003 The Local-first Multiplayer Kanban milestone acts as the north-star integration test for hemx + hemplate + hemx-sync. [north_star]
ts
001 TypeScript definitions for hemx-js runtime are shipped as a single .d.ts file. Types mirror the canonical EffectBatch schema for advanced consumers. Tooling must not depend on these types for core functionality; they are developer convenience only.
build
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.
002 Proc-macros (#[hemx::handler], #[hemx::surface]) are side-effect free. They read generated artifacts (hemx.syms, hemx.generated.rs) but never write files. [north_star]
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.
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.
005 A build.rs failure (missing Surface, version mismatch, stale hash) is a hard error before proc-macro expansion.
006 Proc-macros are local: they know the item they annotate plus pre-generated symbol tables. They do not have global knowledge of all handlers across the crate.
007 Proc-macros never parse .heml or process generic Surface IR. Global codegen lives only in build.rs invoked by hemx_build. [north_star]
008 Global checks for every declared handle having an implementation are deferred to app-mount tests or enabled by an optional #[hemx::component] macro.
009 hemx_build preserves generated artifact timestamps when canonical contents are unchanged, so no-op builds do not invalidate downstream Rust compilation. [north_star]
misc
001 Workspace layout separates hemx-core, hemx-derive, hemx-build, hemx-axum, hemx-js, and optional transition/sync/wasm crates; no kitchen-sink crate.
002 All crates compile on stable Rust. MSRV 1.80. hemx-core has zero proc-macro dependencies.
003 Three execution modes are supported: server-first, client-local WASM, and hybrid sync. Modes are opt-in per handler, not global.
004 The only required user-facing proc-macro in hemx core is #[hemx::handler]; optional ergonomic macros may exist, but no ! call-syntax macros.
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.
006 Authoring hemx attributes use the data-hemx-* prefix. Runtime lowering may emit compact ids. No unprefixed custom attributes.
007 Id allocation is deterministic from canonical symbol paths. Ids are stable across builds unless the symbol path changes. Deploy mismatch is caught by a build-schema version check.
008 Core design rule: add one primitive only if it deletes five special cases. ScopeKey deletes loop keying, component scoping, modal instances, nested forms, and portal boundaries.
009 ResourceId deletes special opcodes per kind, separate registries, separate wire formats, and separate test APIs.
010 Effect::event deletes plugin APIs, custom JS bridges, chart adapters, and map SDK wrappers.
boundary
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.
002 hemplate never interprets data-hemx-* or any other tool-prefixed attributes. It records them faithfully as generic raw attributes in the Surface.
003 hemx never owns .heml parsing semantics directly. It consumes hemplate Surface facts from hemplate.surface.postcard or in-process hemplate Surface extraction.
004 hemx interprets tool-specific conventions such as data-hemx-handle and data-hemx-slot from the generic Surface.
derive_handler
001 #[hemx::handler] validates handle name, params, and IntoEffect return type; generated code registers a static lookup entry keyed by numeric handle id.
002 Handler param inference from data-* attributes allows template data-card-id="{card.id}" to map to a card_id: CardId handler parameter checked by hemx-build.
003 Handler parameters are inferred from Form<T>, triggering-node data-* attributes, integration-supplied route params, and explicit app/context parameters.
004 The common handler signature forms are plain Rust functions, synchronous or async:
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.
005 Missing or incompatible handler params are compile-time errors with source spans; shared params use forms, hidden inputs, scoped context, or explicit data-* attributes, not selector-based hx-include.
derive_app
001 #[hemx::app(...)] marks an application/root registry entry point and composes generated component handler modules into one app registry from the app state.
002 There may be one registry per app/root type, and multiple runtime instances may exist per process; canonical app code avoids process-global singletons and handwritten per-component registration chains.
locality
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.
002 Diagnostics suggest adding data-hemx-slot or h-key to the local element when users attempt a self/row update pattern, and explain which generated target helper would become available.
style
001 Plain CSS and SCSS own appearance; hemx-build discovers static class tokens from .heml, .css, and .scss build inputs and generates CssClass constants. [north_star]
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. [north_star]
003 When a hemplate dynamic class attribute needs multiple class tokens, Rust passes a displayable list of generated CssClass values as view data. [north_star]
004 hemx does not parse CSS selectors for behavior, cascade policy, or layout semantics. [north_star]
005 Unknown Rust class references fail by normal Rust name resolution when the generated constant is absent. [north_star]
006 Conditional state classes compose from generated constants with boring Rust helpers such as classes::card.with_if(selected, classes::is_selected); Rust does not assemble ad hoc class strings for known style tokens. [north_star]
convention
001 hemx-axum and the JS runtime support common UX conventions as attributes, not core effects; these remain orthogonal to the core effect algebra.
002 Default event triggers are submit for forms and click for buttons and links; data-hemx-on overrides the default.
003 data-hemx-debounce, data-hemx-delay, and data-hemx-throttle support simple millisecond values. No trigger mini-language in core.
004 data-hemx-confirm dispatches a native confirm() before handler dispatch.
005 data-hemx-every and data-hemx-interval dispatch a handle at a fixed interval while the element remains in the document.
006 Request concurrency policy (latest, queue, drop, parallel) may be declared per handle with data-hemx-policy.
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.
008 data-hemx-disable-while-pending disables the triggering form controls or button while the request is active and restores them afterward.
009 Unknown data-hemx-* authoring attributes are build errors with a suggested fix; opaque/integration islands must use explicit allowed attributes or their own non-hemx data-* names.
010 Supported cosmetic convention attributes include data-hemx-pending-class, data-hemx-indicator, data-hemx-confirm, and data-hemx-disable-while-pending.
011 Supported timing/trigger convention attributes include data-hemx-debounce, data-hemx-delay, data-hemx-throttle, data-hemx-every, data-hemx-interval, data-hemx-revealed, data-hemx-policy, and data-hemx-on.
012 Runtime-supported delegated events are click, submit, input, change, dragstart, dragover, and drop; unsupported static event names are build errors.
013 Static empty confirmation messages are build errors: they silently disable the guard in browsers. Custom confirm UI belongs to integration crates.
014 data-hemx-revealed dispatches once when the element enters view, with an immediate fallback when IntersectionObserver is unavailable.
015 Duplicate timers/observers per root are avoided.
016 Default concurrency policy for debounced/input handlers is latest; default for form submit is drop while pending.
017 Stale EffectBatches from superseded requests must not be applied.
accessibility
001 Server-first, page-enhanced, client-local, and sync modes preserve semantic HTML and native link/form behavior; enhancement must not remove an available keyboard or no-script path. [north_star]
002 Every generated interaction is keyboard operable. Pointer-specific features such as drag/drop provide an application-declared keyboard action path with equivalent outcome. [north_star]
003 Effect application preserves or deliberately moves focus. Removed focused nodes, validation failures, modal boundaries, navigation, and full-page fallback each have deterministic focus behavior. [north_star]
004 Pending, success, validation, transport, offline, conflict, and recovery state is exposed programmatically through native validity, aria-busy, status/error regions, or generated application targets without relying on color or motion alone. [north_star]
005 Generated resources preserve authored accessible names, roles, values, labels, descriptions, and table/list structure across replacement, insertion, and reconciliation. [north_star]
006 Optional transitions and direct-manipulation effects honor reduced-motion preferences and do not block input, focus, or recovery when animation is disabled. [north_star]
007 The supported example matrix includes automated accessibility checks plus keyboard/focus browser scenarios for forms, page navigation, client-local interaction, offline state, and error recovery. [north_star]
operations
001 Every request, push stream, client-local dispatch, sync command, acknowledgement, and effect application has a stable correlation boundary that integrations can attach to existing tracing without hemx owning a telemetry backend. [north_star]
002 Integration diagnostics distinguish build/version mismatch, transport failure, timeout, cancellation, decode failure, missing target, handler rejection, authorization denial, storage failure, and sync conflict. [north_star]
003 Requests, streams, handlers, and background replay support explicit timeouts and cancellation; cancellation restores pending UI and prevents late effects from applying. [north_star]
004 Push and sync adapters implement bounded buffering, heartbeat/liveness detection, reconnect backoff, and slow-consumer behavior instead of relying on unbounded transport queues. [north_star]
005 Production diagnostics omit sensitive form values, auth material, local command payloads, and rendered private HTML by default; development detail is explicit and cannot silently enable in production. [north_star]
006 Mixed deploy versions fail closed through ABI/fingerprint checks and recover by reload, full navigation, or fresh sync snapshot; rolling deployment must not apply an incompatible partial batch. [north_star]
007 A production reference exposes health/readiness, structured errors, tracing hooks, and metrics for latency, failures, queue pressure, reconnect, and effect decode/apply without requiring a hemx-specific observability stack. [north_star]
008 Process restart, browser reload, network interruption, duplicate delivery, and partial deployment are first-class recovery tests rather than manual release notes. [north_star]
security
001 Hemx-owned HTML and attribute sinks preserve hemplate escaping and explicit SafeHtml trust boundaries in every execution mode; sync, WASM, and push must not introduce a weaker payload path. [north_star]
002 Standard HTTP, navigation, asset, push, and sync connections are same-origin by default. Cross-origin use requires explicit integration configuration and cannot silently forward credentials. [north_star]
003 Mutating HTTP integrations expose testable origin/CSRF enforcement, secure session/cookie boundaries, request-size limits, and content-type validation while leaving application policy to the host framework. [north_star]
004 Every handler invocation and replayed command receives current authenticated identity and authorization context from the integration boundary; numeric generated ids are routing identifiers, never authorization. [north_star]
005 Effect, state, event, command, and sync decoders reject unknown versions, invalid kinds, oversized lengths, truncated values, and trailing incompatible data without panicking or partially applying the batch. [north_star]
006 The browser runtime and generated bootstrap are compatible with a strict Content Security Policy: no eval, dynamic code generation, inline application script requirement, or javascript: URL. [north_star]
007 Local persistence makes sensitive-data exposure explicit: applications can redact, encrypt, expire, export, and delete queued data, and examples never store credentials or session secrets in offline command records. [north_star]
008 Release evidence includes a pinned advisory audit of the committed lockfile, review of unsafe code and licenses, and documented disposition for every accepted advisory or supply-chain exception. [north_star]
009 Hemx claims only the framework controls it proves. Application compliance, identity provider, database encryption, backup, retention, and incident policy remain explicit host responsibilities. [north_star]
performance
001 Performance requirements are measured on named supported configurations with reproducible fixtures; “fast”, “60fps”, and bundle-size claims must have commands, baselines, and regression thresholds. [north_star]
002 Server-first and page-enhanced handlers add bounded framework overhead relative to rendering and transport, and benchmark regressions fail the repository performance gate. [north_star]
003 Client-local direct manipulation responds within 100 ms for ordinary input and keeps pointer-follow work within the tested frame budget without network dependence. [north_star]
004 Initial sync is proportional to the scoped snapshot, incremental sync is proportional to delivered changes, and neither path requires loading unrelated workspace state. [north_star]
005 Effect decode/apply, generated target lookup, queue replay, and keyed reconciliation have adversarial size tests and explicit memory/operation bounds. [north_star]
006 Optional WASM, sync, PWA, transition, and editor assets are separately loadable; server-first applications do not pay their download, initialization, or dependency cost. [north_star]
v1_release
001 Hemx v1 is feature-complete only when server-first, page-enhanced, client-local WASM, and hybrid offline/sync modes share generated resources and EffectBatch semantics and pass their end-to-end proof scenarios. [north_star]
002 The local-first multiplayer Kanban milestone proves direct manipulation, optimistic projection, offline durability, reload, ordered replay, idempotency, conflict/rejection, presence, convergence, and SSR-first fallback without React, Vue, VDOM, or app-authored JavaScript. [north_star]
003 A production reference proves durable persistence, current authorization, CSRF/origin policy, transactions, restart recovery, structured failures, observability hooks, accessibility, and deploy compatibility while keeping vendor policy outside hemx core. [north_star]
004 Public Rust API, generated API, Surface schema, symbol schema, wire ABI, browser runtime ABI, persisted sync schema, MSRV, and supported browser changes each have an explicit compatibility and migration policy. [north_star]
005 V1 documentation teaches one progression: server-first partial swaps, page enhancement, explicit islands/client-local handlers, then durable sync; every level states ownership, failure, recovery, accessibility, security, and operational boundaries. [north_star]
006 Release closure requires formatting, workspace tests, strict all-target linting, compile-fail contracts, browser/WASM/offline scenarios, performance budgets, security audit, documentation checks, examples, and clean-tree reproducibility with no unresolved P0 or P1 defect. [north_star]
007 The supported matrix names Rust/MSRV, browser versions, WASM target/toolchain, operating systems needed for development, and integration crate versions; unsupported combinations fail with actionable diagnostics. [north_star]
008 All canonical examples use public generated APIs and are treated as compatibility tests. No release claim depends on unpublished demo-only glue, ad hoc scripts, or test-only runtime behavior. [north_star]
009 Local release-readiness validation must not publish crates, deploy services, upload artifacts, submit stores, or mutate external systems. Publishing remains a separate explicit human-authorized action. [north_star]
010 Feature-complete does not mean feature-accumulative: v1 refuses a built-in router, auth system, database, mandatory client store, default CRDT, component lifecycle, VDOM, selector language, telemetry vendor, deployment platform, and application policy framework. [north_star]
multipart
001 hemx-axum supports multipart/form-data as an integration boundary for file uploads.
002 Multipart parsing belongs to hemx-axum/axum extractors, not hemx-core.
003 File upload forms preserve native browser fallback behavior.
target
001 hemx does not support response-side selector retargeting. Handlers choose targets by returning generated UI commands for generated resources.
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.