use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OracleMetrics {
pub queries: u64,
pub hits: u64,
pub misses: u64,
pub fixes_applied: u64,
pub fixes_verified: u64,
pub patterns_captured: u64,
pub by_error_code: HashMap<String, ErrorCodeMetrics>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ErrorCodeMetrics {
pub queries: u64,
pub hits: u64,
pub fixes_applied: u64,
pub fixes_verified: u64,
}
impl OracleMetrics {
pub fn hit_rate(&self) -> f64 {
if self.queries == 0 {
0.0
} else {
self.hits as f64 / self.queries as f64
}
}
pub fn fix_success_rate(&self) -> f64 {
if self.fixes_applied == 0 {
0.0
} else {
self.fixes_verified as f64 / self.fixes_applied as f64
}
}
pub fn record_hit(&mut self, error_code: &str) {
self.queries += 1;
self.hits += 1;
self.by_error_code.entry(error_code.to_string()).or_default().queries += 1;
self.by_error_code.entry(error_code.to_string()).or_default().hits += 1;
}
pub fn record_miss(&mut self, error_code: &str) {
self.queries += 1;
self.misses += 1;
self.by_error_code.entry(error_code.to_string()).or_default().queries += 1;
}
pub fn record_fix_applied(&mut self, error_code: &str) {
self.fixes_applied += 1;
self.by_error_code.entry(error_code.to_string()).or_default().fixes_applied += 1;
}
pub fn record_fix_verified(&mut self, error_code: &str) {
self.fixes_verified += 1;
self.by_error_code.entry(error_code.to_string()).or_default().fixes_verified += 1;
}
pub fn record_pattern_captured(&mut self) {
self.patterns_captured += 1;
}
pub fn to_prometheus(&self) -> String {
format!(
r#"# HELP decy_oracle_queries_total Total oracle queries
# TYPE decy_oracle_queries_total counter
decy_oracle_queries_total {}
# HELP decy_oracle_hits_total Oracle hits
# TYPE decy_oracle_hits_total counter
decy_oracle_hits_total {}
# HELP decy_oracle_hit_rate Current hit rate
# TYPE decy_oracle_hit_rate gauge
decy_oracle_hit_rate {}
# HELP decy_oracle_fixes_applied_total Fixes applied
# TYPE decy_oracle_fixes_applied_total counter
decy_oracle_fixes_applied_total {}
# HELP decy_oracle_fixes_verified_total Fixes verified
# TYPE decy_oracle_fixes_verified_total counter
decy_oracle_fixes_verified_total {}
# HELP decy_oracle_fix_success_rate Fix success rate
# TYPE decy_oracle_fix_success_rate gauge
decy_oracle_fix_success_rate {}
"#,
self.queries,
self.hits,
self.hit_rate(),
self.fixes_applied,
self.fixes_verified,
self.fix_success_rate()
)
}
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
}
pub fn meets_ci_thresholds(&self, min_hit_rate: f64, min_fix_rate: f64) -> bool {
if self.queries == 0 {
return true;
}
if self.hit_rate() < min_hit_rate {
return false;
}
if self.fixes_applied > 0 && self.fix_success_rate() < min_fix_rate {
return false;
}
true
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CIReport {
pub metrics: OracleMetrics,
pub hit_rate_pct: f64,
pub fix_success_rate_pct: f64,
pub passed: bool,
pub thresholds: CIThresholds,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CIThresholds {
pub min_hit_rate: f64,
pub min_fix_rate: f64,
}
impl Default for CIThresholds {
fn default() -> Self {
Self {
min_hit_rate: 0.5, min_fix_rate: 0.8, }
}
}
impl CIReport {
pub fn from_metrics(metrics: OracleMetrics, thresholds: CIThresholds) -> Self {
let passed = metrics.meets_ci_thresholds(thresholds.min_hit_rate, thresholds.min_fix_rate);
Self {
hit_rate_pct: metrics.hit_rate() * 100.0,
fix_success_rate_pct: metrics.fix_success_rate() * 100.0,
metrics,
passed,
thresholds,
}
}
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
}
pub fn to_markdown(&self) -> String {
format!(
r#"## Oracle CI Report
| Metric | Value |
|--------|-------|
| Queries | {} |
| Hits | {} |
| Misses | {} |
| Hit Rate | {:.1}% |
| Fixes Applied | {} |
| Fixes Verified | {} |
| Fix Success Rate | {:.1}% |
| Patterns Captured | {} |
### Status: {}
Thresholds: Hit Rate >= {:.0}%, Fix Rate >= {:.0}%
"#,
self.metrics.queries,
self.metrics.hits,
self.metrics.misses,
self.hit_rate_pct,
self.metrics.fixes_applied,
self.metrics.fixes_verified,
self.fix_success_rate_pct,
self.metrics.patterns_captured,
if self.passed { "✅ PASSED" } else { "❌ FAILED" },
self.thresholds.min_hit_rate * 100.0,
self.thresholds.min_fix_rate * 100.0,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_default() {
let metrics = OracleMetrics::default();
assert_eq!(metrics.queries, 0);
assert_eq!(metrics.hit_rate(), 0.0);
}
#[test]
fn test_hit_rate() {
let mut metrics = OracleMetrics::default();
metrics.record_hit("E0382");
metrics.record_hit("E0382");
metrics.record_miss("E0308");
assert_eq!(metrics.queries, 3);
assert_eq!(metrics.hits, 2);
assert!((metrics.hit_rate() - 0.666).abs() < 0.01);
}
#[test]
fn test_by_error_code() {
let mut metrics = OracleMetrics::default();
metrics.record_hit("E0382");
metrics.record_hit("E0382");
metrics.record_miss("E0499");
assert_eq!(metrics.by_error_code["E0382"].hits, 2);
assert_eq!(metrics.by_error_code["E0499"].queries, 1);
assert_eq!(metrics.by_error_code["E0499"].hits, 0);
}
#[test]
fn test_fix_success_rate() {
let mut metrics = OracleMetrics::default();
metrics.record_fix_applied("E0382");
metrics.record_fix_applied("E0382");
metrics.record_fix_verified("E0382");
assert_eq!(metrics.fixes_applied, 2);
assert_eq!(metrics.fixes_verified, 1);
assert!((metrics.fix_success_rate() - 0.5).abs() < 0.01);
}
#[test]
fn test_to_json() {
let mut metrics = OracleMetrics::default();
metrics.record_hit("E0382");
let json = metrics.to_json();
assert!(json.contains("\"queries\": 1"));
assert!(json.contains("\"hits\": 1"));
}
#[test]
fn test_meets_ci_thresholds_no_queries() {
let metrics = OracleMetrics::default();
assert!(metrics.meets_ci_thresholds(0.5, 0.8));
}
#[test]
fn test_meets_ci_thresholds_pass() {
let mut metrics = OracleMetrics::default();
metrics.record_hit("E0382");
metrics.record_hit("E0382");
metrics.record_fix_applied("E0382");
metrics.record_fix_verified("E0382");
assert!(metrics.meets_ci_thresholds(0.5, 0.8));
}
#[test]
fn test_meets_ci_thresholds_fail() {
let mut metrics = OracleMetrics::default();
metrics.record_miss("E0382");
metrics.record_miss("E0382");
assert!(!metrics.meets_ci_thresholds(0.5, 0.8));
}
#[test]
fn test_ci_report_from_metrics() {
let mut metrics = OracleMetrics::default();
metrics.record_hit("E0382");
metrics.record_fix_applied("E0382");
metrics.record_fix_verified("E0382");
let report = CIReport::from_metrics(metrics, CIThresholds::default());
assert!(report.passed);
assert!((report.hit_rate_pct - 100.0).abs() < 0.01);
assert!((report.fix_success_rate_pct - 100.0).abs() < 0.01);
}
#[test]
fn test_ci_report_to_markdown() {
let mut metrics = OracleMetrics::default();
metrics.record_hit("E0382");
let report = CIReport::from_metrics(metrics, CIThresholds::default());
let md = report.to_markdown();
assert!(md.contains("Oracle CI Report"));
assert!(md.contains("| Queries | 1 |"));
assert!(md.contains("PASSED"));
}
#[test]
fn test_ci_thresholds_default() {
let thresholds = CIThresholds::default();
assert!((thresholds.min_hit_rate - 0.5).abs() < 0.01);
assert!((thresholds.min_fix_rate - 0.8).abs() < 0.01);
}
#[test]
fn test_record_pattern_captured() {
let mut metrics = OracleMetrics::default();
assert_eq!(metrics.patterns_captured, 0);
metrics.record_pattern_captured();
assert_eq!(metrics.patterns_captured, 1);
metrics.record_pattern_captured();
assert_eq!(metrics.patterns_captured, 2);
}
#[test]
fn test_to_prometheus() {
let mut metrics = OracleMetrics::default();
metrics.record_hit("E0382");
metrics.record_fix_applied("E0382");
metrics.record_fix_verified("E0382");
metrics.record_pattern_captured();
let prom = metrics.to_prometheus();
assert!(prom.contains("decy_oracle_queries_total 1"), "Got: {}", prom);
assert!(prom.contains("decy_oracle_hits_total 1"), "Got: {}", prom);
assert!(prom.contains("decy_oracle_hit_rate 1"), "Got: {}", prom);
assert!(prom.contains("decy_oracle_fixes_applied_total 1"), "Got: {}", prom);
assert!(prom.contains("decy_oracle_fixes_verified_total 1"), "Got: {}", prom);
assert!(prom.contains("decy_oracle_fix_success_rate 1"), "Got: {}", prom);
}
#[test]
fn test_meets_ci_thresholds_fix_rate_fail() {
let mut metrics = OracleMetrics::default();
metrics.record_hit("E0382");
metrics.record_fix_applied("E0382");
metrics.record_fix_applied("E0382");
assert!(!metrics.meets_ci_thresholds(0.5, 0.8));
}
#[test]
fn test_ci_report_to_json() {
let mut metrics = OracleMetrics::default();
metrics.record_hit("E0382");
let report = CIReport::from_metrics(metrics, CIThresholds::default());
let json = report.to_json();
assert!(json.contains("hit_rate_pct"), "Got: {}", json);
assert!(json.contains("passed"), "Got: {}", json);
}
#[test]
fn test_ci_report_to_markdown_failed() {
let mut metrics = OracleMetrics::default();
metrics.record_miss("E0382");
metrics.record_miss("E0308");
let report = CIReport::from_metrics(metrics, CIThresholds::default());
assert!(!report.passed);
let md = report.to_markdown();
assert!(md.contains("FAILED"), "Got: {}", md);
}
}