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:
- Oracle comparison — the effects a machine’s Fire produces are diffed against a trusted reference implementation for the same input.
- Golden scenarios — committed event sequences are replayed and the final state, emitted effects, and trace are asserted.
- 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
- 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
- type Assertion
- type AssertionResult
- type AssertionType
- type CompareOption
- type ErrConformance
- type ErrSchemaVersion
- type ErrUnknownEvent
- type Event
- type EventCodec
- type EventNamer
- type EventResolver
- type GenerateOption
- type Mismatch
- type Scenario
- type ScenarioResult
- type Trace
- type TraceStep
func CompareMachines
Section titled “func CompareMachines”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) errorCompareMachines 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
Section titled “func RoundTripIdentity”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) errorRoundTripIdentity 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:
- Structural — the IR is byte-stable under a round-trip (serialize, reload, reserialize, compare).
- 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.
type Assertion
Section titled “type Assertion”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"`}type AssertionResult
Section titled “type AssertionResult”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"`}type AssertionType
Section titled “type AssertionType”AssertionType names a declarative scenario assertion.
type AssertionType stringThe 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")type CompareOption
Section titled “type CompareOption”CompareOption configures an oracle or round-trip comparison.
type CompareOption func(*compareConfig)func IgnoreEffects
Section titled “func IgnoreEffects”func IgnoreEffects() CompareOptionIgnoreEffects skips the emitted-effects comparison. Use it only when the two sides legitimately differ on effects (each use is a coverage hole).
func IgnoreTrace
Section titled “func IgnoreTrace”func IgnoreTrace() CompareOptionIgnoreTrace skips the per-step trace comparison, comparing final state and effects only.
type ErrConformance
Section titled “type ErrConformance”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 (*ErrConformance) Error
Section titled “func (*ErrConformance) Error”func (e *ErrConformance) Error() stringtype ErrSchemaVersion
Section titled “type ErrSchemaVersion”ErrSchemaVersion is returned when a serialized artifact carries a schema version this package does not understand.
type ErrSchemaVersion struct { Got int Want int}func (*ErrSchemaVersion) Error
Section titled “func (*ErrSchemaVersion) Error”func (e *ErrSchemaVersion) Error() stringtype ErrUnknownEvent
Section titled “type ErrUnknownEvent”ErrUnknownEvent is returned when a scenario names an event the codec cannot resolve to a typed value.
type ErrUnknownEvent struct { Name string}func (*ErrUnknownEvent) Error
Section titled “func (*ErrUnknownEvent) Error”func (e *ErrUnknownEvent) Error() stringtype Event
Section titled “type Event”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"`}type EventCodec
Section titled “type EventCodec”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]}type EventNamer
Section titled “type EventNamer”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) stringtype EventResolver
Section titled “type EventResolver”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)type GenerateOption
Section titled “type GenerateOption”GenerateOption configures scenario generation.
type GenerateOption func(*generateConfig)func WithMaxDepth
Section titled “func WithMaxDepth”func WithMaxDepth(d int) GenerateOptionWithMaxDepth caps the length of generated event paths. A non-positive value (the default) means no cap beyond the reachable graph.
type Mismatch
Section titled “type Mismatch”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 (Mismatch) String
Section titled “func (Mismatch) String”func (m Mismatch) String() stringtype Scenario
Section titled “type Scenario”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
Section titled “func GenerateScenarios”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
Section titled “func LoadScenario”func LoadScenario(data []byte) (Scenario, error)LoadScenario parses a scenario from its JSON form, rejecting an unsupported schema version.
func (Scenario) MarshalJSON
Section titled “func (Scenario) MarshalJSON”func (s Scenario) MarshalJSON() ([]byte, error)MarshalJSON emits the scenario with its schema version pinned.
type ScenarioResult
Section titled “type ScenarioResult”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
Section titled “func RunAgainst”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 (ScenarioResult[S]) Passed
Section titled “func (ScenarioResult[S]) Passed”func (r ScenarioResult[S]) Passed() boolPassed reports whether every assertion in the result passed.
type Trace
Section titled “type Trace”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 (Trace) MarshalJSON
Section titled “func (Trace) MarshalJSON”func (t Trace) MarshalJSON() ([]byte, error)MarshalJSON emits the trace with its schema version pinned.
type TraceStep
Section titled “type TraceStep”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