use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ConvergenceDistance {
Converged,
Partial {
matching: u32,
total: u32,
pending: Vec<String>,
},
Diverged {
reason: String,
},
Unknown,
}
impl ConvergenceDistance {
pub fn is_converged(&self) -> bool {
matches!(self, Self::Converged)
}
pub fn numeric(&self) -> f64 {
match self {
Self::Converged => 0.0,
Self::Partial {
matching, total, ..
} => {
if *total == 0 {
0.0
} else {
1.0 - (*matching as f64 / *total as f64)
}
}
Self::Diverged { .. } => 1.0,
Self::Unknown => 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConvergenceState {
pub entity_id: String,
pub distance: ConvergenceDistance,
pub rate: f64,
pub oscillating: bool,
pub ticks: u64,
pub last_converged_at: Option<DateTime<Utc>>,
pub time_in_current_state: Duration,
pub damping: f64,
pub recent_phase_changes: u32,
}
impl Default for ConvergenceState {
fn default() -> Self {
Self {
entity_id: String::new(),
distance: ConvergenceDistance::Unknown,
rate: 0.0,
oscillating: false,
ticks: 0,
last_converged_at: None,
time_in_current_state: Duration::zero(),
damping: 1.0,
recent_phase_changes: 0,
}
}
}
impl ConvergenceState {
pub fn new(entity_id: impl Into<String>) -> Self {
Self {
entity_id: entity_id.into(),
..Default::default()
}
}
pub fn update(&mut self, new_distance: ConvergenceDistance, tick_duration_ms: u64) {
let old_numeric = self.distance.numeric();
let new_numeric = new_distance.numeric();
if tick_duration_ms > 0 {
self.rate = (new_numeric - old_numeric) / (tick_duration_ms as f64 / 1000.0);
}
let was_converging = old_numeric > new_numeric;
let direction_changed = (self.rate > 0.0) != (old_numeric > new_numeric);
if direction_changed && self.ticks > 2 {
self.recent_phase_changes += 1;
}
self.oscillating = self.recent_phase_changes > 3;
if self.oscillating {
self.damping = (self.damping * 1.5).min(32.0); } else if self.damping > 1.0 {
self.damping = (self.damping * 0.9).max(1.0); }
if new_distance.is_converged() && !self.distance.is_converged() {
self.last_converged_at = Some(Utc::now());
}
if std::mem::discriminant(&self.distance) != std::mem::discriminant(&new_distance) {
self.time_in_current_state = Duration::zero();
} else {
self.time_in_current_state =
self.time_in_current_state + Duration::milliseconds(tick_duration_ms as i64);
}
self.distance = new_distance;
self.ticks += 1;
}
pub fn should_wait(&self) -> bool {
self.oscillating && self.damping > 2.0
}
pub fn time_since_converged(&self) -> Option<Duration> {
self.last_converged_at.map(|t| Utc::now() - t)
}
}
pub use super::classification::{
AiInterface, AiRole, CalmClassification, ComputationMode, ConvergenceHorizon,
ConvergenceMechanism, ConvergenceOutcome, ConvergencePointType, OptimizationDirection,
SubstrateType,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConvergencePoint {
pub name: String,
pub description: String,
pub monotone: bool,
pub mechanism: ConvergenceMechanism,
pub state: ConvergenceState,
pub boundary: ConvergenceBoundary,
pub point_type: ConvergencePointType,
pub horizon: ConvergenceHorizon,
pub substrate: SubstrateType,
pub computation_mode: ComputationMode,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ConvergenceBoundary {
pub preconditions: Vec<BoundaryCheck>,
pub postconditions: Vec<BoundaryCheck>,
pub input_attestation: Option<String>,
pub output_attestation: Option<String>,
pub phase: BoundaryPhase,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoundaryCheck {
pub name: String,
pub description: String,
pub passed: bool,
pub error: Option<String>,
pub checked_at: Option<DateTime<Utc>>,
}
impl BoundaryCheck {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
passed: false,
error: None,
checked_at: None,
}
}
pub fn pass(&mut self) {
self.passed = true;
self.error = None;
self.checked_at = Some(Utc::now());
}
pub fn fail(&mut self, error: impl Into<String>) {
self.passed = false;
self.error = Some(error.into());
self.checked_at = Some(Utc::now());
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "snake_case")]
pub enum BoundaryPhase {
#[default]
Pending,
Preparing,
Executing,
Verifying,
Attested,
Failed { reason: String },
}
impl BoundaryPhase {
pub fn is_attested(&self) -> bool {
matches!(self, Self::Attested)
}
pub fn is_failed(&self) -> bool {
matches!(self, Self::Failed { .. })
}
pub fn is_gate_open(&self) -> bool {
self.is_attested()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ClusterConvergence {
pub converged: u32,
pub partial: u32,
pub diverged: u32,
pub unknown: u32,
pub overall_distance: f64,
pub time_since_fully_converged: Option<Duration>,
pub entities: HashMap<String, ConvergenceState>,
}
impl ClusterConvergence {
pub fn from_entities(entities: HashMap<String, ConvergenceState>) -> Self {
let mut summary = Self::default();
let mut total_distance = 0.0;
for (_, state) in &entities {
match &state.distance {
ConvergenceDistance::Converged => summary.converged += 1,
ConvergenceDistance::Partial { .. } => summary.partial += 1,
ConvergenceDistance::Diverged { .. } => summary.diverged += 1,
ConvergenceDistance::Unknown => summary.unknown += 1,
}
total_distance += state.distance.numeric();
}
let total = entities.len().max(1) as f64;
summary.overall_distance = total_distance / total;
summary.entities = entities;
summary
}
pub fn is_fully_converged(&self) -> bool {
self.diverged == 0 && self.partial == 0 && self.unknown == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convergence_distance_numeric() {
assert_eq!(ConvergenceDistance::Converged.numeric(), 0.0);
assert_eq!(
ConvergenceDistance::Partial {
matching: 3,
total: 4,
pending: vec![]
}
.numeric(),
0.25
);
assert_eq!(
ConvergenceDistance::Diverged {
reason: "test".into()
}
.numeric(),
1.0
);
assert_eq!(ConvergenceDistance::Unknown.numeric(), 1.0);
}
#[test]
fn test_convergence_state_update() {
let mut state = ConvergenceState::new("alloc-1");
state.update(
ConvergenceDistance::Partial {
matching: 1,
total: 4,
pending: vec!["secrets".into(), "volumes".into(), "driver".into()],
},
1000,
);
assert_eq!(state.ticks, 1);
assert!(state.distance.numeric() > 0.0);
state.update(
ConvergenceDistance::Partial {
matching: 3,
total: 4,
pending: vec!["driver".into()],
},
1000,
);
assert_eq!(state.ticks, 2);
assert!(state.rate < 0.0);
state.update(ConvergenceDistance::Converged, 1000);
assert_eq!(state.ticks, 3);
assert!(state.distance.is_converged());
assert!(state.last_converged_at.is_some());
}
#[test]
fn test_oscillation_detection() {
let mut state = ConvergenceState::new("osc-1");
for i in 0..10 {
let distance = if i % 2 == 0 {
ConvergenceDistance::Partial {
matching: 2,
total: 4,
pending: vec![],
}
} else {
ConvergenceDistance::Diverged {
reason: "unstable".into(),
}
};
state.update(distance, 100);
}
assert!(state.recent_phase_changes > 3);
assert!(state.oscillating);
assert!(state.damping > 1.0);
assert!(state.should_wait());
}
#[test]
fn test_cluster_convergence() {
let mut entities = HashMap::new();
entities.insert(
"a".into(),
ConvergenceState {
distance: ConvergenceDistance::Converged,
..Default::default()
},
);
entities.insert(
"b".into(),
ConvergenceState {
distance: ConvergenceDistance::Partial {
matching: 2,
total: 4,
pending: vec![],
},
..Default::default()
},
);
entities.insert(
"c".into(),
ConvergenceState {
distance: ConvergenceDistance::Diverged {
reason: "test".into(),
},
..Default::default()
},
);
let summary = ClusterConvergence::from_entities(entities);
assert_eq!(summary.converged, 1);
assert_eq!(summary.partial, 1);
assert_eq!(summary.diverged, 1);
assert!(!summary.is_fully_converged());
assert!(summary.overall_distance > 0.0);
assert!(summary.overall_distance < 1.0);
}
#[test]
fn test_fully_converged_cluster() {
let mut entities = HashMap::new();
entities.insert(
"a".into(),
ConvergenceState {
distance: ConvergenceDistance::Converged,
..Default::default()
},
);
entities.insert(
"b".into(),
ConvergenceState {
distance: ConvergenceDistance::Converged,
..Default::default()
},
);
let summary = ClusterConvergence::from_entities(entities);
assert!(summary.is_fully_converged());
assert_eq!(summary.overall_distance, 0.0);
}
#[test]
fn test_damping_recovery() {
let mut state = ConvergenceState::new("damp-1");
state.damping = 8.0; state.oscillating = false;
state.update(ConvergenceDistance::Converged, 1000);
assert!(state.damping < 8.0);
assert!(state.damping >= 1.0);
}
#[test]
fn test_convergence_point() {
let point = ConvergencePoint {
name: "scheduling".into(),
description: "Allocations placed on eligible nodes".into(),
monotone: false,
mechanism: ConvergenceMechanism::Raft,
state: ConvergenceState::new("scheduling"),
boundary: ConvergenceBoundary::default(),
point_type: ConvergencePointType::Transform,
horizon: ConvergenceHorizon::Bounded,
substrate: SubstrateType::Compute,
computation_mode: ComputationMode::Mechanical,
};
assert!(!point.monotone); assert_eq!(point.mechanism, ConvergenceMechanism::Raft);
assert!(!point.boundary.phase.is_gate_open()); }
#[test]
fn test_calm_classification() {
assert_eq!(CalmClassification::Monotone, CalmClassification::Monotone);
assert_eq!(
CalmClassification::NonMonotone,
CalmClassification::NonMonotone
);
}
#[test]
fn test_boundary_check_pass_fail() {
let mut check = BoundaryCheck::new("secrets_resolved", "All secrets fetched");
assert!(!check.passed);
check.pass();
assert!(check.passed);
assert!(check.error.is_none());
assert!(check.checked_at.is_some());
check.fail("akeyless timeout");
assert!(!check.passed);
assert_eq!(check.error.as_deref(), Some("akeyless timeout"));
}
#[test]
fn test_boundary_phase_transitions() {
assert!(!BoundaryPhase::Pending.is_gate_open());
assert!(!BoundaryPhase::Preparing.is_gate_open());
assert!(!BoundaryPhase::Executing.is_gate_open());
assert!(!BoundaryPhase::Verifying.is_gate_open());
assert!(BoundaryPhase::Attested.is_gate_open());
assert!(BoundaryPhase::Attested.is_attested());
assert!(!BoundaryPhase::Failed {
reason: "test".into()
}
.is_gate_open());
assert!(BoundaryPhase::Failed {
reason: "test".into()
}
.is_failed());
}
#[test]
fn test_boundary_attestation_chain() {
let mut boundary_a = ConvergenceBoundary::default();
boundary_a.phase = BoundaryPhase::Attested;
boundary_a.output_attestation = Some("blake3:abc123".to_string());
let mut boundary_b = ConvergenceBoundary::default();
boundary_b.input_attestation = boundary_a.output_attestation.clone();
assert_eq!(
boundary_b.input_attestation.as_deref(),
Some("blake3:abc123")
);
assert!(boundary_a.phase.is_gate_open());
}
#[test]
fn test_convergence_point_with_boundary() {
let point = ConvergencePoint {
name: "secret_resolve".into(),
description: "Fetch secrets from Akeyless".into(),
monotone: true, mechanism: ConvergenceMechanism::Local,
state: ConvergenceState::new("secret_resolve"),
point_type: ConvergencePointType::Transform,
horizon: ConvergenceHorizon::Bounded,
substrate: SubstrateType::Security,
computation_mode: ComputationMode::Mechanical,
boundary: ConvergenceBoundary {
preconditions: vec![BoundaryCheck::new(
"port_allocated",
"Port must be allocated first",
)],
postconditions: vec![
BoundaryCheck::new("secrets_valid", "All secrets non-empty"),
BoundaryCheck::new("secrets_attested", "Secret hashes match tameshi record"),
],
input_attestation: Some("blake3:prev_point_hash".into()),
output_attestation: None, phase: BoundaryPhase::Pending,
},
};
assert!(!point.boundary.phase.is_gate_open());
assert_eq!(point.boundary.preconditions.len(), 1);
assert_eq!(point.boundary.postconditions.len(), 2);
}
}