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