use serde::{Deserialize, Serialize}; use std::fmt; use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub const POLICY_SCHEMA_VERSION: u16 = 1; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum CommitmentSource { Manual, Planner, Recurring, Recovery, Template, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum CommitmentState { Draft, Active, Paused, Completed, Abandoned, Violated, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum RuntimeState { Locked, Planning, Active, Transition, Review, AdminOverride, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum EnforcementLevel { Observe, Warn, Block, Locked, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum EvidenceHealth { Available, Degraded(String), Unavailable(String), } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct AllowedContext { pub window_classes: Vec, pub window_title_substrings: Vec, pub domains: Vec, pub repos: Vec, pub commands: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Commitment { pub id: String, pub created_at_unix_secs: u64, pub source: CommitmentSource, pub project_id: Option, pub template_id: Option, pub next_action: String, pub success_condition: String, pub timebox_secs: u64, pub transition_policy_id: Option, pub state: CommitmentState, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct PolicySnapshot { pub id: String, pub commitment_id: String, pub schema_version: u16, pub created_at_unix_secs: u64, pub runtime_state: RuntimeState, pub enforcement_level: EnforcementLevel, pub allowed_context: AllowedContext, pub required_monitors: Vec, pub violation_actions: Vec, pub expires_at_unix_secs: Option, pub generated_by_agent_version: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum CommitmentValidationError { MissingNextAction, MissingSuccessCondition, MissingTimebox, } impl fmt::Display for CommitmentValidationError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CommitmentValidationError::MissingNextAction => write!(f, "next action is required"), CommitmentValidationError::MissingSuccessCondition => { write!(f, "success condition is required") } CommitmentValidationError::MissingTimebox => write!(f, "timebox must be nonzero"), } } } impl Commitment { pub fn new_manual( next_action: impl Into, success_condition: impl Into, timebox: Duration, ) -> Result { let next_action = next_action.into().trim().to_string(); let success_condition = success_condition.into().trim().to_string(); if next_action.is_empty() { return Err(CommitmentValidationError::MissingNextAction); } if success_condition.is_empty() { return Err(CommitmentValidationError::MissingSuccessCondition); } if timebox.is_zero() { return Err(CommitmentValidationError::MissingTimebox); } let created_at_unix_secs = unix_secs_now(); Ok(Self { id: format!("commitment-{created_at_unix_secs}"), created_at_unix_secs, source: CommitmentSource::Manual, project_id: None, template_id: None, next_action, success_condition, timebox_secs: timebox.as_secs(), transition_policy_id: None, state: CommitmentState::Draft, }) } } impl PolicySnapshot { pub fn for_runtime( commitment_id: String, runtime_state: RuntimeState, enforcement_level: EnforcementLevel, allowed_context: AllowedContext, ) -> Self { let created_at_unix_secs = unix_secs_now(); Self { id: format!("policy-{created_at_unix_secs}"), commitment_id, schema_version: POLICY_SCHEMA_VERSION, created_at_unix_secs, runtime_state, enforcement_level, allowed_context, required_monitors: Vec::new(), violation_actions: Vec::new(), expires_at_unix_secs: None, generated_by_agent_version: env!("CARGO_PKG_VERSION").to_string(), } } } pub fn unix_secs_now() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() } #[cfg(test)] mod tests { use super::*; use std::time::Duration; #[test] fn commitment_requires_concrete_action_and_success_condition() { let err = Commitment::new_manual("", "tests pass", Duration::from_secs(1500)) .expect_err("empty next action must be rejected"); assert_eq!(err, CommitmentValidationError::MissingNextAction); let err = Commitment::new_manual("Refactor state", "", Duration::from_secs(1500)) .expect_err("empty success condition must be rejected"); assert_eq!(err, CommitmentValidationError::MissingSuccessCondition); } #[test] fn commitment_requires_nonzero_timebox() { let err = Commitment::new_manual("Refactor state", "tests pass", Duration::ZERO) .expect_err("zero timebox must be rejected"); assert_eq!(err, CommitmentValidationError::MissingTimebox); } #[test] fn policy_snapshot_carries_runtime_contract() { let commitment = Commitment::new_manual( "Implement domain types", "domain tests pass", Duration::from_secs(1500), ) .unwrap(); let policy = PolicySnapshot::for_runtime( commitment.id.clone(), RuntimeState::Active, EnforcementLevel::Warn, AllowedContext::default(), ); assert_eq!(policy.commitment_id, commitment.id); assert_eq!(policy.runtime_state, RuntimeState::Active); assert_eq!(policy.enforcement_level, EnforcementLevel::Warn); assert_eq!(policy.schema_version, 1); } }