feat: slhx requirements, project structure, and core primitives

This commit is contained in:
2026-05-10 11:33:26 +02:00
commit bd13d0fa0d
7 changed files with 1045 additions and 0 deletions
+280
View File
@@ -0,0 +1,280 @@
# — AGENTS.md
> Auto-generated from REQUIREMENTS.md. Do not edit directly.
> Edit REQUIREMENTS.md and run: redgate agents > AGENTS.md
## Requirements
### req:_boundary/001
- **001** hemplate does not expose a slhx API. It exposes a stable, generic Template Surface IR. slhx is one consumer; a11y tools, test generators, and documentation generators are others.
### req:_boundary/002
- **002** hemplate never interprets `data-slhx-*`, `slhx-*`, or any other tool-prefixed attribute. It records them as raw `name: value` pairs in the Surface.
### req:_boundary/003
- **003** slhx never parses `.heml` directly. It consumes `hemplate.surface.postcard` emitted by `hemplate_build`. slhx interprets tool-specific conventions (`data-slhx-handle`, `data-slhx-slot`, etc.) from the generic Surface.
### req:_build/001
- **001** Build order: `.heml``hemplate_build``hemplate.surface.postcard``slhx_build``slhx.syms` + generated Rust constants.
### req:_build/002
- **002** `slhx-derive` (`#[slhx::handler]`) reads `slhx.syms` at expansion time to validate handle names, slot names, and form signatures.
### req:_build/003
- **003** A `build.rs` failure (missing Surface, version mismatch, stale hash) is a hard error before proc-macro expansion.
### req:_build/004
- **004** Id allocation is deterministic from canonical symbol paths. Stable across builds unless the symbol path changes.
### req:_check/001
- **001** All cross-file references verified at `cargo check`. Unknown handle → hard error. Unknown slot → hard error. Type mismatch between slot and atom → hard error.
### req:_check/002
- **002** Dead handle warning: `#[slhx::handler]` never referenced by any template. Dead slot warning: template node never targeted by any handler.
### req:_check/003
- **003** Renaming a slot or handle breaks `cargo check` immediately with a span pointing to the Rust handler or template source.
### req:_check/004
- **004** Page-scoped slot lookup: JS runtime resolves `data-sid` only within the current Page root element.
### req:_derive_app/001
- **001** `#[slhx::app]` marks the root application struct containing all global atoms. It is the registry entry point for `slhx_build`. Zero- or single-instance per process.
### req:_effect/001
- **001** Effects are a typed command stream describing *what*, *where*, and *how* of a DOM mutation. Declarative: Rust builds the stream; JS applies it.
### req:_effect/002
- **002** The public API is `IntoEffect` (a trait) for Rust ergonomics and zero-allocation encoding. The wire API is a canonical postcard opcode schema (`Op::ReplaceHtml`, `Op::SetText`, `Op::PatchAtom`, `Op::Navigate`, `Op::Focus`, `Op::AddClass`, `Op::RemoveClass`, `Op::RemoveKeyed`, `Op::CustomOp`). IntoEffect writes opcodes directly; advanced users may implement the trait to stream custom opcodes.
### req:_effect/003
- **003** Core effect helpers: `replace(slot, html)`, `patch(slot, atom)`, `text(slot, value)`, `remove_keyed(slot, key)`, `append_keyed(slot, key, html)`, `move_keyed(slot, key, target_slot, target_key, position)`, `class_keyed(slot, key, class, active)`, `navigate(Nav { url, mode, title, scroll })`, `focus(slot)`, `add_class(slot, class)`, `remove_class(slot, class)`, `batch((...))`.
### req:_effect/004
- **004** `batch` composes effects in declared order. No implicit ordering, no priority weights.
### req:_effect/005
- **005** `Effect::navigate` carries `NavMode::Push | Replace | Redirect`, optional `title`, and `ScrollMode`. No separate router framework required for basic cases.
### req:_effect/006
- **006** Core effects may carry an opaque transition token, but core never interprets or implements transitions. Transitions live in `slhx-transition`.
### req:_effect/007
- **007** Zero runtime parsing of selectors. The JS runtime looks up elements by numeric `data-sid` or `data-hid` attributes. Slot ids are allocated deterministically.
### req:_form/001
- **001** Forms are first-class. hemplate exports `FormSurface` with raw `ControlKind` facts. slhx generates/validates Rust form structs from those facts.
### req:_form/002
- **002** `Form<T>` is generated by slhx-build from the Surface. `T` derives from HTML control names and kinds, mapped to Rust types by slhx rules (e.g. `type="number"` + `required``u64`; same without `required``Option<u64>`).
### req:_form/003
- **003** Handler signature mismatch between generated `Form<T>` and the handler parameter is a `cargo check` error.
### req:_form/004
- **004** Progressive enhancement: if JS fails, `<form data-slhx-handle>` degrades to normal submission via hidden `__h` field. Server reads `__h` and dispatches by numeric handle id.
### req:_invariant/001
- **001** User-authored references are symbolic at author time and numeric at runtime.
### req:_invariant/002
- **002** The JS runtime never parses CSS selectors, expressions, or handler names.
### req:_invariant/003
- **003** Rust handlers return effects; they do not imperatively mutate DOM.
### req:_invariant/004
- **004** Cross-file references fail at `cargo check` with a precise span.
### req:_invariant/005
- **005** slhx core owns effects, typed ids, and registries only. Routing, auth, sessions, transport, transitions, and sync are integration concerns.
### req:_js/001
- **001** The JS runtime is a single file under 3 kB minified+gzipped. It reads `data-hid` and `data-sid`, delegates events on `document`, and applies effects by direct DOM mutation.
### req:_js/002
- **002** No build step, no virtual DOM, no diffing, no scheduler. Receiving an effect = apply ops immediately in declared order.
### req:_js/003
- **003** The runtime consists of an Op interpreter (`ReplaceHtml`, `SetText`, `PatchAtom`, `Navigate`, `Focus`, `AddClass`, `RemoveClass`, `RemoveKeyed`, `CustomOp`) reading from a postcard byte stream.
### req:_list/001
- **001** Any `data-slhx-slot` or `data-slhx-handle` inside `@for` requires an explicit `key` expression. Syntax: `@for item in items key item.id { ... }`. Without `key`, slhx-addressable nodes inside the loop are rejected at build time. Keyed identity is `(SlotId, KeyValue)`.
### req:_list/002
- **002** Slots inside a keyed loop receive a composite identity: `(SlotId, KeyValue)`, not a flat id. hemplate records `key_expr` in the Surface; slhx implements keyed slot lookups.
### req:_list/003
- **003** Effects on keyed slots: `replace_keyed(slot, key, value)`, `remove_keyed(slot, key)`, `append_keyed(slot, key, value)`. Mismatch between key type and slot key type is compile-time error.
### req:_misc/001
- **001** Workspace layout: `slhx-core` (types + postcard schema, no_std), `slhx-derive` (proc-macros), `slhx-build` (surface consumer + code generation), `slhx-axum` (integration), `slhx-js` (runtime single file), `slhx-transition` (optional), `slhx-sync` (optional), `slhx-wasm` (optional). No kitchen-sink crate.
### req:_misc/002
- **002** All crates compile on stable Rust. MSRV 1.80. `slhx-core` has zero proc-macro dependencies.
### req:_misc/003
- **003** No auth, no routing, no session storage inside slhx core. `slhx-axum` provides typed route mounting; actual routing is axum/tower.
### req:_misc/004
- **004** Three execution modes supported: server-first (request/response), client-local WASM (requestAnimationFrame, no round-trip), and hybrid sync (local + remote via `slhx-sync`). Modes are opt-in per handler, not global.
### req:_misc/005
- **005** The only user-facing proc-macro is `#[slhx::handler]`. No `!` call-syntax macros. Attribute macros only.
### req:_ms/001
- **001** **Milestone app: Local-first Multiplayer Kanban.** A board with drag-and-drop cards, 60fps pointer-follow, optimistic updates, offline queue, conflict reconciliation, live presence, and SSR-first rendering — all without React/Vue/VDOM, in a single typed Rust codebase. This acts as the north-star integration test for slhx + hemplate + slhx-sync. [north_star]
### req:_pitch/001
- **001** slhx is checked hypermedia for Rust. Authors write HTML templates and Rust handlers. The compiler lowers every cross-file reference to a stable numeric id. The browser runtime only sees ids and effect bytes. [north_star]
### req:_pitch/002
- **002** No CSS selectors. No hx-* strings. No virtual DOM. No client framework. No hidden global proxy magic. No hydration. [north_star]
### req:_pitch/003
- **003** slhx replaces React/Vue not with a UI framework, but with a compiler contract: hemplate knows the surface, Rust knows the types, slhx knows the effects, the browser only executes commands. [north_star]
### req:_resource/001
- **001** Async remote data lives in `Resource<T>` / `Query<K, T>` / `Mutation<I, O>`. These are optional, not core primitives. They provide loading/error/refresh semantics without client-side data libraries.
### req:_resource/002
- **002** `Effect::resource(res).reload()` triggers a re-fetch and re-render. The server sends a new EffectBatch when data is ready.
### req:_state/001
- **001** `Atom<T>` is a typed, stable-id handle to a piece of state. Atoms are the only state primitive in core.
### req:_state/002
- **002** State shape is flat. Nesting is an anti-pattern; compose via multiple atoms.
### req:_state/003
- **003** No proxy magic. State access is explicit: `store.get(atom)` returns `Option<&T>`. Mutations return `impl IntoEffect`, not side-effects.
### req:_state/004
- **004** Page-local transient state lives in JS as `Map<AtomId, unknown>`. Not reactive-by-default.
### req:_state/005
- **005** Global long-lived state is stored server-side in a session-compatible way. On re-render the server injects a `postcard`-encoded blob in `<script type="application/slhx-state">`; JS hydrates it so client-side handlers (WASM-compiled Rust) can read it without round-trips.
### req:_state/006
- **006** The atom model is isomorphic to ECS. Atoms are components, Pages are worlds, Slots are entities. Scales to game-like WASM applications.
### req:_surface/001
- **001** `hemplate_build` scans `.heml` files and emits `$OUT_DIR/hemplate.surface.postcard` (postcard-encoded, deterministic, versioned).
### req:_surface/002
- **002** The Surface contains: nodes (NodeId, parent, scope, element, attrs, source span), scopes (ScopeKind: Root | If | For { binding, key_expr }), forms (form controls with raw HTML types), and component uses.
### req:_surface/003
- **003** Node identity is `NodeId` in a parent/scope graph. No `css_path` is used as a primary identifier. An optional `debug_path` string may exist for diagnostics only.
### req:_surface/004
- **004** Form controls in the Surface carry raw HTML facts: `ControlKind::Text`, `ControlKind::Number { min, max, step }`, `ControlKind::Checkbox`, `ControlKind::Select { multiple, options }`, etc. No Rust type mapping lives in hemplate.
### req:_surface/005
- **005** Loop scopes expose the binding name and an optional `key_expr` (e.g. `todo.id`). hemplate does not enforce key usage; it only records it for consumers.
### req:_surface/006
- **006** Surface schema is versioned (`schema_version: u32`). Postcard encoding, no JSON. `no_std`-compatible schema definition so any tool can read it without heavy dependencies.
### req:_surface/007
- **007** `hemplate-derive` does not write Surface files. Surface generation is a `build.rs` / `hemplate_build` concern, proc-macro side-effect free.
### req:_sync/001
- **001** `slhx-sync` is an optional crate for collaborative / multiplayer state. Provides presence tracking, patch reconciliation, conflict resolution (server-authoritative), and offline queueing. Not part of core.
### req:_sync/002
- **002** `SyncEffect::send_patch(atom, patch)` queues a state diff for server sync. If offline, the patch is stored in a local queue and sent when connection resumes. If online, it is sent immediately via WebSocket/SSE.
### req:_sync/003
- **003** `Effect::ack(atom)` acknowledges a successful server-side mutation, allowing the client to clear its local optimistic queue for that atom.
### req:_sync/004
- **004** `Effect::broadcast(channel, effect_batch)` sends an `EffectBatch` to all subscribers of a named channel. Used for presence updates and live collaboration. The server framework manages the transport (WS/SSE).
### req:_sync/005
- **005** `#[slhx_sync::presence]` is an attribute macro on functions that return `impl IntoEffect` when a user joins or leaves a shared session. Emits `Effect::broadcast` over a presence channel scoped to the session.
### req:_sync/006
- **006** `slhx-sync` uses a flat patch model per atom, not CRDT by default. Server is authoritative; clients apply server-canonical state on conflict. Optional CRDT backend may be provided by a future `slhx-crdt` crate.
### req:_wire/001
- **001** HTML wire format is standard HTML with `data-hid` and `data-sid` attributes only. No custom markup, no hx-* attributes.
### req:_wire/002
- **002** POST bodies carry `application/x-www-form-urlencoded` with distinguished field `__h` (handle id). Server routes by numeric id, not by URL path.
### req:_wire/003
- **003** Responses are `text/html` fragments (or `application/slhx` for push streams). Fragments may contain `<template data-slhx>` elements whose text content is a base64url-encoded postcard `EffectBatch`. JS decodes and applies.
### req:_wire/004
- **004** Server push is supported orthogonally: `Effect::push(stream, effect)` sends a pre-serialized effect batch over an SSE or WebSocket connection. Connection management is a server-framework concern.
## Coverage: 0/68 (100.0% uncited)