feat: integrate 15-point feedback into requirements

- Replace hypothetical @for/@if with real hemplate h-for/h-key/h-if
- Split authoring attrs (data-slhx-*) from runtime attrs (data-hid/data-sid)
- Split ResourceId (compile-time) from ResourceRef (runtime+scope)
- Fix state wording: Effect::set is publish, not subscribe
- Form errors via control ids, not slot ids
- Macro policy: #[slhx::handler] required, optional ergonomic helpers
- Add component/003 (namespaced APIs) and surface/008-010
- Add opt-in content_ids for raw/pre-rendered HTML tracking
- Runtime delegation on root, not document; 5KB target
- Sync/Query effects as external types (SyncEffect/QueryEffect)
- derive_app: no singleton, multiple roots/widgets per process
- hemplate htmx as raw attrs only; migration tool separate
- nav/003: preserve real href links for progressive enhancement
- check/002: honest Rust build model for missing handler checks
This commit is contained in:
slhx agent
2026-05-10 11:54:32 +02:00
parent 705ca3c72b
commit 57dc22b8cc
+33 -19
View File
@@ -91,6 +91,11 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
### req: component/002
002 slhx supports colocated layout: `todo_list.heml` beside `todo_list.rs`, with generated APIs namespaced by component to avoid global symbol soup.
### req: component/003
003 Generated APIs are component-namespaced by default:
`ui::slots::todo_row`, `ui::handles::create`, `ui::forms::create`.
Global exports are opt-in only.
### req: component/004
004 `#[slhx::surface]` bridges generated code into a user module. Users write `#[slhx::surface] mod ui {}` instead of `include!(concat!(env!("OUT_DIR"), ...))`. slhx-build emits `slhx.generated.rs` which the macro expands in place. No direct `$OUT_DIR` includes in user-authored source.
@@ -101,7 +106,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
## codegen
### req: codegen/001
001 `slhx_build` generates three artifacts from the generic Surface IR: (a) `slhx.generated.rs` containing ergonomic resource modules (`slots`, `handles`, `forms`, `atoms`), (b) `slhx.syms` for proc-macro validation, (c) runtime id-lowering tables. `slhx_build` interprets tool-specific conventions (`data-slhx-*`, `@for key`, form controls) from the Surface. [north_star]
001 `slhx_build` generates three artifacts from the generic Surface IR: (a) `slhx.generated.rs` containing ergonomic resource modules (`slots`, `handles`, `forms`, `atoms`), (b) `slhx.syms` for proc-macro validation, (c) runtime id-lowering tables. `slhx_build` interprets tool-specific conventions (`data-slhx-*`, `h-for`, `h-key`, form controls) from the Surface. [north_star]
### req: codegen/002
002 Generated module `slots` exposes ergonomic methods: `Slot<T>::render(value)`, `Slot<T>::text(value)`, `KeyedSlot<K,T>::append(key, value)`, `KeyedSlot<K,T>::replace(key, value)`, `KeyedSlot<K,T>::remove(key)`. Methods return `impl IntoEffect`.
@@ -233,7 +238,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
002 Subscription is explicit: `Effect::set(atoms::FOO, 42)` pushes the new value to all consumers. No automatic component re-render graph.
### req: state/003
003 JS runtime maintains a client-side atom store with identical API to the server: `get<T>(atom)`, `set<T>(atom, value)`, `subscribe<T>(atom, callback)`. Type erased at runtime with `TypeId`.
003 The JS runtime maintains a client-side atom store keyed by `AtomId`. Runtime values are type-erased postcard bytes. Types are compile-time only. A deterministic `TypeHash` may be generated by `slhx_build` for diagnostics, but the JS runtime does not depend on Rust `TypeId`.
### req: state/004
004 SSR pages carry a `data-slhx-st` base64url-encoded postcard blob on the document root. Runtime decodes it into the client atom store. Atoms computed from server state are immediately available to client-side handlers without a round-trip.
@@ -252,14 +257,14 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
002 The handle id is carried as `__h` in POST `application/x-www-form-urlencoded`. A JSON body is allowed at the integration boundary (`application/json`) only if the handler accepts it; core uses form encoding.
### req: form/003
003 handler receives `form: Form<CreateTodo>`. Validation errors are returned as `Effect::form_error(field, message)`, which the JS runtime maps back to the originating input via `data-sid`.
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.
---
## list
### 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)`.
001 Any `data-slhx-slot` or `data-slhx-handle` inside a hemplate `h-for` scope requires a stable key. Preferred syntax: `<template h-for="item in &self.items" h-key="item.id"> ... </template>`. Without a key, slhx-addressable nodes inside the loop are rejected at build time. Keyed identity is `(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.
@@ -272,7 +277,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
## typed_id
### req: typed_id/001
001 All public cross-page identifiers (`Slot`, `Atom`, `Handle`, `Form`) share a single internal primitive `ResourceId { kind: ResourceKind, id: u32, key: Option<ScopeKey> }`. Typed wrappers (`Slot<T>`, `KeyedSlot<K, T>`, `Atom<T>`, `Handle<I>`, `Form<T>`) enforce kind safety at compile time. No special-case opcodes per resource kind; effects address resources uniformly. [north_star]
001 All public cross-page identifiers (`Slot`, `Atom`, `Handle`, `Form`) share a single internal primitive `ResourceId { kind: ResourceKind, id: u32 }`. A concrete runtime target is a `ResourceRef { resource: ResourceId, scope: Option<ScopeKey> }`. Typed wrappers (`Slot<T>`, `KeyedSlot<K, T>`, `Atom<T>`, `Handle<I>`, `Form<T>`) enforce kind safety at compile time. No special-case opcodes per resource kind; effects address resources uniformly. [north_star]
### req: typed_id/002
002 `ResourceKind` is an internal closed enum (Slot, Atom, Handle, Form, Route). External crates may not add variants. Extensibility comes via `Effect::event`, `Effect::emit`, or custom `IntoEffect` implementations, never via new `ResourceKind` variants in core.
@@ -285,21 +290,21 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
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: async_data/002
002 `Effect::resource(res).reload()` triggers a re-fetch and re-render. The server sends a new EffectBatch when data is ready.
002 `QueryEffect::reload(res)` triggers a re-fetch and re-render. The server sends a new `EffectBatch` when data is ready. Query/Resource effects live in a separate API surface to keep core small.
---
## scope
### req: scope/001
001 `Scope` is a first-class primitive. Keyed loops (`@for`), conditional branches (`@if`), component instances, modals, tabs, nested forms — all are scopes. A slhx-addressable node inside any dynamic scope must carry a stable `ScopeKey`. Composite identity is `(ResourceId, ScopeKey)`. [north_star]
001 `Scope` is a first-class primitive. Keyed loops (`h-for`), conditional branches (`h-if`), component instances, modals, tabs, nested forms — all are scopes. A slhx-addressable node inside any dynamic scope must carry a stable `ScopeKey`. Composite identity is `(ResourceId, ScopeKey)`. [north_star]
---
## wire
### req: wire/001
001 HTML wire format is standard HTML with `data-hid` and `data-sid` attributes only. No custom markup, no hx-* attributes.
001 Authoring HTML uses symbolic `data-slhx-*` attributes. Rendered runtime HTML lowers these to compact numeric metadata: `data-hid`, `data-sid`, optional `data-key`, optional atom/bootstrap ids. The browser never sees handler or slot names. `data-slhx-root` marks a scoped root boundary.
### req: wire/002
002 POST bodies carry `application/x-www-form-urlencoded` with distinguished field `__h` (handle id). Server routes by numeric id, not by URL path.
@@ -308,7 +313,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
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.
004 Server push is orthogonal: integration crates stream postcard `EffectBatch` over SSE or WebSocket connections. slhx core owns the effect bytes; transport and connection management are integration concerns.
### req: wire/005
005 No JSON anywhere in slhx-internal artifacts. Public request/response envelopes use `application/x-www-form-urlencoded`; effects and symbols use postcard. `application/json` is acceptable only at integration boundaries.
@@ -321,10 +326,10 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
001 Every slhx tree must declare a root boundary via `data-slhx-root` on an ancestor element. The JS runtime resolves lookups within that root only. Multiple independent slhx apps/widgets/modals may coexist on the same document without ID collision. [north_star]
### req: runtime/002
002 JS runtime attaches a single delegated listener per event type on the root. No per-node listeners. Dispatch resolves target via `data-hid` / `data-sid` attributes on the event path.
002 JS runtime attaches a single delegated listener per event type on each `data-slhx-root`. No per-node listeners. Dispatch resolves target via `data-hid` / `data-sid` attributes on the event path scoped to its root.
### req: runtime/003
003 The JS runtime is a tiny op interpreter (~2KB, no selectors, no VDOM, no scheduler, no expressions). It reads postcard `EffectBatch` bytes and applies them as DOM operations.
003 The core JS runtime target is under 5KB minified+gzipped. It remains a tiny op interpreter (no selectors, no VDOM, no scheduler, no expressions). It reads postcard `EffectBatch` bytes and applies them as DOM operations. Optional sync/transition/WASM helpers are separate files.
### req: runtime/004
004 `RuntimeCaps { binary, wasm, sync, transitions }` is exchanged once at init. Handlers may query capabilities and degrade gracefully (e.g. fall back to server request if WASM unavailable). Core does not know about these features; caps are opaque to the wire protocol and only consulted by integrations.
@@ -334,7 +339,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
## js
### req: js/001
001 Runtime reads attributes `data-hid` and `data-sid`, delegates events on `document`, and applies effects by direct DOM mutation.
001 Runtime reads attributes `data-hid` and `data-sid`, delegates events on each `data-slhx-root`, and applies effects by direct DOM mutation scoped to that root.
### req: js/002
002 No build step, no virtual DOM, no diffing, no scheduler. Receiving an effect = apply ops immediately in declared order.
@@ -350,7 +355,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
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.
002 Dead/missing handle diagnostics are best-effort by default. With `#[slhx::component]`, dead/missing handlers inside the component are checked at `cargo check`. Without it, missing implementations are caught at app mount or in generated registry tests.
### req: check/003
003 Renaming a slot or handle breaks `cargo check` immediately with a span pointing to the Rust handler or template source.
@@ -369,23 +374,26 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
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.
003 Server reconciliation of received patches produces a local `EffectBatch` only when state changes. Accepted mutations patch shared state without hard-coding specific effects.
### req: sync/004
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).
004 `SyncEffect::broadcast(channel, effect_batch)` sends an `EffectBatch` to all subscribers of a named channel. Used for presence updates and live collaboration. The server framework manages the transport (WS/SSE).
### req: sync/005
005 `#[slhx_sync::presence]` is an attribute macro on functions that return `impl IntoEffect` when a user joins or leaves a shared session. Emits `Effect::broadcast` over a presence channel scoped to the session.
005 `#[slhx_sync::presence]` is an attribute macro on functions that return `impl IntoEffect` when a user joins or leaves a shared session. Emits `SyncEffect::broadcast` over a presence channel scoped to the session.
### 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.
006 `SyncEffect::ack(atom)` acknowledges a successful server-side mutation, allowing the client to clear its local optimistic queue for that atom.
### req: sync/007
007 `slhx-sync` uses a flat patch model per atom, not CRDT by default. Server is authoritative; clients apply server-canonical state on conflict. Optional CRDT backend may be provided by a future `slhx-crdt` crate.
---
## interop
### req: interop/001
001 `Effect::event` (see req:effect/006) is the single bridge between slhx and third-party JS. External widgets, charts, and maps listen via native `CustomEvent`. slhx core does not inspect or manage them. [north_star]
001 `Effect::event` is the single bridge between slhx and third-party JS. External widgets, charts, and maps listen via native `CustomEvent`. slhx core does not inspect or manage them. [north_star]
### req: interop/002
002 Web Components and custom elements are valid opaque leaf nodes. slhx does not inspect shadow DOM. Escape hatches are leaves, never app foundations.
@@ -393,6 +401,9 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
### req: interop/003
003 WASM islands (`#[slhx::island]`) compile handler code to WASM for client-local execution. The island is a leaf in the DOM; slhx core is unaware of WASM except via the same `EffectBatch` contract.
### req: interop/004
004 Existing `hx-*` attributes are treated as ordinary raw attributes in the hemplate Surface without slhx semantics. An optional `slhx-htmx-migrate` tool may read Surface `hx-*` attrs and suggest equivalent `data-slhx-*` handlers/effects.
---
## test
@@ -410,6 +421,9 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
### req: nav/002
002 Navigation modes: `Push` (history.pushState), `Replace` (replaceState), `Redirect` (server-side 302). Scroll behaviour: `Preserve`, `Top`, `Element(ResourceId)`. Title is optional.
### req: nav/003
003 Navigation enhancement preserves real anchors. Links keep valid `href`. slhx may intercept enhanced links through `data-slhx-handle` or `data-slhx-nav`, but without JS the browser performs normal navigation.
---
## client_local
@@ -478,7 +492,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
## derive_app
### 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.
001 `#[slhx::app]` marks an application/root state type and registry entry point. There may be one registry per app/root type, and multiple runtime instances may exist per process. slhx does not require a process-global singleton.
---