From 3fe7131f136c87c770846951caaf00db52f8ce8e Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Mon, 25 May 2026 12:52:19 -0400 Subject: [PATCH] Add commitment state machines --- src/state_machine.rs | 170 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 1 deletion(-) diff --git a/src/state_machine.rs b/src/state_machine.rs index c296c7c..9dca0b4 100644 --- a/src/state_machine.rs +++ b/src/state_machine.rs @@ -1 +1,169 @@ -// Placeholder for upcoming state_machine module. +use crate::domain::{CommitmentState, RuntimeState}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RuntimeAction { + EnterPlanning, + CancelOrTimeout, + Activate { policy_accepted: bool }, + StartTransition, + CompleteForReview, + SevereViolation, + ReturnToActive, + SwitchTask, + EndTransitionForReview, + ContinuePlanning, + EndWorkPeriod, + EnterAdminOverride, + ExitAdminOverride(RuntimeState), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CommitmentAction { + Activate, + PauseForTransition, + ReturnFromPause, + Complete, + Abandon, + Violate, + RecoverExplicitly, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TransitionError { + PolicyRejected, + IllegalRuntimeTransition, + IllegalCommitmentTransition, +} + +impl std::fmt::Display for TransitionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TransitionError::PolicyRejected => write!(f, "policy was rejected"), + TransitionError::IllegalRuntimeTransition => write!(f, "illegal runtime transition"), + TransitionError::IllegalCommitmentTransition => { + write!(f, "illegal commitment transition") + } + } + } +} + +impl std::error::Error for TransitionError {} + +pub fn transition_runtime( + current: RuntimeState, + action: RuntimeAction, +) -> Result { + use RuntimeAction::*; + use RuntimeState::*; + + match (current, action) { + (Locked, EnterPlanning) => Ok(Planning), + (Planning, Activate { policy_accepted: true }) => Ok(Active), + (Planning, Activate { policy_accepted: false }) => Err(TransitionError::PolicyRejected), + (Planning, CancelOrTimeout) => Ok(Locked), + (Active, StartTransition) => Ok(Transition), + (Active, CompleteForReview) => Ok(Review), + (Active, SevereViolation) => Ok(Locked), + (Transition, ReturnToActive) => Ok(Active), + (Transition, SwitchTask) => Ok(Planning), + (Transition, EndTransitionForReview) => Ok(Review), + (Transition, CancelOrTimeout) => Ok(Locked), + (Review, ContinuePlanning) => Ok(Planning), + (Review, EndWorkPeriod) => Ok(Locked), + (_, EnterAdminOverride) => Ok(AdminOverride), + (AdminOverride, ExitAdminOverride(next)) => Ok(next), + _ => Err(TransitionError::IllegalRuntimeTransition), + } +} + +pub fn transition_commitment( + current: CommitmentState, + action: CommitmentAction, +) -> Result { + use CommitmentAction::*; + use CommitmentState::*; + + match (current, action) { + (Draft, Activate) => Ok(Active), + (Active, PauseForTransition) => Ok(Paused), + (Paused, ReturnFromPause) => Ok(Active), + (Active, Complete) => Ok(Completed), + (Active, Abandon) => Ok(Abandoned), + (Active, Violate) => Ok(Violated), + (Paused, Abandon) => Ok(Abandoned), + (Violated, RecoverExplicitly) => Ok(Active), + (Violated, Abandon) => Ok(Abandoned), + _ => Err(TransitionError::IllegalCommitmentTransition), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn planning_can_only_activate_with_valid_policy() { + assert_eq!( + transition_runtime( + RuntimeState::Planning, + RuntimeAction::Activate { + policy_accepted: true, + } + ), + Ok(RuntimeState::Active) + ); + assert_eq!( + transition_runtime( + RuntimeState::Planning, + RuntimeAction::Activate { + policy_accepted: false, + } + ), + Err(TransitionError::PolicyRejected) + ); + } + + #[test] + fn active_can_enter_transition_or_review_or_locked() { + assert_eq!( + transition_runtime(RuntimeState::Active, RuntimeAction::StartTransition), + Ok(RuntimeState::Transition) + ); + assert_eq!( + transition_runtime(RuntimeState::Active, RuntimeAction::CompleteForReview), + Ok(RuntimeState::Review) + ); + assert_eq!( + transition_runtime(RuntimeState::Active, RuntimeAction::SevereViolation), + Ok(RuntimeState::Locked) + ); + } + + #[test] + fn locked_cannot_directly_become_active() { + assert_eq!( + transition_runtime( + RuntimeState::Locked, + RuntimeAction::Activate { + policy_accepted: true, + } + ), + Err(TransitionError::IllegalRuntimeTransition) + ); + } + + #[test] + fn commitment_violation_recovery_is_explicit() { + assert_eq!( + transition_commitment( + CommitmentState::Violated, + CommitmentAction::RecoverExplicitly + ), + Ok(CommitmentState::Active) + ); + assert_eq!( + transition_commitment(CommitmentState::Violated, CommitmentAction::ReturnFromPause), + Err(TransitionError::IllegalCommitmentTransition) + ); + } +}