use tatara_core::domain::convergence_state::*;
use tatara_core::domain::emission::*;
pub struct EmissionEvaluator;
impl EmissionEvaluator {
pub fn evaluate_triggers(
schema: &EmissionSchema,
state: &ConvergenceState,
) -> Vec<InstantiationDecision> {
schema
.triggers
.iter()
.filter_map(|trigger| {
let fires = Self::check_condition(&trigger.condition, state);
if !fires {
return None;
}
let limit = schema
.concurrency_limits
.get(&trigger.template_name)
.copied()
.unwrap_or(usize::MAX);
if limit == 0 {
return Some(InstantiationDecision::Defer {
reason: format!("concurrency limit reached for {}", trigger.template_name),
});
}
let template_exists = schema
.templates
.iter()
.any(|t| t.name == trigger.template_name);
if !template_exists {
return Some(InstantiationDecision::Escalate {
reason: format!(
"no template '{}' in emission catalog — schema gap",
trigger.template_name
),
});
}
Some(InstantiationDecision::Instantiate {
template_name: trigger.template_name.clone(),
params: std::collections::HashMap::new(),
})
})
.collect()
}
fn check_condition(condition: &TriggerCondition, state: &ConvergenceState) -> bool {
match condition {
TriggerCondition::Threshold { value, .. } => {
state.distance.numeric() >= *value
}
TriggerCondition::Event { .. } => {
false
}
TriggerCondition::Schedule { .. } => {
false
}
TriggerCondition::Manual => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn make_schema() -> EmissionSchema {
EmissionSchema {
templates: vec![
BoundedPointTemplate {
name: "migration".into(),
point_type: ConvergencePointType::Transform,
substrate: SubstrateType::Compute,
description: "Migrate workload".into(),
preconditions: vec![],
postconditions: vec![],
},
BoundedPointTemplate {
name: "scaling".into(),
point_type: ConvergencePointType::Fork,
substrate: SubstrateType::Compute,
description: "Scale workload".into(),
preconditions: vec![],
postconditions: vec![],
},
],
triggers: vec![
EmissionTrigger {
template_name: "migration".into(),
condition: TriggerCondition::Threshold {
metric: "cost".into(),
value: 0.5,
},
},
EmissionTrigger {
template_name: "scaling".into(),
condition: TriggerCondition::Threshold {
metric: "utilization".into(),
value: 0.8,
},
},
],
concurrency_limits: HashMap::from([("migration".into(), 2), ("scaling".into(), 1)]),
}
}
#[test]
fn test_threshold_trigger_fires() {
let schema = make_schema();
let mut state = ConvergenceState::new("test");
state.distance = ConvergenceDistance::Diverged {
reason: "cost too high".into(),
};
let decisions = EmissionEvaluator::evaluate_triggers(&schema, &state);
assert_eq!(decisions.len(), 2);
assert!(matches!(
&decisions[0],
InstantiationDecision::Instantiate { template_name, .. } if template_name == "migration"
));
}
#[test]
fn test_threshold_below_does_not_fire() {
let schema = make_schema();
let state = ConvergenceState {
distance: ConvergenceDistance::Partial {
matching: 9,
total: 10,
pending: vec![],
}, ..ConvergenceState::new("test")
};
let decisions = EmissionEvaluator::evaluate_triggers(&schema, &state);
assert!(decisions.is_empty());
}
#[test]
fn test_missing_template_escalates() {
let schema = EmissionSchema {
templates: vec![], triggers: vec![EmissionTrigger {
template_name: "nonexistent".into(),
condition: TriggerCondition::Threshold {
metric: "x".into(),
value: 0.0,
},
}],
concurrency_limits: HashMap::new(),
};
let mut state = ConvergenceState::new("test");
state.distance = ConvergenceDistance::Diverged {
reason: "diverged".into(),
};
let decisions = EmissionEvaluator::evaluate_triggers(&schema, &state);
assert_eq!(decisions.len(), 1);
assert!(matches!(
&decisions[0],
InstantiationDecision::Escalate { .. }
));
}
#[test]
fn test_zero_concurrency_defers() {
let schema = EmissionSchema {
templates: vec![BoundedPointTemplate {
name: "blocked".into(),
point_type: ConvergencePointType::Transform,
substrate: SubstrateType::Compute,
description: "blocked".into(),
preconditions: vec![],
postconditions: vec![],
}],
triggers: vec![EmissionTrigger {
template_name: "blocked".into(),
condition: TriggerCondition::Threshold {
metric: "x".into(),
value: 0.0,
},
}],
concurrency_limits: HashMap::from([("blocked".into(), 0)]),
};
let mut state = ConvergenceState::new("test");
state.distance = ConvergenceDistance::Diverged {
reason: "diverged".into(),
};
let decisions = EmissionEvaluator::evaluate_triggers(&schema, &state);
assert_eq!(decisions.len(), 1);
assert!(matches!(&decisions[0], InstantiationDecision::Defer { .. }));
}
#[test]
fn test_converged_state_no_triggers() {
let schema = make_schema();
let state = ConvergenceState {
distance: ConvergenceDistance::Converged, ..ConvergenceState::new("test")
};
let decisions = EmissionEvaluator::evaluate_triggers(&schema, &state);
assert!(decisions.is_empty());
}
#[test]
fn test_manual_trigger_never_fires() {
let schema = EmissionSchema {
templates: vec![BoundedPointTemplate {
name: "manual_op".into(),
point_type: ConvergencePointType::Gate,
substrate: SubstrateType::Regulatory,
description: "manual".into(),
preconditions: vec![],
postconditions: vec![],
}],
triggers: vec![EmissionTrigger {
template_name: "manual_op".into(),
condition: TriggerCondition::Manual,
}],
concurrency_limits: HashMap::new(),
};
let mut state = ConvergenceState::new("test");
state.distance = ConvergenceDistance::Diverged {
reason: "diverged".into(),
};
let decisions = EmissionEvaluator::evaluate_triggers(&schema, &state);
assert!(decisions.is_empty());
}
}