ready-active-safe 0.1.3

Lifecycle engine for externally driven systems
Documentation
# Architecture

High-level overview of `ready-active-safe`: a lifecycle engine for externally driven systems.

---

## Overview

`ready-active-safe` models system lifecycles as a sequence of **modes**. These are deliberate phases that every externally driven system passes through: readiness, activity, and safe shutdown.

The crate provides a pure `Machine` trait that maps `(mode, event)` pairs to `Decision` values. Decisions describe intent. They include mode changes and commands, but never perform side effects. An external runtime feeds events, checks policies, applies mode changes, and dispatches commands.

This architecture separates **what the system decides** from **how decisions are executed**, making lifecycle logic deterministic, testable, and portable across `no_std` and `std` environments.

---

## Design Principles

### 1. Modes, Not States

A mode is a phase of operation with clear entry and exit contracts. The lifecycle is not an arbitrary directed graph. It is a deliberate progression through well-defined phases. This constraint makes the model easier to reason about, test, and audit than a general-purpose finite state machine.

### 2. Sans-I/O

The `Machine` trait is pure. It takes `&self`, a mode reference, and an event reference. It returns a `Decision`. No I/O, no side effects, no runtime coupling. This design is borrowed from the Sans-I/O pattern in protocol design: separate the protocol logic from the transport.

### 3. External Events Drive Transitions

The system never transitions itself. An external runtime owns the current mode, feeds events to the machine, and applies the resulting decisions. This inversion of control means the machine logic never needs to manage its own state, schedule timers, or handle concurrency.

### 4. Recovery as Model

Safe mode and recovery transitions are first-class citizens. They are not error-handling afterthoughts bolted onto a happy-path state machine. The lifecycle model treats shutdown and recovery as phases that deserve the same rigor as startup and normal operation.

---

## Module Map

```
src/
  lib.rs          # Crate root. Top-level docs, cfg attributes, module
                  # declarations, and public re-exports. Always available.
  model.rs        # Core types: Machine trait, Decision, ModeChange, Policy.
                  # Always available. no_std-compatible (uses alloc).
  error.rs        # LifecycleError enum with TransitionDenied variant.
                  # Always available. no_std-compatible.
  time.rs         # Clock, instant, and deadline abstractions.
                  # Feature-gated: requires `time`. no_std-compatible.
  runtime.rs      # Event acceptance, command dispatch, timer coordination.
                  # Feature-gated: requires `runtime` (implies `std`).
  journal.rs      # Transition recording, snapshots, and replay.
                  # Feature-gated: requires `journal` (implies `std`).
```

### no_std Compatibility Table

| Module       | Feature Gate | `no_std` | `alloc` Required | Notes                              |
|--------------|-------------|----------|-------------------|------------------------------------|
| `model.rs`   | (none)      | Yes      | Yes               | `Vec<C>` in `Decision::commands`   |
| `error.rs`   | (none)      | Yes      | No                | `std::error::Error` impl behind `std` |
| `time.rs`    | `time`      | Yes      | No                | Designed for virtual and real clocks |
| `runtime.rs` | `runtime`   | No       | N/A               | Requires `std` for owned state     |
| `journal.rs` | `journal`   | No       | N/A               | Requires `std` for collections     |

---

## Data Flow

```
                        +-------------------+
   External Event ----->|                   |
                        |  Machine::on_event|
   Current Mode ------->|   (&self, &M, &E) |
                        |                   |
                        +--------+----------+
                                 |
                                 v
                        +--------+----------+
                        |    Decision       |
                        |  +-------------+  |
                        |  | mode_change  |  |  Option<ModeChange<M>>
                        |  +-------------+  |
                        |  | commands     |  |  Vec<C>
                        |  +-------------+  |
                        +--------+----------+
                                 |
                    +------------+------------+
                    |                         |
                    v                         v
           +-------+--------+       +--------+-------+
           | Policy check   |       | Command list   |
           | is_allowed(    |       | dispatched by  |
           |   from, to)    |       | external       |
           +-------+--------+       | runtime        |
                   |                +----------------+
          +--------+--------+
          |                 |
          v                 v
   Mode updated      LifecycleError::
   by runtime         TransitionDenied
```

**Step-by-step:**

1. The external runtime holds the current mode and receives an external event.
2. The runtime calls `machine.on_event(&current_mode, &event)`.
3. The machine returns a `Decision` containing an optional `ModeChange` and zero or more commands.
4. If the decision contains a `ModeChange`, the runtime checks the `Policy`.
5. If the policy allows the transition, the runtime updates the current mode.
6. If the policy denies it, the runtime produces a `LifecycleError::TransitionDenied`.
7. The runtime dispatches the commands to external executors.

---

## Key Design Decisions

### Root-Level Types (No `core` Sub-Module)

The core types (`Machine`, `Decision`, `ModeChange`, `Policy`) are defined in `model.rs` and re-exported from `lib.rs` at the crate root. There is no `core` sub-module.

**Rationale:** In `no_std` environments, `use core::...` refers to the Rust `core` crate. A module named `core` inside this crate would shadow that import, forcing users to write `extern crate core as rust_core` or similar workarounds. By keeping types at the root, `use ready_active_safe::Machine` works identically in `std` and `no_std` contexts.

### No Time in the Base Trait

`Machine::on_event` takes `(&self, &Mode, &Event)`. There is no `now: Instant` parameter.

**Rationale:** Time is a concern of the runtime, not the decision logic. Many systems (embedded controllers, protocol parsers, synchronous pipelines) do not need timestamps in their transition logic. Systems that do need time can encode it in their event type (e.g., `Event::Tick(Instant)`) or use the `time` feature module. Keeping time out of the base trait means the core contract is usable in `no_std` environments without clock hardware.

### `alloc` for `Vec<C>` in Decision

`Decision` stores commands in a `Vec<C>`, which requires the `alloc` crate. The crate unconditionally declares `extern crate alloc`.

**Rationale:** Commands are a variable-length list. Alternatives considered:
- **Fixed-size array**: Limits expressiveness and forces users to pick a maximum at compile time.
- **Iterator/callback**: Makes `Decision` non-`Clone`, non-`Debug`, and harder to inspect in tests.
- **`SmallVec`**: Adds a dependency for a marginal benefit.

`alloc` is available on virtually all `no_std` targets that have a heap allocator, which covers the intended embedded use cases. Targets without `alloc` are explicitly out of scope for v0.1.

### `#[non_exhaustive]` on Public Types

Both `Decision` and `ModeChange` are marked `#[non_exhaustive]`. `LifecycleError` is also `#[non_exhaustive]`.

**Rationale:** The crate is at v0.1.0. New fields (e.g., a `reason` on `ModeChange`, a `timestamp` on `Decision`) may be added in minor versions. `#[non_exhaustive]` ensures these additions are not breaking changes.

---

## Feature Flags

| Feature   | Default | Requires | `no_std` | Description                          |
|-----------|---------|----------|----------|--------------------------------------|
| `full`    | Yes     | none     | n/a      | Enables `std`, `runtime`, `time`, `journal` |
| `std`     | No*     | none     | No       | Standard library support, `std::error::Error` impls |
| `runtime` | No*     | `std`    | No       | Event loop, command dispatch, timer coordination |
| `time`    | No*     | none     | Yes      | Clock, instant, and deadline types   |
| `journal` | No*     | `std`    | No       | Transition recording and replay      |

*Enabled by default through the `full` feature.

**Additive guarantee:** Enabling any feature never removes functionality. A program that compiles with `--no-default-features` will also compile with `--all-features`.

---

### Runtime Dependencies

| Crate    | Purpose                    |
|----------|----------------------------|
| *(none)* | No external runtime dependencies |

The core library uses only `core` and `alloc` from the Rust standard library. This is a deliberate choice. The lifecycle kernel should have minimal supply chain risk for downstream users.

### Dev Dependencies

This repository uses `dev-dependencies` for tests and tooling. This does not affect downstream users of the library.

---

## Deliberate Omissions

These are things the crate intentionally does not do at v0.1.0:

| Omission               | Rationale                                                                 |
|------------------------|---------------------------------------------------------------------------|
| Async / `Future`       | The `Machine` trait is synchronous and pure. Async belongs in the runtime adapter, not the decision logic. |
| Serialization (`serde`)| Serialization is an adapter concern. It will be offered as an optional feature, not baked into core types. |
| Persistent journal     | The journal records transitions in memory. Persistence is a storage adapter concern. |
| Built-in timer runtime | The `time` module provides types, not a scheduler. Timer management is the runtime's responsibility. |
| Macro-based DSL        | The API is plain traits and structs. Macros hide complexity and make errors harder to diagnose. |
| Arbitrary state graphs | The crate models lifecycle phases (modes), not general-purpose FSMs. This is a feature, not a limitation. |

---

## Future Considerations

| Area                  | Direction                                                           | Priority |
|-----------------------|---------------------------------------------------------------------|----------|
| Cookbook docs          | Rails Guides-style recipes for the pit-of-success wiring           | P0       |
| `serde` feature        | Optional serialization for `Decision`, `ModeChange`, journal records | P1       |
| Tracing guidance       | Show structured transition logging without adding dependencies      | P1       |
| Allocation stance      | Document heap usage clearly or add an optional fixed-capacity path  | P1       |
| Async runtime adapter  | Separate crate or example(s) that wrap the synchronous runner       | P2       |
| Persistent journal     | Companion crate(s) for file/db adapters, not in the core            | P2       |
| `defmt` support        | Logging for embedded targets without `std`                          | P2       |