use ndarray::Array1;
use serde::{Deserialize, Serialize};
use crate::mesh_core::residual::{Residual, StateVector};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SensorReading {
Terminal { command: String, stdout: String, stderr: String, exit_code: i32 },
FileDiff { path: String, lines_added: usize, lines_removed: usize },
TestLog { passed: usize, failed: usize, failures: Vec<String> },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Actuation {
Dispatch { intent: String },
RunCommand { command: String },
Halt,
}
#[derive(Debug, Clone)]
pub struct CorrectionVector {
pub residual_norm: f32,
pub actions: Vec<Actuation>,
}
fn fnv1a(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf29ce484222325;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
h
}
fn hash_feature(state: &mut Array1<f32>, label: &str, value: f32) {
let h = fnv1a(label.as_bytes());
let idx = (h % state.len() as u64) as usize;
let sign = if (h >> 32) & 1 == 0 { 1.0 } else { -1.0 };
state[idx] += sign * value;
}
pub fn fuse(readings: &[SensorReading], dim: usize) -> StateVector {
let mut state = Array1::zeros(dim);
for r in readings {
match r {
SensorReading::Terminal { command, stderr, exit_code, .. } => {
hash_feature(&mut state, &format!("term:{command}"), 1.0);
hash_feature(&mut state, "term:exit_ok", if *exit_code == 0 { 1.0 } else { -1.0 });
if !stderr.is_empty() {
hash_feature(&mut state, "term:stderr", -1.0);
}
}
SensorReading::FileDiff { path, lines_added, lines_removed } => {
hash_feature(&mut state, &format!("diff:{path}"), 1.0);
hash_feature(&mut state, "diff:churn", (*lines_added + *lines_removed) as f32 / 100.0);
}
SensorReading::TestLog { passed, failed, failures } => {
let total = (passed + failed).max(1) as f32;
hash_feature(&mut state, "tests:pass_rate", *passed as f32 / total);
hash_feature(&mut state, "tests:failing", *failed as f32);
for f in failures {
hash_feature(&mut state, &format!("tests:fail:{f}"), 1.0);
}
}
}
}
StateVector(state)
}
pub struct StateSmoother {
alpha: f32,
estimate: Option<StateVector>,
}
impl StateSmoother {
pub fn new(alpha: f32) -> Self {
assert!(alpha > 0.0 && alpha <= 1.0, "StateSmoother: alpha must be in (0.0, 1.0], got {alpha}");
Self { alpha, estimate: None }
}
pub fn disabled() -> Self {
Self::new(1.0)
}
pub fn update(&mut self, raw: StateVector) -> StateVector {
let smoothed = match &self.estimate {
None => raw,
Some(prev) => StateVector(&prev.0 * (1.0 - self.alpha) + &raw.0 * self.alpha),
};
self.estimate = Some(smoothed.clone());
smoothed
}
}
pub struct IdcController {
goal: StateVector,
pub epsilon: f32,
}
impl IdcController {
pub fn new(goal: StateVector, epsilon: f32) -> Self {
Self { goal, epsilon }
}
pub fn goal(&self) -> &StateVector {
&self.goal
}
pub fn residual(&self, current: &StateVector) -> Residual {
Residual::between(&self.goal, current)
}
pub fn actuate(&self, residual: &Residual, verify_command: Option<&str>) -> CorrectionVector {
let norm = residual.norm();
if residual.converged(self.epsilon) {
return CorrectionVector { residual_norm: norm, actions: vec![Actuation::Halt] };
}
let mut actions = vec![Actuation::Dispatch {
intent: format!("reduce residual (norm {norm:.4}) toward goal state"),
}];
if let Some(cmd) = verify_command {
actions.push(Actuation::RunCommand { command: cmd.to_string() });
}
CorrectionVector { residual_norm: norm, actions }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn passing_tests() -> SensorReading {
SensorReading::TestLog { passed: 10, failed: 0, failures: vec![] }
}
#[test]
fn fusion_is_deterministic() {
let readings = vec![
SensorReading::Terminal {
command: "cargo test".into(),
stdout: "ok".into(),
stderr: String::new(),
exit_code: 0,
},
passing_tests(),
];
assert_eq!(fuse(&readings, 32), fuse(&readings, 32));
}
#[test]
fn failing_tests_move_state_away_from_goal() {
let goal = fuse(&[passing_tests()], 32);
let bad = fuse(
&[SensorReading::TestLog { passed: 5, failed: 5, failures: vec!["t::a".into()] }],
32,
);
let ctl = IdcController::new(goal, 1e-3);
let r_bad = ctl.residual(&bad);
assert!(!r_bad.converged(ctl.epsilon));
let r_good = ctl.residual(&fuse(&[passing_tests()], 32));
assert!(r_good.converged(ctl.epsilon));
}
#[test]
fn actuator_halts_on_convergence_and_acts_otherwise() {
let goal = fuse(&[passing_tests()], 16);
let ctl = IdcController::new(goal.clone(), 1e-3);
let done = ctl.actuate(&ctl.residual(&goal), Some("cargo test"));
assert_eq!(done.actions, vec![Actuation::Halt]);
let gap = ctl.residual(&StateVector::zeros(16));
let cv = ctl.actuate(&gap, Some("cargo test"));
assert!(cv.residual_norm > 0.0);
assert!(matches!(cv.actions[0], Actuation::Dispatch { .. }));
assert_eq!(cv.actions[1], Actuation::RunCommand { command: "cargo test".into() });
}
#[test]
fn disabled_smoother_passes_readings_through_unchanged() {
let mut smoother = StateSmoother::disabled();
let a = StateVector(Array1::from_vec(vec![1.0, -2.0]));
let b = StateVector(Array1::from_vec(vec![5.0, 5.0]));
assert_eq!(smoother.update(a.clone()), a);
assert_eq!(smoother.update(b.clone()), b);
}
#[test]
fn smoother_matches_hand_computed_ema() {
let mut smoother = StateSmoother::new(0.25);
let first = StateVector(Array1::from_vec(vec![0.0]));
let second = StateVector(Array1::from_vec(vec![4.0]));
assert_eq!(smoother.update(first), StateVector(Array1::from_vec(vec![0.0])));
let out = smoother.update(second);
assert!((out.0[0] - 1.0).abs() < 1e-6, "got {}", out.0[0]);
}
#[test]
fn smoother_reduces_variance_of_an_oscillating_signal() {
let raw: Vec<f32> = (0..40).map(|i| if i % 2 == 0 { 10.0 } else { -10.0 }).collect();
let mut smoother = StateSmoother::new(0.1);
let smoothed: Vec<f32> =
raw.iter().map(|&x| smoother.update(StateVector(Array1::from_vec(vec![x]))).0[0]).collect();
let variance = |xs: &[f32]| {
let mean = xs.iter().sum::<f32>() / xs.len() as f32;
xs.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / xs.len() as f32
};
assert!(
variance(&smoothed) < variance(&raw) * 0.1,
"smoothed variance {} should be far below raw variance {}",
variance(&smoothed),
variance(&raw)
);
}
#[test]
#[should_panic(expected = "alpha must be in")]
fn smoother_rejects_out_of_range_alpha() {
StateSmoother::new(0.0);
}
}