Skip to content

Commit 25e8db1

Browse files
committed
docs: refresh state and suite READMEs for the full statechart engine
Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
1 parent b75654c commit 25e8db1

26 files changed

Lines changed: 318 additions & 170 deletions

README.md

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,43 @@ consumer-providable interface with a do-nothing default. You bring *your*
1111
logger, *your* tracer, *your* clock — Crucible never makes you adopt its
1212
choices, and never leaks a third-party type into a public signature.
1313

14-
The pure kernel (`state`) is the extreme end of this: **stdlib-only**, with no
15-
injected IO at all. The IO modules carry the heavier seams via injection, but
16-
follow the same rule — defaults are no-ops, nothing third-party is forced on the
17-
consumer.
14+
The `state` engine is the extreme end of this: **stdlib-only**, with no injected
15+
IO at all. The IO modules carry the heavier seams via injection, but follow the
16+
same rule — defaults are no-ops, nothing third-party is forced on the consumer.
1817

1918
## Modules
2019

2120
Each module is independently versioned (per-module SemVer) and carries its own
2221
stability label.
2322

24-
| Module | What it is | Status |
25-
| --------- | -------------------------------------------------------------------------- | ----------- |
26-
| `state` | Pure, abstract, domain-agnostic state machine kernel. Stdlib-only, no IO. | experimental |
27-
| `telemetry` | Vendor-neutral tracing/metrics interface for the IO modules. Stdlib-only. | experimental |
28-
| `broker` | Message broker seam — publish/subscribe transport with injected adapters. | planned |
29-
| `store` | Durable state/event store seam with graceful lifecycle. | planned |
30-
| `sink` | Effect dispatch / egress seam for emitted effects. | planned |
31-
32-
The kernel emits effects as pure data; the IO modules are the thin seams that
23+
| Module | What it is | Status |
24+
| --------------------- | -------------------------------------------------------------------------------- | ------------ |
25+
| `state` | Full-featured, domain-agnostic statechart engine. Stdlib-only, no IO. | experimental |
26+
| `state/analysis` | Static model-checking and path enumeration over a machine's IR. | experimental |
27+
| `state/evolution` | Diffs two machine definitions and classifies the SemVer bump. | experimental |
28+
| `state/conformance` | Reusable harness for driving golden scenarios against a machine. | experimental |
29+
| `telemetry` | Vendor-neutral tracing/metrics interface for the IO modules. Stdlib-only. | experimental |
30+
| `telemetry/slog` | `log/slog` adapter for the telemetry interface. | experimental |
31+
| `telemetry/otel` | OpenTelemetry adapter for the telemetry interface. | experimental |
32+
| `telemetry/datadog` | Datadog adapter for the telemetry interface. | experimental |
33+
| `broker` | Message broker seam — publish/subscribe transport with injected adapters. | planned |
34+
| `store` | Durable state/event store seam with graceful lifecycle. | planned |
35+
| `sink` | Effect dispatch / egress seam for emitted effects. | planned |
36+
37+
The engine emits effects as pure data; the IO modules are the thin seams that
3338
carry those effects to real transports, stores, and sinks — each
3439
"bring your own adapter," none forced on the consumer.
3540

3641
## Status
3742

38-
Early and evolving. The `state` kernel is implemented — the builder and
39-
transition engine, guards and actions, validation, path planning, batch
40-
helpers, and JSON (de)serialization — with test coverage; treat its API as
41-
experimental until a tagged release. The `broker`, `store`, and `sink` modules
42-
are planned.
43+
Early and evolving. The `state` module is now a complete, embeddable statechart
44+
engine — hierarchical, parallel, and final states; history; guard combinators;
45+
delayed transitions; invoked services; an actor model with message passing;
46+
snapshots; inspection; and JSON (de)serialization — backed by its `analysis`,
47+
`evolution`, and `conformance` companion packages. Treat its API as experimental
48+
until it reaches v1. The `telemetry` interface and its `slog`, `otel`, and
49+
`datadog` adapters are released. The `broker`, `store`, and `sink` modules are
50+
planned.
4351

4452
## Design & discussions
4553

state/README.md

Lines changed: 178 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,194 @@
11
# crucible/state
22

3-
The pure, abstract state machine kernel of the [Crucible](../README.md) suite.
3+
A pure, embeddable **statechart engine** for Go — the kernel of the
4+
[Crucible](../README.md) suite.
45

5-
> **Status:** experimental, released as `state/v0.1.0`. The kernel is complete
6-
> and tested; the API may still change before v1.
6+
> **Status:** experimental, pre-1.0. The engine is feature-complete and
7+
> extensively tested; the API may still change before v1.
78
89
Import path: `github.com/stablekernel/crucible/state`
910

1011
## What it is
1112

12-
`state` is a **portable, domain-agnostic state machine kernel**, generic over
13-
state, event, and context types. It knows nothing about any particular
14-
application domain, so the same machine definition runs unchanged from a unit
15-
test, a synchronous request handler, and an asynchronous event consumer.
16-
17-
It is the extreme end of the suite's thin-seams philosophy: **stdlib-only**, no
18-
injected IO, no forced dependencies. A tiny dependency graph is a tiny attack
19-
surface.
20-
21-
### Highlights
22-
23-
- **Pure-function step semantics** — firing an event returns
24-
`(newState, effects, trace)` with no IO; the caller dispatches the effects.
25-
- **Serializable definition IR** — the canonical machine is pure data, lossless
26-
to and from JSON. Guards, actions, and effects are named references with
27-
serializable params, bound to a host-provided registry.
28-
- **Two front-ends, one IR** — a Go DSL and a future visual editor emit the same
29-
IR; a machine authored in code and one loaded from JSON are the same machine.
30-
- **Foundry vocabulary**`Forge`, `Temper`, `Quench`, `Cast`, `Fire`, `Assay`.
31-
- **Functional options everywhere**, Trace-first observability, injected
32-
clock/ID seams for determinism.
13+
`state` is a **portable, domain-agnostic statechart engine**, generic over
14+
state, event, and context types (`Machine[S, E, C]`). It knows nothing about any
15+
particular application domain, so the same machine definition runs unchanged
16+
from a unit test, a synchronous request handler, and an asynchronous event
17+
consumer.
18+
19+
It is the extreme end of the suite's thin-seams philosophy: the engine is
20+
**stdlib-only** — it imports only the Go standard library and performs no
21+
injected IO. A tiny dependency graph is a tiny attack surface, and the boundary
22+
is enforced mechanically by an import-graph test.
23+
24+
### The pure kernel, host-driven drivers
25+
26+
The heart of the engine is a pure function. Firing an event returns
27+
`(newState, effects, trace)` and performs no IO of its own:
28+
29+
```go
30+
res := inst.Fire(ctx, Submit)
31+
// res.NewState — the resulting configuration
32+
// res.Effects — abstract data the host dispatches
33+
// res.Trace — a structured record of what happened
34+
```
35+
36+
Everything stateful — timers, invoked services, actors, mailboxes — lives in
37+
**host-driven drivers** that the engine feeds with effect *data*. Entering a
38+
state that arms a timer emits a `ScheduleAfter` effect; a host `Scheduler` owns
39+
the real clock and re-fires the delayed event back through `Fire`. The same
40+
pattern carries invoked services (`ServiceRunner`) and actors (`ActorSystem`).
41+
Because the kernel only ever emits data and never starts a goroutine, reads a
42+
clock, or touches the network, it stays portable and statically analyzable, and
43+
every driver is deterministically testable with a `FakeClock`.
44+
45+
## Features
46+
47+
A complete statechart feature surface:
48+
49+
- **State kinds** — atomic, compound (hierarchical), **parallel** (orthogonal
50+
regions), and final states, nesting to arbitrary depth.
51+
- **History** — shallow and deep history pseudo-states that re-enter a compound
52+
state's last active configuration rather than its initial child.
53+
- **Guards** — named guard refs plus the **`And` / `Or` / `Not`** combinators and
54+
the config-aware **`stateIn`** built-in, composable into a serializable boolean
55+
expression tree.
56+
- **Actions & effects** — named action refs with serializable params; actions
57+
return abstract effects the host dispatches.
58+
- **Run-to-completion****eventless (`Always`) transitions**, **`Raise`** for
59+
internal events, and a macrostep loop that drains both to a stable
60+
configuration, with overflow protection.
61+
- **Transition forms****wildcard** catch-alls (`OnAny`), **forbidden**
62+
transitions (`Forbid`), and **`Reenter`** to force the external (exit/entry)
63+
form of an otherwise-internal self-transition.
64+
- **Delayed transitions**`Transition(from).After(delay).On(event).GoTo(...)`,
65+
scheduled and auto-cancelled on exit by a host `Scheduler`.
66+
- **Invoked services** — state-scoped `Invoke(src, onDone, onError)` with
67+
result/error routing, auto-stopped on exit, driven by a host `ServiceRunner`.
68+
- **Actor model** — child-machine actors, a host `ActorSystem`, mailboxes, and
69+
dynamic `Spawn`, with **message passing**`SendTo`, `SendParent`, `Respond`,
70+
`ForwardTo`, and `StopChild` — and sender-tracked routing.
71+
- **Snapshots**`Instance.Snapshot()` captures the full runtime state
72+
(configuration, history, context, traces, pending timers/services/actors);
73+
`Machine.Restore` resumes from it without re-running entry actions, and
74+
`ResumeEffects` re-arms pending children. Actor trees persist recursively.
75+
- **Inspection & waiting** — an `Inspector` observer sink for the live
76+
event/transition/snapshot/actor stream, and `WaitFor(ctx, inst, predicate)`
77+
(plus `WaitInState` / `WaitDone`) that drives an instance until a predicate
78+
over its snapshot holds.
79+
- **Path enumeration**`PlanPath` finds the shortest sequence to a target;
80+
`state/analysis` adds `ShortestPaths` and `SimplePaths` over the whole graph.
81+
82+
## What sets it apart
83+
84+
These are Crucible's own strengths, stated plainly:
85+
86+
- **Static analysis / model-checking**`state/analysis` runs over a machine's
87+
IR to report reachability (unreachable/dead states), dead transitions,
88+
guardless nondeterminism, non-final dead ends, and liveness. Checks are exact
89+
where the IR proves them and heuristic where opaque guards limit certainty.
90+
- **Serializable IR** — the canonical machine is pure data, lossless to and from
91+
JSON. Behavior is not embedded as closures; guards, actions, effects, and
92+
services are named references with serializable params, bound to a host
93+
registry at freeze time. A machine authored in Go and one loaded from JSON are
94+
the same machine.
95+
- **Conformance harness** — a reusable harness drives golden scenarios against
96+
any machine, so a definition can be held to a fixed behavioral contract.
97+
- **Machine-evolution diffing**`state/evolution` classifies the difference
98+
between two definitions as additive or breaking and maps the result onto a
99+
SemVer bump, treating a machine definition as a schema.
100+
- **Visualization** — Mermaid and DOT export, with delayed edges annotated by
101+
their delay.
102+
- **Compile-time type safety** — the engine is generic over `S`, `E`, and `C`;
103+
states, events, and context are checked by the Go type system, not stringly
104+
typed.
105+
- **Pluggable telemetry** — a `WithLogger(*slog.Logger)` seam (no-op by default)
106+
is the only logging hook; the engine never logs unless asked and never imports
107+
a third-party logger. Determinism is preserved by injecting time and ID seams.
108+
109+
## Foundry vocabulary
110+
111+
The lifecycle API uses a small "foundry" verb vocabulary. The noun stays plain —
112+
the type is a `Machine` — only the verbs are themed:
113+
114+
| Verb | Role |
115+
| -------- | ---------------------------------------------------------------------- |
116+
| `Forge` | Open the builder DSL. |
117+
| `Temper` | Optional, non-failing dev-time diagnostics pass (lint / analysis). |
118+
| `Quench` | Freeze the definition into an immutable `Machine`; binds refs. |
119+
| `Cast` | Pour a running instance from the machine. |
120+
| `Fire` | Send an event to an instance and advance it. |
121+
| `Assay` | Check that an externally-built entity is legally in a given state. |
122+
123+
Operations that favor discoverability over metaphor stay plain: `PlanPath`,
124+
`Requirements`, `Trace`, and the `To*` / `LoadFromJSON` serializers.
125+
126+
The public API follows the suite's functional-options convention: required
127+
inputs stay positional; everything optional is a variadic option, so a
128+
zero-option call reads clean and new capability arrives additively.
129+
130+
## Usage
131+
132+
A small document-approval machine, forged, frozen, and fired:
133+
134+
```go
135+
package main
136+
137+
import (
138+
"context"
139+
"fmt"
140+
141+
"github.com/stablekernel/crucible/state"
142+
)
143+
144+
func main() {
145+
m := state.Forge[DocState, DocEvent, *Document]("document").
146+
Guard("hasReviewer", func(ctx state.GuardCtx[*Document]) bool {
147+
return ctx.Entity.ReviewerID != nil
148+
}).
149+
Action("emit", emit).
150+
State(Draft).
151+
State(Submitted).
152+
State(Approved).
153+
Initial(Draft).
154+
CurrentStateFn(func(d *Document) DocState { return d.Status }).
155+
Transition(Draft).On(Submit).GoTo(Submitted).
156+
Do("emit", state.P{"event": "submitted"}).
157+
Transition(Submitted).On(Approve).GoTo(Approved).
158+
When("hasReviewer").
159+
Quench(state.Strict())
160+
161+
doc := &Document{Status: Draft}
162+
res := m.Cast(doc).Fire(context.Background(), Submit)
163+
164+
fmt.Println("state:", res.NewState) // Submitted
165+
fmt.Println("effects:", res.Effects) // [{submitted}]
166+
}
167+
```
168+
169+
`Cast` returns a running `Instance`; `Fire` advances it and returns the new
170+
state, the emitted effects, and the trace. The same machine can be serialized
171+
with `m.ToJSON()`, reloaded with `state.LoadFromJSON`, analyzed with
172+
`analysis.Analyze`, or rendered to Mermaid/DOT — all from the one definition.
173+
174+
## Subpackages
175+
176+
| Package | What it is |
177+
| ------------------- | ------------------------------------------------------------------ |
178+
| `state/analysis` | Static model-checking and path enumeration over a machine's IR. |
179+
| `state/evolution` | Diffs two machine definitions and classifies the SemVer bump. |
180+
| `state/conformance` | Reusable harness for driving golden scenarios against a machine. |
33181

34182
## Stability
35183

36-
Stability label: **experimental** (pre-v1; the API may change).
37-
38-
## What's implemented
39-
40-
The builder and transition engine; guards, actions, and effects; typed errors
41-
and always-recorded traces; hierarchical and orthogonal states; entity
42-
validation; path planning; batch firing; the serializable IR and host registry;
43-
a reusable conformance harness; and Mermaid/DOT export. The kernel depends only
44-
on the standard library.
184+
Stability label: **experimental** (pre-1.0; the API may change). Each module is
185+
independently versioned per-module SemVer.
45186

46-
Reserved for later releases: history states, invoked services, the actor model,
47-
and timed transitions.
187+
## Design & discussions
48188

49-
Design rationale lives on the GitHub Discussions board under the **State
50-
Machine** category — see the
189+
Design rationale lives on the GitHub
190+
[Discussions board](https://github.com/stablekernel/crucible/discussions) under
191+
the **State Machine** category — see the
51192
[Overview](https://github.com/stablekernel/crucible/discussions/1) and
52193
[Kernel Core](https://github.com/stablekernel/crucible/discussions/2)
53194
discussions.

0 commit comments

Comments
 (0)