Skip to content

state/conformance

import "github.com/stablekernel/crucible/state/conformance"

Package conformance proves that a state machine behaves correctly.

It rests on the three pillars from the suite’s conformance design:

  1. Oracle comparison — the effects a machine’s Fire produces are diffed against a trusted reference implementation for the same input.
  2. Golden scenarios — committed event sequences are replayed and the final state, emitted effects, and trace are asserted.
  3. Round-trip identity — a machine authored in Go and the same machine loaded from JSON (then bound via Provide) are proven to behave identically.

Scenarios are derived from the machine graph: GenerateScenarios enumerates the shortest event path to every reachable state by breadth-first search over the IR, mirroring the path-planning model so a small machine yields full coverage without hand-authored fixtures. A Scenario and the Trace a run produces are both first-class, JSON-serializable artifacts: scenarios can be committed as goldens under testdata and replayed in CI, and a captured run can be diffed against a committed expectation.

The package depends only on the state kernel and the standard library, so it adds no third-party dependencies to a consumer that vendors it to prove its own machines correct.

func CompareMachines[S comparable, E comparable, C any](reference, subject *state.Machine[S, E, C], scenarios []Scenario, codec EventCodec[E], startState S, newEntity freshEntity[C], opts ...CompareOption) error

CompareMachines runs every scenario against two machines built from the same state/event/context types and reports the divergences. It is the oracle pillar generalized to two machine implementations: the reference (canonical) and the subject (under test). Both are Cast from an entity drawn fresh per scenario so a mutated run never bleeds into the next comparison.

A nil error means the subject conforms to the reference across every scenario.

func RoundTripIdentity[S comparable, E comparable, C any](forged *state.Machine[S, E, C], reg *state.Registry[C], scenarios []Scenario, codec EventCodec[E], startState S, newEntity freshEntity[C], opts ...CompareOption) error

RoundTripIdentity proves the config/implementation split honest: a machine authored in Go and the same machine after ToJSON -> LoadFromJSON -> Provide -> Quench are the same machine, on both structure and behavior.

It performs two checks:

  1. Structural — the IR is byte-stable under a round-trip (serialize, reload, reserialize, compare).
  2. Behavioral — every scenario produces an identical result against the code-built machine and the JSON-loaded machine: same final state, same effects, same trace. Because behavior is rebound by name from the same registry, identity here is exact, not approximate.

The caller supplies the registry the JSON-loaded machine binds against (the same host palette the DSL registered), a fresh entity per run, and the start state. A divergence means the IR is lossy or the registry binding drifted, and is returned as an *ErrConformance.

Assertion is a declarative expectation about a scenario run. Assertions are descriptions, not predicates: a run records each as pass or fail and leaves the caller to decide whether a failure is fatal.

type Assertion struct {
Type AssertionType `json:"type"`
Expected any `json:"expected"`
}

AssertionResult records one assertion’s verdict after a run.

type AssertionResult struct {
Type AssertionType `json:"type"`
Expected any `json:"expected"`
Actual any `json:"actual"`
Pass bool `json:"pass"`
}

AssertionType names a declarative scenario assertion.

type AssertionType string

The v1 assertion set covers final-state, emitted effects, trace length, and the absence of errors — enough for generated and hand-authored scenarios.

const (
AssertFinalState AssertionType = "FinalState"
AssertEffectsEmitted AssertionType = "EffectsEmitted"
AssertTraceLength AssertionType = "TraceLength"
AssertNoErrors AssertionType = "NoErrors"
)

CompareOption configures an oracle or round-trip comparison.

type CompareOption func(*compareConfig)

func IgnoreEffects() CompareOption

IgnoreEffects skips the emitted-effects comparison. Use it only when the two sides legitimately differ on effects (each use is a coverage hole).

func IgnoreTrace() CompareOption

IgnoreTrace skips the per-step trace comparison, comparing final state and effects only.

ErrConformance aggregates the mismatches found across an oracle comparison or a round-trip identity check. A nil error means the two sides agreed.

type ErrConformance struct {
Mismatches []Mismatch
}

func (e *ErrConformance) Error() string

ErrSchemaVersion is returned when a serialized artifact carries a schema version this package does not understand.

type ErrSchemaVersion struct {
Got int
Want int
}

func (e *ErrSchemaVersion) Error() string

ErrUnknownEvent is returned when a scenario names an event the codec cannot resolve to a typed value.

type ErrUnknownEvent struct {
Name string
}

func (e *ErrUnknownEvent) Error() string

Event is one step of a scenario: the event to fire, named so the artifact is portable across the typed event domain.

type Event struct {
Event string `json:"event"`
}

EventCodec carries both directions of the event-name mapping. A consumer supplies one per event type; for an int-backed enum with a String method the Named direction is fmt.Sprint and the Resolve direction is a small lookup map.

type EventCodec[E comparable] struct {
Named EventNamer[E]
Resolve EventResolver[E]
}

EventNamer renders a typed event to the stable name used in scenarios and traces. It must agree with the kernel’s own rendering (fmt.Sprint of the event), which is what GenerateScenarios reads back from the IR.

type EventNamer[E comparable] func(E) string

EventResolver maps a scenario’s event name back to its typed value so a serialized scenario can be replayed against a typed machine. It returns false for an unknown name.

type EventResolver[E comparable] func(name string) (E, bool)

GenerateOption configures scenario generation.

type GenerateOption func(*generateConfig)

func WithMaxDepth(d int) GenerateOption

WithMaxDepth caps the length of generated event paths. A non-positive value (the default) means no cap beyond the reachable graph.

Mismatch is one field-level divergence found by an oracle comparison.

type Mismatch struct {
// Scenario is the name of the scenario whose run diverged.
Scenario string
// Field names what diverged (e.g. "finalState", "effects", "trace.len").
Field string
// Reference and Subject are the diverging values from each side.
Reference string
Subject string
}

func (m Mismatch) String() string

Scenario describes what to do — fire this sequence of events against a machine from a starting state — plus the assertions to evaluate. It is a serializable artifact: generated scenarios can be committed as goldens and replayed.

type Scenario struct {
SchemaVersion int `json:"schemaVersion"`
MachineID string `json:"machineId"`
Name string `json:"name,omitempty"`
InitialState string `json:"initialState"`
Events []Event `json:"events"`
Assertions []Assertion `json:"assertions,omitempty"`
}

func GenerateScenarios[S comparable, E comparable, C any](m *state.Machine[S, E, C], namer EventNamer[E], opts ...GenerateOption) ([]Scenario, error)

GenerateScenarios derives a scenario for the shortest event path to every reachable state, by breadth-first search over the machine’s IR graph. This is the model-based layer: it mirrors path planning so a machine’s own structure produces its coverage, with no hand-authored fixtures.

Each generated scenario asserts the final state it targets, the trace length (the number of events fired), and that no errors occurred. The namer renders each typed event to the stable name the kernel records, so generated scenarios are directly serializable and replayable.

Generation walks the IR — the same exported, serializable graph ToJSON emits — so it is fully generic: it works for any machine, flat or hierarchical, and never reaches into kernel internals.

func LoadScenario(data []byte) (Scenario, error)

LoadScenario parses a scenario from its JSON form, rejecting an unsupported schema version.

func (s Scenario) MarshalJSON() ([]byte, error)

MarshalJSON emits the scenario with its schema version pinned.

ScenarioResult is the outcome of running a scenario against a machine: the resulting state, the captured trace, the per-assertion verdicts, and any kernel error encountered along the way.

type ScenarioResult[S comparable] struct {
FinalState S
Trace Trace
Assertions []AssertionResult
Effects []string
Err error
}

func RunAgainst[S comparable, E comparable, C any](m *state.Machine[S, E, C], sc Scenario, entity C, codec EventCodec[E], startState S) ScenarioResult[S]

RunAgainst fires the scenario’s event sequence against a freshly Cast instance of the machine and builds a ScenarioResult. The codec resolves each event name to its typed value; an unresolved name is a fatal scenario error. The entity is supplied by the caller (the kernel binds guards and actions to it) and the starting state is taken from the scenario.

func (r ScenarioResult[S]) Passed() bool

Passed reports whether every assertion in the result passed.

Trace is the serializable record of a whole scenario run: the ordered steps plus the spanning from/to state. It is the unifying primitive — it renders a past run and is diffable against a committed expectation.

type Trace struct {
SchemaVersion int `json:"schemaVersion"`
MachineID string `json:"machineId"`
FromState string `json:"fromState"`
ToState string `json:"toState"`
Steps []TraceStep `json:"steps"`
}

func (t Trace) MarshalJSON() ([]byte, error)

MarshalJSON emits the trace with its schema version pinned.

TraceStep is one Fire’s worth of recorded behavior, in serializable form. It mirrors the kernel Trace but renders effects and outcome as their stable string names so a step is portable and diffable.

type TraceStep struct {
Event string `json:"event"`
FromState string `json:"fromState"`
ToState string `json:"toState"`
MatchedAt string `json:"matchedAt,omitempty"`
GuardsEvaluated []string `json:"guardsEvaluated,omitempty"`
EffectsEmitted []string `json:"effectsEmitted,omitempty"`
ExitedStates []string `json:"exitedStates,omitempty"`
EnteredStates []string `json:"enteredStates,omitempty"`
Outcome string `json:"outcome"`
Err string `json:"err,omitempty"`
}

Generated by gomarkdoc