1use std::collections::HashMap;
17
18use dashmap::DashMap;
19use serde::{Deserialize, Serialize};
20use tracing::debug;
21
22pub type EnvironmentId = String;
24
25#[non_exhaustive]
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub enum EnvironmentClass {
29 Development,
32
33 Staging,
36
37 Production,
40
41 Custom {
43 name: String,
45 risk_threshold: f64,
47 },
48}
49
50impl EnvironmentClass {
51 pub fn risk_threshold(&self) -> f64 {
53 match self {
54 EnvironmentClass::Development => 0.9,
55 EnvironmentClass::Staging => 0.6,
56 EnvironmentClass::Production => 0.3,
57 EnvironmentClass::Custom { risk_threshold, .. } => *risk_threshold,
58 }
59 }
60}
61
62impl std::fmt::Display for EnvironmentClass {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 EnvironmentClass::Development => write!(f, "development"),
66 EnvironmentClass::Staging => write!(f, "staging"),
67 EnvironmentClass::Production => write!(f, "production"),
68 EnvironmentClass::Custom { name, .. } => write!(f, "custom({name})"),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct GovernanceScope {
76 #[serde(default = "default_risk_threshold")]
78 pub risk_threshold: f64,
79
80 #[serde(default)]
82 pub human_approval_required: bool,
83
84 #[serde(default)]
86 pub active_branches: GovernanceBranches,
87
88 #[serde(default)]
90 pub audit_level: AuditLevel,
91
92 #[serde(default)]
94 pub learning_mode: LearningMode,
95
96 #[serde(default = "default_max_effect")]
98 pub max_effect_magnitude: f64,
99}
100
101fn default_risk_threshold() -> f64 {
102 0.6
103}
104
105fn default_max_effect() -> f64 {
106 1.0
107}
108
109impl Default for GovernanceScope {
110 fn default() -> Self {
111 Self {
112 risk_threshold: default_risk_threshold(),
113 human_approval_required: false,
114 active_branches: GovernanceBranches::default(),
115 audit_level: AuditLevel::default(),
116 learning_mode: LearningMode::default(),
117 max_effect_magnitude: default_max_effect(),
118 }
119 }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct GovernanceBranches {
125 #[serde(default = "default_true")]
127 pub legislative: bool,
128
129 #[serde(default = "default_true")]
131 pub executive: bool,
132
133 #[serde(default)]
135 pub judicial: bool,
136}
137
138fn default_true() -> bool {
139 true
140}
141
142impl Default for GovernanceBranches {
143 fn default() -> Self {
144 Self {
145 legislative: true,
146 executive: true,
147 judicial: false,
148 }
149 }
150}
151
152#[non_exhaustive]
154#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
155pub enum AuditLevel {
156 #[default]
158 SessionSummary,
159 PerAction,
161 PerActionWithEffects,
163}
164
165#[non_exhaustive]
167#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
168pub enum LearningMode {
169 #[default]
172 Explore,
173 Validate,
176 Exploit,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct Environment {
184 pub id: EnvironmentId,
186
187 pub name: String,
189
190 pub class: EnvironmentClass,
192
193 #[serde(default)]
195 pub governance: GovernanceScope,
196
197 #[serde(default)]
199 pub labels: HashMap<String, String>,
200}
201
202#[non_exhaustive]
204#[derive(Debug, thiserror::Error)]
205pub enum EnvironmentError {
206 #[error("environment already exists: '{id}'")]
208 AlreadyExists {
209 id: EnvironmentId,
211 },
212
213 #[error("environment not found: '{id}'")]
215 NotFound {
216 id: EnvironmentId,
218 },
219
220 #[error("invalid risk threshold {value}: must be between 0.0 and 1.0")]
222 InvalidRiskThreshold {
223 value: f64,
225 },
226}
227
228pub struct EnvironmentManager {
234 environments: DashMap<EnvironmentId, Environment>,
235 active: std::sync::RwLock<Option<EnvironmentId>>,
236}
237
238impl EnvironmentManager {
239 pub fn new() -> Self {
241 Self {
242 environments: DashMap::new(),
243 active: std::sync::RwLock::new(None),
244 }
245 }
246
247 pub fn register(&self, env: Environment) -> Result<(), EnvironmentError> {
249 if env.governance.risk_threshold < 0.0 || env.governance.risk_threshold > 1.0 {
250 return Err(EnvironmentError::InvalidRiskThreshold {
251 value: env.governance.risk_threshold,
252 });
253 }
254
255 if self.environments.contains_key(&env.id) {
256 return Err(EnvironmentError::AlreadyExists { id: env.id });
257 }
258
259 debug!(id = %env.id, name = %env.name, class = %env.class, "registering environment");
260 self.environments.insert(env.id.clone(), env);
261 Ok(())
262 }
263
264 pub fn set_active(&self, id: &str) -> Result<(), EnvironmentError> {
266 if !self.environments.contains_key(id) {
267 return Err(EnvironmentError::NotFound { id: id.to_owned() });
268 }
269 let mut active = self.active.write().unwrap();
270 *active = Some(id.to_owned());
271 Ok(())
272 }
273
274 pub fn active_id(&self) -> Option<EnvironmentId> {
276 self.active.read().unwrap().clone()
277 }
278
279 pub fn active(&self) -> Option<Environment> {
281 let id = self.active_id()?;
282 self.environments.get(&id).map(|e| e.value().clone())
283 }
284
285 pub fn get(&self, id: &str) -> Option<Environment> {
287 self.environments.get(id).map(|e| e.value().clone())
288 }
289
290 pub fn list(&self) -> Vec<(EnvironmentId, EnvironmentClass, bool)> {
292 let active_id = self.active_id();
293 self.environments
294 .iter()
295 .map(|e| {
296 let is_active = active_id.as_deref() == Some(e.key().as_str());
297 (e.key().clone(), e.class.clone(), is_active)
298 })
299 .collect()
300 }
301
302 pub fn remove(&self, id: &str) -> Result<Environment, EnvironmentError> {
304 if self.active_id().as_deref() == Some(id) {
306 let mut active = self.active.write().unwrap();
308 *active = None;
309 }
310
311 self.environments
312 .remove(id)
313 .map(|(_, env)| env)
314 .ok_or_else(|| EnvironmentError::NotFound { id: id.to_owned() })
315 }
316
317 pub fn len(&self) -> usize {
319 self.environments.len()
320 }
321
322 pub fn is_empty(&self) -> bool {
324 self.environments.is_empty()
325 }
326
327 pub fn create_standard_set(&self) -> Result<(), EnvironmentError> {
329 self.register(Environment {
330 id: "dev".into(),
331 name: "Development".into(),
332 class: EnvironmentClass::Development,
333 governance: GovernanceScope {
334 risk_threshold: 0.9,
335 human_approval_required: false,
336 active_branches: GovernanceBranches {
337 legislative: true,
338 executive: true,
339 judicial: false,
340 },
341 audit_level: AuditLevel::SessionSummary,
342 learning_mode: LearningMode::Explore,
343 max_effect_magnitude: 2.0,
344 },
345 labels: HashMap::new(),
346 })?;
347
348 self.register(Environment {
349 id: "staging".into(),
350 name: "Staging".into(),
351 class: EnvironmentClass::Staging,
352 governance: GovernanceScope {
353 risk_threshold: 0.6,
354 human_approval_required: false,
355 active_branches: GovernanceBranches {
356 legislative: true,
357 executive: true,
358 judicial: true,
359 },
360 audit_level: AuditLevel::PerAction,
361 learning_mode: LearningMode::Validate,
362 max_effect_magnitude: 1.0,
363 },
364 labels: HashMap::new(),
365 })?;
366
367 self.register(Environment {
368 id: "prod".into(),
369 name: "Production".into(),
370 class: EnvironmentClass::Production,
371 governance: GovernanceScope {
372 risk_threshold: 0.3,
373 human_approval_required: true,
374 active_branches: GovernanceBranches {
375 legislative: true,
376 executive: true,
377 judicial: true,
378 },
379 audit_level: AuditLevel::PerActionWithEffects,
380 learning_mode: LearningMode::Exploit,
381 max_effect_magnitude: 0.5,
382 },
383 labels: HashMap::new(),
384 })?;
385
386 Ok(())
387 }
388}
389
390impl Default for EnvironmentManager {
391 fn default() -> Self {
392 Self::new()
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 fn make_dev_env() -> Environment {
401 Environment {
402 id: "dev".into(),
403 name: "Development".into(),
404 class: EnvironmentClass::Development,
405 governance: GovernanceScope {
406 risk_threshold: 0.9,
407 ..Default::default()
408 },
409 labels: HashMap::new(),
410 }
411 }
412
413 #[test]
414 fn environment_class_risk_threshold() {
415 assert!((EnvironmentClass::Development.risk_threshold() - 0.9).abs() < f64::EPSILON);
416 assert!((EnvironmentClass::Staging.risk_threshold() - 0.6).abs() < f64::EPSILON);
417 assert!((EnvironmentClass::Production.risk_threshold() - 0.3).abs() < f64::EPSILON);
418 let custom = EnvironmentClass::Custom {
419 name: "test".into(),
420 risk_threshold: 0.75,
421 };
422 assert!((custom.risk_threshold() - 0.75).abs() < f64::EPSILON);
423 }
424
425 #[test]
426 fn environment_class_display() {
427 assert_eq!(EnvironmentClass::Development.to_string(), "development");
428 assert_eq!(EnvironmentClass::Production.to_string(), "production");
429 assert_eq!(
430 EnvironmentClass::Custom {
431 name: "qa".into(),
432 risk_threshold: 0.5
433 }
434 .to_string(),
435 "custom(qa)"
436 );
437 }
438
439 #[test]
440 fn governance_scope_default() {
441 let scope = GovernanceScope::default();
442 assert!((scope.risk_threshold - 0.6).abs() < f64::EPSILON);
443 assert!(!scope.human_approval_required);
444 assert!(scope.active_branches.legislative);
445 assert!(scope.active_branches.executive);
446 assert!(!scope.active_branches.judicial);
447 }
448
449 #[test]
450 fn governance_scope_serde_roundtrip() {
451 let scope = GovernanceScope {
452 risk_threshold: 0.3,
453 human_approval_required: true,
454 active_branches: GovernanceBranches {
455 legislative: true,
456 executive: true,
457 judicial: true,
458 },
459 audit_level: AuditLevel::PerActionWithEffects,
460 learning_mode: LearningMode::Exploit,
461 max_effect_magnitude: 0.5,
462 };
463 let json = serde_json::to_string(&scope).unwrap();
464 let restored: GovernanceScope = serde_json::from_str(&json).unwrap();
465 assert!((restored.risk_threshold - 0.3).abs() < f64::EPSILON);
466 assert!(restored.human_approval_required);
467 assert_eq!(restored.learning_mode, LearningMode::Exploit);
468 }
469
470 #[test]
471 fn register_and_list() {
472 let mgr = EnvironmentManager::new();
473 mgr.register(make_dev_env()).unwrap();
474 let list = mgr.list();
475 assert_eq!(list.len(), 1);
476 }
477
478 #[test]
479 fn register_duplicate_fails() {
480 let mgr = EnvironmentManager::new();
481 mgr.register(make_dev_env()).unwrap();
482 assert!(matches!(
483 mgr.register(make_dev_env()),
484 Err(EnvironmentError::AlreadyExists { .. })
485 ));
486 }
487
488 #[test]
489 fn invalid_risk_threshold() {
490 let mgr = EnvironmentManager::new();
491 let mut env = make_dev_env();
492 env.governance.risk_threshold = 1.5;
493 assert!(matches!(
494 mgr.register(env),
495 Err(EnvironmentError::InvalidRiskThreshold { .. })
496 ));
497 }
498
499 #[test]
500 fn set_active() {
501 let mgr = EnvironmentManager::new();
502 mgr.register(make_dev_env()).unwrap();
503 mgr.set_active("dev").unwrap();
504 assert_eq!(mgr.active_id().as_deref(), Some("dev"));
505 let active = mgr.active().unwrap();
506 assert_eq!(active.name, "Development");
507 }
508
509 #[test]
510 fn set_active_nonexistent_fails() {
511 let mgr = EnvironmentManager::new();
512 assert!(matches!(
513 mgr.set_active("nope"),
514 Err(EnvironmentError::NotFound { .. })
515 ));
516 }
517
518 #[test]
519 fn remove_environment() {
520 let mgr = EnvironmentManager::new();
521 mgr.register(make_dev_env()).unwrap();
522 let removed = mgr.remove("dev").unwrap();
523 assert_eq!(removed.name, "Development");
524 assert!(mgr.is_empty());
525 }
526
527 #[test]
528 fn remove_active_clears_active() {
529 let mgr = EnvironmentManager::new();
530 mgr.register(make_dev_env()).unwrap();
531 mgr.set_active("dev").unwrap();
532 mgr.remove("dev").unwrap();
533 assert!(mgr.active_id().is_none());
534 }
535
536 #[test]
537 fn create_standard_set() {
538 let mgr = EnvironmentManager::new();
539 mgr.create_standard_set().unwrap();
540 assert_eq!(mgr.len(), 3);
541
542 let dev = mgr.get("dev").unwrap();
543 assert!((dev.governance.risk_threshold - 0.9).abs() < f64::EPSILON);
544 assert_eq!(dev.governance.learning_mode, LearningMode::Explore);
545
546 let prod = mgr.get("prod").unwrap();
547 assert!((prod.governance.risk_threshold - 0.3).abs() < f64::EPSILON);
548 assert!(prod.governance.human_approval_required);
549 assert_eq!(prod.governance.learning_mode, LearningMode::Exploit);
550 }
551
552 #[test]
553 fn environment_serde_roundtrip() {
554 let env = make_dev_env();
555 let json = serde_json::to_string(&env).unwrap();
556 let restored: Environment = serde_json::from_str(&json).unwrap();
557 assert_eq!(restored.id, "dev");
558 assert_eq!(restored.class, EnvironmentClass::Development);
559 }
560
561 #[test]
562 fn environment_error_display() {
563 let err = EnvironmentError::NotFound { id: "prod".into() };
564 assert!(err.to_string().contains("prod"));
565
566 let err = EnvironmentError::InvalidRiskThreshold { value: 1.5 };
567 assert!(err.to_string().contains("1.5"));
568 }
569
570 #[test]
571 fn audit_level_default() {
572 assert_eq!(AuditLevel::default(), AuditLevel::SessionSummary);
573 }
574
575 #[test]
576 fn learning_mode_default() {
577 assert_eq!(LearningMode::default(), LearningMode::Explore);
578 }
579}