evolution

package
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: 4 Imported by: 0

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

type DecodeError struct {
	Side string
	Err  error
}

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

func (r Report) Breaking() bool

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) Empty

func (r Report) Empty() bool

Empty reports whether the two definitions were equivalent.

func (Report) SemverBump

func (r Report) SemverBump() Bump

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.

func (Report) String

func (r Report) String() string

String renders the report as one line per change, breaking changes first.

type SerializeError

type SerializeError struct {
	Side string
	Err  error
}

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.

Jump to

Keyboard shortcuts

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