use std::collections::HashMap;
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use tracing::debug;
pub type EnvironmentId = String;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EnvironmentClass {
Development,
Staging,
Production,
Custom {
name: String,
risk_threshold: f64,
},
}
impl EnvironmentClass {
pub fn risk_threshold(&self) -> f64 {
match self {
EnvironmentClass::Development => 0.9,
EnvironmentClass::Staging => 0.6,
EnvironmentClass::Production => 0.3,
EnvironmentClass::Custom { risk_threshold, .. } => *risk_threshold,
}
}
}
impl std::fmt::Display for EnvironmentClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EnvironmentClass::Development => write!(f, "development"),
EnvironmentClass::Staging => write!(f, "staging"),
EnvironmentClass::Production => write!(f, "production"),
EnvironmentClass::Custom { name, .. } => write!(f, "custom({name})"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GovernanceScope {
#[serde(default = "default_risk_threshold")]
pub risk_threshold: f64,
#[serde(default)]
pub human_approval_required: bool,
#[serde(default)]
pub active_branches: GovernanceBranches,
#[serde(default)]
pub audit_level: AuditLevel,
#[serde(default)]
pub learning_mode: LearningMode,
#[serde(default = "default_max_effect")]
pub max_effect_magnitude: f64,
}
fn default_risk_threshold() -> f64 {
0.6
}
fn default_max_effect() -> f64 {
1.0
}
impl Default for GovernanceScope {
fn default() -> Self {
Self {
risk_threshold: default_risk_threshold(),
human_approval_required: false,
active_branches: GovernanceBranches::default(),
audit_level: AuditLevel::default(),
learning_mode: LearningMode::default(),
max_effect_magnitude: default_max_effect(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GovernanceBranches {
#[serde(default = "default_true")]
pub legislative: bool,
#[serde(default = "default_true")]
pub executive: bool,
#[serde(default)]
pub judicial: bool,
}
fn default_true() -> bool {
true
}
impl Default for GovernanceBranches {
fn default() -> Self {
Self {
legislative: true,
executive: true,
judicial: false,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuditLevel {
#[default]
SessionSummary,
PerAction,
PerActionWithEffects,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum LearningMode {
#[default]
Explore,
Validate,
Exploit,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Environment {
pub id: EnvironmentId,
pub name: String,
pub class: EnvironmentClass,
#[serde(default)]
pub governance: GovernanceScope,
#[serde(default)]
pub labels: HashMap<String, String>,
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum EnvironmentError {
#[error("environment already exists: '{id}'")]
AlreadyExists {
id: EnvironmentId,
},
#[error("environment not found: '{id}'")]
NotFound {
id: EnvironmentId,
},
#[error("invalid risk threshold {value}: must be between 0.0 and 1.0")]
InvalidRiskThreshold {
value: f64,
},
}
pub struct EnvironmentManager {
environments: DashMap<EnvironmentId, Environment>,
active: std::sync::RwLock<Option<EnvironmentId>>,
}
impl EnvironmentManager {
pub fn new() -> Self {
Self {
environments: DashMap::new(),
active: std::sync::RwLock::new(None),
}
}
pub fn register(&self, env: Environment) -> Result<(), EnvironmentError> {
if env.governance.risk_threshold < 0.0 || env.governance.risk_threshold > 1.0 {
return Err(EnvironmentError::InvalidRiskThreshold {
value: env.governance.risk_threshold,
});
}
if self.environments.contains_key(&env.id) {
return Err(EnvironmentError::AlreadyExists { id: env.id });
}
debug!(id = %env.id, name = %env.name, class = %env.class, "registering environment");
self.environments.insert(env.id.clone(), env);
Ok(())
}
pub fn set_active(&self, id: &str) -> Result<(), EnvironmentError> {
if !self.environments.contains_key(id) {
return Err(EnvironmentError::NotFound { id: id.to_owned() });
}
let mut active = self.active.write().unwrap();
*active = Some(id.to_owned());
Ok(())
}
pub fn active_id(&self) -> Option<EnvironmentId> {
self.active.read().unwrap().clone()
}
pub fn active(&self) -> Option<Environment> {
let id = self.active_id()?;
self.environments.get(&id).map(|e| e.value().clone())
}
pub fn get(&self, id: &str) -> Option<Environment> {
self.environments.get(id).map(|e| e.value().clone())
}
pub fn list(&self) -> Vec<(EnvironmentId, EnvironmentClass, bool)> {
let active_id = self.active_id();
self.environments
.iter()
.map(|e| {
let is_active = active_id.as_deref() == Some(e.key().as_str());
(e.key().clone(), e.class.clone(), is_active)
})
.collect()
}
pub fn remove(&self, id: &str) -> Result<Environment, EnvironmentError> {
if self.active_id().as_deref() == Some(id) {
let mut active = self.active.write().unwrap();
*active = None;
}
self.environments
.remove(id)
.map(|(_, env)| env)
.ok_or_else(|| EnvironmentError::NotFound { id: id.to_owned() })
}
pub fn len(&self) -> usize {
self.environments.len()
}
pub fn is_empty(&self) -> bool {
self.environments.is_empty()
}
pub fn create_standard_set(&self) -> Result<(), EnvironmentError> {
self.register(Environment {
id: "dev".into(),
name: "Development".into(),
class: EnvironmentClass::Development,
governance: GovernanceScope {
risk_threshold: 0.9,
human_approval_required: false,
active_branches: GovernanceBranches {
legislative: true,
executive: true,
judicial: false,
},
audit_level: AuditLevel::SessionSummary,
learning_mode: LearningMode::Explore,
max_effect_magnitude: 2.0,
},
labels: HashMap::new(),
})?;
self.register(Environment {
id: "staging".into(),
name: "Staging".into(),
class: EnvironmentClass::Staging,
governance: GovernanceScope {
risk_threshold: 0.6,
human_approval_required: false,
active_branches: GovernanceBranches {
legislative: true,
executive: true,
judicial: true,
},
audit_level: AuditLevel::PerAction,
learning_mode: LearningMode::Validate,
max_effect_magnitude: 1.0,
},
labels: HashMap::new(),
})?;
self.register(Environment {
id: "prod".into(),
name: "Production".into(),
class: EnvironmentClass::Production,
governance: GovernanceScope {
risk_threshold: 0.3,
human_approval_required: true,
active_branches: GovernanceBranches {
legislative: true,
executive: true,
judicial: true,
},
audit_level: AuditLevel::PerActionWithEffects,
learning_mode: LearningMode::Exploit,
max_effect_magnitude: 0.5,
},
labels: HashMap::new(),
})?;
Ok(())
}
}
impl Default for EnvironmentManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_dev_env() -> Environment {
Environment {
id: "dev".into(),
name: "Development".into(),
class: EnvironmentClass::Development,
governance: GovernanceScope {
risk_threshold: 0.9,
..Default::default()
},
labels: HashMap::new(),
}
}
#[test]
fn environment_class_risk_threshold() {
assert!((EnvironmentClass::Development.risk_threshold() - 0.9).abs() < f64::EPSILON);
assert!((EnvironmentClass::Staging.risk_threshold() - 0.6).abs() < f64::EPSILON);
assert!((EnvironmentClass::Production.risk_threshold() - 0.3).abs() < f64::EPSILON);
let custom = EnvironmentClass::Custom {
name: "test".into(),
risk_threshold: 0.75,
};
assert!((custom.risk_threshold() - 0.75).abs() < f64::EPSILON);
}
#[test]
fn environment_class_display() {
assert_eq!(EnvironmentClass::Development.to_string(), "development");
assert_eq!(EnvironmentClass::Production.to_string(), "production");
assert_eq!(
EnvironmentClass::Custom {
name: "qa".into(),
risk_threshold: 0.5
}
.to_string(),
"custom(qa)"
);
}
#[test]
fn governance_scope_default() {
let scope = GovernanceScope::default();
assert!((scope.risk_threshold - 0.6).abs() < f64::EPSILON);
assert!(!scope.human_approval_required);
assert!(scope.active_branches.legislative);
assert!(scope.active_branches.executive);
assert!(!scope.active_branches.judicial);
}
#[test]
fn governance_scope_serde_roundtrip() {
let scope = GovernanceScope {
risk_threshold: 0.3,
human_approval_required: true,
active_branches: GovernanceBranches {
legislative: true,
executive: true,
judicial: true,
},
audit_level: AuditLevel::PerActionWithEffects,
learning_mode: LearningMode::Exploit,
max_effect_magnitude: 0.5,
};
let json = serde_json::to_string(&scope).unwrap();
let restored: GovernanceScope = serde_json::from_str(&json).unwrap();
assert!((restored.risk_threshold - 0.3).abs() < f64::EPSILON);
assert!(restored.human_approval_required);
assert_eq!(restored.learning_mode, LearningMode::Exploit);
}
#[test]
fn register_and_list() {
let mgr = EnvironmentManager::new();
mgr.register(make_dev_env()).unwrap();
let list = mgr.list();
assert_eq!(list.len(), 1);
}
#[test]
fn register_duplicate_fails() {
let mgr = EnvironmentManager::new();
mgr.register(make_dev_env()).unwrap();
assert!(matches!(
mgr.register(make_dev_env()),
Err(EnvironmentError::AlreadyExists { .. })
));
}
#[test]
fn invalid_risk_threshold() {
let mgr = EnvironmentManager::new();
let mut env = make_dev_env();
env.governance.risk_threshold = 1.5;
assert!(matches!(
mgr.register(env),
Err(EnvironmentError::InvalidRiskThreshold { .. })
));
}
#[test]
fn set_active() {
let mgr = EnvironmentManager::new();
mgr.register(make_dev_env()).unwrap();
mgr.set_active("dev").unwrap();
assert_eq!(mgr.active_id().as_deref(), Some("dev"));
let active = mgr.active().unwrap();
assert_eq!(active.name, "Development");
}
#[test]
fn set_active_nonexistent_fails() {
let mgr = EnvironmentManager::new();
assert!(matches!(
mgr.set_active("nope"),
Err(EnvironmentError::NotFound { .. })
));
}
#[test]
fn remove_environment() {
let mgr = EnvironmentManager::new();
mgr.register(make_dev_env()).unwrap();
let removed = mgr.remove("dev").unwrap();
assert_eq!(removed.name, "Development");
assert!(mgr.is_empty());
}
#[test]
fn remove_active_clears_active() {
let mgr = EnvironmentManager::new();
mgr.register(make_dev_env()).unwrap();
mgr.set_active("dev").unwrap();
mgr.remove("dev").unwrap();
assert!(mgr.active_id().is_none());
}
#[test]
fn create_standard_set() {
let mgr = EnvironmentManager::new();
mgr.create_standard_set().unwrap();
assert_eq!(mgr.len(), 3);
let dev = mgr.get("dev").unwrap();
assert!((dev.governance.risk_threshold - 0.9).abs() < f64::EPSILON);
assert_eq!(dev.governance.learning_mode, LearningMode::Explore);
let prod = mgr.get("prod").unwrap();
assert!((prod.governance.risk_threshold - 0.3).abs() < f64::EPSILON);
assert!(prod.governance.human_approval_required);
assert_eq!(prod.governance.learning_mode, LearningMode::Exploit);
}
#[test]
fn environment_serde_roundtrip() {
let env = make_dev_env();
let json = serde_json::to_string(&env).unwrap();
let restored: Environment = serde_json::from_str(&json).unwrap();
assert_eq!(restored.id, "dev");
assert_eq!(restored.class, EnvironmentClass::Development);
}
#[test]
fn environment_error_display() {
let err = EnvironmentError::NotFound { id: "prod".into() };
assert!(err.to_string().contains("prod"));
let err = EnvironmentError::InvalidRiskThreshold { value: 1.5 };
assert!(err.to_string().contains("1.5"));
}
#[test]
fn audit_level_default() {
assert_eq!(AuditLevel::default(), AuditLevel::SessionSummary);
}
#[test]
fn learning_mode_default() {
assert_eq!(LearningMode::default(), LearningMode::Explore);
}
}