Files
hemx/.agents/skills/idiomatic-hemx/SKILL.md
T
2026-08-03 00:09:43 +02:00

338 lines
18 KiB
Markdown

---
name: idiomatic-hemx
description: >-
Use when a user explicitly asks how to design, implement, review,
productionize, or scale an application built with hemx and hemplate,
including deciding whether or where microservices belong. Preserve hemx's
generated-resource and server-owned effect model, then choose the smallest
evidence-backed scale stage. Do not use for generic Rust/Axum architecture,
generic microservice advice, or development of the hemx framework itself.
---
# Idiomatic hemx
## One job
Choose, explain, or implement the smallest production architecture that keeps a
hemx/hemplate application boring as it grows. The high-leverage move is usually
to preserve one direction of travel:
```text
semantic .heml surface
-> generated typed resources
-> ordinary Rust domain/application code
-> generated partial/page effects
-> tiny browser runtime
```
Scale providers and deployment topology around that loop. Do not replace it
with selectors, a client state graph, raw wire operations, or speculative
services.
## Trigger boundary
Load this skill for explicit hemx/hemplate application usage, architecture,
production-readiness, scaling, or a review of those decisions. It also applies
when the user asks whether a hemx application should become microservices.
Do not load it for:
- generic Rust, Axum, HTML, CSS, database, or microservice questions with no hemx
application decision;
- visual design alone;
- changing hemx/hemplate internals rather than using their public model;
- a tiny obvious application patch where no authoring or scaling judgment is at
stake.
## Authority before taste
Inside the hemx repository, read `AGENTS.md`, then
`docs/v1-product-evidence.md` and `REQUIREMENTS.md`. Use `PLAN.md` only as the
mutable implementation cursor. Inspect only the nearest authoritative material
needed for the decision:
- `docs/hemplate-syntax.md` for real `.heml` syntax;
- `docs/tutorial-saas.md` for the production-shaped application boundary;
- `docs/recipes/reusable-partials.md` for composition;
- the auth, persistence, observability, deploy/versioning, offline, and local
command-log recipes for their named concerns;
- the nearest maintained example and current public Rust API before naming an
API in code.
Project requirements and observed code outrank this skill. Outside this
repository, establish the application's versions and elected contracts instead
of assuming current-main APIs. If auth, storage, transport, replay, deployment,
or service contracts are absent, identify the missing decision; do not invent a
provider or abstraction that makes the system look complete.
When invoked for a Hemx application repository, inspect its elected `AGENTS.md`
or equivalent instruction surface. Ensure it explicitly requires all HTML
surfaces and fragments to be authored as semantic `.heml` templates rendered
through Hemplate's generated typed resources, and explicitly forbids constructing,
concatenating, interpolating, or formatting HTML in Rust. If the current request
authorizes repository edits, patch that instruction surface before application
implementation; otherwise report the missing policy as a blocker. Do not copy the
rest of this skill into project instructions.
## Decision loop
### 1. Start with the user job and one load-bearing path
Name the concrete request, mutation, navigation, push update, or recovery path
that must work. Trace it end to end before discussing topology:
```text
HTTP/browser input
-> normal auth, CSRF, and typed validation
-> application command/query
-> authoritative state transition
-> rendered generated target/page
-> EffectBatch response or canonical push bytes
-> root-scoped runtime application
-> visible success or explicit recovery
```
If this path is unclear, architecture diagrams and service boundaries are
premature.
### 2. Use the canonical authoring level
Default to:
- semantic `.heml` templates plus ordinary Rust; never construct, concatenate,
interpolate, or format HTML strings in Rust, including fragments for source
rendering, Markdown, errors, or test-facing pages;
- `#[hemx::app]`, plain `#[hemx::handler]`, generated components/resources,
typed form models, view wrappers, render/page helpers, and `IntoEffect`;
- generated target, form, control, keyed-slot, and event helpers;
- typed partial swaps expressed as generated target + rendered partial + swap
kind;
- real links and GET forms for navigation and history;
- `data-hemx-revealed-ahead` for viewport-ahead loading while the observed sentinel remains in normal document flow; never move the observed target with CSS to fake prefetch distance;
- plain CSS/SCSS for appearance;
- shared runtime paths and Axum adapters supplied by `hemx-axum`.
Keep domain types, authorization, persistence, routing, sessions, flags,
observability, and transport policy in the application or integration crate.
Keep `hemx-core` about typed resources and the closed effect/wire contract.
Compose reusable UI from templates, generated components, and ordinary Rust
functions rather than creating a client component framework.
Use advanced layers only when the job proves the need:
- opaque island JavaScript is a leaf adapter for high-frequency local behavior;
- client-local/wasm handlers are explicit opt-ins, not the default state model;
- atoms are addressable bootstrap/sync resources, not a general reactive store;
- SSE/WebSocket transport carries canonical versioned `EffectBatch` bytes; it
does not define domain policy.
### 3. Keep truth on the right side of the boundary
The browser DOM, stored HTML patches, and stored `EffectBatch` values are not
business truth. Keep authoritative state in ordinary application/domain models
and durable providers. Derive UI effects from that state.
For local/offline products, use explicit commands, events, and projections.
Replay, deletion, conflict resolution, reconciliation, and export semantics are
product contracts; stop when they have not been chosen. For server products,
normal HTTP security semantics remain authoritative even when interactions are
enhanced.
Expected failures must become typed, local, useful outcomes: generated field
errors/focus for validation, a root-scoped error outlet for recoverable request
failure, and fail-closed handling for malformed or incompatible responses.
Never turn infrastructure failure into a success-looking empty effect.
Prefer a **functional core with an imperative shell**. Keep validation,
normalization, authorization decisions, command application, state transitions,
and view-model/projection derivation as deterministic functions over explicit
inputs where that is honest. A useful shape is `state + command -> outcome` or
`facts -> view model`, with typed errors rather than hidden mutation. This makes
the largest behavior space cheap to unit-test and mutation-test.
Keep HTTP extraction, sessions, database I/O, clocks, randomness/IDs, queues,
push connections, and host capabilities in a thin handler/application shell.
Read their results once, pass ordinary values into the core, persist the returned
outcome, then render generated effects. Pass time, identity, or policy as data
when only the value matters; do not create a trait for every function merely to
mock it. Use a real adapter boundary when ownership or side effects are real.
Purity is a locality tool, not a religion: orchestration and I/O are inherently
effectful, and `EffectBatch` remains UI output rather than domain state.
### 4. Scale one pressure at a time
Use this ladder. Enter a stage only when measured load, availability goals,
ownership, compliance, or a distinct failure/resource profile requires it.
| Stage | Default shape | Required proof before moving on |
| --- | --- | --- |
| One process | One deployable; app-owned adapters; simplest durable store that meets the product contract | The real product path, restart behavior, backup/recovery needs, and current bottleneck are known |
| Production monolith | One release unit for server, generated output, and runtime asset; external durable database/session providers as required | Integration tests cover auth, persistence, failures, migration, fingerprint mismatch, and rollback/reload behavior |
| Horizontal web tier | Stateless request replicas around shared authoritative providers; process memory is cache/ephemeral only unless affinity and loss semantics are explicit | Load evidence shows replica scale helps; migrations, readiness, draining, cache invalidation, and session/CSRF behavior work across replicas |
| Push/fan-out tier | Server-owned ongoing SSE/WebSocket connections; add a broker/backplane only when updates must cross processes | Reconnect, ordering, duplicate, authorization, backpressure, and replay expectations are explicit and tested at the required level |
| Worker or read-model split | Isolate a measured CPU, latency, queue, or failure domain while the application remains one understandable product | Job ownership, idempotency, timeout, retry, deduplication, observability, and recovery contracts exist |
| Microservices | Split an independently owned bounded capability with its own release/scaling/SLO pressure and explicit data/API/event contract | The boundary removes a demonstrated constraint and its distributed failure modes are cheaper than the monolith |
Prefer vertical resource tuning, query/index fixes, caching with explicit
freshness, bounded concurrency, and horizontal replicas before service
splitting. A large codebase is a module-boundary problem before it is a network
boundary problem.
### 5. Preserve the hemx boundary across services
When microservices are justified:
- keep hemplate rendering, generated resources, and UI `EffectBatch` creation in
the presentation/application edge that owns the page;
- exchange typed domain requests, responses, and events across services—not CSS
selectors, DOM instructions, raw hemx opcodes, or app-authored JSON versions
of the hemx wire format;
- name one authority for each write model and do not let services casually share
mutation ownership;
- version service contracts independently, while deploying each hemx server,
its generated metadata, build fingerprint, and runtime asset as a compatible
release unit;
- preserve end-user credentials, authorization, CSRF, tenancy, deadlines, and
trace context explicitly at each real trust boundary;
- make partial failure visible. Define timeout, idempotency, retry,
deduplication, ordering, compensation, and replay only where the chosen
interaction requires them—never as generic middleware theater.
Do not put a network hop between a handler and its renderer merely to claim
microservices. Do not use `EffectBatch` as a business event bus or durable event
log.
### 6. Organize by cohesive product responsibility
Start small; do not pre-create an architecture directory tree. A production app
usually needs only these durable seams:
```text
build.rs # hemx-build/global code generation only
src/main.rs # config, concrete providers, process/bootstrap
src/lib.rs # app/router composition and a testable app surface
src/<feature>/ # add only when a feature is already a real seam
templates/app_shell.heml # document shell
templates/<feature>.heml # feature surface
templates/partials/ # genuinely reused or independently swapped pieces
tests/<journey>.rs # process/integration behavior
tests/browser_<journey>.rs # only browser-dependent behavior
```
Treat that as a responsibility map, not mandatory scaffolding. A small cohesive
app can remain in `lib.rs`; file count is not architecture. When a feature has
its own state/commands, handlers, rendering, and tests, move that whole seam
together. Keep its domain model, typed input, application operation, handler,
and generated view calls near one another. Do not spread every request across
generic `controllers/`, `services/`, `repositories/`, `dto/`, and `utils/`
directories.
Keep `main.rs` boring and hard to test because it contains almost no policy.
Expose app construction or mounting from `lib.rs` so integration tests can use
the real router with controlled concrete providers. Put normal routing,
auth/session, persistence, queues, and transport adapters at the app boundary;
do not move them into hemx core or generated template modules. Keep generated
artifacts in the build output rather than copying them into source control.
Put tiny unit tests beside the responsible module, especially around pure
transitions, validation, authorization decisions, and projections. Put
cross-module HTTP, persistence, and process tests in `tests/`, named for behavior
rather than implementation. Keep browser journeys few and load-bearing. Extract
a shared partial or helper only after actual reuse or independent swap identity
appears.
If a microservice boundary becomes real, that service owns its contract,
provider/migrations, operational entry point, and contract tests; do not mirror
hypothetical services in the source tree first.
### 7. Prove behavior at the cheapest authoritative boundary
Choose the tool by what must be observed:
| Boundary | Default tool | What it proves |
| --- | --- | --- |
| Domain/state | Rust `#[test]` / `#[tokio::test]` | Parsing, invariants, commands, projections, and failure classes |
| Template/build | Compiler plus hemx-build diagnostics | Real `.heml` syntax, generated resources/forms, and cross-file references |
| Handler/effect | `hemx_test` | Generated targets/handles, rendered partials, form bodies, and effect batches without raw ids |
| Router/integration | Real Axum router, usually `tower::ServiceExt`, plus concrete test providers | HTTP status/headers/body, auth, CSRF, sessions, persistence, and malformed requests |
| Static rendered HTML | Rust `scraper` crate (HTML parser + CSS-selector queries) | Escaping, semantic structure, links/forms/attributes, and server-rendered fragments without launching a browser |
| Process lifecycle | `hemx_test::TestProcess` | Readiness, real sockets, child cleanup, restart, and production-binary behavior |
| Browser runtime | Rust `thirtyfour` crate (WebDriver client) with the repository-owned browser smoke | Delegated events, history, focus, polling/revealed bindings, keyed DOM identity, SSE, and recovery |
| Test quality/release | hemx xtask mutation/full-test commands | Mutation resistance and the elected bounded workspace verification path |
`scraper` is a Rust HTML parsing and CSS-selector library, not a browser or web
framework. Use it when inspecting final server-rendered HTML is enough. It does
not run JavaScript, apply `EffectBatch`, maintain focus/history, or prove
SSE/runtime behavior. `thirtyfour` is a Rust WebDriver client crate; use it only
when an actual browser semantic is the subject;
do not turn every handler assertion into a WebDriver journey. In this repository
`thirtyfour` is the elected Rust browser adapter, so do not add Playwright,
Fantoccini, or another competing browser stack merely from preference. Outside
this repository, preserve the application's existing runner unless a concrete
missing capability justifies migration.
For WebDriver tests, start the real process through the RAII `TestProcess`
harness, keep selectors in test adapters, and prefer generated-resource or
stable semantic helpers over copied implementation selectors. Browser selector
helpers are not authoring APIs. Avoid sleeps when the runner can wait for the
observable condition.
Match proof to risk:
1. Let compilation reject broken templates and generated contracts.
2. Unit-test ordinary domain parsing and state transitions without a browser.
3. Test handlers through `hemx_test` with useful generated-resource assertions.
4. Test route/auth/session/CSRF/persistence and wire failure behavior at the app
integration boundary.
5. Parse static HTML with `scraper` only for facts that do not require runtime
execution.
6. Use focused repository-owned browser smoke for dynamic attributes,
navigation/history, keyed reconciliation, polling/revealed behavior,
no-reload interaction, and runtime recovery.
7. Add compatibility, rolling-deploy, process-restart, multi-replica, load, or
fault tests only when the selected scale stage makes those behaviors part of
the contract.
In this repository, prefer the stable commands named by `AGENTS.md`, especially
focused crate tests and `cargo run -p hemx-xtask -- test`; use
`cargo run -p hemx-xtask -- html-examples-smoke` for the pattern gallery and
runtime behavior, and the xtask mutation command rather than direct `mutest`.
Do not substitute a passing literal lowering fixture, static HTML parse, or
mocks-only test for proof at the rendered/runtime consumer boundary.
## Refuse fake sophistication
Do not introduce:
- handwritten resource ids, selector retargeting, raw effect constructors, raw
registries, runtime opcodes, manual wire parsing, or hard-coded runtime URLs in
normal app code;
- a VDOM, client router, general client state graph, expression runtime,
per-node listeners, or handwritten JavaScript for ordinary forms/lists/swaps;
- core-owned auth, sessions, persistence, routing, multipart, transport, sync,
analytics, flags, or provider policy;
- a generic repository/service/provider interface before a concrete second use
or required external contract exists;
- microservices by entity name, team aspiration, file count, or hypothetical
scale;
- shared process memory presented as durable or cross-replica state;
- retries, caches, queues, brokers, sagas, event sourcing, CQRS, Kubernetes, or a
service mesh without a named failure/load contract and verification path.
“Suckless” here means fewer authorities and mechanisms, not fewer safety checks.
The elegant design keeps HTML semantics, typed boundaries, explicit ownership,
and failure truth while deleting accidental layers.
## Handoff
For architecture or review requests, report compactly:
- the user-visible path and current bottleneck/risk;
- the selected scale stage and why the previous stage is insufficient;
- state, rendering, transport, and service ownership;
- the smallest end-to-end change;
- complexity explicitly refused;
- proof run and any unresolved provider/product contract.
For implementation, make that slice reachable and verify it; do not leave a
“scalable” abstraction that no real path uses.