ready-active-safe 0.1.3

Lifecycle engine for externally driven systems
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
# 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.

| If you need… | Teams will usually pick… |
|-------------|--------------------------|
| 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.

| Trap | Who Fell In | Why It's Bad |
|------|-------------|--------------|
| 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.