use crate::{DimensionResult, Sample, SimulationDimension};
#[derive(Debug, Clone)]
pub struct OperationalSimulatorConfig {
pub max_team_utilization: f64,
pub max_system_load: f64,
pub confidence_threshold: f64,
}
impl Default for OperationalSimulatorConfig {
fn default() -> Self {
Self {
max_team_utilization: 0.85,
max_system_load: 0.80,
confidence_threshold: 0.5,
}
}
}
pub struct OperationalSimulator {
config: OperationalSimulatorConfig,
}
impl OperationalSimulator {
#[must_use]
pub fn new(config: OperationalSimulatorConfig) -> Self {
Self { config }
}
fn extract_team_utilization(plan: &serde_json::Value) -> Option<f64> {
plan.get("annotation")
.and_then(|a| a.get("team_utilization"))
.and_then(serde_json::Value::as_f64)
}
fn extract_system_load(plan: &serde_json::Value) -> Option<f64> {
plan.get("annotation")
.and_then(|a| a.get("system_load"))
.and_then(serde_json::Value::as_f64)
}
fn extract_dependencies(plan: &serde_json::Value) -> Vec<Dependency> {
plan.get("annotation")
.and_then(|a| a.get("dependencies"))
.and_then(|d| d.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| {
Some(Dependency {
name: v.get("name").and_then(|n| n.as_str())?.to_string(),
available: v
.get("available")
.and_then(serde_json::Value::as_bool)
.unwrap_or(true),
lead_time_days: v
.get("lead_time_days")
.and_then(serde_json::Value::as_u64)
.map(|n| u32::try_from(n).unwrap_or(0)),
})
})
.collect()
})
.unwrap_or_default()
}
fn extract_timeline_days(plan: &serde_json::Value) -> Option<u32> {
plan.get("annotation")
.and_then(|a| a.get("timeline_days"))
.and_then(serde_json::Value::as_u64)
.map(|n| u32::try_from(n).unwrap_or(0))
}
fn sample(confidence: f64) -> Vec<Sample> {
let buckets = 5;
let mut samples = Vec::with_capacity(buckets);
for i in 0..buckets {
let bucket_center = (f64::from(u32::try_from(i).unwrap_or(0)) + 0.5)
/ f64::from(u32::try_from(buckets).unwrap_or(5));
let distance = (bucket_center - confidence).abs();
let weight = (-distance * 4.0).exp();
samples.push(Sample {
value: bucket_center,
probability: weight,
});
}
let total: f64 = samples.iter().map(|s| s.probability).sum();
if total > 0.0 {
for s in &mut samples {
s.probability /= total;
}
}
samples
}
pub fn simulate(&self, plan: &serde_json::Value) -> DimensionResult {
let team_util = Self::extract_team_utilization(plan);
let system_load = Self::extract_system_load(plan);
let deps = Self::extract_dependencies(plan);
let timeline = Self::extract_timeline_days(plan);
let mut findings = Vec::new();
let mut penalties = 0.0_f64;
match team_util {
Some(util) if util > self.config.max_team_utilization => {
findings.push(format!(
"team overloaded: {:.0}% utilization exceeds {:.0}% cap",
util * 100.0,
self.config.max_team_utilization * 100.0,
));
penalties += (util - self.config.max_team_utilization) * 2.0;
}
Some(util) => {
findings.push(format!(
"team utilization {:.0}% within capacity",
util * 100.0
));
}
None => {
findings.push("no team utilization declared".into());
}
}
match system_load {
Some(load) if load > self.config.max_system_load => {
findings.push(format!(
"system overloaded: {:.0}% load exceeds {:.0}% cap",
load * 100.0,
self.config.max_system_load * 100.0,
));
penalties += (load - self.config.max_system_load) * 2.0;
}
Some(load) => {
findings.push(format!("system load {:.0}% within capacity", load * 100.0));
}
None => {
findings.push("no system load declared".into());
}
}
let unavailable: Vec<&Dependency> = deps.iter().filter(|d| !d.available).collect();
if !unavailable.is_empty() {
for dep in &unavailable {
findings.push(format!("dependency unavailable: {}", dep.name));
}
penalties += 0.3 * f64::from(u32::try_from(unavailable.len()).unwrap_or(1));
}
if let Some(days) = timeline {
let max_lead = deps
.iter()
.filter_map(|d| d.lead_time_days)
.max()
.unwrap_or(0);
if max_lead > days {
findings.push(format!(
"timeline {days} days but dependency needs {max_lead} days lead time",
));
penalties += 0.2;
}
}
let confidence = (1.0 - penalties).clamp(0.0, 1.0);
let passed = confidence >= self.config.confidence_threshold;
let samples = Self::sample(confidence);
if !passed {
findings.push(format!(
"below threshold: {confidence:.2} < {:.2}",
self.config.confidence_threshold,
));
}
DimensionResult {
dimension: SimulationDimension::Operational,
passed,
confidence,
findings,
samples,
}
}
}
struct Dependency {
name: String,
available: bool,
lead_time_days: Option<u32>,
}
use crate::types::{SimulationRecommendation, SimulationVerdict};
use converge_pack::{AgentEffect, Context, ContextKey, ProposedFact, Suggestor};
pub struct OperationalSimulationAgent {
simulator: OperationalSimulator,
}
impl OperationalSimulationAgent {
#[must_use]
pub fn new(config: OperationalSimulatorConfig) -> Self {
Self {
simulator: OperationalSimulator::new(config),
}
}
#[must_use]
pub fn default_config() -> Self {
Self {
simulator: OperationalSimulator::new(OperationalSimulatorConfig::default()),
}
}
}
#[async_trait::async_trait]
#[allow(clippy::unnecessary_literal_bound)]
impl Suggestor for OperationalSimulationAgent {
fn name(&self) -> &'static str {
"operational-simulation"
}
fn dependencies(&self) -> &[ContextKey] {
&[ContextKey::Strategies]
}
fn accepts(&self, ctx: &dyn Context) -> bool {
ctx.has(ContextKey::Strategies) && !ctx.has(ContextKey::Evaluations)
}
async fn execute(&self, ctx: &dyn Context) -> AgentEffect {
let strategies = ctx.get(ContextKey::Strategies);
let mut proposals = Vec::new();
for fact in strategies {
let plan_json: serde_json::Value = serde_json::from_str(&fact.content)
.unwrap_or_else(|_| serde_json::json!({"description": fact.content}));
let result = self.simulator.simulate(&plan_json);
let verdict = SimulationVerdict {
strategy_id: fact.id.clone(),
dimension: crate::SimulationDimension::Operational,
passed: result.passed,
confidence: result.confidence,
findings: result.findings,
recommendation: if result.passed {
None
} else {
Some(SimulationRecommendation::DoNotProceed)
},
};
let key = if result.passed {
ContextKey::Evaluations
} else {
ContextKey::Constraints
};
proposals.push(ProposedFact::new(
key,
verdict.fact_id(),
verdict.to_json(),
"operational-simulation",
));
}
AgentEffect::with_proposals(proposals)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn default_simulator() -> OperationalSimulator {
OperationalSimulator::new(OperationalSimulatorConfig::default())
}
#[test]
fn within_capacity_passes() {
let sim = default_simulator();
let plan = json!({
"annotation": {
"team_utilization": 0.7,
"system_load": 0.6
}
});
let result = sim.simulate(&plan);
assert_eq!(result.dimension, SimulationDimension::Operational);
assert!(result.passed);
}
#[test]
fn team_overload_penalized() {
let sim = default_simulator();
let plan = json!({
"annotation": {
"team_utilization": 0.95,
"system_load": 0.5
}
});
let result = sim.simulate(&plan);
assert!(result.findings.iter().any(|f| f.contains("overloaded")));
assert!(result.confidence < 1.0);
}
#[test]
fn system_overload_penalized() {
let sim = default_simulator();
let plan = json!({
"annotation": {
"team_utilization": 0.5,
"system_load": 0.95
}
});
let result = sim.simulate(&plan);
assert!(
result
.findings
.iter()
.any(|f| f.contains("system overloaded"))
);
}
#[test]
fn unavailable_dependency_penalized() {
let sim = default_simulator();
let plan = json!({
"annotation": {
"team_utilization": 0.5,
"system_load": 0.5,
"dependencies": [
{"name": "payment-api", "available": false},
{"name": "auth-service", "available": true}
]
}
});
let result = sim.simulate(&plan);
assert!(result.findings.iter().any(|f| f.contains("unavailable")));
}
#[test]
fn timeline_vs_lead_time() {
let sim = default_simulator();
let plan = json!({
"annotation": {
"team_utilization": 0.5,
"system_load": 0.5,
"timeline_days": 14,
"dependencies": [
{"name": "vendor-api", "available": true, "lead_time_days": 30}
]
}
});
let result = sim.simulate(&plan);
assert!(result.findings.iter().any(|f| f.contains("lead time")));
}
#[test]
fn no_annotations_passes() {
let sim = default_simulator();
let plan = json!({});
let result = sim.simulate(&plan);
assert!(result.passed);
}
#[test]
fn extreme_overload_fails() {
let sim = default_simulator();
let plan = json!({
"annotation": {
"team_utilization": 1.0,
"system_load": 1.0,
"dependencies": [
{"name": "a", "available": false},
{"name": "b", "available": false}
]
}
});
let result = sim.simulate(&plan);
assert!(!result.passed);
}
}