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 ### req: component/002
002 slhx supports colocated layout: `todo_list.heml` beside `todo_list.rs`, with generated APIs namespaced by component to avoid global symbol soup. 002 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 ### req: component/004
004 `#[slhx::surface]` bridges generated code into a user module. Users write `#[slhx::surface] mod ui {}` instead of `include!(concat!(env!("OUT_DIR"), ...))`. slhx-build emits `slhx.generated.rs` which the macro expands in place. No direct `$OUT_DIR` includes in user-authored source. 004 `#[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 ## codegen
### req: codegen/001 ### 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 ### 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`. 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. 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 ### 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 ### 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. 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. 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 ### 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 ## list
### req: list/001 ### 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 ### 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. 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 ## typed_id
### req: typed_id/001 ### 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 ### 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. 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. 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 ### 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 ## scope
### req: scope/001 ### 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 ## wire
### req: wire/001 ### 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 ### 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. 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. 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 ### 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 ### req: wire/005
005 No JSON anywhere in slhx-internal artifacts. Public request/response envelopes use `application/x-www-form-urlencoded`; effects and symbols use postcard. `application/json` is acceptable only at integration boundaries. 005 No JSON anywhere in 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] 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 ### 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 ### 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 ### 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. 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 ## js
### req: js/001 ### 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 ### req: js/002
002 No build step, no virtual DOM, no diffing, no scheduler. Receiving an effect = apply ops immediately in declared order. 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. 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 ### 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 ### req: check/003
003 Renaming a slot or handle breaks `cargo check` immediately with a span pointing to the Rust handler or template source. 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. 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 ### 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 ### 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 ### 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 ### 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 ## interop
### req: interop/001 ### 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 ### 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. 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 ### 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. 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 ## test
@@ -410,6 +421,9 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
### req: nav/002 ### req: nav/002
002 Navigation modes: `Push` (history.pushState), `Replace` (replaceState), `Redirect` (server-side 302). Scroll behaviour: `Preserve`, `Top`, `Element(ResourceId)`. Title is optional. 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 ## client_local
@@ -478,7 +492,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
## derive_app ## derive_app
### req: derive_app/001 ### 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.
--- ---