state/evolution
import "github.com/stablekernel/crucible/state/evolution"Package evolution classifies the difference between two versions of a state machine definition as additive (backward-compatible) or breaking, following the Crucible Evolution Guide.
A machine definition is a schema. Renaming or removing a state, retargeting a transition, or moving the initial state breaks entities already persisted under the old definition; adding states, transitions, events, or optional metadata is safe. The guide maps these onto a deprecation lifecycle and a semantic-version bump: additive changes are minor, breaking changes are major.
This package operates on the serializable [state.IR], which is the canonical, versioned snapshot of a machine (the committed machine.json). A consumer commits a golden IR and gates their machine changes in CI by diffing the live machine against it:
report, err := evolution.DiffJSON[State, Event, *Entity](goldenBytes, currentBytes)if err != nil { return err}if report.Breaking() { return fmt.Errorf("breaking machine change requires a major version bump:\n%s", report)}The package imports only [state] and the standard library, preserving the kernel’s stdlib-only dependency stance.
- type Bump
- type Change
- type ChangeKind
- type DecodeError
- type Report
- func Diff[S comparable, E comparable, C any](old, updated *state.IR[S, E, C]) Report
- func DiffJSON[S comparable, E comparable, C any](old, updated []byte) (Report, error)
- func DiffMachines[S comparable, E comparable, C any](old, updated *state.Machine[S, E, C]) (Report, error)
- func (r Report) Breaking() bool
- func (r Report) Empty() bool
- func (r Report) SemverBump() Bump
- func (r Report) String() string
- type SerializeError
type Bump
Section titled “type Bump”Bump is a semantic-version increment recommendation.
type Bump stringSemantic-version bump recommendations.
const ( // Patch: no schema changes (only changes the differ never surfaces, e.g. // source positions, which are stripped before diffing). Patch Bump = "patch" // Minor: additive, backward-compatible changes only. Minor Bump = "minor" // Major: at least one breaking change. Major Bump = "major")type Change
Section titled “type Change”Change is a single classified difference between two machine definitions.
type Change struct { Kind ChangeKind // Path locates the change in the machine graph (e.g. a state name, or // "state/On" for a transition, with a dotted prefix for nested states). Path string // Description is a human-readable explanation. For flagged cases it is // prefixed with "[FLAGGED: ...]". Description string Breaking bool}type ChangeKind
Section titled “type ChangeKind”ChangeKind names the category of a single structural difference between two machine definitions.
type ChangeKind stringThe kinds of change the differ recognizes. Each maps to a fixed breaking/additive classification (see the Evolution Guide), except KindUnknown, which is always treated as breaking and flagged.
const ( // Additive (backward-compatible) kinds. KindStateAdded ChangeKind = "state_added" KindTransitionAdded ChangeKind = "transition_added" KindGuardAdded ChangeKind = "guard_added" KindGuardRemoved ChangeKind = "guard_removed" KindEffectAdded ChangeKind = "effect_added" KindEffectRemoved ChangeKind = "effect_removed" KindMetadataChanged ChangeKind = "metadata_changed" KindWaitModeChanged ChangeKind = "waitmode_changed"
// Breaking kinds. KindStateRemoved ChangeKind = "state_removed" KindTransitionRemoved ChangeKind = "transition_removed" KindTransitionRetargeted ChangeKind = "transition_retargeted" KindInitialChanged ChangeKind = "initial_changed" KindMachineRenamed ChangeKind = "machine_renamed" KindFinalChanged ChangeKind = "final_changed"
// KindUnknown marks a delta the differ has no explicit rule for. It is always // breaking and is flagged for human review, per the Evolution Guide's // "unknown -> breaking" default. KindUnknown ChangeKind = "unknown")type DecodeError
Section titled “type DecodeError”DecodeError reports that one side of a JSON diff could not be loaded into an IR. Side is “old” or “new”; the wrapped Err is the underlying decode failure.
type DecodeError struct { Side string Err error}func (*DecodeError) Error
Section titled “func (*DecodeError) Error”func (e *DecodeError) Error() stringfunc (*DecodeError) Unwrap
Section titled “func (*DecodeError) Unwrap”func (e *DecodeError) Unwrap() errorUnwrap exposes the underlying decode error for errors.Is / errors.As.
type Report
Section titled “type Report”Report is the full set of classified changes between two machine definitions. The zero Report (no changes) means the definitions are equivalent.
type Report struct { Changes []Change}func Diff
Section titled “func Diff”func Diff[S comparable, E comparable, C any](old, updated *state.IR[S, E, C]) ReportDiff classifies the difference between two machine IRs as additive or breaking, following the Evolution Guide. The result is deterministic: changes are ordered breaking-first, then by path.
Example
ExampleDiff classifies an additive change (a new state plus the transition that reaches it) and a breaking change (a retargeted transition), and reports the recommended version bump for each.
package main
import ( "fmt"
"github.com/stablekernel/crucible/state" "github.com/stablekernel/crucible/state/evolution")
func main() { old := &state.IR[string, string, any]{ Name: "order", Initial: "open", HasInitial: true, States: []state.State[string, string, any]{ {Name: "open", Transitions: []state.Transition[string, string, any]{ {From: "open", On: "pay", To: "paid"}, }}, {Name: "paid"}, }, }
// Additive: add a "shipped" state reachable from "paid". additive := &state.IR[string, string, any]{ Name: "order", Initial: "open", HasInitial: true, States: []state.State[string, string, any]{ {Name: "open", Transitions: []state.Transition[string, string, any]{ {From: "open", On: "pay", To: "paid"}, }}, {Name: "paid", Transitions: []state.Transition[string, string, any]{ {From: "paid", On: "ship", To: "shipped"}, }}, {Name: "shipped"}, }, }
// Breaking: "pay" now lands in "shipped" instead of "paid". breaking := &state.IR[string, string, any]{ Name: "order", Initial: "open", HasInitial: true, States: []state.State[string, string, any]{ {Name: "open", Transitions: []state.Transition[string, string, any]{ {From: "open", On: "pay", To: "shipped"}, }}, {Name: "paid"}, {Name: "shipped"}, }, }
a := evolution.Diff(old, additive) fmt.Printf("additive: breaking=%v bump=%s\n", a.Breaking(), a.SemverBump())
b := evolution.Diff(old, breaking) fmt.Printf("breaking: breaking=%v bump=%s\n", b.Breaking(), b.SemverBump())
}Output
Section titled “Output”additive: breaking=false bump=minorbreaking: breaking=true bump=majorfunc DiffJSON
Section titled “func DiffJSON”func DiffJSON[S comparable, E comparable, C any](old, updated []byte) (Report, error)DiffJSON classifies the difference between two serialized machine IRs. This is the form a CI gate uses: diff a committed golden machine.json against the current machine’s serialized IR.
func DiffMachines
Section titled “func DiffMachines”func DiffMachines[S comparable, E comparable, C any](old, updated *state.Machine[S, E, C]) (Report, error)DiffMachines classifies the difference between two Quenched machines. Both are serialized to their position-independent IR (source positions are stripped, so file/line churn never registers as a change) and then diffed.
func (Report) Breaking
Section titled “func (Report) Breaking”func (r Report) Breaking() boolBreaking reports whether any change is breaking. A breaking change requires a major version bump and the full deprecation lifecycle from the Evolution Guide before the old definition can be removed.
func (Report) Empty
Section titled “func (Report) Empty”func (r Report) Empty() boolEmpty reports whether the two definitions were equivalent.
func (Report) SemverBump
Section titled “func (Report) SemverBump”func (r Report) SemverBump() BumpSemverBump maps the report onto a recommended version bump: Major if any change is breaking, Minor if there are additive changes only, Patch if the definitions are equivalent.
func (Report) String
Section titled “func (Report) String”func (r Report) String() stringString renders the report as one line per change, breaking changes first.
type SerializeError
Section titled “type SerializeError”SerializeError reports that a machine could not be serialized to its IR before diffing. Side is “old” or “new”.
type SerializeError struct { Side string Err error}func (*SerializeError) Error
Section titled “func (*SerializeError) Error”func (e *SerializeError) Error() stringfunc (*SerializeError) Unwrap
Section titled “func (*SerializeError) Unwrap”func (e *SerializeError) Unwrap() errorUnwrap exposes the underlying serialize error for errors.Is / errors.As.
Generated by gomarkdoc