docs(agent): track idiomatic Hemx guidance
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user