# Audit
A comprehensive evaluation of `ready-active-safe` against the competitive landscape,
user experience research, and production readiness criteria.
This is a living document. Update it as the crate evolves.
---
## Executive Summary (2026-03-12)
### What This Crate Is
`ready-active-safe` is a **lifecycle engine** for **externally driven systems**.
It is for problems where “state” is really an *operational phase* (mode) with
clear contracts: startup/readiness, active operation, safe mode, and recovery.
### What This Crate Is Not
It is not a general-purpose finite state machine framework, and it is not a
workflow engine.
If you need:
- hierarchical / nested states (statecharts)
- an internal event queue or scheduler
- an async runtime / actor system
- effects/callbacks that run inside the “machine” itself
…this crate will intentionally feel “too small”.
### Where It Is Genuinely Useful (vs `enum` + `match`)
Teams should consider adopting this crate when they want at least one of:
- a **purity boundary** (Sans-I/O decision logic) that makes testing and replay cheap
- **decisions as data** (mode change + ordered commands) so runtimes own effects
- **policy enforcement** as a separate, auditable concern
- a **journal** that records transitions and enables deterministic replay
- a default, boring runtime loop (`runtime::Runner`) to remove repetitive plumbing
- strict **safety posture** (no `unsafe`, no panics in library code, runnable docs)
If a team has a small lifecycle and doesn’t need auditability, policy enforcement,
or a shared vocabulary, they will often prefer to write the `match` directly.
That is a rational choice.
### Adoption Forecast (Opinionated, Based on Rust Norms)
This crate is likely to be adopted by teams where:
- failures are costly (safe mode / recovery matters)
- runtime integration is custom (embedded, XR, robotics, simulation harnesses)
- determinism is valued (debuggability, replay, testability)
- supply-chain risk matters (zero deps, no `unsafe` is a meaningful posture)
It will be rejected by teams where:
- the “state machine” is really app workflow orchestration (queues, retries, sagas)
- they want statecharts/hierarchy and a rich built-in runtime
- they need a no-heap/no-`alloc` story (today the kernel requires `alloc`)
The “future” of this crate is plausible and attractive if we keep the kernel
small and stable, and ship optional interop patterns (serde/tracing/async wrappers)
without turning the crate into a framework.
### Highest-ROI Path to 1.0 Adoption
1. **Interop that teams expect**: `serde` feature for core/journal types; tracing guidance.
2. **Async usage guidance**: examples that wrap the synchronous runner (no async runtime in core).
3. **Clear allocation stance**: document heap behavior hard, or add an optional fixed-capacity path.
4. **Evidence**: at least two real integrations (internal/external) before 1.0 freeze.
---
## Competitive Comparison (Rust Ecosystem)
This is not an exhaustive list; it highlights the choices teams actually make.
### “No Library”: `enum` + `match`
**Why teams pick it:**
- zero dependency and zero concepts to learn
- maximum flexibility (effects, logging, time) in one place
**Why it fails in production:**
- semantics drift across codebases (each team reinvents ordering, denial handling, logging)
- hard to replay/audit because effects and decisions are interleaved
- no shared vocabulary (review/maintenance becomes “read the whole match”)
### `statig` (Hierarchical State Machines)
`statig` targets hierarchical state machines with optional macros, async handlers, and a strong
embedded story (no heap allocations). This is a good fit when hierarchy/local storage is the
right model.
Sources:
- https://docs.rs/statig
**Where `ready-active-safe` differs:**
- enforces Sans-I/O by design (decisions as data; effects are external)
- favors lifecycle phases over general statecharts/hierarchy
- provides journaling/replay as a first-class concept
### `statechart` (Statecharts)
`statechart` is an implementation of statecharts in Rust (hierarchy and richer
state-machine semantics). It can be a better fit when you need statecharts
features rather than lifecycle phases.
Sources:
- https://docs.rs/statechart
**Where `ready-active-safe` differs:**
- smaller semantic surface: lifecycle phases, not statecharts
- no interpreter/event-queue model in the core API
- effects remain external (commands returned as data)
### `smlang` (Transition-Table DSL)
`smlang` generates a state machine from a procedural-macro DSL, with guards/actions and support
for async and non-async code. This is great when a table is the best representation, but it is
still a macro DSL with generated code as part of the user experience.
Sources:
- https://docs.rs/crate/smlang-macros/latest
**Where `ready-active-safe` differs:**
- no proc macro DSL required for the happy path (plain Rust match arms)
- keeps actions/side effects outside the machine boundary
### `finny` (Batteries-Included FSM)
`finny` is a proc-macro-driven FSM framework with guards, actions, timers, and
an internal “run-to-completion” runtime model. This can be a good fit when you
want a richer built-in engine and are comfortable with framework semantics.
Sources:
- https://docs.rs/finny
**Where `ready-active-safe` differs:**
- no internal queue/scheduler model: events are externally driven
- no inline actions: commands are returned as data and executed by your runtime
- focuses on lifecycle phases (Ready/Active/Safe), not general FSM frameworks
### Macro-Driven Static FSMs (`state-machines`, `sfsm`, `typed-fsm`, etc.)
The ecosystem has multiple macro-based FSM crates that emphasize compile-time guarantees,
embedded/no-alloc stories, or transition DSLs. These can be a better fit when you want
transition tables and/or static machines with no heap usage, but they inherit the usual trade:
generated code, macro ergonomics, and different debugging workflows.
Sources:
- https://docs.rs/state-machines
- https://docs.rs/state-machines-macro
- https://docs.rs/sfsm
- https://docs.rs/crate/typed-fsm/0.4.2
**Where `ready-active-safe` differs:**
- optimizes for readability and debugging (“just Rust”)
- targets lifecycle semantics (safe/recovery), not arbitrary FSM graphs
---
## Decision Guide (What Teams Will Pick Instead)
The most common “competition” for this crate is not another crate: it is *no crate*.
Teams will (correctly) start with `enum` + `match` unless they feel pain.
This crate should win when it removes repeated pain without adding framework gravity.
| 2–3 modes, no audit/replay, one-off lifecycle | `enum` + `match` |
| hierarchical/nested states, superstates, internal handling | `statig` / statecharts-style libraries |
| a transition table DSL with generated code | `smlang` |
| no-heap/no-`alloc` embedded story | macro-driven FSM crates or hand-written code |
| pure decision logic + external runtime + audit trail | `ready-active-safe` |
**Important adoption advantage:** migrating into (or out of) `ready-active-safe` is low-risk.
The core of your lifecycle remains a readable `match (mode, event)`.
---
## Supply Chain and Trust Posture (Why “Zero Deps” Matters)
Rust package publishing is not curated/reviewed by default, and the ecosystem has had real
malicious-crate incidents (typically typosquatting).
Sources:
- https://blog.rust-lang.org/2022/05/10/malicious-crate-rustdecimal/
- https://blog.rust-lang.org/inside-rust/2023/09/01/crates-io-malware-postmortem/
- https://blog.rust-lang.org/2025/09/24/crates.io-malicious-crates-fasterlog-and-asyncprintln/
- https://blog.rust-lang.org/2025/12/05/crates.io-malicious-crates-finch-rust-and-sha-rust/
- https://stackoverflow.com/questions/77419850/is-it-safe-to-install-rust-crates-is-crates-io-curated-or-reviewed-for-malwar
This crate’s choices (no dependencies, forbid `unsafe`, strict linting, runnable examples) are
not just aesthetic: they are part of why a team might accept a dependency at all, especially in
embedded and safety-conscious environments.
Sources:
- https://tockos.org/blog/2017/crates-are-not-safe/
---
## Future Outlook: Will Teams Use This?
### Why This Can Win
1. **It’s a “kernel”, not a framework.** Teams can keep their runtimes while sharing lifecycle semantics.
2. **It is honest about non-goals.** That makes it easier to trust long-term.
3. **It creates leverage for teams with multiple lifecycles.** Once the pattern is learned, the next
subsystem is cheaper to build and easier to review.
### Why Teams Will Walk Away
1. **If it drifts into a general FSM framework.** The value proposition collapses (too much surface area).
2. **If it stays “too pure” without interop.** Production teams still need serde, logging, and async wiring.
3. **If it surprises embedded users.** `alloc` and `Vec` behavior must be explicit to avoid footguns.
### The Most Likely Outcome
If we keep the guardrails and ship the interop story as optional, the crate can become a
“default choice” for lifecycle-shaped problems (especially in embedded/XR/robotics/services).
If we don’t, teams will either:
- keep using “`enum` + `match`” and hand-roll logging/policy/journals, or
- choose a richer state machine framework (hierarchy/async/event queues) that matches their needs.
---
## Risk Register (Pre-1.0)
These are the realistic adoption killers to manage intentionally:
1. **API churn without migration guidance.** 0.x users expect movement, but they also need stability signals.
2. **Feature creep into “generic FSM framework”.** This dilutes the identity and increases the maintenance burden.
3. **Interop gap.** Without serde/logging/async guidance, production teams will choose a different approach.
4. **Embedded surprises.** `alloc` + `Vec` command collection must be explicit; unexpected allocations lose trust.
5. **Bus factor.** A small kernel is sustainable, but a crate still needs governance and predictable maintenance.
Mitigations are already captured in `docs/GUARDRAILS.md` and `docs/ROADMAP.md`.
---
## What We Do Right
These are validated architectural decisions. Do not revisit them.
### 1. Decisions as Data
The `Decision` type is inert data describing intent. The runtime interprets it.
This is the single best design choice in the crate.
**Why it matters:**
- XState does this too, but wraps it in 16.7 kB of framework.
We do it in zero dependencies.
- Spring Statemachine coupled effects to transitions and created
memory leaks, race conditions, and inconsistent rollback behavior.
- AASM interleaves 14 callback points with transitions, making
ordering a constant source of bugs.
Our approach means machine logic is tested with plain assertions.
No mocking, no test harness, no setup/teardown.
### 2. Pure Machine Trait
`Machine::on_event` takes `&self`. No mutation, no I/O, no global state.
Same input always produces the same output.
**Why it matters:**
- Deterministic replay is free. Run the same events, get the same decisions.
- Testing is trivial. No async harness, no runtime, no mocking.
- The same machine works in sync loops, async tasks, embedded firmware,
and simulation harnesses.
### 3. No Proc Macros
The crate is plain Rust: enums, structs, traits, pattern matching.
**Why it matters:**
- Rust proc-macro crates have terrible error messages. `syn::parse_error()`
is an empty struct with no message. Users resort to `cargo expand` and
`panic!()` insertion to debug macro output.
- `statig` and `smlang` users struggle with opaque compiler errors when
their macro definitions have typos.
- Minitest beat RSpec partly because it is "just Ruby". We are "just Rust".
IDEs understand traits. Grep finds implementations. The debugger works.
### 4. Policy Separate from Machine
The machine decides what **should** happen.
The policy decides what **may** happen.
These are different concerns.
**Why it matters:**
- Statesman (Ruby) beat AASM specifically because it decoupled the
state machine from the entity it manages. GoCardless built Statesman
after AASM's coupling caused production bugs.
- Policies can be swapped without changing machine logic: permissive
in tests, strict in production.
- Policies are auditable independently.
### 5. Zero Callbacks
We have no callback system. None. This is correct.
**Why it matters:**
- AASM has 14 callback points for a single transition:
`begin -> event_before -> event_guards -> transition_guards ->
old_state_before_exit -> old_state_exit -> after_all_transitions ->
transition_after -> new_state_before_enter -> new_state_enter ->
...update_state... -> event_success -> old_state_after_exit ->
new_state_after_enter -> event_after`
- Callback ordering is the #1 FAQ in AASM issues.
- Spring Statemachine's `@OnTransition` callbacks caused race conditions
because `after_commit` operated on a "custom implementation of
transaction pattern rather than a real DB transaction."
If we ever add hooks, maximum two: `before_transition` and `after_transition`.
Never more.
### 6. Zero Dependencies
The published crate depends only on `core` and `alloc`.
**Why it matters:**
- 60% of open-source maintainers have quit or considered quitting.
61% of unpaid maintainers work alone. Express (17M weekly downloads)
has a bus factor of one person.
- Every dependency is a maintenance liability. Every dependency is a
supply chain risk. Every dependency is a compilation cost.
- Small surface area = sustainable maintenance = longer life.
### 7. Recovery as First-Class
Safe mode is not an error handler. It is a lifecycle phase with the same
status as Ready and Active. Recovery transitions (Safe to Ready) are
modeled with the same `Decision` mechanism as forward transitions.
**Why it matters:**
- No competitor does this. Most FSM libraries treat error states as
terminal or exceptional. We model them as normal lifecycle phases.
- Real systems recover. Industrial controllers, XR sessions, and
services all have recovery paths. Making these first-class means
they get tested, documented, and policy-checked like any other transition.
### 8. no_std Core
Core types work in `no_std` environments with `alloc`.
**Why it matters:**
- Table stakes for Rust crates targeting embedded, robotics, and firmware.
- Differentiator vs XState (JavaScript), AASM (Ruby), Spring SM (Java).
- Feature gates are strictly additive: enabling a feature never breaks
`no_std` compatibility for core types.
### 9. initial_mode()
Every machine declares its starting mode. Added based on competitive research.
**Why it matters:**
- Every single competitor has this: AASM (`initial: true`),
XState (`initial`), Statesman, `rust-fsm`, `sm!`, Machinery (Elixir).
- Without it, users must track the initial mode externally and hope
they got the right value.
### 10. ignore()
Free function alias for `stay()` that communicates intent in catch-all arms.
**Why it matters:**
- XState's strict mode proves this matters. There is a difference between
"I processed this event and chose to stay" (`stay()`) and "this event
is not relevant here" (`ignore()`).
- The `_ => stay()` pattern silently swallows unhandled events.
`_ => ignore()` documents that the silence is deliberate.
---
## What We Do Wrong
These are problems to fix. Prioritized by impact.
### Audit Update (2026-03-12)
This is an updated, DX-focused audit after implementing the minimal `runtime`,
`time`, and `journal` modules.
- Verified locally:
- `cargo test --all-features`
- `cargo test --no-default-features`
- `cargo clippy --all-features --all-targets -- -D warnings`
- `RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features`
- `bash scripts/test-markdown-docs.sh`
- High-level scorecard (subjective, for prioritization):
- Core lifecycle kernel: **86/100** (clear semantics, strong safety posture)
- Batteries-included DX: **82/100** (minimal runtime/time/journal now exist, but onboarding can be smoother)
- Key wins since the last audit:
- `runtime::Runner` replaces the repetitive “8-line loop”
- `journal::InMemoryJournal` provides record/query/replay
- `time` provides `Clock`, `Instant`, `Deadline`, `ManualClock` (+ `SystemClock` with `std`)
- `prelude::*` now re-exports feature types when enabled (copy/paste friendly)
- `docs/COOKBOOK.md` provides Rails Guides-style recipes (kept doctested)
- Highest ROI next steps (DX):
- Add optional integration examples for `tracing`, `serde`, and async wrappers
- Clarify the allocation stance (document vs fixed-capacity option)
### 1. Prelude Must Stay “One Import” (DX Guardrail)
Status: **fixed** — `prelude::*` re-exports `Runner`, journal types, and time types
when the corresponding features are enabled.
**Why it matters for DX:**
- It breaks the “Rails-like” expectation: one obvious import, one obvious path.
- It makes examples less copy/paste friendly (extra `use` lines).
**Guardrail:** Keep `prelude::*` the “one import” happy path as new feature modules ship.
### 2. We Need Cookbook-Grade Guidance (Not Just API Docs)
Status: **improved** — `docs/COOKBOOK.md` exists and is included in markdown doctests.
**Why it matters:**
- API docs tell you what exists. Cookbooks teach you how to combine pieces into
a reliable integration with fewer wrong turns.
### 3. The E Type Parameter Is Confusing
`Decision<M, C, E = Infallible>` and `Machine<E = Infallible>` — most users
never use `E`. It clutters type signatures and requires `PhantomData`.
**Why it's bad:**
- Users write `Decision<Mode, Command, ()>` or `Decision<Mode, Command>`
and wonder what the third parameter is.
- The `PhantomData<E>` field adds cognitive overhead when reading the
`Decision` struct.
- 95% of machines are infallible. The default helps but does not eliminate
the confusion.
**Assessment:** The E parameter enables fallible machines (important for
some use cases). The default of `Infallible` mitigates the issue. Do not
remove it, but ensure docs clearly explain when and why to use a non-default E.
### 4. `Vec` Commands Can Allocate on Hot Paths
`Decision` stores commands in a `Vec<C>`. This is simple and ergonomic, but it can allocate when commands are emitted.
**Why it's risky:**
- In embedded or low-latency loops, unexpected allocations can become a production footgun.
**Fix:** Decide a stance:
- Document the heap requirement and recommended patterns, or
- Add an *optional* fixed-capacity path later (without making the core API clever).
### 5. Policy Only Sees (from, to)
`Policy` receives only `(from, to)`. This keeps the policy contract small and auditable, but it means rules that depend on *events* live elsewhere.
**Why it's risky:**
- Users may try to encode event-based policy into the policy layer and fight the API.
**Fix:** Keep `Policy` minimal, but document the pattern:
- event-based rules belong in machine logic or in runtime checks before/after `decide()`.
### 6. No Serde Support
Production systems need to serialize modes, decisions, and errors.
**Why it's bad:**
- Serialization is table stakes for production use. Without it, users
cannot persist journal records, send decisions over the wire, or
log structured transition data.
- Every competitor that targets production offers serde or equivalent.
**Fix:** Add `serde` feature flag (v0.3+, not v0.1.0).
---
## Competitive Intelligence
### XState (JavaScript) — Lessons
**What went wrong:**
1. **Overkill for most use cases.** XState is a statecharts runtime. Many
projects only need a handful of top-level modes and a clear “decision boundary”.
2. **Surprising error ergonomics.** Users report confusion around “silent” action
failures and error handling that depends on runtime wiring.
3. **TypeScript complexity.** The type-level surface is powerful, but can become
difficult to work with at scale.
4. **The ecosystem needed a lighter tool.** XState created `@xstate/store` as a
lightweight alternative for many non-statecharts cases.
5. **Large breaking migration.** v5 renamed core APIs and reworked key concepts
(`interpret` → `createActor`, actor model changes). Migrations can be non-trivial.
**Our takeaway:**
- Stay small. Never add hierarchy, concurrency, or statecharts.
- Never rename core APIs after 1.0.
- Every invalid operation must produce a clear error.
Sources:
- XState v5 migration guide: https://stately.ai/docs/migration
- XState “silent” action failure reports: https://github.com/statelyai/xstate/issues/3822
- XState error swallowing reports: https://github.com/statelyai/xstate/issues/3812
- XState TypeScript complexity discussions: https://github.com/statelyai/xstate/issues/3861
- `@xstate/store`: https://github.com/statelyai/xstate/tree/main/packages/xstate-store
- “Introducing Stately Store”: https://stately.ai/blog/introducing-stately-store
### AASM (Ruby) — Lessons
**What went wrong:**
1. **14 callback points per transition.** Callback ordering is the #1 FAQ.
2. **Callback systems become the API.** As callback matrices grow, the ordering
becomes the contract, and debugging becomes “which hook fired?”
3. **Type coercion mismatch.** AASM state names are commonly symbols while
persistence/serialization paths often produce strings.
4. **IDE blindness.** Code analysis tools cannot recognize DSL-generated
methods. "Instead of auto-complete, you have to use grep."
5. **"Just use enums" argument.** In many apps, a plain enum and a few methods
cover 80% of the need. A dependency must justify itself with semantics and auditability.
**Our takeaway:**
- Never add callbacks. If hooks are needed, maximum two.
- Stay IDE-friendly: traits, not generated methods.
- Add value above what plain enums provide. If users can do it with
a Rust enum and a match, we have not earned the dependency.
Sources:
- AASM callback order: https://aasm.readthedocs.io/en/latest/callbacks/
- AASM symbol/string mismatch reports: https://github.com/aasm/aasm/issues/330
- AASM after_commit behavior discussion: https://github.com/aasm/aasm/issues/408
### Statesman (Ruby) — Lessons
**What went right:**
1. Decoupled state machine from the entity (separate class).
2. Stored every transition in the database for auditing and rollback.
3. Made debugging, auditing, and replaying transitions straightforward.
**Our takeaway:**
- We already decouple (Machine is separate from runtime).
- Journal feature must ship. Audit trail is the killer feature that
justifies the dependency.
Sources:
- Statesman README: https://github.com/gocardless/statesman
- “Why I wrote statesman”: https://joebew42.github.io/2014/02/09/why-i-wrote-statesman/
### Spring Statemachine (Java) — Lessons
**What went wrong:**
1. **Heavy instantiation.** The framework itself recommends pooling machines
rather than instantiating them repeatedly.
2. **Entity vs application confusion.** Designed for application-wide
state management but users consistently tried to use it for entity
lifecycle management.
3. **In-memory rollback failures.** Does not roll back in-memory state
when a database transaction rolls back.
4. **Maintenance mode and EOL.** The project is in maintenance mode.
v4.0.x is the last open-source line; future releases are available
only to commercial customers.
5. Multiple alternatives created specifically because it was too heavy:
`spring-flow-statemachine`, `simple-state-machine`, `squirrel-foundation`.
**Our takeaway:**
- Entity lifecycle and application workflow are different problems. Pick one.
- Stay focused on entity/object lifecycles.
- Framework coupling kills adoption.
- Communicate with users. Document governance.
Sources:
- Spring Statemachine docs (pooling/instantiation guidance): https://docs.spring.io/spring-statemachine/docs/current/reference/#statemachine-instantiation
- Spring Statemachine maintenance mode notice: https://github.com/spring-projects/spring-statemachine
- Spring Statemachine end-of-open-source announcement: https://spring.io/blog/2025/04/21/spring-cloud-data-flow-end-of-open-source
### Rust State Machine Crates — Lessons
**The landscape is fragmented.** `sm`, `machine`, `rust-fsm`, `statig`,
`sfsm`, `state-machines` — most are stalled or minimally maintained.
**Typestate vs runtime:** Typestate (each state is a unique type) enforces
transitions at compile time but cannot handle dynamic external events.
You end up needing an enum wrapper anyway. `statig` chose runtime over
typestate for this reason.
**Macro debugging is painful.** Users resort to `cargo expand` and
`panic!()` insertion. The `proc-macro-error` crate exists specifically
to work around the limitations of proc-macro error messages.
**Our takeaway:**
- The Rust ecosystem needs a well-maintained, non-macro, enum-based
lifecycle library. We are filling a real gap.
- Runtime enum approach is correct for dynamic/external events.
- Never require proc macros for core functionality.
Sources:
- rust-analyzer proc-macro UX issues: https://github.com/rust-lang/rust-analyzer/issues/17944
- syn proc-macro error limitations: https://docs.rs/syn/latest/syn/parse/struct.Error.html
- proc-macro-error motivation: https://docs.rs/proc-macro-error/latest/proc_macro_error/
### RSpec vs Minitest — Lessons
**What Minitest got right:**
- "It's just Ruby." No DSL to learn. Standard syntax.
- 20-30% faster test suites vs RSpec for comparable test counts.
- Ships with the language. No external dependency.
- Entire implementation is tiny. Easy to read, understand, extend.
**What RSpec overcomplicated:**
- DSL addiction: `is_expected`, `subject`, `shared_examples`,
`include_context`, `let`, `described_class`.
- Nesting nightmare: unlimited `describe`/`context` nesting.
- Scattered setup: `let` statements sprinkled across files.
- Mental overhead: teams must define conventions for test structure.
**Our takeaway:**
- Be Minitest, not RSpec. Feel like "just Rust."
- Keep tests flat. Behave DSL is good but keep nesting shallow.
- Provide good failure messages (Minitest's weakness).
---
## UX Assessment
### First 5 Minutes (GOOD)
- `cargo add ready-active-safe` — clean name, easy to remember
- `use ready_active_safe::prelude::*` — one import for everything
- Define enums, impl Machine, write match — familiar Rust patterns
- `transition(Active).emit(Initialize)` — reads like English
### First 15 Minutes (MIXED)
- `runtime::Runner` removes the repetitive boilerplate loop (GOOD)
- Built-in `AllowAll`/`DenyAll` reduce testing/policy boilerplate (GOOD)
- `decision.apply(mode)` reads well (GOOD)
- `ignore()` communicates intent (GOOD)
- `is_transition()` / `is_stay()` make conditionals clear (GOOD)
### First Hour (GAPS / NEXT)
- "How do I log transitions?" — no `tracing` guide/example yet
- "How do I serialize journal records?" — no `serde` feature yet
- "How do I run this in async?" — no tokio/async wrapper example yet
- "How do I avoid allocations under load?" — needs a clearer stance/story
### Production Use (MISSING)
- No serde support for journal/decision types
- No persistent journal adapter (file/db) story (should be a companion crate, not core)
- No async guidance example (should wrap the sync `Runner`)
---
## Things We Must Never Do
These are traps that killed or damaged competitors. Avoid them permanently.
| Add hierarchical/nested states | XState | 90% of users don't need it. Massively increases complexity. Not a lifecycle concern. |
| Add a callback system | AASM (14 callbacks) | Callbacks are an attractive nuisance. Composition beats callbacks. |
| Couple to a specific runtime | Spring SM | Framework lock-in kills adoption. |
| Require proc macros for basic usage | statig, smlang | Error messages are terrible. Debugging is painful. |
| Add an event queue / scheduler | Spring SM | Entity lifecycle is not application workflow. Pick one. |
| Silent failures | XState | Every invalid operation must produce a clear, actionable error. |
| Break core API after 1.0 | XState v4 to v5 | Get it right pre-1.0, then freeze. |
| Over-configure | Spring SM | Make decisions so users don't have to. |
| Grow the dependency tree | Many dead libraries | Zero deps = sustainable. Every dep is a maintenance liability. |
| Add "just in case" features | Spring SM | Speculative generality is dead weight. Rule of Three applies. |
| Mix entity lifecycle with application workflow | Spring SM | These are fundamentally different problems. We do entity lifecycle. |
| Auto-generate methods that IDEs can't find | AASM | Stay trait-based. IDEs understand traits. Grep finds implementations. |
---
## The One-Sentence Identity
We are Minitest to XState's RSpec: simple enough that "just Rust" is the
whole learning curve, opinionated enough that the pit of success is the
default path, and production-ready enough that Safe mode and audit trails
work out of the box.