use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug, Default)]
pub struct EscapeRateTracker {
pub concrete_usages: AtomicUsize,
pub depyler_value_usages: AtomicUsize,
}
impl EscapeRateTracker {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn record_concrete(&self) {
self.concrete_usages.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn record_depyler_value(&self) {
self.depyler_value_usages.fetch_add(1, Ordering::Relaxed);
}
pub fn escape_rate(&self) -> f64 {
let concrete = self.concrete_usages.load(Ordering::Relaxed);
let dv = self.depyler_value_usages.load(Ordering::Relaxed);
let total = concrete + dv;
if total == 0 {
0.0
} else {
dv as f64 / total as f64
}
}
pub fn is_falsified(&self) -> bool {
self.escape_rate() > ESCAPE_RATE_FALSIFICATION_THRESHOLD
}
pub fn counts(&self) -> (usize, usize) {
(
self.concrete_usages.load(Ordering::Relaxed),
self.depyler_value_usages.load(Ordering::Relaxed),
)
}
pub fn reset(&self) {
self.concrete_usages.store(0, Ordering::Relaxed);
self.depyler_value_usages.store(0, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaselineMetrics {
pub files_processed: usize,
pub transpile_success: usize,
pub transpile_rate: f64,
pub compile_errors: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OraclePerformance {
pub type_mismatch_confidence: f64,
pub trait_bound_confidence: f64,
pub borrow_checker_confidence: f64,
pub missing_import_confidence: f64,
}
impl Default for OraclePerformance {
fn default() -> Self {
Self {
type_mismatch_confidence: 0.0,
trait_bound_confidence: 0.0,
borrow_checker_confidence: 0.0,
missing_import_confidence: 0.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoiMetrics {
pub high_confidence_errors: usize,
pub medium_confidence_errors: usize,
pub total_classifiable: usize,
pub classifiable_rate: f64,
pub estimated_savings_cents: u64,
#[serde(default)]
pub fixes_available: usize,
#[serde(default)]
pub fix_availability_rate: f64,
#[serde(default)]
pub concrete_type_usages: usize,
#[serde(default)]
pub depyler_value_usages: usize,
#[serde(default)]
pub escape_rate: f64,
#[serde(default)]
pub escape_rate_falsified: bool,
}
pub const ESCAPE_RATE_FALSIFICATION_THRESHOLD: f64 = 0.20;
impl Default for RoiMetrics {
fn default() -> Self {
Self {
high_confidence_errors: 0,
medium_confidence_errors: 0,
total_classifiable: 0,
classifiable_rate: 0.0,
estimated_savings_cents: 0,
fixes_available: 0,
fix_availability_rate: 0.0,
concrete_type_usages: 0,
depyler_value_usages: 0,
escape_rate: 0.0,
escape_rate_falsified: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OracleRoiMetrics {
pub timestamp: DateTime<Utc>,
pub session: String,
pub baseline: BaselineMetrics,
pub error_distribution: HashMap<String, usize>,
pub oracle_performance: OraclePerformance,
pub roi_metrics: RoiMetrics,
#[serde(skip_serializing_if = "Option::is_none")]
pub issue: Option<String>,
}
impl OracleRoiMetrics {
pub fn from_convergence(
state: &super::ConvergenceState,
classifications: &[super::ErrorClassification],
session_name: &str,
) -> Self {
let total_files = state.examples.len();
let passing_files = state.examples.iter().filter(|e| e.compiles).count();
let total_errors: usize = state.examples.iter().map(|e| e.errors.len()).sum();
let mut error_distribution: HashMap<String, usize> = HashMap::new();
for classification in classifications {
let key = format!(
"{}_{}",
classification.error.code,
classification.subcategory.replace(' ', "_")
);
*error_distribution.entry(key).or_insert(0) += 1;
}
let high_conf = classifications
.iter()
.filter(|c| c.confidence > 0.8)
.count();
let medium_conf = classifications
.iter()
.filter(|c| c.confidence > 0.5 && c.confidence <= 0.8)
.count();
let total_classifiable = high_conf + medium_conf;
let estimated_savings = (high_conf * 4) as u64;
let fixes_available = classifications
.iter()
.filter(|c| c.suggested_fix.is_some())
.count();
let fix_availability_rate = if !classifications.is_empty() {
fixes_available as f64 / classifications.len() as f64
} else {
0.0
};
let mut category_confidences: HashMap<String, (f64, usize)> = HashMap::new();
for c in classifications {
let entry = category_confidences
.entry(c.subcategory.clone())
.or_insert((0.0, 0));
entry.0 += c.confidence;
entry.1 += 1;
}
let avg_confidence = |key: &str| -> f64 {
category_confidences
.get(key)
.map(|(sum, count)| if *count > 0 { sum / *count as f64 } else { 0.0 })
.unwrap_or(0.0)
};
Self {
timestamp: Utc::now(),
session: session_name.to_string(),
baseline: BaselineMetrics {
files_processed: total_files,
transpile_success: passing_files,
transpile_rate: if total_files > 0 {
passing_files as f64 / total_files as f64
} else {
0.0
},
compile_errors: total_errors,
},
error_distribution,
oracle_performance: OraclePerformance {
type_mismatch_confidence: avg_confidence("type_inference"),
trait_bound_confidence: avg_confidence("trait_bound"),
borrow_checker_confidence: avg_confidence("borrow_checker"),
missing_import_confidence: avg_confidence("missing_import"),
},
roi_metrics: RoiMetrics {
high_confidence_errors: high_conf,
medium_confidence_errors: medium_conf,
total_classifiable,
classifiable_rate: if !classifications.is_empty() {
total_classifiable as f64 / classifications.len() as f64
} else {
0.0
},
estimated_savings_cents: estimated_savings,
fixes_available,
fix_availability_rate,
concrete_type_usages: 0,
depyler_value_usages: 0,
escape_rate: 0.0,
escape_rate_falsified: false,
},
issue: None,
}
}
pub fn from_convergence_with_escape_rate(
state: &super::ConvergenceState,
classifications: &[super::ErrorClassification],
session_name: &str,
escape_tracker: Option<&EscapeRateTracker>,
) -> Self {
let mut metrics = Self::from_convergence(state, classifications, session_name);
if let Some(tracker) = escape_tracker {
let (concrete, dv) = tracker.counts();
let escape_rate = tracker.escape_rate();
let falsified = tracker.is_falsified();
metrics.roi_metrics.concrete_type_usages = concrete;
metrics.roi_metrics.depyler_value_usages = dv;
metrics.roi_metrics.escape_rate = escape_rate;
metrics.roi_metrics.escape_rate_falsified = falsified;
}
metrics
}
pub fn write_to_docs(&self) -> anyhow::Result<()> {
let docs_path = Path::new("docs/oracle_roi_metrics.json");
self.write_to(docs_path)
}
pub fn write_to(&self, path: &Path) -> anyhow::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(self)?;
std::fs::write(path, json)?;
let escape_status = if self.roi_metrics.escape_rate_falsified {
"⚠️ FALSIFIED (>20%)"
} else {
"✅ OK"
};
tracing::info!(
"Wrote Oracle ROI metrics to {}: {} high-conf, {} classifiable ({}%), {} fixes available ({}%), escape_rate: {:.1}% {}",
path.display(),
self.roi_metrics.high_confidence_errors,
self.roi_metrics.total_classifiable,
(self.roi_metrics.classifiable_rate * 100.0).round() as u32,
self.roi_metrics.fixes_available,
(self.roi_metrics.fix_availability_rate * 100.0).round() as u32,
self.roi_metrics.escape_rate * 100.0,
escape_status
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn test_config() -> super::super::ConvergenceConfig {
super::super::ConvergenceConfig {
input_dir: PathBuf::from("/tmp"),
target_rate: 80.0,
max_iterations: 10,
auto_fix: false,
dry_run: false,
verbose: false,
fix_confidence_threshold: 0.8,
checkpoint_dir: None,
parallel_jobs: 4,
display_mode: super::super::DisplayMode::Silent,
oracle: false,
explain: false,
use_cache: true,
patch_transpiler: false,
apr_file: None,
}
}
#[test]
fn test_oracle_roi_metrics_from_empty_state() {
let config = test_config();
let state = super::super::ConvergenceState::new(config);
let classifications: Vec<super::super::ErrorClassification> = vec![];
let metrics = OracleRoiMetrics::from_convergence(&state, &classifications, "test-session");
assert_eq!(metrics.session, "test-session");
assert_eq!(metrics.baseline.files_processed, 0);
assert_eq!(metrics.roi_metrics.total_classifiable, 0);
}
#[test]
fn test_oracle_roi_metrics_serialization() {
let metrics = OracleRoiMetrics {
timestamp: Utc::now(),
session: "test".to_string(),
baseline: BaselineMetrics {
files_processed: 10,
transpile_success: 7,
transpile_rate: 0.7,
compile_errors: 15,
},
error_distribution: HashMap::new(),
oracle_performance: OraclePerformance::default(),
roi_metrics: RoiMetrics::default(),
issue: Some("#172".to_string()),
};
let json = serde_json::to_string_pretty(&metrics).unwrap();
assert!(json.contains("test"));
assert!(json.contains("files_processed"));
let parsed: OracleRoiMetrics = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.baseline.files_processed, 10);
}
#[test]
fn test_oracle_roi_metrics_write_to_temp() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("roi_metrics.json");
let metrics = OracleRoiMetrics {
timestamp: Utc::now(),
session: "test-write".to_string(),
baseline: BaselineMetrics {
files_processed: 5,
transpile_success: 3,
transpile_rate: 0.6,
compile_errors: 10,
},
error_distribution: HashMap::new(),
oracle_performance: OraclePerformance::default(),
roi_metrics: RoiMetrics::default(),
issue: None,
};
metrics.write_to(&path).unwrap();
assert!(path.exists());
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("test-write"));
}
#[test]
fn test_baseline_metrics_default() {
let baseline = BaselineMetrics {
files_processed: 0,
transpile_success: 0,
transpile_rate: 0.0,
compile_errors: 0,
};
assert_eq!(baseline.files_processed, 0);
}
#[test]
fn test_roi_metrics_default() {
let roi = RoiMetrics::default();
assert_eq!(roi.high_confidence_errors, 0);
assert_eq!(roi.estimated_savings_cents, 0);
}
#[test]
fn test_oracle_performance_default() {
let perf = OraclePerformance::default();
assert_eq!(perf.type_mismatch_confidence, 0.0);
}
#[test]
fn test_escape_rate_tracker_new() {
let tracker = EscapeRateTracker::new();
let (concrete, dv) = tracker.counts();
assert_eq!(concrete, 0);
assert_eq!(dv, 0);
assert_eq!(tracker.escape_rate(), 0.0);
assert!(!tracker.is_falsified());
}
#[test]
fn test_escape_rate_tracker_record_concrete() {
let tracker = EscapeRateTracker::new();
tracker.record_concrete();
tracker.record_concrete();
tracker.record_concrete();
let (concrete, dv) = tracker.counts();
assert_eq!(concrete, 3);
assert_eq!(dv, 0);
assert_eq!(tracker.escape_rate(), 0.0); assert!(!tracker.is_falsified());
}
#[test]
fn test_escape_rate_tracker_record_depyler_value() {
let tracker = EscapeRateTracker::new();
tracker.record_depyler_value();
tracker.record_depyler_value();
let (concrete, dv) = tracker.counts();
assert_eq!(concrete, 0);
assert_eq!(dv, 2);
assert_eq!(tracker.escape_rate(), 1.0); assert!(tracker.is_falsified()); }
#[test]
fn test_escape_rate_tracker_mixed_usage() {
let tracker = EscapeRateTracker::new();
for _ in 0..80 {
tracker.record_concrete();
}
for _ in 0..20 {
tracker.record_depyler_value();
}
let (concrete, dv) = tracker.counts();
assert_eq!(concrete, 80);
assert_eq!(dv, 20);
assert!((tracker.escape_rate() - 0.20).abs() < 0.001); assert!(!tracker.is_falsified()); }
#[test]
fn test_escape_rate_tracker_falsification_threshold() {
let tracker = EscapeRateTracker::new();
for _ in 0..79 {
tracker.record_concrete();
}
for _ in 0..21 {
tracker.record_depyler_value();
}
assert!(tracker.escape_rate() > ESCAPE_RATE_FALSIFICATION_THRESHOLD);
assert!(tracker.is_falsified()); }
#[test]
fn test_escape_rate_tracker_reset() {
let tracker = EscapeRateTracker::new();
tracker.record_concrete();
tracker.record_depyler_value();
tracker.reset();
let (concrete, dv) = tracker.counts();
assert_eq!(concrete, 0);
assert_eq!(dv, 0);
}
#[test]
fn test_roi_metrics_escape_rate_fields() {
let roi = RoiMetrics::default();
assert_eq!(roi.concrete_type_usages, 0);
assert_eq!(roi.depyler_value_usages, 0);
assert_eq!(roi.escape_rate, 0.0);
assert!(!roi.escape_rate_falsified);
}
#[test]
fn test_roi_metrics_escape_rate_serialization() {
let roi = RoiMetrics {
high_confidence_errors: 10,
medium_confidence_errors: 5,
total_classifiable: 15,
classifiable_rate: 0.75,
estimated_savings_cents: 40,
fixes_available: 8,
fix_availability_rate: 0.53,
concrete_type_usages: 80,
depyler_value_usages: 20,
escape_rate: 0.20,
escape_rate_falsified: false,
};
let json = serde_json::to_string_pretty(&roi).unwrap();
assert!(json.contains("concrete_type_usages"));
assert!(json.contains("depyler_value_usages"));
assert!(json.contains("escape_rate"));
assert!(json.contains("escape_rate_falsified"));
let parsed: RoiMetrics = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.concrete_type_usages, 80);
assert_eq!(parsed.depyler_value_usages, 20);
assert!((parsed.escape_rate - 0.20).abs() < 0.001);
assert!(!parsed.escape_rate_falsified);
}
#[test]
fn test_from_convergence_with_escape_rate() {
let config = test_config();
let state = super::super::ConvergenceState::new(config);
let classifications: Vec<super::super::ErrorClassification> = vec![];
let tracker = EscapeRateTracker::new();
for _ in 0..70 {
tracker.record_concrete();
}
for _ in 0..30 {
tracker.record_depyler_value();
}
let metrics = OracleRoiMetrics::from_convergence_with_escape_rate(
&state,
&classifications,
"escape-test",
Some(&tracker),
);
assert_eq!(metrics.roi_metrics.concrete_type_usages, 70);
assert_eq!(metrics.roi_metrics.depyler_value_usages, 30);
assert!((metrics.roi_metrics.escape_rate - 0.30).abs() < 0.001);
assert!(metrics.roi_metrics.escape_rate_falsified); }
#[test]
fn test_from_convergence_without_escape_tracker() {
let config = test_config();
let state = super::super::ConvergenceState::new(config);
let classifications: Vec<super::super::ErrorClassification> = vec![];
let metrics = OracleRoiMetrics::from_convergence_with_escape_rate(
&state,
&classifications,
"no-tracker",
None,
);
assert_eq!(metrics.roi_metrics.concrete_type_usages, 0);
assert_eq!(metrics.roi_metrics.depyler_value_usages, 0);
assert_eq!(metrics.roi_metrics.escape_rate, 0.0);
assert!(!metrics.roi_metrics.escape_rate_falsified);
}
}