From de40fe73c797d7a064a4b29c97f7e9a0babc1b86 Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Sun, 31 May 2026 11:41:40 -0400 Subject: [PATCH] Add runtime and commitment state machines Co-Authored-By: Claude Opus 4.8 --- internal/statemachine/statemachine.go | 115 +++++++++++++++++++++ internal/statemachine/statemachine_test.go | 54 ++++++++++ 2 files changed, 169 insertions(+) create mode 100644 internal/statemachine/statemachine.go create mode 100644 internal/statemachine/statemachine_test.go diff --git a/internal/statemachine/statemachine.go b/internal/statemachine/statemachine.go new file mode 100644 index 0000000..18f6e92 --- /dev/null +++ b/internal/statemachine/statemachine.go @@ -0,0 +1,115 @@ +// Package statemachine holds the pure runtime and commitment transition +// functions ported from the Rust implementation. No I/O, no shared state. +package statemachine + +import ( + "fmt" + + "antidrift/internal/domain" +) + +// RuntimeAction enumerates runtime transitions. Activate's policy acceptance +// and admin-override exit targets from the Rust enum are flattened into +// distinct actions so the action type stays a simple value. +type RuntimeAction string + +const ( + EnterPlanning RuntimeAction = "enter_planning" + CancelOrTimeout RuntimeAction = "cancel_or_timeout" + ActivateAccepted RuntimeAction = "activate_accepted" + ActivateRejected RuntimeAction = "activate_rejected" + StartTransition RuntimeAction = "start_transition" + CompleteForReview RuntimeAction = "complete_for_review" + SevereViolation RuntimeAction = "severe_violation" + ReturnToActive RuntimeAction = "return_to_active" + SwitchTask RuntimeAction = "switch_task" + EndTransitionForReview RuntimeAction = "end_transition_for_review" + ContinuePlanning RuntimeAction = "continue_planning" + EndWorkPeriod RuntimeAction = "end_work_period" + EnterAdminOverride RuntimeAction = "enter_admin_override" + ExitAdminOverrideToLocked RuntimeAction = "exit_admin_override_to_locked" + ExitAdminOverrideToPlanning RuntimeAction = "exit_admin_override_to_planning" +) + +type CommitmentAction string + +const ( + CommitmentActivate CommitmentAction = "activate" + PauseForTransition CommitmentAction = "pause_for_transition" + ReturnFromPause CommitmentAction = "return_from_pause" + Complete CommitmentAction = "complete" + Abandon CommitmentAction = "abandon" + Violate CommitmentAction = "violate" + RecoverExplicitly CommitmentAction = "recover_explicitly" +) + +// IllegalTransitionError reports a rejected transition. +type IllegalTransitionError struct { + From string + Action string +} + +func (e IllegalTransitionError) Error() string { + return fmt.Sprintf("illegal transition from %s via %s", e.From, e.Action) +} + +// PolicyRejectedError reports an activation attempted with an unaccepted policy. +type PolicyRejectedError struct{} + +func (PolicyRejectedError) Error() string { return "policy was rejected" } + +func TransitionRuntime(current domain.RuntimeState, action RuntimeAction) (domain.RuntimeState, error) { + type key struct { + s domain.RuntimeState + a RuntimeAction + } + table := map[key]domain.RuntimeState{ + {domain.RuntimeLocked, EnterPlanning}: domain.RuntimePlanning, + {domain.RuntimePlanning, ActivateAccepted}: domain.RuntimeActive, + {domain.RuntimePlanning, CancelOrTimeout}: domain.RuntimeLocked, + {domain.RuntimeActive, StartTransition}: domain.RuntimeTransition, + {domain.RuntimeActive, CompleteForReview}: domain.RuntimeReview, + {domain.RuntimeActive, SevereViolation}: domain.RuntimeLocked, + {domain.RuntimeTransition, ReturnToActive}: domain.RuntimeActive, + {domain.RuntimeTransition, SwitchTask}: domain.RuntimePlanning, + {domain.RuntimeTransition, EndTransitionForReview}: domain.RuntimeReview, + {domain.RuntimeTransition, CancelOrTimeout}: domain.RuntimeLocked, + {domain.RuntimeReview, ContinuePlanning}: domain.RuntimePlanning, + {domain.RuntimeReview, EndWorkPeriod}: domain.RuntimeLocked, + {domain.RuntimeAdminOverride, ExitAdminOverrideToLocked}: domain.RuntimeLocked, + {domain.RuntimeAdminOverride, ExitAdminOverrideToPlanning}: domain.RuntimePlanning, + } + + if action == ActivateRejected && current == domain.RuntimePlanning { + return current, PolicyRejectedError{} + } + if action == EnterAdminOverride { + return domain.RuntimeAdminOverride, nil + } + if next, ok := table[key{current, action}]; ok { + return next, nil + } + return current, IllegalTransitionError{From: string(current), Action: string(action)} +} + +func TransitionCommitment(current domain.CommitmentState, action CommitmentAction) (domain.CommitmentState, error) { + type key struct { + s domain.CommitmentState + a CommitmentAction + } + table := map[key]domain.CommitmentState{ + {domain.CommitmentDraft, CommitmentActivate}: domain.CommitmentActive, + {domain.CommitmentActive, PauseForTransition}: domain.CommitmentPaused, + {domain.CommitmentPaused, ReturnFromPause}: domain.CommitmentActive, + {domain.CommitmentActive, Complete}: domain.CommitmentCompleted, + {domain.CommitmentActive, Abandon}: domain.CommitmentAbandoned, + {domain.CommitmentActive, Violate}: domain.CommitmentViolated, + {domain.CommitmentPaused, Abandon}: domain.CommitmentAbandoned, + {domain.CommitmentViolated, RecoverExplicitly}: domain.CommitmentActive, + {domain.CommitmentViolated, Abandon}: domain.CommitmentAbandoned, + } + if next, ok := table[key{current, action}]; ok { + return next, nil + } + return current, IllegalTransitionError{From: string(current), Action: string(action)} +} diff --git a/internal/statemachine/statemachine_test.go b/internal/statemachine/statemachine_test.go new file mode 100644 index 0000000..813eb49 --- /dev/null +++ b/internal/statemachine/statemachine_test.go @@ -0,0 +1,54 @@ +package statemachine + +import ( + "testing" + + "antidrift/internal/domain" +) + +func TestPlanningActivatesOnlyWithAcceptedPolicy(t *testing.T) { + got, err := TransitionRuntime(domain.RuntimePlanning, ActivateAccepted) + if err != nil || got != domain.RuntimeActive { + t.Fatalf("accepted: got %s err %v", got, err) + } + _, err = TransitionRuntime(domain.RuntimePlanning, ActivateRejected) + if err == nil { + t.Fatalf("rejected policy should error") + } +} + +func TestActiveCanCompleteForReview(t *testing.T) { + got, err := TransitionRuntime(domain.RuntimeActive, CompleteForReview) + if err != nil || got != domain.RuntimeReview { + t.Fatalf("got %s err %v", got, err) + } +} + +func TestReviewEndsToLocked(t *testing.T) { + got, err := TransitionRuntime(domain.RuntimeReview, EndWorkPeriod) + if err != nil || got != domain.RuntimeLocked { + t.Fatalf("got %s err %v", got, err) + } +} + +func TestLockedCannotBecomeActiveDirectly(t *testing.T) { + _, err := TransitionRuntime(domain.RuntimeLocked, ActivateAccepted) + if err == nil { + t.Fatalf("locked->active must be illegal") + } +} + +func TestCommitmentDraftActivatesAndViolatedRecovers(t *testing.T) { + got, err := TransitionCommitment(domain.CommitmentDraft, CommitmentActivate) + if err != nil || got != domain.CommitmentActive { + t.Fatalf("draft->active: got %s err %v", got, err) + } + got, err = TransitionCommitment(domain.CommitmentViolated, RecoverExplicitly) + if err != nil || got != domain.CommitmentActive { + t.Fatalf("violated->active: got %s err %v", got, err) + } + _, err = TransitionCommitment(domain.CommitmentViolated, ReturnFromPause) + if err == nil { + t.Fatalf("violated->returnFromPause must be illegal") + } +}