Add runtime and commitment state machines
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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)}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user