Documentation
¶
Overview ¶
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.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Bump ¶
type Bump string
Bump is a semantic-version increment recommendation.
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" )
Semantic-version bump recommendations.
type Change ¶
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
}
Change is a single classified difference between two machine definitions.
type ChangeKind ¶
type ChangeKind string
ChangeKind names the category of a single structural difference between two machine definitions.
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" )
The 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.
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.
func (*DecodeError) Error ¶
func (e *DecodeError) Error() string
func (*DecodeError) Unwrap ¶
func (e *DecodeError) Unwrap() error
Unwrap exposes the underlying decode error for errors.Is / errors.As.
type Report ¶
type Report struct {
Changes []Change
}
Report is the full set of classified changes between two machine definitions. The zero Report (no changes) means the definitions are equivalent.
func Diff ¶
func Diff[S comparable, E comparable, C any](old, updated *state.IR[S, E, C]) Report
Diff 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: additive: breaking=false bump=minor breaking: breaking=true bump=major
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 ¶
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 ¶
Breaking 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) SemverBump ¶
SemverBump 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.
type SerializeError ¶
SerializeError reports that a machine could not be serialized to its IR before diffing. Side is "old" or "new".
func (*SerializeError) Error ¶
func (e *SerializeError) Error() string
func (*SerializeError) Unwrap ¶
func (e *SerializeError) Unwrap() error
Unwrap exposes the underlying serialize error for errors.Is / errors.As.