use std::collections::{HashMap, VecDeque};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Priority {
Low = 0,
Medium = 1,
High = 2,
Critical = 3,
}
impl std::fmt::Display for Priority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Priority::Low => write!(f, "low"),
Priority::Medium => write!(f, "medium"),
Priority::High => write!(f, "high"),
Priority::Critical => write!(f, "critical"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Complexity {
Trivial,
Moderate,
Complex,
}
impl std::fmt::Display for Complexity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Complexity::Trivial => write!(f, "trivial"),
Complexity::Moderate => write!(f, "moderate"),
Complexity::Complex => write!(f, "complex"),
}
}
}
#[derive(Debug, Clone)]
pub enum DegradationSignal {
Anomaly {
metric: String,
severity: AnomalySeverity,
observed: f64,
baseline: f64,
},
MetricDegradation {
metric: String,
current: f64,
baseline: f64,
fraction: f64,
},
ErrorSpike { stage: String, error_rate: f64 },
BudgetExceeded {
backend: String,
spend_usd: f64,
ceiling_usd: f64,
},
Manual {
description: String,
affected_files: Vec<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnomalySeverity {
Info,
Warn,
Critical,
}
impl DegradationSignal {
pub fn dedup_key(&self) -> String {
match self {
DegradationSignal::Anomaly {
metric, severity, ..
} => {
format!("anomaly:{}:{:?}", metric, severity)
}
DegradationSignal::MetricDegradation { metric, .. } => {
format!("degradation:{}", metric)
}
DegradationSignal::ErrorSpike { stage, .. } => {
format!("error_spike:{}", stage)
}
DegradationSignal::BudgetExceeded { backend, .. } => {
format!("budget:{}", backend)
}
DegradationSignal::Manual { description, .. } => {
let key: String = description.chars().take(40).collect();
format!("manual:{}", key)
}
}
}
pub fn priority(&self) -> Priority {
match self {
DegradationSignal::Anomaly { severity, .. } => match severity {
AnomalySeverity::Info => Priority::Low,
AnomalySeverity::Warn => Priority::Medium,
AnomalySeverity::Critical => Priority::Critical,
},
DegradationSignal::MetricDegradation { fraction, .. } => {
if *fraction >= 0.5 {
Priority::Critical
} else if *fraction >= 0.25 {
Priority::High
} else if *fraction >= 0.1 {
Priority::Medium
} else {
Priority::Low
}
}
DegradationSignal::ErrorSpike { error_rate, .. } => {
if *error_rate >= 0.1 {
Priority::Critical
} else if *error_rate >= 0.05 {
Priority::High
} else {
Priority::Medium
}
}
DegradationSignal::BudgetExceeded { .. } => Priority::High,
DegradationSignal::Manual { .. } => Priority::Medium,
}
}
pub fn complexity(&self) -> Complexity {
match self {
DegradationSignal::Anomaly { .. } => Complexity::Moderate,
DegradationSignal::MetricDegradation { .. } => Complexity::Trivial,
DegradationSignal::ErrorSpike { .. } => Complexity::Moderate,
DegradationSignal::BudgetExceeded { .. } => Complexity::Trivial,
DegradationSignal::Manual { affected_files, .. } => {
if affected_files.len() > 3 {
Complexity::Complex
} else if affected_files.len() > 1 {
Complexity::Moderate
} else {
Complexity::Trivial
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct GeneratedTask {
pub id: String,
pub name: String,
pub description: String,
pub affected_files: Vec<String>,
pub acceptance_criteria: Vec<String>,
pub priority: Priority,
pub complexity: Complexity,
pub generated_at_ms: u64,
pub source_signal: DegradationSignal,
}
impl GeneratedTask {
pub fn to_toml(&self) -> String {
let files = self
.affected_files
.iter()
.map(|f| format!(" \"{}\",", f))
.collect::<Vec<_>>()
.join("\n");
let criteria = self
.acceptance_criteria
.iter()
.map(|c| format!(" \"{}\",", c))
.collect::<Vec<_>>()
.join("\n");
format!(
"[[task]]\nid = \"{}\"\nname = \"{}\"\npriority = \"{}\"\ncomplexity = \"{}\"\ngenerated_at_ms = {}\naffected_files = [\n{}\n]\nacceptance_criteria = [\n{}\n]\ndescription = \"\"\"\n{}\n\"\"\"\n",
self.id,
self.name,
self.priority,
self.complexity,
self.generated_at_ms,
files,
criteria,
self.description,
)
}
}
struct RateLimiter {
max_per_window: usize,
window: Duration,
timestamps: VecDeque<Instant>,
}
impl RateLimiter {
fn new(max_per_window: usize, window: Duration) -> Self {
Self {
max_per_window,
window,
timestamps: VecDeque::new(),
}
}
fn check_and_record(&mut self, now: Instant) -> bool {
while let Some(&front) = self.timestamps.front() {
if now.duration_since(front) >= self.window {
self.timestamps.pop_front();
} else {
break;
}
}
if self.timestamps.len() < self.max_per_window {
self.timestamps.push_back(now);
true
} else {
false
}
}
fn current_count(&self, now: Instant) -> usize {
self.timestamps
.iter()
.filter(|&&t| now.duration_since(t) < self.window)
.count()
}
}
#[derive(Debug, Clone)]
pub struct TaskGenConfig {
pub max_per_window: usize,
pub rate_window: Duration,
pub dedup_ttl: Duration,
pub dedup_capacity: usize,
}
impl Default for TaskGenConfig {
fn default() -> Self {
Self {
max_per_window: 10,
rate_window: Duration::from_secs(60),
dedup_ttl: Duration::from_secs(300),
dedup_capacity: 256,
}
}
}
pub struct TaskGenerator {
config: TaskGenConfig,
rate_limiter: RateLimiter,
dedup: HashMap<String, Instant>,
sequence: u64,
}
impl TaskGenerator {
pub fn new(config: TaskGenConfig) -> Self {
let rl = RateLimiter::new(config.max_per_window, config.rate_window);
Self {
config,
rate_limiter: rl,
dedup: HashMap::new(),
sequence: 0,
}
}
pub fn generate(&mut self, signal: DegradationSignal) -> Option<GeneratedTask> {
self.generate_at(signal, Instant::now(), 0)
}
pub fn generate_at(
&mut self,
signal: DegradationSignal,
now: Instant,
now_ms: u64,
) -> Option<GeneratedTask> {
let key = signal.dedup_key();
if let Some(&last) = self.dedup.get(&key) {
if now.duration_since(last) < self.config.dedup_ttl {
return None;
}
}
if !self.rate_limiter.check_and_record(now) {
return None;
}
if self.dedup.len() >= self.config.dedup_capacity {
let ttl = self.config.dedup_ttl;
self.dedup.retain(|_, &mut t| now.duration_since(t) < ttl);
}
self.dedup.insert(key.clone(), now);
self.sequence += 1;
let task = self.build_task(&signal, &key, now_ms);
Some(task)
}
fn build_task(&self, signal: &DegradationSignal, key: &str, now_ms: u64) -> GeneratedTask {
let id = format!("gen-{}-{}", self.sequence, &key[..key.len().min(20)]);
let priority = signal.priority();
let complexity = signal.complexity();
let (name, description, affected_files, acceptance_criteria) = match signal {
DegradationSignal::Anomaly {
metric,
severity,
observed,
baseline,
} => (
format!("Investigate anomaly in {}", metric),
format!(
"Anomaly detected in metric '{}' (severity: {:?}). \
Observed: {:.3}, Baseline: {:.3}. \
Investigate root cause and apply corrective tuning.",
metric, severity, observed, baseline
),
vec![
format!("src/self_tune/telemetry_bus.rs"),
format!("src/self_tune/controller.rs"),
],
vec![
format!("Metric '{}' returns to within 2σ of baseline", metric),
format!("No new anomalies on '{}' for 5 minutes", metric),
format!("Unit tests pass with updated tuning parameters"),
],
),
DegradationSignal::MetricDegradation {
metric,
current,
baseline,
fraction,
} => (
format!("Fix degradation in {}", metric),
format!(
"Metric '{}' has degraded {:.1}% from baseline. \
Current: {:.3}, Baseline: {:.3}. \
Identify the parameter change responsible and roll back or re-tune.",
metric,
fraction * 100.0,
current,
baseline,
),
vec![
format!("src/self_tune/controller.rs"),
format!("src/self_tune/snapshot.rs"),
],
vec![
format!(
"'{}' returns to within 10% of baseline ({:.3})",
metric, baseline
),
format!("Config snapshot shows improvement vs previous revision"),
format!("cargo test --features self-tune passes"),
],
),
DegradationSignal::ErrorSpike { stage, error_rate } => (
format!("Resolve error spike in stage '{}'", stage),
format!(
"Error rate in pipeline stage '{}' has spiked to {:.1}%. \
Investigate recent changes, check circuit-breaker state, \
and restore normal operation.",
stage,
error_rate * 100.0,
),
vec![
format!("src/{}.rs", stage.to_lowercase().replace(' ', "_")),
format!("src/self_tune/controller.rs"),
],
vec![
format!("Error rate in '{}' drops below 1%", stage),
format!("Circuit breaker (if open) closes within 60s"),
format!("Chaos test for '{}' stage passes", stage),
],
),
DegradationSignal::BudgetExceeded {
backend,
spend_usd,
ceiling_usd,
} => (
format!("Reduce spend on backend '{}'", backend),
format!(
"Backend '{}' spend ${:.4} has exceeded ceiling ${:.4}. \
Shift traffic to cheaper backends or reduce token usage.",
backend, spend_usd, ceiling_usd,
),
vec![
format!("src/self_tune/cost.rs"),
format!("src/self_tune/controller.rs"),
],
vec![
format!(
"Backend '{}' daily spend drops below ${:.4}",
backend, ceiling_usd
),
format!("Budget pressure level returns to Normal"),
format!("Cost optimizer unit tests pass"),
],
),
DegradationSignal::Manual {
description,
affected_files,
} => (
format!("Manual task: {}", &description[..description.len().min(50)]),
description.clone(),
affected_files.clone(),
vec![
"All affected module unit tests pass".to_string(),
"cargo clippy -- -D warnings is clean".to_string(),
"Test-to-production ratio ≥ 1.5:1".to_string(),
],
),
};
GeneratedTask {
id,
name,
description,
affected_files,
acceptance_criteria,
priority,
complexity,
generated_at_ms: now_ms,
source_signal: signal.clone(),
}
}
pub fn window_count(&self) -> usize {
self.rate_limiter.current_count(Instant::now())
}
pub fn total_generated(&self) -> u64 {
self.sequence
}
pub fn clear_dedup(&mut self) {
self.dedup.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg_fast() -> TaskGenConfig {
TaskGenConfig {
max_per_window: 5,
rate_window: Duration::from_secs(60),
dedup_ttl: Duration::from_secs(300),
dedup_capacity: 64,
}
}
fn signal_anomaly(metric: &str) -> DegradationSignal {
DegradationSignal::Anomaly {
metric: metric.to_string(),
severity: AnomalySeverity::Warn,
observed: 120.0,
baseline: 50.0,
}
}
fn signal_degradation(metric: &str, pct: f64) -> DegradationSignal {
DegradationSignal::MetricDegradation {
metric: metric.to_string(),
current: 100.0 * (1.0 + pct),
baseline: 100.0,
fraction: pct,
}
}
fn signal_error(stage: &str, rate: f64) -> DegradationSignal {
DegradationSignal::ErrorSpike {
stage: stage.to_string(),
error_rate: rate,
}
}
fn signal_budget(backend: &str, spend: f64, ceiling: f64) -> DegradationSignal {
DegradationSignal::BudgetExceeded {
backend: backend.to_string(),
spend_usd: spend,
ceiling_usd: ceiling,
}
}
fn signal_manual(desc: &str, files: Vec<&str>) -> DegradationSignal {
DegradationSignal::Manual {
description: desc.to_string(),
affected_files: files.into_iter().map(|s| s.to_string()).collect(),
}
}
#[test]
fn test_priority_anomaly_info_is_low() {
let s = DegradationSignal::Anomaly {
metric: "x".into(),
severity: AnomalySeverity::Info,
observed: 1.0,
baseline: 1.0,
};
assert_eq!(s.priority(), Priority::Low);
}
#[test]
fn test_priority_anomaly_warn_is_medium() {
let s = DegradationSignal::Anomaly {
metric: "x".into(),
severity: AnomalySeverity::Warn,
observed: 1.0,
baseline: 1.0,
};
assert_eq!(s.priority(), Priority::Medium);
}
#[test]
fn test_priority_anomaly_critical_is_critical() {
let s = DegradationSignal::Anomaly {
metric: "x".into(),
severity: AnomalySeverity::Critical,
observed: 1.0,
baseline: 1.0,
};
assert_eq!(s.priority(), Priority::Critical);
}
#[test]
fn test_priority_degradation_50pct_is_critical() {
assert_eq!(
signal_degradation("lat", 0.50).priority(),
Priority::Critical
);
}
#[test]
fn test_priority_degradation_25pct_is_high() {
assert_eq!(signal_degradation("lat", 0.25).priority(), Priority::High);
}
#[test]
fn test_priority_degradation_10pct_is_medium() {
assert_eq!(signal_degradation("lat", 0.10).priority(), Priority::Medium);
}
#[test]
fn test_priority_degradation_5pct_is_low() {
assert_eq!(signal_degradation("lat", 0.05).priority(), Priority::Low);
}
#[test]
fn test_priority_error_spike_10pct_is_critical() {
assert_eq!(signal_error("dedup", 0.10).priority(), Priority::Critical);
}
#[test]
fn test_priority_error_spike_5pct_is_high() {
assert_eq!(signal_error("dedup", 0.05).priority(), Priority::High);
}
#[test]
fn test_priority_budget_is_high() {
assert_eq!(signal_budget("claude", 1.5, 1.0).priority(), Priority::High);
}
#[test]
fn test_priority_manual_is_medium() {
assert_eq!(
signal_manual("do something", vec![]).priority(),
Priority::Medium
);
}
#[test]
fn test_complexity_anomaly_is_moderate() {
assert_eq!(signal_anomaly("lat").complexity(), Complexity::Moderate);
}
#[test]
fn test_complexity_degradation_is_trivial() {
assert_eq!(
signal_degradation("lat", 0.1).complexity(),
Complexity::Trivial
);
}
#[test]
fn test_complexity_error_spike_is_moderate() {
assert_eq!(
signal_error("cache", 0.05).complexity(),
Complexity::Moderate
);
}
#[test]
fn test_complexity_budget_is_trivial() {
assert_eq!(
signal_budget("gpt", 5.0, 4.0).complexity(),
Complexity::Trivial
);
}
#[test]
fn test_complexity_manual_single_file_is_trivial() {
assert_eq!(
signal_manual("fix x", vec!["src/a.rs"]).complexity(),
Complexity::Trivial
);
}
#[test]
fn test_complexity_manual_two_files_is_moderate() {
assert_eq!(
signal_manual("fix x", vec!["a.rs", "b.rs"]).complexity(),
Complexity::Moderate
);
}
#[test]
fn test_complexity_manual_four_files_is_complex() {
assert_eq!(
signal_manual("big change", vec!["a", "b", "c", "d"]).complexity(),
Complexity::Complex
);
}
#[test]
fn test_dedup_key_anomaly_is_stable() {
let k1 = signal_anomaly("p95_latency_ms").dedup_key();
let k2 = signal_anomaly("p95_latency_ms").dedup_key();
assert_eq!(k1, k2);
}
#[test]
fn test_dedup_key_different_metrics_differ() {
let k1 = signal_anomaly("p95_latency_ms").dedup_key();
let k2 = signal_anomaly("error_rate").dedup_key();
assert_ne!(k1, k2);
}
#[test]
fn test_dedup_key_budget_contains_backend() {
let k = signal_budget("claude", 1.0, 0.5).dedup_key();
assert!(k.contains("claude"));
}
#[test]
fn test_generate_returns_task_on_first_signal() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
let task = gen.generate_at(signal_anomaly("lat"), now, 1000);
assert!(task.is_some());
}
#[test]
fn test_generated_task_has_correct_priority() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
let task = gen.generate_at(signal_anomaly("lat"), now, 0).unwrap();
assert_eq!(task.priority, Priority::Medium); }
#[test]
fn test_generated_task_id_is_unique() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
let t1 = gen.generate_at(signal_anomaly("lat"), now, 0).unwrap();
let t2 = gen
.generate_at(signal_error("cache", 0.05), now, 0)
.unwrap();
assert_ne!(t1.id, t2.id);
}
#[test]
fn test_generated_task_toml_contains_id() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
let task = gen.generate_at(signal_anomaly("p95"), now, 0).unwrap();
let toml = task.to_toml();
assert!(toml.contains(&task.id));
}
#[test]
fn test_generated_task_toml_contains_priority() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
let task = gen
.generate_at(signal_degradation("lat", 0.5), now, 0)
.unwrap();
let toml = task.to_toml();
assert!(toml.contains("critical"));
}
#[test]
fn test_generated_task_has_affected_files() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
let task = gen.generate_at(signal_anomaly("lat"), now, 0).unwrap();
assert!(!task.affected_files.is_empty());
}
#[test]
fn test_generated_task_has_acceptance_criteria() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
let task = gen.generate_at(signal_anomaly("lat"), now, 0).unwrap();
assert!(!task.acceptance_criteria.is_empty());
}
#[test]
fn test_total_generated_increments() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
gen.generate_at(signal_anomaly("a"), now, 0);
gen.generate_at(signal_error("b", 0.1), now, 0);
assert_eq!(gen.total_generated(), 2);
}
#[test]
fn test_dedup_suppresses_same_signal_within_ttl() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
let t1 = gen.generate_at(signal_anomaly("lat"), now, 0);
let t2 = gen.generate_at(signal_anomaly("lat"), now, 0);
assert!(t1.is_some());
assert!(t2.is_none(), "duplicate should be suppressed");
}
#[test]
fn test_dedup_allows_different_metrics() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
let t1 = gen.generate_at(signal_anomaly("lat"), now, 0);
let t2 = gen.generate_at(signal_anomaly("err"), now, 0);
assert!(t1.is_some());
assert!(t2.is_some());
}
#[test]
fn test_dedup_allows_reemission_after_ttl() {
let cfg = TaskGenConfig {
dedup_ttl: Duration::from_millis(1),
..cfg_fast()
};
let mut gen = TaskGenerator::new(cfg);
let now = Instant::now();
gen.generate_at(signal_anomaly("lat"), now, 0);
let later = now + Duration::from_millis(5);
let t2 = gen.generate_at(signal_anomaly("lat"), later, 5);
assert!(t2.is_some(), "should re-emit after TTL expires");
}
#[test]
fn test_clear_dedup_allows_immediate_reemission() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
gen.generate_at(signal_anomaly("lat"), now, 0);
gen.clear_dedup();
let t2 = gen.generate_at(signal_anomaly("lat"), now, 0);
assert!(t2.is_some());
}
#[test]
fn test_rate_limit_blocks_after_max_per_window() {
let cfg = TaskGenConfig {
max_per_window: 3,
..cfg_fast()
};
let mut gen = TaskGenerator::new(cfg);
let now = Instant::now();
gen.generate_at(signal_anomaly("a"), now, 0);
gen.generate_at(signal_anomaly("b"), now, 0);
gen.generate_at(signal_anomaly("c"), now, 0);
let t4 = gen.generate_at(signal_anomaly("d"), now, 0);
assert!(t4.is_none(), "should be rate-limited");
}
#[test]
fn test_rate_limit_resets_after_window() {
let cfg = TaskGenConfig {
max_per_window: 2,
rate_window: Duration::from_millis(10),
..cfg_fast()
};
let mut gen = TaskGenerator::new(cfg);
let now = Instant::now();
gen.generate_at(signal_anomaly("a"), now, 0);
gen.generate_at(signal_anomaly("b"), now, 0);
let later = now + Duration::from_millis(20);
let t3 = gen.generate_at(signal_anomaly("c"), later, 20);
assert!(t3.is_some(), "window should have reset");
}
#[test]
fn test_window_count_reflects_recent_generations() {
let cfg = TaskGenConfig {
max_per_window: 10,
..cfg_fast()
};
let mut gen = TaskGenerator::new(cfg);
let now = Instant::now();
gen.generate_at(signal_anomaly("a"), now, 0);
gen.generate_at(signal_error("b", 0.1), now, 0);
assert!(gen.window_count() >= 0); }
#[test]
fn test_budget_task_mentions_backend() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
let task = gen
.generate_at(signal_budget("gpt-4o", 5.0, 4.0), now, 0)
.unwrap();
assert!(task.description.contains("gpt-4o"));
}
#[test]
fn test_error_spike_task_mentions_stage() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
let task = gen.generate_at(signal_error("dedup", 0.2), now, 0).unwrap();
assert!(task.description.contains("dedup"));
}
#[test]
fn test_manual_task_uses_provided_files() {
let mut gen = TaskGenerator::new(cfg_fast());
let now = Instant::now();
let task = gen
.generate_at(
signal_manual("fix the thing", vec!["src/foo.rs", "src/bar.rs"]),
now,
0,
)
.unwrap();
assert!(task.affected_files.contains(&"src/foo.rs".to_string()));
assert!(task.affected_files.contains(&"src/bar.rs".to_string()));
}
#[test]
fn test_priority_ord_critical_greater_than_high() {
assert!(Priority::Critical > Priority::High);
}
#[test]
fn test_priority_ord_low_less_than_medium() {
assert!(Priority::Low < Priority::Medium);
}
#[test]
fn test_priority_display_values() {
assert_eq!(Priority::Low.to_string(), "low");
assert_eq!(Priority::Medium.to_string(), "medium");
assert_eq!(Priority::High.to_string(), "high");
assert_eq!(Priority::Critical.to_string(), "critical");
}
#[test]
fn test_complexity_display_values() {
assert_eq!(Complexity::Trivial.to_string(), "trivial");
assert_eq!(Complexity::Moderate.to_string(), "moderate");
assert_eq!(Complexity::Complex.to_string(), "complex");
}
}