pub mod comparator;
pub mod grader;
pub mod regression;
pub mod replay;
pub mod report;
pub mod runner;
pub mod server;
pub mod trigger;
pub use comparator::AbComparator;
pub use grader::LlmGrader;
pub use regression::RegressionSuite;
pub use replay::TrajectoryReplay;
pub use report::generate_html;
pub use runner::EvalRunner;
pub use server::generate_review_html;
pub use trigger::TriggerAccuracy;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvalCase {
pub id: String,
pub name: String,
#[serde(default)]
pub description: String,
pub task: String,
#[serde(default)]
pub project_fixture: Option<PathBuf>,
pub success_criteria: SuccessCriteria,
#[serde(default)]
pub constraints: EvalConstraints,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SuccessCriteria {
TestPass { command: String },
OutputContains { substring: String },
ToolUsed { tool_name: String },
ToolNotUsed { tool_name: String },
AllOf(Vec<SuccessCriteria>),
AnyOf(Vec<SuccessCriteria>),
LlmGraded {
assertions: Vec<crate::eval::grader::Assertion>,
},
SweBench {
repo_url: String,
base_commit: String,
test_patch: String,
test_command: String,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EvalConstraints {
#[serde(default)]
pub max_files_changed: Option<usize>,
#[serde(default)]
pub max_tool_calls: Option<usize>,
#[serde(default)]
pub forbidden_paths: Vec<String>,
#[serde(default)]
pub required_read_before_edit: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvalMetric {
pub name: String,
pub score: f64,
pub detail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvalResult {
pub case_id: String,
pub success: bool,
pub score: f64,
pub metrics: Vec<EvalMetric>,
pub violations: Vec<String>,
pub run_id: Option<String>,
pub duration_ms: u64,
#[serde(default)]
pub tool_calls: usize,
#[serde(default)]
pub tokens_in: u32,
#[serde(default)]
pub tokens_out: u32,
#[serde(default)]
pub file_changes: usize,
#[serde(default)]
pub compile_ok: Option<bool>,
#[serde(default)]
pub tests_pass: Option<bool>,
}
impl EvalResult {
pub fn new(case_id: &str, success: bool) -> Self {
Self {
case_id: case_id.to_string(),
success,
score: if success { 1.0 } else { 0.0 },
metrics: Vec::new(),
violations: Vec::new(),
run_id: None,
duration_ms: 0,
tool_calls: 0,
tokens_in: 0,
tokens_out: 0,
file_changes: 0,
compile_ok: None,
tests_pass: None,
}
}
pub fn with_metric(mut self, name: &str, score: f64, detail: &str) -> Self {
self.metrics.push(EvalMetric {
name: name.to_string(),
score,
detail: detail.to_string(),
});
self.recompute_score();
self
}
pub fn with_violation(mut self, violation: &str) -> Self {
self.violations.push(violation.to_string());
self.success = false;
self
}
pub fn with_run_id(mut self, run_id: &str) -> Self {
self.run_id = Some(run_id.to_string());
self
}
fn recompute_score(&mut self) {
if self.metrics.is_empty() {
self.score = if self.success { 1.0 } else { 0.0 };
} else {
self.score =
self.metrics.iter().map(|m| m.score).sum::<f64>() / self.metrics.len() as f64;
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvalReport {
pub total: usize,
pub passed: usize,
pub failed: usize,
pub avg_score: f64,
#[serde(default)]
pub std_dev: f64,
#[serde(default)]
pub min_score: f64,
#[serde(default)]
pub max_score: f64,
#[serde(default)]
pub delta_vs_baseline: Option<f64>,
#[serde(default)]
pub total_tool_calls: usize,
#[serde(default)]
pub total_tokens_in: u32,
#[serde(default)]
pub total_tokens_out: u32,
#[serde(default)]
pub total_file_changes: usize,
pub results: Vec<EvalResult>,
}
impl EvalReport {
pub fn new(results: Vec<EvalResult>) -> Self {
let total = results.len();
let passed = results.iter().filter(|r| r.success).count();
let failed = total - passed;
let avg_score = if total > 0 {
results.iter().map(|r| r.score).sum::<f64>() / total as f64
} else {
0.0
};
let total_tool_calls: usize = results.iter().map(|r| r.tool_calls).sum();
let total_tokens_in: u32 = results.iter().map(|r| r.tokens_in).sum();
let total_tokens_out: u32 = results.iter().map(|r| r.tokens_out).sum();
let total_file_changes: usize = results.iter().map(|r| r.file_changes).sum();
let scores: Vec<f64> = results.iter().map(|r| r.score).collect();
let variance = if total > 1 {
scores.iter().map(|s| (s - avg_score).powi(2)).sum::<f64>() / (total - 1) as f64
} else {
0.0
};
let std_dev = variance.sqrt();
let min_score = scores.iter().cloned().fold(f64::MAX, f64::min);
let max_score = scores.iter().cloned().fold(f64::MIN, f64::max);
Self {
total,
passed,
failed,
avg_score,
std_dev,
min_score: if total > 0 { min_score } else { 0.0 },
max_score: if total > 0 { max_score } else { 0.0 },
delta_vs_baseline: None,
total_tool_calls,
total_tokens_in,
total_tokens_out,
total_file_changes,
results,
}
}
}