Add commitment domain types

This commit is contained in:
2026-05-25 12:39:31 -04:00
parent 8e282ce9f0
commit 54e9433835
+217 -1
View File
@@ -1 +1,217 @@
// Placeholder for upcoming domain module. 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<String>,
pub window_title_substrings: Vec<String>,
pub domains: Vec<String>,
pub repos: Vec<String>,
pub commands: Vec<String>,
}
#[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<String>,
pub template_id: Option<String>,
pub next_action: String,
pub success_condition: String,
pub timebox_secs: u64,
pub transition_policy_id: Option<String>,
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<String>,
pub violation_actions: Vec<String>,
pub expires_at_unix_secs: Option<u64>,
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<String>,
success_condition: impl Into<String>,
timebox: Duration,
) -> Result<Self, CommitmentValidationError> {
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);
}
}