Add commitment domain types
This commit is contained in:
+217
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user