diff --git a/AGENTS.md b/AGENTS.md index a8e5abf..87b2351 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,7 @@ Keep it stable. Prefer pointers to canonical sources over copied structure, file - Use the same Workout command surface for tests, production build, and mobile release: `cargo run -p hemx-xtask -- workout test`, `cargo run -p hemx-xtask -- workout build`, `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-release`, and `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-verify`; Android/iOS SDKs, store submission targets, and signing remain external blockers, not repo-owned secrets, and do not imply a broad `hemx-mobile` framework. req: examples/006 req: examples/011 req: examples/013 - hemx core stays small: effects, typed ids, registries, and wire schema only; keep features in core only when they fit typed resources plus the closed EffectBatch op set, and treat DOM details as runtime lowering. Workspace crates stay separated, stable-Rust-compatible, and free of kitchen-sink boundaries; new primitives must delete special cases. Public identifiers should flow through typed wrappers over internal `ResourceId`/`ResourceRef`, not special-case opcodes. Wire output lowers symbolic authoring names to compact metadata, the versioned canonical hemx `EffectBatch` codec, postcard surface facts, and form-encoded public requests—not JSON. ABI/schema versions and build fingerprints must guard runtime/server compatibility. v0 scope is the checked hypermedia core plus page/runtime/wire/diagnostic/test/axum proof, not optional sync/wasm/query/auth/router breadth. req: v0_scope/001 req: v0_scope/002 req: v0_scope/005 req: laws/001 req: invariant/001 req: invariant/005 req: typed_id/001 req: typed_id/003 req: effect_algebra/001 req: effect_algebra/006 req: wire/001 req: wire/002 req: wire/003 req: wire/004 req: wire/005 req: wire/006 req: abi/001 req: abi/002 req: abi/003 req: abi/004 req: abi/005 req: misc/001 req: misc/002 req: misc/003 req: misc/004 req: misc/005 req: misc/006 req: misc/007 req: misc/008 req: misc/009 req: misc/010 - Routing, auth, sessions, transport, transitions, sync, async data helpers, multipart parsing/uploads, and storage belong in integration/user crates; hemx-axum preserves normal HTTP auth, credentials, CSRF, multipart/browser fallback, and progressive-enhancement semantics rather than defining policy in core. Sync is optional integration state reconciliation over push/transport, not core. req: auth/001 req: auth/002 req: auth/003 req: auth/004 req: auth/005 req: async_data/001 req: async_data/002 req: async_data/003 req: multipart/001 req: multipart/002 req: multipart/003 req: sync/001 req: sync/008 +- The SaaS production reference must use real links/URLs for navigation and an ongoing server-owned canonical SSE stream; bounded one-event behavior is a test probe, not the public transport contract. req: nav/001 req: nav/002 req: push/003 req: examples/014 - Public examples and beginner APIs should use templates plus Rust, generated component APIs, resources, view wrappers, render/page helpers, `#[hemx::app]`, plain `#[hemx::handler]` functions, and `IntoEffect`, not atoms, raw ids, selectors, wire formats, runtime opcodes, manual registries, `$OUT_DIR` includes, raw render/lower calls, raw HTML construction, imperative DOM mutation, or raw effect constructors; keep advanced layers out of starters. req: canonical_authoring/001 req: canonical_authoring/004 req: canonical_authoring/006 req: canonical_authoring/010 req: canonical_authoring/015 req: invariant/003 req: dx/001 req: dx/002 req: dx/010 req: component/003 req: component/004 req: component/005 req: view/001 req: view/002 req: view/003 req: html_safety/001 req: html_safety/003 req: html_safety/005 req: public_api/001 req: public_api/002 req: public_api/003 req: public_api/005 req: public_api/006 req: progressive_disclosure/001 req: progressive_disclosure/002 req: progressive_disclosure/003 req: derive_app/001 req: derive_app/002 req: derive_handler/001 req: derive_handler/002 req: derive_handler/003 req: derive_handler/004 req: derive_handler/005 - Typed partial swaps should stay expressed as generated target plus rendered partial plus swap kind, not selector-driven rerendering or response-side selector retargeting; HTTP, page navigation, push, and island behavior adapt around that loop, and docs should layer new primitives progressively. Navigation is an effect/page-swap concern, not a core router framework; enhanced links and GET forms preserve real URL/history semantics so page state stays reloadable/shareable without a client state graph. Push streams carry canonical versioned hemx `EffectBatch` bytes over server-owned SSE/WebSocket transport and keep `data-hemx-sse` root-scoped/same-origin by default. Preserve keyed/optional scope identity for addressable loop nodes, reconcile filtered keyed collections without clearing retained rows, prefer generated keyed-slot helpers over low-level keyed calls, and route self/row-update diagnostics toward local `data-hemx-slot`/`h-key` targets. req: canonical_authoring/002 req: canonical_authoring/014 req: modes/001 req: scope/001 req: list/001 req: list/002 req: list/003 req: list/004 req: list/005 req: list/006 req: nav/001 req: nav/002 req: nav/003 req: nav/004 req: nav/005 req: push/001 req: push/002 req: push/003 req: push/004 req: push/005 req: push/006 req: push/007 req: progressive_disclosure/004 req: page_swap/001 req: page_swap/002 req: page_swap/003 req: locality/001 req: locality/002 req: target_policy/001 req: target_policy/002 - `examples/html_examples` is the copy-paste HTML pattern gallery for htmx-style examples; keep exact htmx URL slugs visible while translating behavior to boring `.heml`, generated resources, and server-owned Rust state, not HTMX syntax, selector targeting, or user-authored browser JavaScript. Shared runtime loading and declarative `data-hemx-*` are allowed. Boost containers enhance same-origin descendants only and preserve native external/download/new-tab behavior. req: htmx_equivalents/001 req: htmx_equivalents/003 req: htmx_equivalents/005 req: examples/005 req: examples/007 req: examples/012 req: page_swap/007 req: page_swap/008 diff --git a/PLAN.md b/PLAN.md index 763a246..959fd83 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,41 +1,21 @@ -# Active frontier — v1 production readiness +# Active frontier — peak idiomatic examples -**Parent outcome** +**Parent ID:** `examples-idiom/001` -- **User value:** Rust teams can adopt hemx v1 knowing its public contracts, failure recovery, compatibility, dependency policy, and release proof are explicit and reproducible. -- **State:** In progress — the product and representative runtime/browser/performance/security paths are implemented, but v1 production readiness is not yet closed. -- **Blocked by:** the repository owner must approve a distribution license policy before the legal release gate can close; this does not block mutation hardening or local release verification. -- **Non-goals:** no new framework surface, second wire format, WASM runtime decoder, deployment, publication, signing, store submission, or feature expansion. -- **Proof:** every child below is done; `docs/v1-readiness.md` reports GO without contradicting `REQUIREMENTS.md`; the tree is committed and clean. +- **User value:** A newcomer can move from first app to production reference and advanced integration while seeing one coherent hemx model: plain `.heml`, generated resources, typed Rust handlers/effects, real links/forms/URLs, server-owned truth, native fallback, and explicit leaf adapters. +- **State:** Ready — the canonical starter and Workout exemplar are already strong; bounded teaching leaks remain in the HTML gallery, client-local example, SaaS reference, and advanced Kanban boundary. +- **Non-goals:** no new framework primitives, client router, VDOM, selector authoring API, reactive expression language, global client store, CSS framework/design system, generalized asset pipeline, visual redesign, or feature expansion. Do not churn `examples/workout` or `examples/techdemo` without a concrete failing contract. +- **Build:** + - **`examples-idiom/002` — Done: the SaaS reference tells the production truth.** Settings is a real `/settings` link enhanced by the existing page-swap runtime, direct `/settings` renders the fallback page, and `/events` now sends an initial canonical batch followed by ongoing server-owned status updates; `?once` remains only as a bounded production-reference probe. The removed handler no longer simulates navigation with response effects. Raw selector/form construction remains confined to test adapters because generated handles are the asserted boundary, not an app authoring API. req: canonical_authoring/001 req: nav/001 req: nav/002 req: nav/004 req: push/003 req: examples/003 req: examples/004 req: examples/014 + - **`examples-idiom/003` — Make the HTML pattern gallery mechanically copyable.** In `examples/html_examples/src/main.rs`, generated `gallery` resources, and nearby templates/tests, express the dependent-select flow through typed generated values/partials instead of duplicated string-to-option mapping, and remove request fields discarded only to satisfy example plumbing where the generated handler contract permits it. Preserve each visible htmx slug, native form semantics, server-owned state, inserted-content behavior, and no-reload smoke. req: htmx_equivalents/001 req: htmx_equivalents/003 req: htmx_equivalents/005 req: canonical_authoring/001 req: form/004 + - **`examples-idiom/004` — Make client-local code visibly a leaf adapter.** In `examples/client_local/src/lib.rs` and its `.heml` surface, project the generated client event/state into a tiny typed counter-domain input/output instead of rendering raw event kind and encoded state as the example's product value. Keep the ordinary `#[hemx::handler(client)]` shape, generated event/state boundary, native event semantics, and no durable client state graph. req: client_local/001 req: client_local/003 req: client_local/004 req: canonical_authoring/017 + - **`examples-idiom/005` — Separate the advanced Kanban adapter from ordinary hemx app code.** Move the cohesive sync/presence/session/storage transport responsibility from `examples/kanban/src/main.rs` behind one clearly named local integration module with a small route/state contract; keep board templates and ordinary handlers nearby and unchanged where possible. Update `examples/kanban/README.md` to label the fixture as the advanced local/offline/sync north-star, route beginners to the starter/Workout/gallery first, and name legacy `/sync-demo`/`sync.js` as a compatibility probe rather than recommended authoring. Preserve replay, export, deletion, reconnection, auth, and multiplayer browser proof. req: state/001 req: state/002 req: local/001 req: local/002 req: milestone/001 req: sync/001 req: sync/008 + - **`examples-idiom/006` — Publish and enforce the example ladder.** In `README.md`, example READMEs, and the nearest existing xtask/example checks, identify `app new`/`examples/v0` as the first canonical app, `html_examples` as the pattern gallery, Workout as the product exemplar, SaaS as the production integration reference, `client_local` as the narrow leaf-adapter proof, Techdemo as exhaustive verification, and Kanban as advanced north-star integration. Add the smallest repository-owned guard that fails when beginner/reference authoring regresses to raw IDs, selectors, raw wire/effect constructors, `$OUT_DIR` includes, or app-authored DOM mutation; keep legitimate advanced/test adapters scoped rather than banning tokens globally. req: examples/001 req: examples/003 req: examples/004 req: examples/005 req: examples/007 req: examples/008 req: examples/009 req: canonical_authoring/001 req: progressive_disclosure/001 req: progressive_disclosure/002 req: progressive_disclosure/003 +- **Blocked by:** none. The separate v1 legal release gate remains blocked on the owner license decision but does not block example work. +- **Proof:** each slice must make its user path observable, preserve the named native/recovery path, and pass its focused package/browser proof. Parent closure requires `cargo run -p hemx-xtask -- html-examples-smoke`, `cargo run -p hemx-xtask -- workout test`, focused SaaS/client-local/Kanban package tests, `cargo run -p hemx-xtask -- test`, `cargo check --workspace`, `cargo fmt --check`, `redgate list`, `redgate refs`, and a clean diff. Frustration signals to reject: a beginner must author selectors/raw IDs/wire ops; navigation loses URL/history; JavaScript becomes durable truth; an example claims live/recovery behavior with a one-shot stub; or advanced sync machinery appears to be the default app model. -## 1. Checkpoint the current robustness slice +## Blocked release decision retained -- [x] **State:** Done — committed as the current robustness slice after full workspace, focused compile-fail, requirements-reference, formatting, and check proof. -- **User value:** users receive one explicit typed `EffectBatch` codec and stronger fail-closed form, sync, host, WASM, macro, test-harness, and generated-contract boundaries without parallel magic. -- **Build:** review the current diff as one coherent behavior/requirements slice; retain the canonical non-WASM `EffectBatch` codec and remove the duplicate postcard batch API; keep only mutation-driven tests and simplifications that prove public behavior; run the full local test authority; commit the slice. -- **Blocked by:** none. -- **Proof:** `cargo run -p hemx-xtask -- test`, `cargo fmt --check`, `cargo check --workspace`, `redgate list`, and `redgate refs` pass; `git diff --check` is clean; the focused mutest receipts for the effect codec, `hemx`, `hemx-host`, `hemx-js`, `hemx-wasm`, `hemx-sync`, `hemx-sync-macros`, `hemx-test` public helpers, and `hemx-axum` form/SSE codecs contain no missed mutants; one Conventional Commit records the requirement IDs. - -## 2. Make mutation testing a reproducible release gate - -- [x] **State:** Done — the package-native capped xtask entry point rejects unknown packages and invalid shards, propagates mutest failure, and every mutation-applicable library/proc-macro package passes its complete gate or deterministic shard set with no unexplained survivor. -- **User value:** maintainers can run one bounded repository command and trust that meaningful Rust logic across every mutation-applicable library is either killed or explicitly justified. -- **Build:** add a capped `hemx-xtask` mutation command that invokes `/opt/repositories/mutest`/`mutest` through package-native test targets rather than the broken workspace-wide example path; enumerate only current mutation-applicable library/proc-macro packages; finish adversarial tests or simplify code until every survivor is classified; keep equivalent, invariant-only, and infrastructure-inapplicable classifications inspectable and minimal; document the exact local release command in the existing readiness surface. -- **Blocked by:** none. `hemx-lsp` is binary-only and deliberately outside the mutation-applicable library/proc-macro set elected by test/020; its package tests remain in workspace verification. The runner creates first-use shard output parents, accepts validated one-based shards, maps them to native zero-based shards, uses shard-specific output, grants compiler probes a 120-second floor, and preserves the unsharded gate. Browser-backed `hemx-wasm` runs one mutest worker without a parent jobserver, preventing nested Cargo/Firefox deadlock while preserving the detected Cargo budget; its four shards pass with 59 mutants (52 caught, 7 unviable) after replay projections were batched without changing ordered effect application, Firefox background work was disabled, and the explicit 64-command debug-browser replay budget was set to 250 ms. The complete `hemx` (30 mutants), `hemx-host` (76), `hemx-sync` (164), `hemx-sync-macros` (33), `hemx-axum` (470), `hemx-build` (1,304), and `hemx-derive` (480) gates or deterministic shard sets pass, alongside the previously clean `hemx-core`, `hemx-js`, and `hemx-test` gates. -- **Proof:** the new xtask mutation command exits zero within its documented bound, covers each applicable package, emits no unexplained missed mutant, and a deliberate adjacent mutation makes it fail. `cargo run -p hemx-xtask -- test` remains green. req: test/020 req: test/021 - -## 3. Elect and enforce the release license policy - -- [ ] **State:** Needs decision -- **User value:** adopters can legally evaluate and redistribute hemx with a machine-checked dependency license boundary. -- **Build:** owner chooses the repository distribution license and accepted dependency licenses; add the corresponding root license file(s) and minimal `deny.toml`; classify workspace crates and the current dependency set; run strict license, advisory, and source checks; reject unknown/unlicensed dependencies rather than silently broadening policy. -- **Blocked by:** owner legal decision: choose the repository license and whether weak-copyleft dependencies are acceptable. Current dependency licenses observed by `cargo deny list` include Apache-2.0, Apache-2.0 WITH LLVM-exception, BSD-3-Clause, BSL-1.0, MIT, Unicode-3.0, Unlicense, plus unlicensed workspace packages because the repository has no elected license. -- **Proof:** `cargo deny check licenses advisories sources` exits zero from repository configuration; every allowed license is explicit; the repository license is visible at the root; adding a disallowed/unlicensed fixture fails the gate. req: security/007 req: v1_release/006 - -## 4. Issue the final v1 GO/NO-GO decision - -- [ ] **State:** Blocked by slices 1–3 -- **User value:** users get an honest release candidate whose documented support, recovery, accessibility, performance, examples, and operational behavior match what was actually proven. -- **Build:** rerun the complete local release matrix from `docs/v1-readiness.md`, including mutation and license gates; verify the pinned Rust/browser/WASM/Axum compatibility matrix and migration fixture; run browser, offline/reconnect, mixed-deploy recovery, accessibility, security, performance, docs, and canonical-example proofs; independently audit requirement-to-proof coverage and README/readiness contradictions; fix only release-blocking defects; mark GO only when no P0/P1 or unexplained gate failure remains. -- **Blocked by:** slices 1–3; publishing/deployment authority remains separate and is not required for a local GO decision. -- **Proof:** all commands in `docs/v1-readiness.md` pass from a clean checkout within documented bounds; `redgate list` and `redgate refs` pass; known installed `redgate health`/`lint` corpus-format warnings are either resolved or accurately documented rather than hidden; `docs/v1-readiness.md` says GO and names zero open release blockers; independent contradiction review agrees. req: v1_release/001 req: v1_release/002 req: v1_release/003 req: v1_release/004 req: v1_release/005 req: v1_release/006 req: v1_release/007 req: v1_release/008 req: v1_release/009 req: v1_release/010 +- [ ] **State:** Needs decision — choose the repository distribution license and approved third-party SPDX set, then add root license file(s), workspace package metadata, `deny.toml`, and rerun the release matrix. +- **Blocked by:** owner/legal authority. Current third-party set includes Apache-2.0, Apache-2.0 WITH LLVM-exception, BSD-3-Clause, BSL-1.0, MIT, Unicode-3.0, and Unlicense; all 20 workspace packages currently lack license metadata. +- **Proof:** `cargo deny check advisories sources licenses` and the full `docs/v1-readiness.md` matrix pass, then readiness changes from NO-GO to GO without publication or deployment. diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index b511f8b..64edf70 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -852,6 +852,8 @@ what a valid business email is. [north_star] 013 Workout mobile commands must not add a broad `hemx-mobile` framework. [north_star] +014 The SaaS production reference uses real URL navigation and an ongoing server-owned SSE status stream; its bounded test probe emits the same canonical hemx event shape. [north_star] + --- ## ms diff --git a/examples/saas/src/lib.rs b/examples/saas/src/lib.rs index cb8cfe9..64c6f7b 100644 --- a/examples/saas/src/lib.rs +++ b/examples/saas/src/lib.rs @@ -2,7 +2,7 @@ pub mod ui {} use hemplate::Hemplate; -use hemx::{push, Html, IntoEffect}; +use hemx::{Html, IntoEffect}; use hemx_axum::{ interactions, runtime_js_path, Form, HandlerErrorContext, HandlerFailure, IntoHandlerFailure, Registry, State, @@ -565,15 +565,6 @@ mod dashboard_handlers { dashboard::live_status.set(format!("{total} projects persisted locally")), )) } - - #[hemx::handler] - pub async fn open_settings(State(_ctx): State) -> impl IntoEffect { - ( - dashboard::page_panel.put(&SettingsPage::production_boundaries()), - dashboard::nav.set("Settings"), - push("/settings"), - ) - } } pub fn live_status(projects: usize) -> impl IntoEffect { @@ -740,23 +731,10 @@ mod tests { assert!(created.updates_text(dashboard::live_status)); } - #[tokio::test] - async fn page_swap_and_push_shape_use_generated_targets() { - // req: page_swap/002 req: push/003 req: examples/001 + #[test] + fn live_status_uses_the_generated_dashboard_target() { + // req: push/003 req: examples/014 let ctx = AppContext::demo(); - let settings = inspect_batch( - InteractionRequest::from(form(dashboard::open_settings, &[])) - .dispatch_async(registry(ctx.clone())) - .await - .unwrap() - .batch, - ); - assert!( - settings.updates_html_containing(dashboard::page_panel, "explicit app integrations") - ); - assert!(settings.updates_text(dashboard::nav)); - assert!(settings.pushes_to("/settings")); - let heartbeat = inspect(live_status(ctx.projects().len())); assert!(heartbeat.updates_text(dashboard::live_status)); assert!(heartbeat.payload_contains("heartbeat")); diff --git a/examples/saas/src/main.rs b/examples/saas/src/main.rs index 672668b..999e677 100644 --- a/examples/saas/src/main.rs +++ b/examples/saas/src/main.rs @@ -5,14 +5,14 @@ use axum::middleware::{self, Next}; use axum::response::{IntoResponse, Response}; use axum::routing::{get, post}; use axum::Router; -use futures_util::stream; +use futures_util::{stream, StreamExt}; use hemx::IntoEffect; use hemx_axum::{runtime_js, runtime_js_path, sse, EffectResponse, InteractionRequest}; use hemx_saas_example::{home_page, live_status, registry, settings_page, ui, AppContext}; use std::collections::BTreeMap; use std::convert::Infallible; use std::path::PathBuf; -use std::time::Instant; +use std::time::{Duration, Instant}; #[tokio::main] async fn main() -> Result<(), Box> { @@ -81,16 +81,22 @@ async fn events( Query(params): Query>, State(ctx): State, ) -> impl IntoResponse { - let count = ctx.projects().len(); + // The production reference exposes an ongoing server-owned stream; `once` + // keeps a bounded probe for package tests without changing the public path. + // req: examples/014 + let event = |ctx: &AppContext| { + Ok::<_, Infallible>(live_status(ctx.projects().len()).into_batch(ui::BUILD_FINGERPRINT)) + }; + let initial = stream::once(std::future::ready(event(&ctx))); if params.contains_key("once") { - return sse(stream::iter([Ok::<_, Infallible>( - live_status(count).into_batch(ui::BUILD_FINGERPRINT), - )])); + return sse(initial.left_stream()); } - sse(stream::iter([Ok::<_, Infallible>( - live_status(count).into_batch(ui::BUILD_FINGERPRINT), - )])) + let updates = stream::unfold(ctx, move |ctx| async move { + tokio::time::sleep(Duration::from_secs(15)).await; + Some((event(&ctx), ctx)) + }); + sse(initial.chain(updates).right_stream()) } // req: auth/001 req: auth/002 req: auth/004 diff --git a/examples/saas/templates/dashboard.heml b/examples/saas/templates/dashboard.heml index 26c5618..bc2efdf 100644 --- a/examples/saas/templates/dashboard.heml +++ b/examples/saas/templates/dashboard.heml @@ -7,7 +7,7 @@
diff --git a/examples/saas/tests/production_reference.rs b/examples/saas/tests/production_reference.rs index 84921ef..f027ed9 100644 --- a/examples/saas/tests/production_reference.rs +++ b/examples/saas/tests/production_reference.rs @@ -125,6 +125,25 @@ fn authenticated_project_mutation_is_atomic_and_survives_restart() { ); assert!(!home.contains("