state

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 30, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

README

crucible/state

A pure, embeddable statechart engine for Go — the kernel of the Crucible suite.

Status: experimental, pre-1.0. The engine is feature-complete and extensively tested; the API may still change before v1.

Import path: github.com/stablekernel/crucible/state

What it is

state is a portable, domain-agnostic statechart engine, generic over state, event, and context types (Machine[S, E, C]). It knows nothing about any particular application domain, so the same machine definition runs unchanged from a unit test, a synchronous request handler, and an asynchronous event consumer.

It is the extreme end of the suite's thin-seams philosophy: the engine is stdlib-only — it imports only the Go standard library and performs no injected IO. A tiny dependency graph is a tiny attack surface, and the boundary is enforced mechanically by an import-graph test.

The pure kernel, host-driven drivers

The heart of the engine is a pure function. Firing an event returns (newState, effects, trace) and performs no IO of its own:

res := inst.Fire(ctx, Submit)
// res.NewState — the resulting configuration
// res.Effects  — abstract data the host dispatches
// res.Trace    — a structured record of what happened

Everything stateful — timers, invoked services, actors, mailboxes — lives in host-driven drivers that the engine feeds with effect data. Entering a state that arms a timer emits a ScheduleAfter effect; a host Scheduler owns the real clock and re-fires the delayed event back through Fire. The same pattern carries invoked services (ServiceRunner) and actors (ActorSystem). Because the kernel only ever emits data and never starts a goroutine, reads a clock, or touches the network, it stays portable and statically analyzable, and every driver is deterministically testable with a FakeClock.

Features

A complete statechart feature surface:

  • State kinds — atomic, compound (hierarchical), parallel (orthogonal regions), and final states, nesting to arbitrary depth.
  • History — shallow and deep history pseudo-states that re-enter a compound state's last active configuration rather than its initial child.
  • Guards — named guard refs plus the And / Or / Not combinators and the config-aware stateIn built-in, composable into a serializable boolean expression tree.
  • Actions & effects — named action refs with serializable params; actions return abstract effects the host dispatches.
  • Run-to-completioneventless (Always) transitions, Raise for internal events, and a macrostep loop that drains both to a stable configuration, with overflow protection.
  • Transition formswildcard catch-alls (OnAny), forbidden transitions (Forbid), and Reenter to force the external (exit/entry) form of an otherwise-internal self-transition.
  • Delayed transitionsTransition(from).After(delay).On(event).GoTo(...), scheduled and auto-cancelled on exit by a host Scheduler.
  • Invoked services — state-scoped Invoke(src, onDone, onError) with result/error routing, auto-stopped on exit, driven by a host ServiceRunner.
  • Actor model — child-machine actors, a host ActorSystem, mailboxes, and dynamic Spawn, with message passingSendTo, SendParent, Respond, ForwardTo, and StopChild — and sender-tracked routing.
  • SnapshotsInstance.Snapshot() captures the full runtime state (configuration, history, context, traces, pending timers/services/actors); Machine.Restore resumes from it without re-running entry actions, and ResumeEffects re-arms pending children. Actor trees persist recursively.
  • Inspection & waiting — an Inspector observer sink for the live event/transition/snapshot/actor stream, and WaitFor(ctx, inst, predicate) (plus WaitInState / WaitDone) that drives an instance until a predicate over its snapshot holds.
  • Path enumerationPlanPath finds the shortest sequence to a target; state/analysis adds ShortestPaths and SimplePaths over the whole graph.

What sets it apart

These are Crucible's own strengths, stated plainly:

  • Static analysis / model-checkingstate/analysis runs over a machine's IR to report reachability (unreachable/dead states), dead transitions, guardless nondeterminism, non-final dead ends, and liveness. Checks are exact where the IR proves them and heuristic where opaque guards limit certainty.
  • Serializable IR — the canonical machine is pure data, lossless to and from JSON. Behavior is not embedded as closures; guards, actions, effects, and services are named references with serializable params, bound to a host registry at freeze time. A machine authored in Go and one loaded from JSON are the same machine.
  • Conformance harness — a reusable harness drives golden scenarios against any machine, so a definition can be held to a fixed behavioral contract.
  • Machine-evolution diffingstate/evolution classifies the difference between two definitions as additive or breaking and maps the result onto a SemVer bump, treating a machine definition as a schema.
  • Visualization — Mermaid and DOT export, with delayed edges annotated by their delay.
  • Compile-time type safety — the engine is generic over S, E, and C; states, events, and context are checked by the Go type system, not stringly typed.
  • Pluggable telemetry — a WithLogger(*slog.Logger) seam (no-op by default) is the only logging hook; the engine never logs unless asked and never imports a third-party logger. Determinism is preserved by injecting time and ID seams.

Foundry vocabulary

The lifecycle API uses a small "foundry" verb vocabulary. The noun stays plain — the type is a Machine — only the verbs are themed:

Verb Role
Forge Open the builder DSL.
Temper Optional, non-failing dev-time diagnostics pass (lint / analysis).
Quench Freeze the definition into an immutable Machine; binds refs.
Cast Pour a running instance from the machine.
Fire Send an event to an instance and advance it.
Assay Check that an externally-built entity is legally in a given state.

Operations that favor discoverability over metaphor stay plain: PlanPath, Requirements, Trace, and the To* / LoadFromJSON serializers.

The public API follows the suite's functional-options convention: required inputs stay positional; everything optional is a variadic option, so a zero-option call reads clean and new capability arrives additively.

Usage

A small document-approval machine, forged, frozen, and fired:

package main

import (
	"context"
	"fmt"

	"github.com/stablekernel/crucible/state"
)

func main() {
	m := state.Forge[DocState, DocEvent, *Document]("document").
		Guard("hasReviewer", func(ctx state.GuardCtx[*Document]) bool {
			return ctx.Entity.ReviewerID != nil
		}).
		Action("emit", emit).
		State(Draft).
		State(Submitted).
		State(Approved).
		Initial(Draft).
		CurrentStateFn(func(d *Document) DocState { return d.Status }).
		Transition(Draft).On(Submit).GoTo(Submitted).
		Do("emit", state.P{"event": "submitted"}).
		Transition(Submitted).On(Approve).GoTo(Approved).
		When("hasReviewer").
		Quench(state.Strict())

	doc := &Document{Status: Draft}
	res := m.Cast(doc).Fire(context.Background(), Submit)

	fmt.Println("state:", res.NewState)   // Submitted
	fmt.Println("effects:", res.Effects)  // [{submitted}]
}

Cast returns a running Instance; Fire advances it and returns the new state, the emitted effects, and the trace. The same machine can be serialized with m.ToJSON(), reloaded with state.LoadFromJSON, analyzed with analysis.Analyze, or rendered to Mermaid/DOT — all from the one definition.

Subpackages

Package What it is
state/analysis Static model-checking and path enumeration over a machine's IR.
state/evolution Diffs two machine definitions and classifies the SemVer bump.
state/conformance Reusable harness for driving golden scenarios against a machine.

Stability

Stability label: experimental (pre-1.0; the API may change). Each module is independently versioned per-module SemVer.

Design & discussions

Design rationale lives on the GitHub Discussions board under the State Machine category — see the Overview and Kernel Core discussions.

License

Apache-2.0. See the repository LICENSE and NOTICE.

Documentation

Overview

Package state is the pure, abstract state machine kernel of the Crucible suite — a portable, domain-agnostic engine for forging event-driven services in Go.

Import path: github.com/stablekernel/crucible/state

What this kernel is

state is an abstract, domain-agnostic state machine kernel built once and usable everywhere. It is generic over state, event, and context types (conceptually Machine[S, E, C]) and knows nothing about any particular application domain. The same machine definition runs unchanged from a unit test, a synchronous request handler, and an asynchronous event consumer.

The kernel is stdlib-only. It imports only the Go standard library and performs no injected IO. This is the extreme end of the suite's "thin seams, no-op defaults, no forced dependencies" philosophy: a tiny dependency graph is a tiny attack surface, and the kernel stays a clean, extractable unit forever. The stdlib-only boundary is enforced mechanically by an import-graph test.

Pure-function step semantics

Firing an event returns (newState, effects, trace) without performing any IO. The caller dispatches the effects however it likes — publish to a broker, write to a store, call an RPC. Effects are abstract at the kernel (the kernel never inspects the payload) and concrete at your domain layer. This is what makes one machine usable across tests, handlers, and consumers without change.

The definition IR is the spec

The canonical machine is a serializable definition IR: pure data, lossless to and from JSON. Behavior is not embedded as closures in the IR; every guard, action, and effect is a named reference with serializable params, bound to host-provided implementations through a registry at freeze time. Binding fails loudly if any reference does not resolve.

This is the config/implementation split: structure is dual-authored (code or, eventually, a visual UI) while behavior is always code, surfaced to authors as a named palette. The Go DSL and a future UI are two front-ends that emit the same IR; a machine authored in Go and a machine loaded from JSON are the same machine.

Foundry vocabulary

The lifecycle API uses a small "foundry" verb vocabulary. The noun stays plain — the type is a Machine — only the verbs are themed:

  • Forge — open the builder DSL.
  • Temper — optional, non-failing dev-time diagnostics pass (lint / static analysis), chainable before Quench.
  • Quench — freeze the definition into an immutable Machine; the always-call finalizer that binds refs and panics on misconfiguration.
  • Cast — pour a running instance from the machine.
  • Fire — send an event to an instance and advance it.
  • Assay — check that an externally-constructed entity is legally in a given state.

Operations that favor discoverability over metaphor stay plain: PlanPath, Requirements, Trace, and the To*/LoadFromJSON serializers.

Design

The public API follows the suite's functional-options convention: every public constructor and operation takes a variadic option tail. Required inputs stay positional; everything optional or extensible is an option; a zero-option call reads clean. New capability arrives as a new option — additive-only, never a signature or breaking change. The kernel idiom is fail-fast by default, with resilience and aggregation available opt-in via options.

Observability is Trace-first: the structured Trace is the canonical surface, recording matched transitions, guard and policy evaluations, emitted effects, and the outcome as pure data. An optional WithLogger(*slog.Logger) (no-op by default) is the only logging seam; the kernel never logs unless asked and never imports a third-party logger. Determinism is preserved by injecting time and identifier seams rather than calling time.Now or rand directly.

As a library, the kernel never exits the process — it never calls os.Exit or log.Fatal on an operational error. Panics are reserved strictly for programmer error at construction time (Quench).

Status

The kernel implements the Forge/Temper/Quench build path, Cast/Fire pure step semantics with guards, actions, typed errors and an always-recorded Trace, Assay/Requirements, PlanPath (BFS), FireSeq/FireEach batch helpers, and lossless ToJSON/LoadFromJSON/Provide round-trip.

Hierarchical and orthogonal states extend the same surface: a state may declare nested substates with an initial child (compound states) or parallel regions (orthogonal states). Superstates nest to arbitrary depth — a SuperState block may contain another SuperState block — and parallel regions may contain nested compounds. Events resolve child-first and bubble to ancestors; orthogonal regions each receive the event and resolve independently; transitions run the standard exit/entry cascade across the hierarchy; and final states drive done-event completion, including the all-regions-final join for parallel states. The hierarchy serializes, so a nested machine round-trips through JSON losslessly.

History pseudo-states (shallow and deep) let a transition re-enter a compound state's last active configuration rather than its initial child; the pseudo-states serialize while the recorded per-instance configuration is runtime state threaded through the pure Fire step.

Delayed (`after`) transitions are drivable: entering a state with an `after` transition emits a ScheduleAfter effect and exiting it a CancelScheduled effect (auto-cancel-on-exit), while Fire stays pure — a host Scheduler driver owns the real timer and re-fires the delayed event, with a deterministic FakeClock for testing.

Invoked services (`invoke`) are drivable: entering a state that declares an invoke emits a StartService effect and exiting it before the service completes emits a StopService effect (auto-stop-on-exit), while Fire stays pure — a host ServiceRunner runs the bound service and re-fires the invocation's onDone (with the result) or onError (with the error) back through Fire, with a deterministic settle-by-id harness for testing. Child-machine actors (invoking another Machine as a sub-actor) and the actor model remain reserved-but-inert; they arrive with the instance mailbox.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ActorID added in v0.2.0

func ActorID[S comparable](machine string, from S, idx int) string

ActorID returns the stable identifier the kernel assigns to the child-machine actor invocation at index idx on owning state `from` of machine `machine` when the invocation declares no explicit ID. A host or test uses it to correlate a SpawnActor with a later StopActor, to Deliver an event to the actor, or to assert which actor a StopActor targets.

func InvokeID added in v0.2.0

func InvokeID[S comparable](machine string, from S, idx int) string

InvokeID returns the stable identifier the kernel assigns to the invoked service at index idx on owning state `from` of machine `machine` when the invocation declares no explicit ID. A host or test uses it to correlate a StartService with a later StopService, or to assert which service a StopService targets.

func MarshalSnapshot added in v0.2.0

func MarshalSnapshot[S comparable, E comparable, C any](snap Snapshot[S, E, C], opts ...SnapshotCodecOption[C]) ([]byte, error)

MarshalSnapshot serializes snap to JSON, encoding its context through codec (or the default JSON codec when codec is nil). It is the explicit serialization entry point when a non-JSON-marshalable context needs a custom codec; for a JSON-marshalable context, json.Marshal(snap) works directly via the snapshot's own MarshalJSON.

func ScheduleID added in v0.2.0

func ScheduleID[S comparable](machine string, from S, idx int) string

ScheduleID returns the stable schedule identifier the kernel assigns to the delayed (`after`) transition at index idx on source state `from` of machine `machine`. A host or test uses it to correlate a ScheduleAfter with a later Cancel, or to assert which timer a CancelScheduled targets.

Types

type ActionCtx

type ActionCtx[C any] struct {
	Entity C
	Params map[string]any
}

ActionCtx is passed to a bound action function at run time.

type ActionFn

type ActionFn[C any] func(ctx ActionCtx[C]) (Effect, error)

ActionFn produces an effect (or error) for a transition.

type ActorBehavior added in v0.2.0

type ActorBehavior func(input map[string]any) (ActorInstance, error)

ActorBehavior creates a fresh child-machine actor instance bound to the given input. It is the actor-palette analog of a ServiceFn: a host registers one per child-machine src name, and the ActorSystem calls it to spawn an actor when it absorbs a SpawnActor effect for that src. The returned ActorInstance erases the child's own (S, E, C) generic parameters behind the ActorInstance interface, so a parent of any type can host children of any type. The input is the SpawnActor Input is the actor input; a behavior typically Casts its child machine with a WithInitialState derived from input.

type ActorInstance added in v0.2.0

type ActorInstance interface {
	// DeliverFire fires one event through the actor, returning whether the actor
	// reached its final state and the output it exposes on completion. The event is
	// the actor's own event type, passed type-erased; an implementation type-asserts
	// it and ignores an event of the wrong type (a no-op, mirroring the kernel's
	// effect-type guards). A backing *Instance implementation also surfaces the
	// SpawnActor / StopActor effects the child itself emitted, so the system can run
	// nested actors — those are returned via ChildEffects.
	DeliverFire(ctx context.Context, event any) (done bool, output any)
	// ChildEffects returns the actor effects the actor emitted on its most recent
	// DeliverFire (and on its initial entry): the SpawnActor / StopActor lifecycle
	// effects so the ActorSystem can spawn or stop the actor's own children, and the
	// SendTo / SendParent / RespondToSender / ForwardEvent communication effects so
	// the system can route the actor's outbound messages. It returns a fresh slice
	// each call and drains the buffer.
	ChildEffects() []Effect
	// Output returns the actor's completion output once it has reached its final
	// state, or nil before then. It lets a host expose a snapshot's output.
	Output() any
}

ActorInstance is a running child actor as the ActorSystem sees it, with the child's own (S, E, C) generic parameters erased. A host obtains one by wrapping a Cast child *Instance with NewActor; the deterministic test driver and the production driver both drive actors purely through this interface.

func NewActor added in v0.2.0

func NewActor[S comparable, E comparable, C any](inst *Instance[S, E, C], output func(*Instance[S, E, C]) any) ActorInstance

NewActor adapts a Cast child *Instance into an ActorInstance an ActorSystem can run as a child-machine actor. output, when non-nil, extracts the actor's v5 `output` from the child instance once it reaches its final state (typically reading the child entity); pass nil for an actor whose completion carries no output. The returned ActorInstance is what an ActorBehavior returns. The child's initial-entry actor effects (StartEffects) are buffered immediately, so the system spawns any actors the child invokes on entry.

type ActorKind added in v0.2.0

type ActorKind int

ActorKind tags an Invocation as either a host-run service or a child-machine actor. The default (ActorKindService) preserves the invoked-services contract verbatim; ActorKindMachine marks the invocation as spawning a child MACHINE actor, so entering the owning state emits a SpawnActor effect instead of a StartService effect, and the host's ActorSystem (not a ServiceRunner) runs it.

const (
	ActorKindService ActorKind = iota
	ActorKindMachine
)

Actor kinds. ActorKindService is the invoked-services default (a host-run unit of work); ActorKindMachine invokes a child machine as an actor.

type ActorPhase added in v0.2.0

type ActorPhase string

ActorPhase distinguishes the lifecycle point of an InspectActor event.

const (
	// ActorSpawned marks an actor created and started.
	ActorSpawned ActorPhase = "spawned"
	// ActorStopped marks an actor stopped (completed, errored, or auto-stopped on
	// exit).
	ActorStopped ActorPhase = "stopped"
)

type ActorRef added in v0.2.0

type ActorRef struct {
	// ID is the actor's registry key in the ActorSystem.
	ID string
	// SystemID is the optional system-scoped name the actor registered under
	// (its systemId); empty when the actor was spawned without one.
	SystemID string
	// Src is the actor ref name the actor was spawned from, for diagnostics.
	Src string
}

ActorRef is the runtime handle a machine stores in its context to address a spawned actor later (an actor ref). It is created by the ActorSystem when the actor is spawned and surfaced to the spawning machine through the system's API, never through the IR — refs are runtime, not serializable definition. A ref carries the actor's ID (and optional system-scoped SystemID) so the holder can Deliver events to it or read its snapshot through the system.

type ActorSystem added in v0.2.0

type ActorSystem[S comparable, E comparable, C any] struct {
	// contains filtered or unexported fields
}

ActorSystem is the reusable host-driver that turns the kernel's SpawnActor / StopActor effects into running child-machine actors, owns each actor's mailbox, routes delivered events into mailboxes, steps actors via Fire, and re-fires the parent's onDone / onError when a child completes or fails. It is concurrency-safe. Construct one per parent instance with NewActorSystem, then Register the child-machine behaviors that resolve SpawnActor Src refs; drive it by passing each Fire's effects (and the parent's StartEffects) to Absorb, and step actors with Deliver / Step.

In the deterministic form the system records each spawned actor and steps it only when the test calls Deliver / Step, so actor machines are exercised with no real concurrency; a production host instead runs each actor's Step on its own goroutine fed by the mailbox.

func NewActorSystem added in v0.2.0

func NewActorSystem[S comparable, E comparable, C any](parent *Instance[S, E, C]) *ActorSystem[S, E, C]

NewActorSystem returns an ActorSystem driving parent: the instance whose SpawnActor / StopActor effects spawn and stop child actors, and through whose Fire a completed child's onDone / onError is routed. Register child-machine behaviors with Register before absorbing spawn effects.

func (*ActorSystem[S, E, C]) Absorb added in v0.2.0

func (s *ActorSystem[S, E, C]) Absorb(ctx context.Context, effects []Effect)

Absorb scans effects, spawning an actor for each SpawnActor (resolving its Src against the palette and running the child machine) and stopping the actor for each StopActor (auto-stop-on-exit, recursively stopping the actor's children). It is how a host wires Fire's output back into the system; call it with the effects of every Fire (and once with the parent's StartEffects for the initial state). A SpawnActor whose OnDone/OnError is not the parent's event type still spawns the actor (a fire-and-forget child) but routes no completion event.

A SpawnActor whose Src does not resolve against the palette is settled immediately as an error: its OnError (when usable) is fired through the parent so the parent routes onError rather than hanging, mirroring the ServiceRunner's unbound-service handling.

func (*ActorSystem[S, E, C]) AbsorbFor added in v0.2.0

func (s *ActorSystem[S, E, C]) AbsorbFor(ctx context.Context, event any, effects []Effect)

AbsorbFor is Absorb for the effects of a host-driven parent Fire(event): it additionally lets a ForwardEvent the parent emits forward event verbatim to a child. Use it (rather than Absorb) when the parent itself runs forwardTo on a host-injected event; Absorb suffices for sendTo / sendParent / respond and all lifecycle effects.

func (*ActorSystem[S, E, C]) Deliver added in v0.2.0

func (s *ActorSystem[S, E, C]) Deliver(ctx context.Context, ref ActorRef, event any) bool

Deliver routes event into the mailbox of the actor identified by ref, then drains the actor (Step) so the delivered event is processed and any resulting completion is routed to the parent. It returns whether the actor was found running. It is the delivery mechanism the sendTo / sendParent / respond / forwardTo action sugar routes through; a host (or a test) may also call it directly to inject an event into an actor from outside.

func (*ActorSystem[S, E, C]) DeliverByID added in v0.2.0

func (s *ActorSystem[S, E, C]) DeliverByID(ctx context.Context, id string, event any) bool

DeliverByID is Deliver keyed by raw actor id, for a host that tracks ids rather than refs.

func (*ActorSystem[S, E, C]) IDs added in v0.2.0

func (s *ActorSystem[S, E, C]) IDs() []string

IDs returns the ids of all live actors, sorted, for deterministic host iteration (e.g. delivering to or stepping every actor in a stable order).

func (*ActorSystem[S, E, C]) IsRunning added in v0.2.0

func (s *ActorSystem[S, E, C]) IsRunning(id string) bool

IsRunning reports whether an actor with the given id is live.

func (*ActorSystem[S, E, C]) LastError added in v0.2.0

func (s *ActorSystem[S, E, C]) LastError() error

LastError returns the error the most recently settled actor produced, or nil when the last settlement was a success or none has occurred.

func (*ActorSystem[S, E, C]) LastOutput added in v0.2.0

func (s *ActorSystem[S, E, C]) LastOutput() (any, bool)

LastOutput returns the output the most recently settled actor produced, and true when that settlement was a success. The parent action bound to an actor's onDone transition reads it to consume the child's output; it is valid only during the synchronous parent Fire the settlement triggers. It returns false after a failure or before any settlement.

func (*ActorSystem[S, E, C]) Ref added in v0.2.0

func (s *ActorSystem[S, E, C]) Ref(id string) (ActorRef, bool)

Ref returns the ActorRef for the running actor under id, and whether such an actor is running. The spawning machine stores the ref in its context (the host's spawn action reads it from the system after Absorb) to address the actor later.

func (*ActorSystem[S, E, C]) RefBySystemID added in v0.2.0

func (s *ActorSystem[S, E, C]) RefBySystemID(systemID string) (ActorRef, bool)

RefBySystemID returns the ActorRef for the actor registered under the given its systemId, and whether one is running. It lets a sibling address an actor by its well-known system name rather than by spawn id.

func (*ActorSystem[S, E, C]) Register added in v0.2.0

func (s *ActorSystem[S, E, C]) Register(src string, behavior ActorBehavior) *ActorSystem[S, E, C]

Register binds a child-machine behavior under src in the system's actor palette, so a SpawnActor whose Src.Name is src resolves to behavior. It is the actor-model analog of Registry.Service: a host registers each child machine it can spawn. Registering returns the system for chaining.

func (*ActorSystem[S, E, C]) RestoreActors added in v0.2.0

func (s *ActorSystem[S, E, C]) RestoreActors(ctx context.Context, actors map[string]json.RawMessage) error

RestoreActors re-establishes the system's child actors from the snapshots SnapshotActors produced, recursively: each actor is re-spawned from the system's palette under its original id, its captured state reloaded (resuming it in place without re-running entry actions) when it was resumable, and its nested children restored beneath it. A not-yet-done actor whose Src does not resolve against the palette is skipped (the host registered a different palette); a done actor is not re-spawned. Register the same child-machine behaviors before calling it, exactly as for the original Absorb.

An actor recorded as not resumable (its ActorInstance did not implement Snapshotter) is re-spawned fresh rather than resumed — the one deferred actor-tree depth, flagged on the snapshot's Resumed field.

func (*ActorSystem[S, E, C]) Running added in v0.2.0

func (s *ActorSystem[S, E, C]) Running() int

Running reports the number of live (spawned, not-stopped, not-completed) actors. A test asserts on it to confirm an actor spawned or was auto-stopped on exit.

func (*ActorSystem[S, E, C]) SettleError added in v0.2.0

func (s *ActorSystem[S, E, C]) SettleError(ctx context.Context, id string, err error) (FireResult[S], bool)

SettleError fails the running actor under id explicitly (e.g. a host-detected child crash), routing the parent's onError. It returns the parent FireResult and true, or false when id is not running or routes no onError.

func (*ActorSystem[S, E, C]) SnapshotActors added in v0.2.0

func (s *ActorSystem[S, E, C]) SnapshotActors() (map[string]json.RawMessage, error)

SnapshotActors captures the runtime state of every live child actor the system runs, recursively (each actor's own spawned children are captured beneath it), as a JSON document keyed by actor id. It is the actor-tree companion to Instance.Snapshot: a host that persists a parent instance also calls SnapshotActors to persist the parent's spawned children, and stores the result under the parent snapshot's Actors map. It is a pure read of the system's actor registry and never fires or mutates an actor.

Call it at a quiescent point (after draining mailboxes with Step), so no in-flight mailbox backlog is lost. An actor whose ActorInstance does not implement Snapshotter is recorded as present but not resumable (Resumed false) and is re-spawned fresh on RestoreActors.

func (*ActorSystem[S, E, C]) Step added in v0.2.0

func (s *ActorSystem[S, E, C]) Step(ctx context.Context, id string) []FireResult[S]

Step drains the mailbox of the actor under id, firing each queued event through the actor in order. When the actor reaches its final state it is settled: the parent's onDone event (carrying the child's output) is fired through the parent and the resulting effects absorbed; nested-child effects the actor emits are absorbed too. It returns the parent FireResults produced by completion routing, in order (empty when the actor did not complete). Step is safe to call with an empty mailbox (a no-op) and is how the deterministic driver advances an actor; a production driver runs it from the actor's own goroutine.

func (*ActorSystem[S, E, C]) Stop added in v0.2.0

func (s *ActorSystem[S, E, C]) Stop(ref ActorRef)

Stop stops the actor identified by ref (and its children), so a machine that holds an ActorRef can explicitly tear an actor down. Stopping an unknown actor is a no-op.

func (*ActorSystem[S, E, C]) WithActorInspector added in v0.2.0

func (s *ActorSystem[S, E, C]) WithActorInspector(insp Inspector) *ActorSystem[S, E, C]

WithActorInspector wires a live observer sink fed the ActorSystem's actor-lifecycle and inter-actor message inspection events — actor spawned / stopped, and message sent / delivered (the actor-to-actor flavor of an event). Pass the same Inspector also wired to the parent instance (WithInspector) to observe the whole system on one sink. It is off by default; an un-inspected system pays nothing.

type AssayError

type AssayError struct {
	Failures []RequirementFailure
}

AssayError aggregates one or more failing requirements found by Assay.

func (*AssayError) Error

func (e *AssayError) Error() string

type AssayOption

type AssayOption func(*assayConfig)

AssayOption configures Assay.

func WithAggregate

func WithAggregate() AssayOption

WithAggregate makes Assay collect all failing requirements in one pass.

type BatchResult

type BatchResult[S comparable] struct {
	Steps []FireResult[S]
	Trace Trace
	Err   error
}

BatchResult is the result of a batch fire (FireSeq / FireEach).

type Builder

type Builder[S comparable, E comparable, C any] struct {
	// contains filtered or unexported fields
}

Builder is the Forge DSL front-end. It builds the IR and registers implementations by name.

func Forge

func Forge[S comparable, E comparable, C any](name string, opts ...ForgeOption) *Builder[S, E, C]

Forge opens a builder.

Example

ExampleForge builds a document-approval machine with the Forge DSL and fires a single event, showing the resulting state and the effect the transition emitted.

m := buildDocMachine()
doc := &Document{Status: Draft}
res := m.Cast(doc).Fire(context.Background(), Submit)

fmt.Println("state:", res.NewState)
fmt.Println("effects:", res.Effects)
Output:
state: Submitted
effects: [{submitted}]

func (*Builder[S, E, C]) Action

func (b *Builder[S, E, C]) Action(name string, fn ActionFn[C]) *Builder[S, E, C]

Action registers a named action into the builder's palette.

func (*Builder[S, E, C]) After added in v0.2.0

func (b *Builder[S, E, C]) After(delay time.Duration) *Builder[S, E, C]

After opens a delayed ("after") transition from the most-recent state: a transition that the host's runtime fires once `delay` elapses while the source state stays active. Chain On(event).GoTo(target) to name the delayed event the host re-fires and the target it lands in (When/Do as usual). On entering the source state the kernel emits a ScheduleAfter effect; on exiting it before the delay elapses, a CancelScheduled effect (auto-cancel-on-exit). The kernel never sleeps — the host owns the timer and feeds the delayed event back through Fire. This is the DSL form of a delayed (after) transition.

func (*Builder[S, E, C]) Always added in v0.2.0

func (b *Builder[S, E, C]) Always() *Builder[S, E, C]

Always opens an eventless ("always") transition from the most-recent state. It carries no triggering event and is auto-fired by the run-to-completion loop whenever its guards pass and the state is active, within the firing macrostep. Chain GoTo/When/Do as usual. This is the DSL form of an eventless transition.

func (*Builder[S, E, C]) Cancel added in v0.2.0

func (b *Builder[S, E, C]) Cancel(id string) *Builder[S, E, C]

Cancel attaches the kernel Cancel built-in to the most-recent transition: when the transition fires, the kernel emits a CancelScheduled effect for the given schedule id, so a machine can explicitly cancel a pending delayed (`after`) event before its delay elapses. The id is the ScheduleAfter ID the host received; ScheduleID derives it for a known source state and delayed-edge index. Canceling an unknown id is a host-side no-op. The built-in needs no host registration, mirroring the stateIn guard built-in.

func (*Builder[S, E, C]) CurrentStateFn

func (b *Builder[S, E, C]) CurrentStateFn(fn func(C) S) *Builder[S, E, C]

CurrentStateFn declares how to derive an instance's current state.

func (*Builder[S, E, C]) DefaultTo added in v0.2.0

func (b *Builder[S, E, C]) DefaultTo(target S) *Builder[S, E, C]

DefaultTo sets the fallback target of the most-recent history pseudo-state, entered when its owning compound has no recorded history yet. It is a no-op (recorded as a lint at Quench) when the most-recent state is not a history pseudo-state.

func (*Builder[S, E, C]) Do

func (b *Builder[S, E, C]) Do(actionName string, params ...map[string]any) *Builder[S, E, C]

Do attaches a named action ref with params to the most-recent transition.

func (*Builder[S, E, C]) EndRegion

func (b *Builder[S, E, C]) EndRegion() *Builder[S, E, C]

EndRegion closes the most-recent Region block.

func (*Builder[S, E, C]) EndSuperState

func (b *Builder[S, E, C]) EndSuperState() *Builder[S, E, C]

EndSuperState closes the most-recent SuperState block.

func (*Builder[S, E, C]) Final

func (b *Builder[S, E, C]) Final() *Builder[S, E, C]

Final marks the most-recent state as terminal.

func (*Builder[S, E, C]) Forbid added in v0.2.0

func (b *Builder[S, E, C]) Forbid(event E) *Builder[S, E, C]

Forbid declares that the most-recent state blocks the given event: the event is consumed and ignored there and does NOT bubble to ancestors, distinct from having no handler (which bubbles). This is the DSL form of a `on: { E: undefined }`. A forbidden transition takes no target, guards, or effects.

func (*Builder[S, E, C]) ForbidAny added in v0.2.0

func (b *Builder[S, E, C]) ForbidAny() *Builder[S, E, C]

ForbidAny declares a forbidden wildcard: every event not otherwise handled is consumed and ignored at the most-recent state instead of bubbling. This is the DSL form of a forbidden wildcard transition.

func (*Builder[S, E, C]) ForwardTo added in v0.2.0

func (b *Builder[S, E, C]) ForwardTo(targetID string, opts ...SendOption) *Builder[S, E, C]

ForwardTo attaches the kernel forwardTo built-in to the most-recent transition: when the transition fires, the kernel emits a ForwardEvent effect so the host's ActorSystem forwards the event the emitting actor is currently handling, verbatim, to the actor registered under targetID. Address an actor by its system-scoped id instead with WithSendToSystemID. The built-in needs no host registration. This is the DSL form of forwarding the current event to another actor.

func (*Builder[S, E, C]) GoTo

func (b *Builder[S, E, C]) GoTo(to S) *Builder[S, E, C]

GoTo sets the target of the most-recent transition.

func (*Builder[S, E, C]) Guard

func (b *Builder[S, E, C]) Guard(name string, fn GuardFn[C]) *Builder[S, E, C]

Guard registers a named guard into the builder's palette.

func (*Builder[S, E, C]) History added in v0.2.0

func (b *Builder[S, E, C]) History(name S, kind HistoryType) *Builder[S, E, C]

History declares a history pseudo-state inside the current SuperState block. The pseudo-state remembers the owning compound's last active configuration: HistoryShallow restores the compound's last active direct child, HistoryDeep restores its full nested leaf configuration. Transition to it (by name) to re-enter the remembered configuration instead of the compound's Initial. Use DefaultTo to declare the target entered when no history has been recorded yet; without it the resolver falls back to the compound's Initial.

A history pseudo-state is structure, not a leaf: it never appears in the active configuration and is not eligible as a compound's Initial. Declaring one outside a SuperState block is a Quench lint.

func (*Builder[S, E, C]) Initial

func (b *Builder[S, E, C]) Initial(name S) *Builder[S, E, C]

Initial sets the entry state. At the top level it sets the machine's initial state; inside a SuperState or Region block it sets that block's initial child.

func (*Builder[S, E, C]) Invoke added in v0.2.0

func (b *Builder[S, E, C]) Invoke(src string, onDone, onError E, opts ...InvokeOption) *Builder[S, E, C]

Invoke declares an invoked service on the most-recent state (an `invoke`). src names the service in the registry (bind it with Service); onDone and onError name the events the host re-fires through Fire when the service completes or fails, routed by ordinary transitions from this state. Configure the input passed to the service and an explicit id with the variadic InvokeOptions (WithInput, WithInvokeID); omitting WithInvokeID derives a stable id via InvokeID. On entering this state the kernel emits a StartService effect; on exiting it before the service completes, a StopService effect (auto-stop-on-exit). The kernel never runs the service — a host ServiceRunner does, keeping Fire pure.

func (*Builder[S, E, C]) InvokeActor added in v0.2.0

func (b *Builder[S, E, C]) InvokeActor(src string, onDone, onError E, opts ...InvokeOption) *Builder[S, E, C]

InvokeActor declares a child-MACHINE actor invoked while the most-recent state is active (invoke of a child machine). src names the child-machine factory registered in the host's ActorSystem actor palette; onDone and onError name the events the host re-fires through the PARENT's Fire when the child reaches its final state (carrying its output) or fails (carrying the error), routed by ordinary transitions from this state. Configure the input passed to the child, an explicit id, and a system-scoped id with WithInput / WithInvokeID / WithSystemID. On entering this state the kernel emits a SpawnActor effect; on exiting it before the child completes, a StopActor effect (auto-stop-on-exit). The kernel never runs the actor — a host ActorSystem does, keeping Fire pure. Unlike Invoke (a host-run service), the src here is bound at the ActorSystem, not the registry, so it is not subject to the registry's unbound-ref lint.

func (*Builder[S, E, C]) On

func (b *Builder[S, E, C]) On(event E) *Builder[S, E, C]

On sets the triggering event of the most-recent transition. When no transition is currently open — or the open one already has its event set (a completed `.On(...).GoTo(...)` clause) — On opens a fresh transition from the most-recent state. This lets the hierarchical DSL read `.SubState(X).On(e1).GoTo(Y).On(e2).GoTo(Z)` and `.SubState(X).On(e).GoTo(Y)` without an explicit Transition call.

func (*Builder[S, E, C]) OnAny added in v0.2.0

func (b *Builder[S, E, C]) OnAny() *Builder[S, E, C]

OnAny opens a wildcard (catch-all) transition from the most-recent state. It matches any event no specific On-keyed transition of the state handles, and is the lowest-priority candidate — tried only after every specific match fails, before the event bubbles to an ancestor. Chain GoTo/When/Do/Reenter/Raise as usual. This is the DSL form of a wildcard transition.

func (*Builder[S, E, C]) OnDone

func (b *Builder[S, E, C]) OnDone(actionName string, params ...map[string]any) *Builder[S, E, C]

OnDone attaches a named done-action ref to the most-recent state. It runs when the state completes — a compound state when its active leaf is final, a parallel state when every region is final.

func (*Builder[S, E, C]) OnEntry

func (b *Builder[S, E, C]) OnEntry(actionName string, params ...map[string]any) *Builder[S, E, C]

OnEntry attaches a named entry-action ref to the most-recent state.

func (*Builder[S, E, C]) OnExit

func (b *Builder[S, E, C]) OnExit(actionName string, params ...map[string]any) *Builder[S, E, C]

OnExit attaches a named exit-action ref to the most-recent state.

func (*Builder[S, E, C]) OwnedBy

func (b *Builder[S, E, C]) OwnedBy(owner string) *Builder[S, E, C]

OwnedBy tags the most-recent state's ownership.

func (*Builder[S, E, C]) Quench

func (b *Builder[S, E, C]) Quench(opts ...QuenchOption) *Machine[S, E, C]

Quench binds refs, lints, and freezes into an immutable Machine. It panics on any misconfiguration (programmer error) with a file:line pointer.

func (*Builder[S, E, C]) Raise added in v0.2.0

func (b *Builder[S, E, C]) Raise(events ...E) *Builder[S, E, C]

Raise attaches internal events to the most-recent transition. After the transition's effects run, each raised event is processed within the same Fire macrostep by the run-to-completion loop, before Fire returns. This is the DSL form of raising an internal event.

func (*Builder[S, E, C]) Reenter added in v0.2.0

func (b *Builder[S, E, C]) Reenter() *Builder[S, E, C]

Reenter marks the most-recent transition external: a self- or ancestor- targeted transition that would otherwise be internal (the v5 default) instead runs the full exit/entry cascade of its target. This is the DSL form of the v5 `reenter: true`.

func (*Builder[S, E, C]) Region

func (b *Builder[S, E, C]) Region(name string) *Builder[S, E, C]

Region opens an orthogonal region inside the current SuperState block. States declared until the matching EndRegion belong to the region, and Initial names the region's initial state.

func (*Builder[S, E, C]) Requires

func (b *Builder[S, E, C]) Requires(req Requirement[C]) *Builder[S, E, C]

Requires attaches a requirement to the most-recent state.

func (*Builder[S, E, C]) Respond added in v0.2.0

func (b *Builder[S, E, C]) Respond(event E) *Builder[S, E, C]

Respond attaches the kernel respond built-in to the most-recent transition: when the transition fires, the kernel emits a RespondToSender effect so the host's ActorSystem delivers event back to the sender of the event currently being handled (the actor that sent it via SendTo / ForwardTo). When the current event has no identifiable sender it is a host-side no-op. The built-in needs no host registration. This is the DSL form of replying to an event's origin (the `respond` / `sendBack`).

func (*Builder[S, E, C]) SendParent added in v0.2.0

func (b *Builder[S, E, C]) SendParent(event E) *Builder[S, E, C]

SendParent attaches the kernel sendParent built-in to the most-recent transition: when the transition fires, the kernel emits a SendParent effect so the host's ActorSystem delivers event to the emitting actor's parent. Emitted by a top-level machine with no parent it is a host-side no-op. The built-in needs no host registration. This is the DSL form of sending an event to the parent.

func (*Builder[S, E, C]) SendTo added in v0.2.0

func (b *Builder[S, E, C]) SendTo(targetID string, event E, opts ...SendOption) *Builder[S, E, C]

SendTo attaches the kernel sendTo built-in to the most-recent transition: when the transition fires, the kernel emits a SendTo effect so the host's ActorSystem delivers event to the actor registered under targetID. Address an actor by its system-scoped id instead with WithSendToSystemID. The built-in needs no host registration, mirroring Spawn / Cancel. This is the DSL form of `sendTo(target, event)`.

func (*Builder[S, E, C]) Service added in v0.2.0

func (b *Builder[S, E, C]) Service(name string, fn ServiceFn[C]) *Builder[S, E, C]

Service registers a named invoked-service implementation into the builder's palette, bound by an invoke's Src ref. An unbound service ref fails Quench with the typed *ErrUnboundRef, mirroring guards and actions.

func (*Builder[S, E, C]) Spawn added in v0.2.0

func (b *Builder[S, E, C]) Spawn(src, id string, opts ...SpawnOption) *Builder[S, E, C]

Spawn attaches the kernel spawn built-in to the most-recent transition: when the transition fires, the kernel emits a SpawnActor effect so a machine creates an actor dynamically (spawn). src names the child-machine factory in the host's ActorSystem actor palette; id is the actor's registry key (the holder later stores the ActorSystem-returned ActorRef in its context to address it). Configure input and a system-scoped id with the SpawnOptions. The built-in needs no host registration, mirroring Cancel. The ActorSystem creates and runs the actor; routing the spawned actor's done/error is configured with WithSpawnOnDone / WithSpawnOnError.

func (*Builder[S, E, C]) State

func (b *Builder[S, E, C]) State(name S) *Builder[S, E, C]

State declares a state node. Inside a SuperState or Region block it declares a substate of that block (equivalent to SubState); at the top level it declares a top-level state.

func (*Builder[S, E, C]) StopActor added in v0.2.0

func (b *Builder[S, E, C]) StopActor(id string) *Builder[S, E, C]

StopActor attaches the kernel stop-actor built-in to the most-recent transition: when the transition fires, the kernel emits a StopActor effect for the given actor id, so a machine can explicitly stop a spawned actor before its natural completion (stopping an actor). Stopping an unknown id is a host-side no-op. The built-in needs no host registration, mirroring Cancel.

func (*Builder[S, E, C]) StopChild added in v0.2.0

func (b *Builder[S, E, C]) StopChild(id string) *Builder[S, E, C]

StopChild attaches the kernel stopChild built-in to the most-recent transition: when the transition fires, the kernel emits a StopActor effect for the given actor id, so a machine can explicitly stop a spawned child actor (the `stopChild`). It is the action-level twin of StopActor and shares its effect; stopping an unknown id is a host-side no-op. The built-in needs no host registration.

func (*Builder[S, E, C]) SubState

func (b *Builder[S, E, C]) SubState(name S) *Builder[S, E, C]

SubState declares a substate of the current SuperState or Region block.

func (*Builder[S, E, C]) SuperState

func (b *Builder[S, E, C]) SuperState(name S) *Builder[S, E, C]

SuperState declares a compound (hierarchical) state and opens its block. The substates declared until the matching EndSuperState become its children, and Initial inside the block names the child entered when the superstate is entered.

func (*Builder[S, E, C]) Temper

func (b *Builder[S, E, C]) Temper(opts ...TemperOption) []Diagnostic

Temper runs a non-failing diagnostics pass over the builder's current definition, returning the same findings Quench would panic on — as data.

func (*Builder[S, E, C]) Transition

func (b *Builder[S, E, C]) Transition(from S) *Builder[S, E, C]

Transition opens a new edge from the given state.

func (*Builder[S, E, C]) Use

func (b *Builder[S, E, C]) Use(mw ...Middleware[S, E, C]) *Builder[S, E, C]

Use installs middleware that wraps every Fire.

func (*Builder[S, E, C]) WaitMode

func (b *Builder[S, E, C]) WaitMode(m WaitMode) *Builder[S, E, C]

WaitMode tags the most-recent transition's synchronization mode.

func (*Builder[S, E, C]) When

func (b *Builder[S, E, C]) When(guardName string, params ...map[string]any) *Builder[S, E, C]

When attaches a named guard ref with params to the most-recent transition.

func (*Builder[S, E, C]) WhenExpr added in v0.2.0

func (b *Builder[S, E, C]) WhenExpr(expr GuardNode[S]) *Builder[S, E, C]

WhenExpr attaches a composite guard expression to the most-recent transition: a boolean tree over named-ref leaves (Guard), the stateIn built-in (StateIn), and the And/Or/Not combinators, with short-circuit semantics. It is evaluated alongside any When guards — the transition is enabled only when both pass. Use When for the common single-guard case and WhenExpr when a transition needs composition or stateIn.

type CancelScheduled added in v0.2.0

type CancelScheduled struct {
	// ID identifies the timer to cancel. It matches the ID of the ScheduleAfter
	// that armed it (auto-cancel-on-exit), or an ID supplied to Cancel.
	ID string
}

CancelScheduled is the effect the kernel emits when an instance exits a state that had a pending delayed (`after`) timer, or when a Cancel action runs. The host cancels the timer registered under ID; canceling an unknown ID is a no-op. A state's `after` timers are auto-canceled when the state is exited before the delay elapses.

type CastOption

type CastOption[S comparable] func(*castConfig[S])

CastOption configures Cast.

func WithClock added in v0.2.0

func WithClock[S comparable](c Clock) CastOption[S]

WithClock injects the time seam an instance's delayed-transition driver uses. It is consumed only by a Scheduler / host driver wired to the instance — never by the pure Fire step, which neither reads a clock nor sleeps. Supply SystemClock() in production or a fake clock in a test to drive `after` transitions deterministically. When omitted, an instance defaults to SystemClock().

func WithInitialState

func WithInitialState[S comparable](s S) CastOption[S]

WithInitialState supplies the instance's starting state explicitly. Use it when the machine declares no CurrentStateFn (i.e. the current state cannot be derived from the entity). When both are present, the explicit initial state takes precedence over CurrentStateFn.

func WithInspector added in v0.2.0

func WithInspector[S comparable](insp Inspector) CastOption[S]

WithInspector registers a live observer sink fed inspection events as the instance advances — event received, transition taken, snapshot update — mirroring the live inspection stream. It is off by default: with no inspector the instance never calls one, so inspection adds zero overhead and the pure Fire step performs no IO. The same inspector can be wired to an ActorSystem (WithActorInspector) so actor lifecycle and inter-actor messages are observed on the same sink. The inspector is notified synchronously and must not block or mutate the instance.

type Clock added in v0.2.0

type Clock interface {
	// Now reports the current time.
	Now() time.Time
	// After returns a channel that receives once the duration elapses, mirroring
	// time.After. A driver selects on it to learn when a delayed event is due.
	After(d time.Duration) <-chan time.Time
}

Clock is the deterministic time seam used by host drivers (never by the kernel). A real host wires a wall-clock implementation; a test wires a fake clock so `after` machines are exercised deterministically. The kernel's Fire step never calls a Clock — only effect-consuming drivers do.

func SystemClock added in v0.2.0

func SystemClock() Clock

SystemClock returns the wall-clock Clock backed by the standard library, for a production host driver.

type ContextCodec added in v0.2.0

type ContextCodec[C any] interface {
	Encode(C) ([]byte, error)
	Decode([]byte) (C, error)
}

ContextCodec encodes and decodes an instance context C to and from bytes for a Snapshot, for a context type that is not directly JSON-marshalable (or needs a custom wire form). Encode is called by Snapshot.MarshalJSON; Decode by Snapshot.UnmarshalJSON. When no codec is supplied, the default codec marshals C with encoding/json, so C must be JSON-marshalable by default.

type Diagnostic

type Diagnostic struct {
	Severity string
	Message  string
	SrcFile  string
	SrcLine  int
}

Diagnostic is a non-failing finding from Temper.

type Effect

type Effect = any

Effect is an abstract, domain-defined payload. The kernel never inspects it.

type ErrActionFailed

type ErrActionFailed struct {
	TransitionName string
	ActionName     string
	Cause          error
}

ErrActionFailed wraps a bound action that returned an error during emission.

func (*ErrActionFailed) Error

func (e *ErrActionFailed) Error() string

func (*ErrActionFailed) Unwrap

func (e *ErrActionFailed) Unwrap() error

type ErrGuardFailed

type ErrGuardFailed struct {
	GuardName string
	Reason    string
}

ErrGuardFailed is returned when a named guard returned false.

func (*ErrGuardFailed) Error

func (e *ErrGuardFailed) Error() string

type ErrGuardPanic

type ErrGuardPanic struct {
	GuardName string
	Recovered any
}

ErrGuardPanic is returned when a guard panicked and was recovered.

func (*ErrGuardPanic) Error

func (e *ErrGuardPanic) Error() string

type ErrInvalidTransition

type ErrInvalidTransition struct {
	From   string
	To     string
	Event  string
	Reason string
}

ErrInvalidTransition is returned when no transition matched (current, event), or all matching transitions had failing guards.

func (*ErrInvalidTransition) Error

func (e *ErrInvalidTransition) Error() string

type ErrMicrostepOverflow added in v0.2.0

type ErrMicrostepOverflow struct {
	Limit int
	State string
}

ErrMicrostepOverflow is returned when a single Fire macrostep does not reach a stable configuration within the run-to-completion step budget. It indicates a cycle of raised internal events or eventless ("always") transitions that never settles.

func (*ErrMicrostepOverflow) Error added in v0.2.0

func (e *ErrMicrostepOverflow) Error() string

type ErrNoInitialState

type ErrNoInitialState struct {
	Machine string
}

ErrNoInitialState is returned/panicked by Cast when neither a CurrentStateFn is declared on the machine nor an explicit initial state is supplied via WithInitialState — there is no way to derive the instance's starting state. This is a programmer error, consistent with Quench's panic-on-misuse posture.

func (*ErrNoInitialState) Error

func (e *ErrNoInitialState) Error() string

type ErrNoPath

type ErrNoPath struct {
	From string
	To   string
}

ErrNoPath is returned by PlanPath when no event sequence connects from->to.

func (*ErrNoPath) Error

func (e *ErrNoPath) Error() string

type ErrPolicyDenied

type ErrPolicyDenied struct {
	PolicyName string
	Reason     string
}

ErrPolicyDenied is returned when a policy returned Deny.

func (*ErrPolicyDenied) Error

func (e *ErrPolicyDenied) Error() string

type ErrUnboundActor added in v0.2.0

type ErrUnboundActor struct {
	Name string
}

ErrUnboundActor is returned by an ActorSystem when a SpawnActor's Src does not resolve against the system's actor palette — no child-machine factory was registered under that name. The actor is settled as an error so the parent still routes its onError rather than hanging.

func (*ErrUnboundActor) Error added in v0.2.0

func (e *ErrUnboundActor) Error() string

type ErrUnboundRef

type ErrUnboundRef struct {
	Kind string // "guard" | "action" | "effect"
	Name string
}

ErrUnboundRef is returned when a guard/action/effect ref in the IR did not resolve against the registry (raised at Quench / Provide).

func (*ErrUnboundRef) Error

func (e *ErrUnboundRef) Error() string

type ErrUndeclaredState

type ErrUndeclaredState struct {
	State string
}

ErrUndeclaredState is returned when a state value was never declared.

func (*ErrUndeclaredState) Error

func (e *ErrUndeclaredState) Error() string

type ErrUnknownBuiltin added in v0.2.0

type ErrUnknownBuiltin struct {
	Name string
}

ErrUnknownBuiltin is returned when a ref names a kernel built-in action the kernel does not recognize. It is a defensive programmer-error signal: the DSL and lint only ever produce known built-in names, so this surfaces only a hand-constructed or corrupted ref.

func (*ErrUnknownBuiltin) Error added in v0.2.0

func (e *ErrUnknownBuiltin) Error() string

type FakeClock added in v0.2.0

type FakeClock struct {
	// contains filtered or unexported fields
}

FakeClock is a deterministic Clock for tests: time advances only when Advance is called. It implements Clock; pair it with a Scheduler (via WithClock at Cast) to drive `after` transitions with no real waiting. It is concurrency-safe.

func NewFakeClock added in v0.2.0

func NewFakeClock(start time.Time) *FakeClock

NewFakeClock returns a FakeClock starting at the given instant. The zero instant is fine; only relative advances matter for delayed transitions.

func (*FakeClock) Advance added in v0.2.0

func (c *FakeClock) Advance(d time.Duration)

Advance moves the fake clock forward by d. After advancing, call Scheduler.Tick to fire any now-due timers.

func (*FakeClock) After added in v0.2.0

func (c *FakeClock) After(d time.Duration) <-chan time.Time

After returns a channel that fires once the fake clock has advanced by at least d from the call. It is provided for Clock conformance; the Scheduler drives elapses through Now + Tick rather than this channel, so a test never blocks on it.

func (*FakeClock) Now added in v0.2.0

func (c *FakeClock) Now() time.Time

Now returns the fake clock's current instant.

type FireFunc

type FireFunc[S comparable, E comparable, C any] func(ctx context.Context, event E) FireResult[S]

FireFunc is the inner step the middleware chain wraps.

type FireOption

type FireOption func(*fireConfig)

FireOption configures Fire / FireSeq / FireEach.

func CollectAll

func CollectAll() FireOption

CollectAll makes a batch fire run every step and gather all errors instead of stopping at the first.

type FireResult

type FireResult[S comparable] struct {
	NewState S
	Effects  []Effect
	Trace    Trace
	Err      error
}

FireResult is the result of a single Fire.

func FireEach

func FireEach[S comparable, E comparable, C any](
	ctx context.Context, instances []*Instance[S, E, C], event E, opts ...FireOption,
) []FireResult[S]

FireEach fans one event across an explicit set of instances, preserving per-instance attribution.

type ForgeOption

type ForgeOption func(*forgeConfig)

ForgeOption configures Forge.

type ForwardEvent added in v0.2.0

type ForwardEvent struct {
	// TargetID is the registry id of the actor to forward the current event to.
	// Empty when the target is addressed by SystemID instead.
	TargetID string
	// SystemID is the system-scoped name of the target actor, used when TargetID is
	// empty.
	SystemID string
}

ForwardEvent is the effect the kernel emits for the forwardTo built-in: forward the event the emitting actor is currently handling, verbatim, to the actor addressed by TargetID (or SystemID). The kernel does not embed the forwarded event — the host already has it as the event it just delivered — so this effect carries only the target. The host's ActorSystem routes the current event into the target's mailbox; addressing an unknown actor is a no-op. This realizes forwards the current event verbatim to another actor.

type GuardCtx

type GuardCtx[C any] struct {
	Entity C
	Params map[string]any
}

GuardCtx is passed to a bound guard function at run time.

type GuardFn

type GuardFn[C any] func(ctx GuardCtx[C]) bool

GuardFn is a pure predicate on the entity.

type GuardNode added in v0.2.0

type GuardNode[S comparable] struct {
	Op GuardOp `json:"op"`

	// Ref is the named-ref guard for a GuardLeaf node. Zero for every other op.
	Ref *Ref `json:"ref,omitempty"`

	// In is the target state for a GuardStateIn node: the guard is true when this
	// state is in the instance's active configuration (its leaves and their
	// ancestor spine). Zero for every other op.
	In *S `json:"in,omitempty"`

	// Children are the operands of an and/or/not node. And/Or take one or more;
	// Not takes exactly one. Empty for leaf and stateIn nodes.
	Children []GuardNode[S] `json:"children,omitempty"`
}

GuardNode is one node of a serializable guard expression tree. A leaf references a host-provided guard by name (with serializable params) or is the built-in stateIn guard; internal nodes compose children with and/or/not.

The tree is pure, serializable data: like every other behavioral reference in the IR, leaf guards are named — never embedded closures — so a UI- or JSON-authored composite guard binds against the host registry at Provide and round-trips to and from JSON without losing structure. Arbitrary nesting is supported, e.g. And(Or(g1, g2), Not(g3)).

The common case — a single named guard — stays the plain Transition.Guards slice; GuardNode is used only when a transition needs boolean composition or the stateIn built-in.

func And added in v0.2.0

func And[S comparable](nodes ...GuardNode[S]) GuardNode[S]

And composes guards into a node true only when every operand is true, short-circuiting at the first false — consistent with the AND short-circuit of a plain multi-guard transition. Operands may be named-ref leaves, stateIn, or other combinators, nested arbitrarily.

Example

ExampleAnd composes named-ref guards and the stateIn built-in into a single boolean guard expression on a transition with And/Or/Not, exercising the guard combinators. The transition fires only when the composite passes; And short-circuits at the first false and Or at the first true.

package main

import (
	"context"
	"fmt"

	"github.com/stablekernel/crucible/state"
)

// access is the entity the combinator example guards against.
type access struct {
	admin   bool
	auditor bool
}

// ExampleAnd composes named-ref guards and the stateIn built-in into a single
// boolean guard expression on a transition with And/Or/Not, exercising the
// guard combinators. The transition fires only when the composite passes; And
// short-circuits at the first false and Or at the first true.
func main() {
	m := state.Forge[string, string, access]("door").
		Guard("admin", func(c state.GuardCtx[access]) bool { return c.Entity.admin }).
		Guard("auditor", func(c state.GuardCtx[access]) bool { return c.Entity.auditor }).
		State("locked").
		Transition("locked").On("open").GoTo("open").
		// Enabled while in "locked" AND (admin OR auditor).
		WhenExpr(state.And(
			state.StateIn("locked"),
			state.Or(state.Guard[string]("admin"), state.Guard[string]("auditor")),
		)).
		State("open").
		Initial("locked").
		Quench()

	denied := m.Cast(access{}, state.WithInitialState("locked"))
	denied.Fire(context.Background(), "open")
	fmt.Println("no role:", denied.Current())

	allowed := m.Cast(access{auditor: true}, state.WithInitialState("locked"))
	allowed.Fire(context.Background(), "open")
	fmt.Println("auditor:", allowed.Current())
}
Output:
no role: locked
auditor: open

func Guard added in v0.2.0

func Guard[S comparable](name string, params ...map[string]any) GuardNode[S]

Guard builds a named-ref guard leaf with optional serializable params, the composable form of a single transition guard. It is the leaf used inside And/Or/Not.

func Not added in v0.2.0

func Not[S comparable](node GuardNode[S]) GuardNode[S]

Not inverts a single guard.

func Or added in v0.2.0

func Or[S comparable](nodes ...GuardNode[S]) GuardNode[S]

Or composes guards into a node true when any operand is true, short-circuiting at the first true. Operands may be named-ref leaves, stateIn, or other combinators, nested arbitrarily.

func StateIn added in v0.2.0

func StateIn[S comparable](state S) GuardNode[S]

StateIn builds the built-in in-state guard leaf: true when the instance's active configuration includes state. It is config-aware — it reads the live active leaves and their ancestors at evaluation time, so it works for atomic, compound, and parallel configurations ("in" means the state is somewhere in the active set/spine). It is a first-class built-in: the consumer never registers it. The name is stateIn for guard parity; renaming to In would break that documented parity contract.

func (*GuardNode[S]) LeafRefs added in v0.2.0

func (g *GuardNode[S]) LeafRefs() []Ref

LeafRefs returns the named-ref guard leaves of a guard expression tree, in left-to-right order. The stateIn built-in carries no host ref and is omitted. It lets tooling (e.g. evolution diffing) enumerate the host guards a composite expression depends on.

func (*GuardNode[S]) StateInTargets added in v0.2.0

func (g *GuardNode[S]) StateInTargets() []S

StateInTargets returns the target states of every stateIn leaf in the tree, in left-to-right order, so tooling can account for in-state dependencies a composite guard introduces.

type GuardOp added in v0.2.0

type GuardOp string

GuardOp tags the kind of a node in a guard expression tree.

const (
	// GuardLeaf is a named-ref guard leaf: it carries a Ref bound to a host
	// GuardFn at Provide/Quench time, exactly like a plain transition guard.
	GuardLeaf GuardOp = "leaf"
	// GuardStateIn is the built-in in-state guard leaf: it is true when the
	// instance's active configuration includes the named state. It needs no
	// registration — the kernel evaluates it directly against the live spine.
	GuardStateIn GuardOp = "stateIn"
	// GuardAnd is true when every child is true; it short-circuits at the first
	// false child.
	GuardAnd GuardOp = "and"
	// GuardOr is true when any child is true; it short-circuits at the first
	// true child.
	GuardOr GuardOp = "or"
	// GuardNot inverts its single child.
	GuardNot GuardOp = "not"
)

Guard expression operators. A leaf is either a named-ref guard (resolved against the host registry) or the built-in stateIn guard; the internal nodes compose child results with boolean and/or/not. The string form is stable so the tree round-trips losslessly through JSON.

type HistoryType

type HistoryType int

HistoryType is the reserved drop-in surface for shallow/deep history states.

const (
	HistoryNone HistoryType = iota
	HistoryShallow
	HistoryDeep
)

History kinds. HistoryNone is the v1 default (no history); shallow and deep are reserved for the deferred history-state feature.

type IR

type IR[S comparable, E comparable, C any] struct {
	Name       string           `json:"name"`
	States     []State[S, E, C] `json:"states,omitempty"`
	Initial    S                `json:"initial"`
	HasInitial bool             `json:"hasInitial"`
}

IR is the serializable definition produced and consumed by the data front-end. It is the canonical machine: pure, lossless data. Behavior lives in a host registry and is referenced by name (via Ref), never embedded, so the IR round-trips to and from JSON without losing structure or bindings' identity.

Non-serializable concerns — CurrentStateFn, requirement predicates, and middleware — are pure-runtime and are intentionally absent from the IR; a machine rehydrated from JSON is Cast from an explicit state and bound to a registry via Provide.

func LoadFromJSON

func LoadFromJSON[S comparable, E comparable, C any](b []byte, opts ...LoadOption) (*IR[S, E, C], error)

LoadFromJSON rehydrates an IR from JSON.

func (*IR[S, E, C]) Provide

func (ir *IR[S, E, C]) Provide(reg *Registry[C], opts ...ProvideOption) *Builder[S, E, C]

Provide binds every Ref in the IR against the host registry and returns a Builder ready to Quench. Refs that do not resolve are surfaced at Quench as the typed *ErrUnboundRef (the same failure the DSL raises for an unregistered ref), so a UI/JSON-authored machine and a DSL-authored machine fail identically.

type InspectKind added in v0.2.0

type InspectKind string

InspectKind names a category of inspection event, covering the inspection event types.

const (
	// InspectEvent marks an event received by an instance.
	InspectEvent InspectKind = "event"
	// InspectTransition marks a transition taken — a macrostep that changed (or
	// re-entered) the configuration, carrying its from/to and the Trace detail
	// (guards, effects, exit/entry cascade). It is the kernel's microstep/transition
	// inspection surface.
	InspectTransition InspectKind = "transition"
	// InspectSnapshot marks a snapshot update: the instance's observable state after
	// an event settled.
	InspectSnapshot InspectKind = "snapshot"
	// InspectActor marks an actor lifecycle change — spawned or stopped
	// — an actor lifecycle change.
	InspectActor InspectKind = "actor"
	// InspectMessage marks a message sent from one actor to another and/or delivered
	// to its target (the actor-to-actor flavor of an event).
	InspectMessage InspectKind = "message"
)

type InspectionEvent added in v0.2.0

type InspectionEvent struct {
	// Kind tags which observation this is and which fields are populated.
	Kind InspectKind

	// Machine names the machine the observed instance was cast from. Always set.
	Machine string

	// Event is the string rendering of the event that triggered this observation,
	// for InspectEvent, InspectTransition, and InspectSnapshot. Empty for actor
	// lifecycle events with no triggering instance event.
	Event string

	// From and To name the configuration's primary leaf before and after a
	// transition (InspectTransition) or the settled leaf (InspectSnapshot). For an
	// InspectEvent, From is the leaf the event was received in and To is empty.
	From string
	To   string

	// Trace is the structured Fire record for an InspectTransition — the live twin
	// of the entry History() later reports. Nil for non-transition kinds.
	Trace *Trace

	// Configuration is every active leaf after the observed step settled, for
	// InspectSnapshot and InspectTransition. It is a copy; an Inspector may retain
	// it.
	Configuration []string

	// Status is the instance's lifecycle status for InspectSnapshot
	// (running/done/error), so an inspector can observe completion without polling.
	Status Status

	// ActorID, ActorSrc, and ActorPhase describe an InspectActor lifecycle event:
	// the actor's registry id, the ref name it was spawned from, and whether it was
	// spawned or stopped.
	ActorID    string
	ActorSrc   string
	ActorPhase ActorPhase

	// SenderID, TargetID, MessagePhase, and Message describe an InspectMessage
	// event: the originating actor (empty for a host-injected send), the target
	// actor, whether the message was observed on send or on delivery, and the
	// string rendering of the message event.
	SenderID     string
	TargetID     string
	MessagePhase MessagePhase
	Message      string
}

InspectionEvent is one live observation of an instance's runtime activity. It is an inspection event: a tagged record whose populated fields depend on Kind. A field that does not apply to a Kind is left zero.

The event is read-only; an Inspector must not retain references to mutable values it does not own. The Trace, when present, is the same structured record Fire records in History — surfaced live rather than after the fact.

type Inspector added in v0.2.0

type Inspector interface {
	// Inspect receives every inspection event. The event's Kind selects the
	// populated fields. A single entry point keeps the interface stable as new
	// kinds are added — a new InspectKind never changes this signature.
	Inspect(ev InspectionEvent)
}

Inspector is the observer sink an instance (and its ActorSystem) feeds live inspection events to. It is registered at Cast with WithInspector and is off by default — a nil inspector is never called, so an un-inspected instance pays nothing. An Inspector must not mutate the instance or perform blocking IO on the hot path; it is the telemetry-style sink the kernel notifies synchronously, in the same spirit as the existing Trace/observer ethos.

All methods receive a by-value InspectionEvent so an implementation can retain it safely. Implement only the methods that matter and embed BaseInspector to no-op the rest.

type InspectorFunc added in v0.2.0

type InspectorFunc func(ev InspectionEvent)

InspectorFunc adapts a plain function to the Inspector interface, for the common case of a single closure sink.

func (InspectorFunc) Inspect added in v0.2.0

func (f InspectorFunc) Inspect(ev InspectionEvent)

Inspect calls the underlying function.

type Instance

type Instance[S comparable, E comparable, C any] struct {
	// contains filtered or unexported fields
}

Instance binds a Machine to one entity and carries trace history.

func (*Instance[S, E, C]) Clock added in v0.2.0

func (i *Instance[S, E, C]) Clock() Clock

Clock returns the time seam wired to this instance at Cast (SystemClock() by default). A host driver reads it to schedule delayed (`after`) transitions; the pure Fire step never consults it.

func (*Instance[S, E, C]) Configuration

func (i *Instance[S, E, C]) Configuration() []S

Configuration returns all currently-active leaves, in declaration order. len == 1 for a flat or single-spine machine; len == N when N regions are active in parallel.

func (*Instance[S, E, C]) Current

func (i *Instance[S, E, C]) Current() S

Current returns the primary (first) active leaf — the common "what state am I really in?" answer, back-compatible with flat machines.

func (*Instance[S, E, C]) Entity

func (i *Instance[S, E, C]) Entity() C

Entity returns the entity this instance is bound to.

func (*Instance[S, E, C]) Fire

func (i *Instance[S, E, C]) Fire(ctx context.Context, event E, opts ...FireOption) FireResult[S]

Fire runs the full transition pipeline for a single event.

func (*Instance[S, E, C]) FireSeq

func (i *Instance[S, E, C]) FireSeq(ctx context.Context, events []E, opts ...FireOption) BatchResult[S]

FireSeq drives a sequence of events into one instance, threading intermediate state and merging the per-step traces into one ordered Trace.

Example

ExampleInstance_FireSeq drives a machine through a sequence of events, walking a document from Draft to Published in one batch.

m := buildDocMachine()
doc := &Document{Status: Draft, ReviewerID: strptr("rev-1")}
batch := m.Cast(doc).FireSeq(context.Background(), []DocEvent{Submit, Approve, Publish})

fmt.Println("steps:", len(batch.Steps))
fmt.Println("final:", batch.Steps[len(batch.Steps)-1].NewState)
Output:
steps: 3
final: Published

func (*Instance[S, E, C]) History

func (i *Instance[S, E, C]) History() []Trace

History returns the ordered traces recorded on this instance.

func (*Instance[S, E, C]) InFinal added in v0.2.0

func (i *Instance[S, E, C]) InFinal() bool

InFinal reports whether the instance's current primary leaf is a final state — the signal an ActorSystem reads to learn that a child-machine actor has reached completion and its parent's onDone should be routed. It is a pure read of the active configuration against the machine definition; it never mutates the instance and consults no clock or IO. For a parallel active configuration it reports whether the whole configuration is complete (every region's active leaf final), so a child whose root is parallel completes only when all regions do.

func (*Instance[S, E, C]) ResumeEffects added in v0.2.0

func (i *Instance[S, E, C]) ResumeEffects() []Effect

ResumeEffects returns the re-arm effects a host absorbs after Restore to re-establish the instance's pending timers, invoked services, and spawned actors for its restored configuration: a ScheduleAfter per pending delayed transition, a StartService per invoked service, and a SpawnActor per child-machine actor invocation active in the configuration. It is the restore twin of StartEffects (which arms an initial Cast configuration) extended with the delayed-timer effects, so a restored instance re-establishes its invoked/spawned children. Like StartEffects it is a pure read of the configuration and emits no IO; route the effects through the same Scheduler / ServiceRunner / ActorSystem the host drives for Fire.

Entry actions are NOT re-run: ResumeEffects emits only the lifecycle re-arm effects, never the states' OnEntry actions, so a restored instance resumes rather than re-enters.

func (*Instance[S, E, C]) Snapshot added in v0.2.0

func (i *Instance[S, E, C]) Snapshot() Snapshot[S, E, C]

Snapshot captures the instance's full runtime state into a serializable Snapshot: the active configuration, recorded history, context, lifecycle status, and the IDs of the pending timers / services / actors armed for the active configuration. It is a pure read — it never fires, mutates the instance, or consults a clock — so Fire stays pure and a snapshot may be taken at any quiescent point between Fires.

The returned Snapshot's Context holds the live entity value; serialize the whole snapshot with MarshalSnapshot (or json.Marshal once the default codec suffices) to obtain the wire form. Status is derived from the active configuration (StatusDone when the whole configuration is final, else StatusRunning); a host that tracks an explicit failure sets StatusError and Error on the returned snapshot before persisting.

func (*Instance[S, E, C]) StartEffects added in v0.2.0

func (i *Instance[S, E, C]) StartEffects() []Effect

StartEffects returns the StartService effects for the invoked services declared on the instance's initial active configuration, so a host can arm the services of the state(s) entered at Cast — the entry that Fire never observes because no event drove it. Call it once, right after Cast, and route the effects through the same ServiceRunner used for Fire's effects. It is a pure read of the configuration and emits no IO, consistent with the kernel's effects-as-data contract. A flat or single-spine instance reports its single starting state's services; a parallel initial configuration reports every active region's.

type Invocation added in v0.2.0

type Invocation[S comparable, E comparable, C any] struct {
	// ID identifies this invocation for the lifetime of the owning state's
	// activation. It is stable per (machine, owning state, invoke index), so the
	// StartService emitted on entry and the StopService emitted on exit pair up,
	// and a host keys its running-service table by ID. When omitted in the DSL it
	// defaults to the derived InvokeID.
	ID string `json:"id,omitempty"`
	// Src is the named reference (plus serializable params) to the host-provided
	// service implementation, bound from the service registry at Provide/Quench
	// time exactly like a guard or action ref. An unbound Src fails Quench with
	// the typed *ErrUnboundRef (Kind "service").
	Src Ref `json:"src"`
	// Input is the serializable input passed to the service when it starts,
	// surfaced on the StartService effect as input. It is data only;
	// the kernel never inspects it.
	Input map[string]any `json:"input,omitempty"`
	// OnDone is the event the host re-fires through Fire when the service
	// completes successfully; the service result rides along as the StartService
	// host contract's done payload. It routes the result through an ordinary
	// transition keyed on this event from the owning state.
	OnDone E `json:"onDone"`
	// OnError is the event the host re-fires through Fire when the service fails;
	// the error rides along as the host contract's error payload. It routes the
	// failure through an ordinary transition keyed on this event from the owning
	// state.
	OnError E `json:"onError"`

	// Kind tags this invocation as a host-run service (the default,
	// ActorKindService) or a child-MACHINE actor (ActorKindMachine). A service
	// invocation emits StartService / StopService and is driven by a ServiceRunner;
	// an actor invocation emits SpawnActor / StopActor and is driven by an
	// ActorSystem that runs the child machine as an actor and routes its done/error
	// back through the parent. The field serializes, so the distinction round-trips
	// losslessly through JSON.
	Kind ActorKind `json:"kind,omitempty"`
	// SystemID is the optional system-scoped name a child-machine actor registers
	// under in the ActorSystem (its systemId), so a sibling can address it
	// by a well-known name. It is meaningful only for an ActorKindMachine
	// invocation and serializes for lossless round-trip.
	SystemID string `json:"systemId,omitempty"`
}

Invocation is a declarative invoked service on a state. On entering the owning state the kernel emits a StartService effect carrying Src and Input; the host runs the bound service and re-fires OnDone with the result or OnError with the error back through Fire. On exiting the state before the service completes, the kernel emits a StopService effect so the host stops the in-flight service (auto-stop-on-exit). The whole struct serializes, so an invoke block round-trips losslessly through JSON.

type InvokeOption added in v0.2.0

type InvokeOption func(*invokeConfig)

InvokeOption configures a Builder.Invoke declaration.

func WithInput added in v0.2.0

func WithInput(input map[string]any) InvokeOption

WithInput sets the serializable input passed to an invoked service when it starts, surfaced as input on the StartService effect.

func WithInvokeID added in v0.2.0

func WithInvokeID(id string) InvokeOption

WithInvokeID sets an explicit, stable id for an invoked service instead of the derived InvokeID. Use it when a host or a Cancel-style coordination needs a known id independent of the invocation's declaration order.

func WithServiceParams added in v0.2.0

func WithServiceParams(params map[string]any) InvokeOption

WithServiceParams sets the serializable params on an invoked service's Src ref, available to the bound ServiceFn as ServiceCtx.Params — the per-ref configuration knob, distinct from the per-start Input.

func WithSystemID added in v0.2.0

func WithSystemID(id string) InvokeOption

WithSystemID sets the system-scoped name a child-machine actor (InvokeActor) registers under in the ActorSystem (its systemId), so a sibling can address it by a well-known name rather than by ref. It is meaningful only for InvokeActor; on a plain service Invoke it is ignored.

type LoadOption

type LoadOption func(*loadConfig)

LoadOption configures LoadFromJSON.

type Machine

type Machine[S comparable, E comparable, C any] struct {
	// contains filtered or unexported fields
}

Machine is the immutable, Quenched definition.

func (*Machine[S, E, C]) Assay

func (m *Machine[S, E, C]) Assay(s S, entity C, opts ...AssayOption) error

Assay checks that an externally-constructed entity legally satisfies a state's declarative requirements, without firing. The default mode is fail-fast (the returned *AssayError carries the first failure); WithAggregate collects every failure in one pass. The error type is uniform across modes.

Example

ExampleMachine_Assay checks an externally-built entity against a state's declarative requirements without firing a transition.

m := buildDocMachine()

missing := m.Assay(Approved, &Document{Status: Approved})
ok := m.Assay(Approved, &Document{Status: Approved, ReviewerID: strptr("rev-1")})

fmt.Println("missing reviewer:", missing != nil)
fmt.Println("with reviewer:", ok)
Output:
missing reviewer: true
with reviewer: <nil>

func (*Machine[S, E, C]) Cast

func (m *Machine[S, E, C]) Cast(entity C, opts ...CastOption[S]) *Instance[S, E, C]

Cast pours a fresh running instance from the machine, binding it to the given entity. The instance's starting state is derived from the entity via the machine's CurrentStateFn; if no CurrentStateFn was declared, an explicit initial state must be supplied via WithInitialState. When both are present, WithInitialState wins. With neither, Cast panics with *ErrNoInitialState — a programmer error, consistent with Quench's panic-on-misuse posture.

The entity value is held on the Instance and supplied to guards and actions at Fire time; it is never threaded through context.

Example (Hierarchical)

ExampleMachine_Cast_hierarchical enters a hierarchical machine: casting into a compound state descends to its initial child, so the job starts in Starting under the Running superstate.

m := buildJobMachine()
job := &Job{Status: Queued}
inst := m.Cast(job)
res := inst.Fire(context.Background(), Enqueue)

fmt.Println("state:", res.NewState)
Output:
state: Starting

func (*Machine[S, E, C]) Name

func (m *Machine[S, E, C]) Name() string

Name returns the machine name.

func (*Machine[S, E, C]) PlanPath

func (m *Machine[S, E, C]) PlanPath(from, to S, entity C, opts ...PlanOption) ([]E, error)

PlanPath returns the shortest event sequence that drives an instance from the `from` state to the `to` state, found by breadth-first search over the static transition graph. Guards are honored against the supplied entity, so the returned path is one the entity can actually traverse. The entity is never mutated. ErrNoPath is returned when no sequence connects from->to.

Example

ExampleMachine_PlanPath finds the shortest event sequence that drives a document from Draft to Published, honoring guards against the entity.

m := buildDocMachine()
doc := &Document{Status: Draft, ReviewerID: strptr("rev-1")}
path, err := m.PlanPath(Draft, Published, doc)

fmt.Println("err:", err)
fmt.Println("steps:", len(path))
Output:
err: <nil>
steps: 3

func (*Machine[S, E, C]) Requirements

func (m *Machine[S, E, C]) Requirements(s S) []Requirement[C]

Requirements returns the declarative requirements for a state, or nil if the state declares none (or is undeclared).

func (*Machine[S, E, C]) Restore added in v0.2.0

func (m *Machine[S, E, C]) Restore(snap Snapshot[S, E, C], opts ...RestoreOption[S]) (*Instance[S, E, C], error)

Restore rebuilds a running Instance from snap, resuming at the snapshot's configuration, context, and recorded history WITHOUT re-running any entry actions (resume, not re-enter). The restored instance picks up at the persisted snapshot. The snapshot's Machine must match m's name, every configuration leaf must be a declared state, and the configuration must be non-empty; a violation returns a typed *SnapshotError. The restored instance is wired to the supplied clock (WithRestoreClock) or SystemClock by default, exactly as Cast wires it.

After Restore, a host that drove timers/services/actors re-arms them by absorbing the instance's ResumeEffects through the same drivers it uses for Fire — Restore itself fires nothing and performs no IO, so Fire stays pure.

func (*Machine[S, E, C]) Services added in v0.2.0

func (m *Machine[S, E, C]) Services() map[string]ServiceFn[C]

Services returns the machine's bound invoked-service palette by name, for a host that constructs a ServiceRunner from the machine's own registry. The map is a copy; mutating it does not affect the machine.

func (*Machine[S, E, C]) ToDOT

func (m *Machine[S, E, C]) ToDOT(opts ...VizOption) string

ToDOT renders the machine as GraphViz DOT for richer SVG output — slides, docs sites, and large hierarchical machines where Mermaid grows unreadable.

Compound and parallel states become subgraph clusters, final states draw a double border, owners encode as node fillcolor, and the layout defaults to rankdir=LR (well suited to lifecycles).

func (*Machine[S, E, C]) ToJSON

func (m *Machine[S, E, C]) ToJSON(opts ...ToJSONOption) ([]byte, error)

ToJSON serializes the machine's IR losslessly.

Example

ExampleMachine_ToJSON serializes a machine's IR and reports that the canonical definition round-trips: loading the JSON and reserializing yields identical bytes.

m := buildDocMachine()
data, _ := m.ToJSON()

ir, _ := state.LoadFromJSON[DocState, DocEvent, *Document](data)
m2 := ir.Provide(docRegistry()).Quench()
data2, _ := m2.ToJSON()

fmt.Println("stable:", string(data) == string(data2))
Output:
stable: true

func (*Machine[S, E, C]) ToMermaid

func (m *Machine[S, E, C]) ToMermaid(opts ...VizOption) string

ToMermaid renders the machine as a GitHub-renderable Mermaid stateDiagram-v2.

Transitions render as labeled edges (Event, with guards as a bracketed suffix); the initial state is reached from the [*] start marker and final states point back to [*]. Compound states render as nested state blocks and parallel states use the -- region divider. Owner tags render as classDef color-coding, since stateDiagram-v2 has no native swim lanes.

Example

ExampleMachine_ToMermaid renders a hierarchical machine as a Mermaid stateDiagram-v2: the initial marker, the Running superstate as a nested block with its own initial child, the cross-cutting Cancel transition, and the final-state markers.

fmt.Println(buildJobMachine().ToMermaid())
Output:
stateDiagram-v2
    [*] --> Queued
    state Running {
        [*] --> Running__Starting
        Running__Starting
        Running__Executing
        Running__Starting --> Running__Executing: Begin
    }
    JobDone --> [*]
    Canceled --> [*]
    Queued --> Running: Enqueue
    Running --> Canceled: Cancel
    Running__Executing --> JobDone: Finish
    classDef owner_Scheduler fill:#f0d9ff
    classDef owner_Worker fill:#d9f2f2
    class Queued owner_Scheduler
    class Running owner_Worker

type MessagePhase added in v0.2.0

type MessagePhase string

MessagePhase distinguishes the lifecycle point of an InspectMessage event: a message is observed when it is sent, and again when the host delivers it.

const (
	// MessageSent marks a message emitted toward a target actor (a SendTo /
	// SendParent / Respond / Forward effect being routed).
	MessageSent MessagePhase = "sent"
	// MessageDelivered marks a message handed to its target actor's mailbox.
	MessageDelivered MessagePhase = "delivered"
)

type Middleware

type Middleware[S comparable, E comparable, C any] func(next FireFunc[S, E, C]) FireFunc[S, E, C]

Middleware wraps a Fire, outside-in.

type MultiRegionErr

type MultiRegionErr struct {
	Errors []error
}

MultiRegionErr aggregates the errors raised by more than one orthogonal region firing on a single event. Its Unwrap returns each region's error so errors.As finds any region's typed error.

func (*MultiRegionErr) Error

func (e *MultiRegionErr) Error() string

func (*MultiRegionErr) Unwrap

func (e *MultiRegionErr) Unwrap() []error

Unwrap exposes the per-region errors for errors.As / errors.Is traversal.

type Outcome

type Outcome int

Outcome classifies the result recorded in a Trace.

const (
	OutcomeSuccess Outcome = iota
	OutcomeInvalidTransition
	OutcomeGuardFailed
	OutcomeGuardPanic
	OutcomePolicyDenied
	OutcomeEffectError
)

Outcomes recorded in a Trace, one per Fire: success or the specific failure class that stopped the transition.

type P

type P = map[string]any

P is a convenience alias for serializable params attached to a named Ref.

type PendingRefs added in v0.2.0

type PendingRefs struct {
	// Timers are the schedule IDs of the pending delayed (`after`) transitions
	// armed for the active configuration.
	Timers []string `json:"timers,omitempty"`
	// Services are the IDs of the invoked services running for the active
	// configuration.
	Services []string `json:"services,omitempty"`
	// Actors are the IDs of the child-machine actors invoked for the active
	// configuration.
	Actors []string `json:"actors,omitempty"`
}

PendingRefs is the descriptive inventory of an instance's live timers, invoked services, and spawned actors at snapshot time, by stable ID. It mirrors what ResumeEffects re-arms; a host can assert on it or display it without replaying effects.

type PlanOption

type PlanOption func(*planConfig)

PlanOption configures PlanPath.

type ProvideOption

type ProvideOption func(*provideConfig)

ProvideOption configures Provide.

type QuenchOption

type QuenchOption func(*quenchConfig)

QuenchOption configures Quench.

func Strict

func Strict() QuenchOption

Strict makes Quench reject any lint warning, not just hard errors.

type Ref

type Ref struct {
	Name   string         `json:"name"`
	Params map[string]any `json:"params,omitempty"`
}

Ref is a named reference to a host-provided implementation plus serializable params. The IR carries Refs; the registry binds Name -> func at Provide/Quench time.

type Region

type Region[S comparable, E comparable, C any] struct {
	Name         string           `json:"name"`
	States       []State[S, E, C] `json:"states,omitempty"`
	InitialChild *S               `json:"initialChild,omitempty"`
}

Region is one orthogonal region of a parallel state: a self-contained set of substates with its own initial child. When the owning parallel state is active, every region is active simultaneously, each tracking its own leaf.

type Registry

type Registry[C any] struct {
	// contains filtered or unexported fields
}

Registry holds the host behavior palette, by name.

func NewRegistry

func NewRegistry[C any]() *Registry[C]

NewRegistry returns an empty host registry.

func (*Registry[C]) Action

func (r *Registry[C]) Action(name string, fn ActionFn[C]) *Registry[C]

Action registers a named action implementation.

func (*Registry[C]) Guard

func (r *Registry[C]) Guard(name string, fn GuardFn[C]) *Registry[C]

Guard registers a named guard implementation.

func (*Registry[C]) Service added in v0.2.0

func (r *Registry[C]) Service(name string, fn ServiceFn[C]) *Registry[C]

Service registers a named invoked-service implementation. An invoke's Src ref binds to it at Provide/Quench time exactly like a guard or action ref; an unbound service ref fails Quench with the typed *ErrUnboundRef (Kind "service"). The runner resolves and runs it when the owning state is entered.

type Requirement

type Requirement[C any] struct {
	Name      string
	Predicate func(C) bool
	Setter    func(C) // optional: mutate a zero entity to satisfy Predicate
}

Requirement is a declarative condition for a state, used by Assay.

type RequirementFailure

type RequirementFailure struct {
	Name   string
	Reason string
}

RequirementFailure records one unmet requirement.

type RespondToSender added in v0.2.0

type RespondToSender struct {
	// Event is the serializable reply delivered to the current event's sender,
	// type-erased for the abstract effect surface; an ActorSystem keeps it typed.
	Event any
}

RespondToSender is the effect the kernel emits for the respond built-in: reply with Event to the sender of the event the emitting actor is currently handling. The kernel cannot know the sender (it is host routing state), so it emits this effect with only the reply Event; the host's ActorSystem resolves the target from the routing context it recorded when it delivered the current event. When there is no identifiable sender the host treats it as a no-op. This realizes the reply-to-the-event's-origin semantic.

type RestoreOption added in v0.2.0

type RestoreOption[S comparable] func(*restoreConfig[S])

RestoreOption configures Machine.Restore.

func WithRestoreClock added in v0.2.0

func WithRestoreClock[S comparable](c Clock) RestoreOption[S]

WithRestoreClock wires the time seam a restored instance's delayed-transition driver reads, mirroring WithClock at Cast. It is consumed only by a Scheduler / host driver, never by the pure Fire step. When omitted, a restored instance defaults to SystemClock().

type ScheduleAfter added in v0.2.0

type ScheduleAfter struct {
	// ID identifies the pending timer. It is stable across the schedule/cancel
	// pair for one source state on one instance, so a host keys its timer table
	// by ID.
	ID string
	// Delay is the wall-clock duration the host should wait before re-firing.
	Delay time.Duration
	// Event is the delayed event to feed back through Fire when Delay elapses.
	// It is the transition's On event, type-erased for the abstract effect
	// surface; a host driver built with NewScheduler keeps it typed.
	Event any
	// State names the source state whose entry scheduled this timer, for
	// diagnostics and host bookkeeping.
	State string
}

ScheduleAfter is the effect the kernel emits when an instance enters a state that declares a delayed (`after`) transition. The host's runtime is expected to start a timer for Delay and, when it elapses, call Fire with Event. ID is stable per (instance, source state, delayed edge), so a later CancelScheduled with the same ID cancels exactly this timer.

The kernel never starts the timer itself: it emits this as data alongside the transition's other effects, keeping Fire pure (no clock, no goroutine, no IO).

type Scheduler added in v0.2.0

type Scheduler[S comparable, E comparable, C any] struct {
	// contains filtered or unexported fields
}

Scheduler is the reusable host-driver that turns the kernel's ScheduleAfter / CancelScheduled effects into real timers and re-fires delayed events through its instance. It is concurrency-safe. Construct one per instance with NewScheduler; drive it by passing each Fire's effects to Absorb. With a FakeClock it is fully deterministic — timers fire only when the test advances the clock via FakeClock.Advance.

Example

ExampleScheduler drives a delayed (`after`) transition deterministically with a FakeClock and the reusable Scheduler host-driver. The kernel stays pure: entering "pending" emits a ScheduleAfter effect, the Scheduler arms a timer, and advancing the fake clock past the delay fires the delayed event back through Fire — driving a delayed (after) transition with no real waiting.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/stablekernel/crucible/state"
)

func main() {
	type cart struct{}
	m := state.Forge[string, string, cart]("checkout").
		State("active").
		State("pending").
		State("expired").
		Initial("active").
		Transition("active").On("submit").GoTo("pending").
		// After 15 minutes in "pending" with no action, the cart expires.
		Transition("pending").After(15 * time.Minute).On("timeout").GoTo("expired").
		State("expired").Final().
		Quench()

	clk := state.NewFakeClock(time.Unix(0, 0))
	inst := m.Cast(cart{}, state.WithInitialState("active"), state.WithClock[string](clk))
	sch := state.NewScheduler(inst)
	ctx := context.Background()

	// Entering "pending" emits the ScheduleAfter effect; the host absorbs every
	// Fire's effects into the Scheduler, which arms the timer.
	res := inst.Fire(ctx, "submit")
	sch.Absorb(ctx, res.Effects)
	fmt.Println("before:", inst.Current(), "pending timers:", sch.Pending())

	// Nothing happens until the delay elapses; advancing the fake clock and
	// ticking the Scheduler fires the delayed "timeout" event.
	clk.Advance(15 * time.Minute)
	sch.Tick(ctx)
	fmt.Println("after: ", inst.Current(), "pending timers:", sch.Pending())
}
Output:
before: pending pending timers: 1
after:  expired pending timers: 0

func NewScheduler added in v0.2.0

func NewScheduler[S comparable, E comparable, C any](inst *Instance[S, E, C]) *Scheduler[S, E, C]

NewScheduler returns a Scheduler driving inst, reading the time seam wired to inst at Cast (WithClock). With a FakeClock the Scheduler is deterministic.

func (*Scheduler[S, E, C]) Absorb added in v0.2.0

func (s *Scheduler[S, E, C]) Absorb(ctx context.Context, effects []Effect)

Absorb scans effects, arming a timer for each ScheduleAfter and dropping the timer for each CancelScheduled. It is how a host wires Fire's output back into the scheduler; call it with the effects of every Fire (including those the Scheduler itself triggers — Fire-on-elapse re-enters Absorb automatically). A ScheduleAfter whose Event is not the instance's event type is ignored, since the kernel cannot have produced it.

func (*Scheduler[S, E, C]) HasPending added in v0.2.0

func (s *Scheduler[S, E, C]) HasPending(id string) bool

HasPending reports whether a timer with the given schedule id is armed.

func (*Scheduler[S, E, C]) Pending added in v0.2.0

func (s *Scheduler[S, E, C]) Pending() int

Pending reports the number of armed (not-yet-fired, not-canceled) timers. A test asserts on it to confirm a timer was scheduled or auto-canceled on exit.

func (*Scheduler[S, E, C]) Tick added in v0.2.0

func (s *Scheduler[S, E, C]) Tick(ctx context.Context) []FireResult[S]

Tick fires every timer whose due time is at or before the Scheduler clock's current time, in due-time order (ties broken by id for determinism). Each due timer is removed, then its delayed event is fired through the instance and the resulting effects are absorbed (so a chained `after` arms its successor). It returns the FireResults of the events it fired, in order. With a FakeClock a test calls FakeClock.Advance then Tick (or uses the Advance helper) to drive elapses deterministically; with SystemClock a host calls Tick from its own timer loop.

type SendOption added in v0.2.0

type SendOption func(*sendConfig)

SendOption configures a Builder.SendTo / Builder.ForwardTo declaration (the actor-communication send built-ins).

func WithSendToSystemID added in v0.2.0

func WithSendToSystemID(id string) SendOption

WithSendToSystemID addresses the send target by its system-scoped id (the `systemId`) instead of its registry id, so a sibling actor is addressed by a well-known name. When set it takes precedence over the positional target id.

type SendParent added in v0.2.0

type SendParent struct {
	// Event is the serializable event delivered to the parent, type-erased for the
	// abstract effect surface; an ActorSystem keeps it typed.
	Event any
}

SendParent is the effect the kernel emits for the sendParent built-in: a child actor sends Event to its parent. The host's ActorSystem routes it to the parent instance (the one driving the system). Emitted by a top-level machine with no parent it is a host-side no-op. It routes an event to the actor's parent.

type SendTo added in v0.2.0

type SendTo struct {
	// TargetID is the registry id of the actor to deliver Event to. Empty when the
	// target is addressed by SystemID instead.
	TargetID string
	// SystemID is the system-scoped name of the target actor (its systemId),
	// used when TargetID is empty so a sibling can be addressed by a well-known name.
	SystemID string
	// Event is the serializable event delivered to the target actor's mailbox,
	// type-erased for the abstract effect surface; an ActorSystem keeps it typed.
	Event any
}

SendTo is the effect the kernel emits for the sendTo built-in: deliver Event to the actor addressed by TargetID (or SystemID when TargetID is empty). The host's ActorSystem routes it into that actor's mailbox; addressing an unknown actor is a no-op. It delivers an event to a named actor.

type ServiceCtx added in v0.2.0

type ServiceCtx[C any] struct {
	Entity C
	Params map[string]any
	Input  map[string]any
}

ServiceCtx is passed to a bound service at run time. It carries the entity the instance is bound to and the start contract the kernel emitted.

type ServiceFn added in v0.2.0

type ServiceFn[C any] func(ctx context.Context, in ServiceCtx[C]) (any, error)

ServiceFn is a host-provided invoked-service implementation, bound by name into a Registry exactly like a guard or action. It receives the entity it is bound to and the StartService effect (Src params, Input) the kernel emitted, and returns its result on success or an error on failure. A one-shot (promise-style) service returns directly; a streaming service is a host-side wrapper that ultimately resolves to a single done/error through this contract. A ServiceFn never mutates the instance; it returns data, and the runner routes that data through the invocation's onDone / onError event via Fire — so Fire, not the service, owns every state change.

type ServiceRunner added in v0.2.0

type ServiceRunner[S comparable, E comparable, C any] struct {
	// contains filtered or unexported fields
}

ServiceRunner is the reusable host-driver that turns the kernel's StartService / StopService effects into real service executions and re-fires each result through its instance via the invocation's onDone / onError event. It is concurrency-safe. Construct one per instance with NewServiceRunner, binding the service registry that resolves Src refs; drive it by passing each Fire's effects (and the instance's StartEffects) to Absorb.

In the deterministic form the runner records each started service as pending and settles it only when the test calls SettleDone / SettleError, so invoke machines are exercised with no real IO; a production host instead resolves and runs the bound ServiceFn on its own goroutine and calls SettleDone / SettleError (or the convenience Run) when it finishes.

func NewServiceRunner added in v0.2.0

func NewServiceRunner[S comparable, E comparable, C any](inst *Instance[S, E, C], reg *Registry[C]) *ServiceRunner[S, E, C]

NewServiceRunner returns a ServiceRunner driving inst, resolving Src refs against reg's service palette. reg may be nil for a pure deterministic driver that never resolves a ServiceFn (the test settles services directly by ID).

func (*ServiceRunner[S, E, C]) Absorb added in v0.2.0

func (r *ServiceRunner[S, E, C]) Absorb(ctx context.Context, effects []Effect)

Absorb scans effects, recording a running service for each StartService and dropping the running service for each StopService (auto-stop-on-exit). It is how a host wires Fire's output back into the runner; call it with the effects of every Fire (and once with the instance's StartEffects for the initial state). A StartService whose OnDone/OnError is not the instance's event type is ignored, since the kernel cannot have produced it.

func (*ServiceRunner[S, E, C]) HasPending added in v0.2.0

func (r *ServiceRunner[S, E, C]) HasPending(id string) bool

HasPending reports whether a service with the given invoke id is in flight.

func (*ServiceRunner[S, E, C]) LastError added in v0.2.0

func (r *ServiceRunner[S, E, C]) LastError() error

LastError returns the error the most recently settled service produced, or nil when the last settlement was a success or none has occurred. The host action bound to an onError transition reads it to consume the failure.

func (*ServiceRunner[S, E, C]) LastResult added in v0.2.0

func (r *ServiceRunner[S, E, C]) LastResult() (any, bool)

LastResult returns the result the most recently settled service produced, and true when that settlement was a success (SettleDone). The host action bound to an onDone transition reads it to consume the service output; it is valid only during the synchronous Fire the settlement triggers. It returns false after a SettleError or before any settlement.

func (*ServiceRunner[S, E, C]) Pending added in v0.2.0

func (r *ServiceRunner[S, E, C]) Pending() int

Pending reports the number of in-flight (started, not-yet-settled, not-stopped) services. A test asserts on it to confirm a service was started or auto-stopped on exit.

func (*ServiceRunner[S, E, C]) PendingIDs added in v0.2.0

func (r *ServiceRunner[S, E, C]) PendingIDs() []string

PendingIDs returns the ids of all in-flight services, sorted, for deterministic host iteration (e.g. running every armed service in a stable order).

func (*ServiceRunner[S, E, C]) Run added in v0.2.0

func (r *ServiceRunner[S, E, C]) Run(ctx context.Context, id string) (FireResult[S], bool)

Run resolves and runs the in-flight service id against the bound registry, settling it with the ServiceFn's result or error. It is the production convenience that couples resolve + run + settle: a host that arms services from Absorb and wants the runner to execute them calls Run(ctx, id) (typically from its own goroutine). It returns the routed FireResult and true, or false when id is not in flight or no registry / ServiceFn resolves it (in which case the service is settled as an error so the machine still routes onError rather than hanging).

func (*ServiceRunner[S, E, C]) SettleDone added in v0.2.0

func (r *ServiceRunner[S, E, C]) SettleDone(ctx context.Context, id string, result any) (FireResult[S], bool)

SettleDone completes the in-flight service id successfully: it drops the service and fires its OnDone event (carrying result) through the instance, then absorbs the resulting effects so a chained invoke arms its successor. It returns the FireResult and true, or the zero result and false when id names no in-flight service (already stopped or settled). result is delivered to the onDone transition's effects through the instance entity by the host's actions — the kernel routes the event; the action reads the result.

func (*ServiceRunner[S, E, C]) SettleError added in v0.2.0

func (r *ServiceRunner[S, E, C]) SettleError(ctx context.Context, id string, err error) (FireResult[S], bool)

SettleError fails the in-flight service id: it drops the service and fires its OnError event (carrying err) through the instance, then absorbs the resulting effects. It returns the FireResult and true, or the zero result and false when id names no in-flight service.

type Snapshot added in v0.2.0

type Snapshot[S comparable, E comparable, C any] struct {
	// Machine names the machine the snapshot was taken from. Restore rejects a
	// snapshot whose Machine does not match the target machine with a typed
	// *SnapshotError, so a snapshot is never restored against the wrong definition.
	Machine string `json:"machine"`

	// Current is the primary (first) active leaf — the back-compatible
	// "what state am I in?" answer, equal to Configuration[0].
	Current S `json:"current"`
	// Configuration is every currently-active leaf, in declaration order: length 1
	// for a flat or single-spine instance, length N when N parallel regions are
	// active. Restore activates exactly this configuration without re-entering it.
	Configuration []S `json:"configuration"`

	// Context is the instance's bound entity C at snapshot time. With the default
	// codec it must be JSON-marshalable; with WithContextCodec the supplied codec
	// owns its encoding. In JSON it is held as a raw message so the snapshot
	// envelope marshals once and the context decodes through the chosen codec.
	Context C `json:"-"`
	// ContextRaw is the JSON (or codec-encoded) form of Context, populated when the
	// snapshot is marshaled and consumed when it is unmarshaled. It is the wire
	// form of Context; callers read Context, not ContextRaw.
	ContextRaw json.RawMessage `json:"context,omitempty"`

	// HistoryShallow records each compound's last-active direct child, and
	// HistoryDeep each compound's last-active leaf configuration, for history
	// pseudo-state restoration. Both are restored verbatim so a history-targeted
	// transition after restore behaves identically to before the snapshot.
	HistoryShallow map[S]S   `json:"historyShallow,omitempty"`
	HistoryDeep    map[S][]S `json:"historyDeep,omitempty"`

	// Traces is the instance's recorded Fire history, preserved so History()
	// reports the same ordered traces after restore.
	Traces []Trace `json:"traces,omitempty"`

	// Status is the instance's lifecycle status at snapshot time. Output carries an
	// instance's completion output (when StatusDone) and Error a settled instance's
	// failure message (when StatusError); both are optional and host-supplied.
	Status Status          `json:"status"`
	Output json.RawMessage `json:"output,omitempty"`
	Error  string          `json:"error,omitempty"`

	// Pending records the IDs/metadata of the timers, invoked services, and spawned
	// actors that were live for the active configuration, so a host can confirm
	// what ResumeEffects re-arms. It is descriptive: the authoritative re-arm is the
	// effect slice ResumeEffects returns, derived from the same configuration.
	Pending PendingRefs `json:"pending,omitempty"`

	// Actors carries the recursively-captured snapshots of the instance's spawned
	// child actors, keyed by actor id, when an ActorSystem snapshots the instance.
	// Each entry is an opaque per-child snapshot envelope a matching ActorSystem
	// restores. It is empty for an instance with no spawned children, or when only
	// the instance core (not the actor tree) is snapshotted.
	Actors map[string]json.RawMessage `json:"actors,omitempty"`
}

Snapshot is the serializable, deep runtime state of one Instance at a point in time. It captures the active configuration (all active leaves, in declaration order, plus the primary leaf), the recorded per-compound history (shallow and deep), the instance context, the lifecycle status and optional output/error, and the metadata of the pending timers, invoked services, and spawned actors so a host can re-arm them on restore. Child-actor snapshots are carried under Actors when an ActorSystem snapshots the instance's spawned children recursively.

A Snapshot round-trips losslessly through JSON when the context type C is JSON-marshalable (the default requirement) or a context codec is supplied via WithContextCodec. The machine definition is NOT carried here — restore binds the snapshot back to a live Machine, exactly as Cast binds an entity — so a snapshot stays small and a definition change is detected at restore rather than silently absorbed.

func UnmarshalSnapshot added in v0.2.0

func UnmarshalSnapshot[S comparable, E comparable, C any](b []byte, opts ...SnapshotCodecOption[C]) (Snapshot[S, E, C], error)

UnmarshalSnapshot deserializes a snapshot from JSON, decoding its context through codec (or the default JSON codec when codec is nil). It is the inverse of MarshalSnapshot; for a JSON-marshalable context, json.Unmarshal into a Snapshot works directly via the snapshot's own UnmarshalJSON.

func WaitFor added in v0.2.0

func WaitFor[S comparable, E comparable, C any](
	ctx context.Context,
	inst *Instance[S, E, C],
	predicate WaitPredicate[S, E, C],
	opts ...WaitOption[S, E, C],
) (Snapshot[S, E, C], error)

WaitFor drives inst until predicate holds over its Snapshot, or until the supplied context is canceled or the wait budget elapses. It returns the matching snapshot on success, or the zero snapshot and a typed *WaitTimeoutError when the budget elapses (or the wrapped context error when ctx is canceled) without the predicate ever holding.

The predicate is checked once immediately, before any advance, so an instance already in the desired state returns at once without driving. When it does not yet hold, WaitFor advances its driver one step at a time and rechecks: by default it ticks a Scheduler over a FakeClock (WithWaitScheduler), advancing the fake clock by a fixed step each iteration so `after` machines progress deterministically; a caller with a different driver supplies WithWaitStep.

With no driver option WaitFor cannot make progress (an undriven instance never changes on its own), so it checks the predicate once and, if unmet, waits out the budget and returns the typed timeout — the correct result for "the instance will never reach this state without being driven".

WaitFor never reads the wall clock: time is measured by the driver's clock (the Scheduler's, a FakeClock in tests), so the whole helper is deterministic under a fake clock.

func (Snapshot[S, E, C]) MarshalJSON added in v0.2.0

func (snap Snapshot[S, E, C]) MarshalJSON() ([]byte, error)

MarshalJSON serializes the snapshot, encoding its context with the default JSON codec. It is the convenient path for a JSON-marshalable context; a context that needs a custom codec is serialized with MarshalSnapshot(snap, WithContextCodec).

func (*Snapshot[S, E, C]) UnmarshalJSON added in v0.2.0

func (snap *Snapshot[S, E, C]) UnmarshalJSON(b []byte) error

UnmarshalJSON deserializes the snapshot, decoding its context with the default JSON codec. The inverse of MarshalJSON.

type SnapshotCodecOption added in v0.2.0

type SnapshotCodecOption[C any] func(*snapshotCodecConfig[C])

SnapshotCodecOption configures MarshalSnapshot / UnmarshalSnapshot.

func WithContextCodec added in v0.2.0

func WithContextCodec[C any](codec ContextCodec[C]) SnapshotCodecOption[C]

WithContextCodec supplies a custom ContextCodec for a snapshot context that is not directly JSON-marshalable (or needs a bespoke wire form). When omitted, the default codec marshals the context with encoding/json, so the context type must be JSON-marshalable by default. Pass it to MarshalSnapshot / UnmarshalSnapshot to override the default.

type SnapshotError added in v0.2.0

type SnapshotError struct {
	Op     string
	State  string
	Reason string
}

SnapshotError is returned by Restore / MarshalSnapshot / UnmarshalSnapshot when an instance snapshot cannot be captured, serialized, or restored: a snapshot whose Machine does not match the target, a configuration leaf that is not a declared state, an empty configuration with an unknown current state, or a context encode/decode failure. Op names the failing operation ("restore" | "marshal" | "unmarshal"), State (when set) names the offending configuration leaf, and Reason carries the detail.

func (*SnapshotError) Error added in v0.2.0

func (e *SnapshotError) Error() string

type Snapshotter added in v0.2.0

type Snapshotter interface {
	// SnapshotJSON captures the actor's runtime state as JSON.
	SnapshotJSON() ([]byte, error)
	// RestoreJSON reloads the actor's runtime state from JSON produced by
	// SnapshotJSON, resuming the actor in place without re-running entry actions.
	RestoreJSON([]byte) error
}

Snapshotter is implemented by an ActorInstance that can capture and reload its own runtime state as JSON, so an ActorSystem can persist it recursively. The actorAdapter (the standard wrapper for a child *Instance) satisfies it; a host's bespoke ActorInstance may implement it to participate in deep persistence, and an ActorInstance that does not is re-spawned fresh on restore rather than resumed.

type SpawnActor added in v0.2.0

type SpawnActor struct {
	// ID identifies the spawned actor. It is stable across the spawn/stop pair for
	// one owning state on one instance (static invoke) or supplied explicitly (a
	// dynamic spawn), so a host keys its actor registry by ID.
	ID string
	// Src is the actor ref (name + params) the host resolves against its actor
	// palette to obtain the child machine to run.
	Src Ref
	// Input is the serializable input passed to the child actor at spawn. It
	// is data only; the kernel never inspects it.
	Input map[string]any
	// OnDone is the event the host re-fires through the PARENT's Fire (carrying the
	// child's output) when the child actor reaches its final state, type-erased for
	// the abstract effect surface; an ActorSystem keeps it typed.
	OnDone any
	// OnError is the event the host re-fires through the PARENT's Fire (carrying the
	// error) when the child actor fails, type-erased for the abstract effect
	// surface.
	OnError any
	// State names the owning state whose entry spawned this actor, for diagnostics
	// and host bookkeeping. Empty for a dynamic spawn emitted from a transition.
	State string
	// SystemID is the optional, stable system-scoped identifier the actor registers
	// under in the ActorSystem (its systemId), so a sibling can address it
	// by a well-known name rather than by ref. Empty when unset.
	SystemID string
}

SpawnActor is the effect the kernel emits when an instance enters a state that invokes a child MACHINE actor, or when the built-in spawn action runs. The host's ActorSystem is expected to create the actor named by Src (resolved to a child machine factory against the system's actor palette), run it with Input, register it under ID, and — when the child reaches its final state — re-fire OnDone (carrying the child's output) through the PARENT's Fire, or on the child's failure re-fire OnError. ID is stable per (instance, owning state, invoke index) for a static invoke, or carried explicitly for a dynamic spawn, so a later StopActor with the same ID stops exactly this actor.

The kernel never runs the actor itself: it emits this as data alongside the transition's other effects, keeping Fire pure (no goroutine, no mailbox, no IO).

type SpawnOption added in v0.2.0

type SpawnOption func(*spawnConfig)

SpawnOption configures a Builder.Spawn declaration (the dynamic spawn built-in).

func WithSpawnInput added in v0.2.0

func WithSpawnInput(input map[string]any) SpawnOption

WithSpawnInput sets the serializable input passed to a dynamically spawned actor when it is created, surfaced as input on the SpawnActor effect.

func WithSpawnOnDone added in v0.2.0

func WithSpawnOnDone[E comparable](onDone E) SpawnOption

WithSpawnOnDone sets the event the host re-fires through the parent's Fire when a dynamically spawned actor reaches its final state, routing the child's output through an ordinary transition from the spawning state. Omit it for a fire-and-forget spawn whose completion the parent does not observe.

func WithSpawnOnError added in v0.2.0

func WithSpawnOnError[E comparable](onError E) SpawnOption

WithSpawnOnError sets the event the host re-fires through the parent's Fire when a dynamically spawned actor fails, routing the error through an ordinary transition from the spawning state.

func WithSpawnSystemID added in v0.2.0

func WithSpawnSystemID(id string) SpawnOption

WithSpawnSystemID sets the system-scoped name a dynamically spawned actor registers under in the ActorSystem (its systemId).

type StartService added in v0.2.0

type StartService struct {
	// ID identifies the running service. It is stable across the start/stop pair
	// for one owning state on one instance, so a host keys its service table by ID.
	ID string
	// Src is the service ref (name + params) the host resolves against its service
	// registry to obtain the implementation to run.
	Src Ref
	// Input is the serializable input passed to the service at start.
	Input map[string]any
	// OnDone is the event the host re-fires (with the service result) when the
	// service completes successfully, type-erased for the abstract effect surface;
	// a host driver built with NewServiceRunner keeps it typed.
	OnDone any
	// OnError is the event the host re-fires (with the error) when the service
	// fails, type-erased for the abstract effect surface.
	OnError any
	// State names the owning state whose entry started this service, for
	// diagnostics and host bookkeeping.
	State string
}

StartService is the effect the kernel emits when an instance enters a state that declares an invoked service. The host is expected to run the service named by Src with Input and, on completion, re-fire OnDone with the result through Fire, or on failure re-fire OnError with the error. ID is stable per (instance, owning state, invoke index), so a later StopService with the same ID stops exactly this service.

The kernel never runs the service itself: it emits this as data alongside the transition's other effects, keeping Fire pure (no goroutine, no IO).

type State

type State[S comparable, E comparable, C any] struct {
	Name        S                     `json:"name"`
	OwnedBy     string                `json:"ownedBy,omitempty"`
	Transitions []Transition[S, E, C] `json:"transitions,omitempty"`

	OnEntry []Ref `json:"onEntry,omitempty"`
	OnExit  []Ref `json:"onExit,omitempty"`
	IsFinal bool  `json:"isFinal,omitempty"`
	OnDone  []Ref `json:"onDone,omitempty"`

	// Hierarchy. Children holds the nested substates of a compound state, and
	// InitialChild names the substate entered transitively when the compound
	// state is entered. Both serialize, so the hierarchy round-trips through
	// JSON. Parent is a runtime-only back-pointer rebuilt after Quench/Provide.
	Children     []State[S, E, C] `json:"children,omitempty"`
	InitialChild *S               `json:"initialChild,omitempty"`

	// Regions holds the orthogonal regions of a parallel state. Mutually
	// exclusive with Children/InitialChild.
	Regions []Region[S, E, C] `json:"regions,omitempty"`

	// History. HistoryType marks this node as a history pseudo-state (shallow or
	// deep) belonging to its parent compound; HistoryNone (the default) is an
	// ordinary state. HistoryDefault names the target entered when the owning
	// compound has no recorded history yet; nil falls back to the compound's
	// InitialChild. Both serialize, so history pseudo-states round-trip through
	// JSON; the per-instance recorded configuration is runtime state, not IR.
	HistoryType    HistoryType `json:"historyType,omitempty"`
	HistoryDefault *S          `json:"historyDefault,omitempty"`

	// Invoke declares the services invoked while this state is active (the
	// `invoke`). Entering the state emits a StartService effect per invocation;
	// exiting it before a service completes emits a StopService effect
	// (auto-stop-on-exit). Each invocation routes its result through OnDone and its
	// error through OnError. The whole block serializes, so it round-trips
	// losslessly through JSON. A host's ServiceRunner runs the services and re-fires
	// onDone/onError through Fire, keeping Fire pure.
	Invoke []Invocation[S, E, C] `json:"invoke,omitempty"`
	// Parent is a runtime-only back-pointer to the compound state owning this node,
	// rebuilt after Quench/Provide; it never serializes. An ActorKindMachine entry
	// in Invoke marks a child-machine actor whose lifecycle the host ActorSystem
	// drives (the actor model); the per-instance actor mailboxes live on the host
	// ActorSystem, not on this definition.
	Parent *State[S, E, C] `json:"-"`
}

State is a node in the machine graph.

A state is one of three shapes: a leaf (no Children, no Regions), a compound (hierarchical) state declaring Children plus an InitialChild, or a parallel state declaring Regions. A state is never both compound and parallel.

type Status added in v0.2.0

type Status int

Status classifies a snapshotted instance's lifecycle. It mirrors the runtime status. StatusRunning is an instance still advancing; StatusDone is an instance whose active configuration is entirely final (every active leaf is a final state); StatusError is an instance the host settled as failed, carrying the error message on the snapshot.

const (
	// StatusRunning is the default: the instance has not reached completion.
	StatusRunning Status = iota
	// StatusDone marks an instance whose whole active configuration is final.
	StatusDone
	// StatusError marks an instance the host explicitly failed; Snapshot.Error
	// carries the message.
	StatusError
)

Instance lifecycle statuses recorded on a Snapshot.

func (Status) String added in v0.2.0

func (s Status) String() string

String renders a Status for diagnostics and stable JSON.

type StopActor added in v0.2.0

type StopActor struct {
	// ID identifies the actor to stop. It matches the ID of the SpawnActor that
	// began it (auto-stop-on-exit), or an ID supplied to the stop built-in.
	ID string
}

StopActor is the effect the kernel emits when an instance exits a state that had a running child-machine actor (auto-stop-on-exit), or when the built-in stop action runs. The host's ActorSystem stops the actor registered under ID (and, transitively, that actor's own children); stopping an unknown ID is a no-op. A state's invoked actors are auto-stopped when the state is exited before they complete.

type StopService added in v0.2.0

type StopService struct {
	// ID identifies the service to stop. It matches the ID of the StartService
	// that began it (auto-stop-on-exit).
	ID string
}

StopService is the effect the kernel emits when an instance exits a state that had an in-flight invoked service. The host stops the service registered under ID; stopping an unknown ID is a no-op. A state's invoked services are auto-stopped when the state is exited before they complete.

type TemperOption

type TemperOption func(*temperConfig)

TemperOption configures Temper.

type ToJSONOption

type ToJSONOption func(*toJSONConfig)

ToJSONOption configures ToJSON.

func WithoutSrcPos

func WithoutSrcPos() ToJSONOption

WithoutSrcPos omits the diagnostic source-position fields (srcFile/srcLine) from the serialized IR. Source positions are captured from the builder via runtime.Caller, so they carry the absolute filesystem path of the worktree that authored the machine — which makes them non-portable across checkouts. They are diagnostic-only metadata ("defined at machine.go:84" tooltips) and have no effect on loading or behavior, so stripping them yields a stable, position-independent serialization. Use it for committed goldens and any interchange that must be byte-identical regardless of where it was generated.

type Trace

type Trace struct {
	Machine            string                     `json:"machine,omitempty"`
	Event              string                     `json:"event,omitempty"`
	FromState          string                     `json:"fromState,omitempty"`
	SelectedTransition *Transition[any, any, any] `json:"-"`
	GuardsEvaluated    []string                   `json:"guardsEvaluated,omitempty"`
	PoliciesEvaluated  []string                   `json:"policiesEvaluated,omitempty"`
	EffectsEmitted     []string                   `json:"effectsEmitted,omitempty"`
	Microsteps         []string                   `json:"microsteps,omitempty"`

	// MatchedAt names the state whose transition actually fired. For a flat
	// machine it equals FromState; for an HSM it may be an ancestor reached by
	// the child-first bubble.
	MatchedAt string `json:"matchedAt,omitempty"`
	// ExitedStates and EnteredStates record the transition's exit/entry cascade
	// in execution order (exit innermost-first, entry outermost-first).
	ExitedStates  []string `json:"exitedStates,omitempty"`
	EnteredStates []string `json:"enteredStates,omitempty"`

	Outcome Outcome `json:"outcome"`
}

Trace is the kernel's canonical observability surface — pure data recorded on every Fire.

type Transition

type Transition[S comparable, E comparable, C any] struct {
	From S `json:"from"`
	To   S `json:"to"`
	On   E `json:"on"`

	Guards   []Ref    `json:"guards,omitempty"`
	Effects  []Ref    `json:"effects,omitempty"`
	WaitMode WaitMode `json:"waitMode,omitempty"`

	// GuardExpr is an optional composite guard: a serializable boolean
	// expression tree over named-ref leaves, the stateIn built-in, and the
	// and/or/not combinators. When set it is evaluated in
	// addition to every Ref in Guards — the transition is enabled only when both
	// the plain guards and the expression pass — so the common single-guard case
	// stays the plain Guards slice and composition is purely additive. The tree
	// serializes and round-trips losslessly through JSON.
	GuardExpr *GuardNode[S] `json:"guardExpr,omitempty"`

	Internal  bool           `json:"internal,omitempty"`
	EventLess bool           `json:"eventLess,omitempty"`
	After     *time.Duration `json:"after,omitempty"`

	// Wildcard marks a catch-all transition: it matches any event that no
	// specific-event transition of the same state handles. Wildcard transitions
	// are the lowest-priority candidates in a state, tried only after every
	// On-keyed match fails, and resolution still bubbles to ancestors when no
	// wildcard fires. On is ignored when Wildcard is set. This is the
	// `on: { '*': ... }`.
	Wildcard bool `json:"wildcard,omitempty"`

	// Forbidden marks an event as explicitly blocked at this state: the event is
	// consumed and ignored, and — unlike "no handler declared" — it does NOT
	// bubble to ancestor states. To has no meaning for a forbidden transition.
	// This is a forbidden transition: the event is consumed and ignored.
	Forbidden bool `json:"forbidden,omitempty"`

	// Reenter makes a transition external. By default (v5 semantics) a transition
	// whose target is the source itself or an ancestor of the source is internal:
	// its effects run but the source is not exited and re-entered. Setting Reenter
	// forces the external form, running the full exit/entry cascade of the target.
	// For an unrelated target (an ordinary state change) the cascade always runs;
	// Reenter only changes the self/ancestor case. This is the
	// `reenter: true`.
	Reenter bool `json:"reenter,omitempty"`

	// Raise lists internal events this transition enqueues. They are appended to
	// the macrostep's internal queue after the transition's own effects run, and
	// drained by Fire's run-to-completion loop within the SAME macrostep — before
	// Fire returns and before any externally-sent event. This is the
	// `raise(...)`. The queue is local to the macrostep, so Fire stays pure.
	Raise []E `json:"raise,omitempty"`

	SrcFile string `json:"srcFile,omitempty"`
	SrcLine int    `json:"srcLine,omitempty"`
	// contains filtered or unexported fields
}

Transition is a directed edge.

type VizOption

type VizOption func(*vizConfig)

VizOption configures the ToMermaid and ToDOT renderers.

func LeftToRight

func LeftToRight() VizOption

LeftToRight lays the diagram out left-to-right (Mermaid direction LR, DOT rankdir=LR).

func TopToBottom

func TopToBottom() VizOption

TopToBottom lays the diagram out top-to-bottom (Mermaid default, DOT rankdir=TB).

func WithoutGuards

func WithoutGuards() VizOption

WithoutGuards omits the bracketed guard annotations from transition labels.

func WithoutOwners

func WithoutOwners() VizOption

WithoutOwners omits owner color-coding (Mermaid classDef / DOT fillcolor).

type WaitMode

type WaitMode int

WaitMode tags a transition's synchronization expectation. The kernel only stores the tag; the consumer acts on it.

const (
	SyncReply WaitMode = iota
	FireAndForget
	ValidatePoll
)

Wait modes. SyncReply awaits a reply, FireAndForget emits and moves on, and ValidatePoll signals the consumer to poll the entity (re-running Assay) until it validates.

type WaitOption added in v0.2.0

type WaitOption[S comparable, E comparable, C any] func(*waitConfig[S, E, C])

WaitOption configures WaitFor.

func WithWaitScheduler added in v0.2.0

func WithWaitScheduler[S comparable, E comparable, C any](sch *Scheduler[S, E, C]) WaitOption[S, E, C]

WithWaitScheduler drives the wait by advancing the FakeClock the Scheduler reads and ticking it each iteration, so `after`-driven transitions fire and the instance progresses toward the predicate deterministically. It is the common driver for delayed-transition machines: cast with WithClock(fakeClock), build a Scheduler, then WaitFor(ctx, inst, pred, WithWaitScheduler(sch)). The Scheduler's clock must be the instance's clock (it is, by construction of NewScheduler).

func WithWaitStep added in v0.2.0

func WithWaitStep[S comparable, E comparable, C any](d time.Duration) WaitOption[S, E, C]

WithWaitStep sets the per-iteration advance increment WaitFor applies to the driver's clock between predicate checks. A smaller step lands closer to the exact instant a delayed transition becomes due; a larger step polls less often.

func WithWaitStepFunc added in v0.2.0

func WithWaitStepFunc[S comparable, E comparable, C any](
	advance func(ctx context.Context, clock Clock, step time.Duration),
) WaitOption[S, E, C]

WithWaitStepFunc supplies a custom driver advance: a function WaitFor calls each iteration to move time forward and fire any due work (e.g. a ServiceRunner a test settles, or a bespoke host loop). The function should advance the supplied clock by step when it is a FakeClock so the wait budget is consumed deterministically.

func WithWaitTimeout added in v0.2.0

func WithWaitTimeout[S comparable, E comparable, C any](d time.Duration) WaitOption[S, E, C]

WithWaitTimeout sets the wait budget, measured on the instance's clock. When the budget elapses before the predicate holds, WaitFor returns a *WaitTimeoutError.

type WaitPredicate added in v0.2.0

type WaitPredicate[S comparable, E comparable, C any] func(snap Snapshot[S, E, C]) bool

WaitPredicate is the condition WaitFor waits to become true. It is evaluated against the instance's live Snapshot after each advance (and once before any advance). It must be a pure read of the snapshot — WaitFor never mutates the instance on the predicate's behalf.

func WaitDone added in v0.2.0

func WaitDone[S comparable, E comparable, C any]() WaitPredicate[S, E, C]

WaitDone returns a WaitPredicate that holds when the instance has reached completion (its whole active configuration is final), mirroring `waitFor(actor, (s) => s.status === 'done')`.

func WaitInState added in v0.2.0

func WaitInState[S comparable, E comparable, C any](target S) WaitPredicate[S, E, C]

WaitInState returns a WaitPredicate that holds when the instance's primary active leaf equals target — the common "wait until it reaches state X" case (waiting until the instance's snapshot satisfies a predicate).

type WaitTimeoutError added in v0.2.0

type WaitTimeoutError struct {
	Machine string
	Timeout time.Duration
	Last    string
}

WaitTimeoutError is returned by WaitFor when its wait budget elapses (measured on the instance's clock) before the predicate ever held — the typed timeout returned when a WaitFor budget elapses. Machine names the instance's machine, Timeout the budget that elapsed, and Last the primary active leaf the instance was in when the wait gave up, for diagnostics.

func (*WaitTimeoutError) Error added in v0.2.0

func (e *WaitTimeoutError) Error() string

Directories

Path Synopsis
Package analysis performs static model-checking over a Quenched Crucible state machine.
Package analysis performs static model-checking over a Quenched Crucible state machine.
Package conformance proves that a state machine behaves correctly.
Package conformance proves that a state machine behaves correctly.
Package evolution classifies the difference between two versions of a state machine definition as additive (backward-compatible) or breaking, following the Crucible Evolution Guide.
Package evolution classifies the difference between two versions of a state machine definition as additive (backward-compatible) or breaking, following the Crucible Evolution Guide.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL