decy_core/
metrics.rs

1//! Compile success rate metrics for transpilation pipeline.
2//!
3//! **Ticket**: DECY-181 - Add compile success rate metrics
4//!
5//! This module tracks compile success rates to measure progress toward
6//! the 80% single-shot compile target.
7
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Metrics for tracking compile success rate.
12///
13/// Tracks the number of successful and failed compilations,
14/// along with error codes for analysis.
15#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct CompileMetrics {
17    /// Total transpilation attempts
18    total_attempts: u64,
19
20    /// Successful first-try compilations
21    successes: u64,
22
23    /// Failed compilations
24    failures: u64,
25
26    /// Error code histogram for failure analysis
27    error_counts: HashMap<String, u64>,
28}
29
30impl CompileMetrics {
31    /// Create a new empty metrics collector.
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// Record a successful compilation.
37    pub fn record_success(&mut self) {
38        self.total_attempts += 1;
39        self.successes += 1;
40    }
41
42    /// Record a failed compilation with error message.
43    ///
44    /// The error message is parsed to extract the error code (e.g., "E0308").
45    pub fn record_failure(&mut self, error_message: &str) {
46        self.total_attempts += 1;
47        self.failures += 1;
48
49        // Extract error code from message (e.g., "E0308: mismatched types" -> "E0308")
50        let error_code = extract_error_code(error_message);
51        *self.error_counts.entry(error_code).or_insert(0) += 1;
52    }
53
54    /// Get total number of transpilation attempts.
55    pub fn total_attempts(&self) -> u64 {
56        self.total_attempts
57    }
58
59    /// Get number of successful compilations.
60    pub fn successes(&self) -> u64 {
61        self.successes
62    }
63
64    /// Get number of failed compilations.
65    pub fn failures(&self) -> u64 {
66        self.failures
67    }
68
69    /// Calculate success rate as a value between 0.0 and 1.0.
70    pub fn success_rate(&self) -> f64 {
71        if self.total_attempts == 0 {
72            0.0
73        } else {
74            self.successes as f64 / self.total_attempts as f64
75        }
76    }
77
78    /// Check if the success rate meets a target threshold.
79    ///
80    /// # Arguments
81    /// * `target` - Target rate as a value between 0.0 and 1.0 (e.g., 0.80 for 80%)
82    pub fn meets_target(&self, target: f64) -> bool {
83        self.success_rate() >= target
84    }
85
86    /// Get the error code histogram for failure analysis.
87    pub fn error_histogram(&self) -> &HashMap<String, u64> {
88        &self.error_counts
89    }
90
91    /// Reset all metrics to zero.
92    pub fn reset(&mut self) {
93        self.total_attempts = 0;
94        self.successes = 0;
95        self.failures = 0;
96        self.error_counts.clear();
97    }
98
99    /// Generate a markdown report of the metrics.
100    pub fn to_markdown(&self) -> String {
101        let rate_pct = self.success_rate() * 100.0;
102        let target_status = if self.meets_target(0.80) {
103            "✅ PASS"
104        } else {
105            "❌ FAIL"
106        };
107
108        let mut report = format!(
109            "## Compile Success Rate Metrics\n\n\
110             | Metric | Value |\n\
111             |--------|-------|\n\
112             | Total Attempts | {} |\n\
113             | Successes | {} |\n\
114             | Failures | {} |\n\
115             | Success Rate | {:.1}% |\n\
116             | Target (80%) | {} |\n",
117            self.total_attempts, self.successes, self.failures, rate_pct, target_status
118        );
119
120        if !self.error_counts.is_empty() {
121            report.push_str("\n### Error Breakdown\n\n");
122            report.push_str("| Error Code | Count |\n");
123            report.push_str("|------------|-------|\n");
124
125            let mut sorted_errors: Vec<_> = self.error_counts.iter().collect();
126            sorted_errors.sort_by(|a, b| b.1.cmp(a.1));
127
128            for (code, count) in sorted_errors {
129                report.push_str(&format!("| {} | {} |\n", code, count));
130            }
131        }
132
133        report
134    }
135
136    /// Serialize metrics to JSON for CI integration.
137    pub fn to_json(&self) -> String {
138        serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
139    }
140}
141
142/// Extract error code from Rust compiler error message.
143///
144/// # Examples
145/// - "E0308: mismatched types" -> "E0308"
146/// - "error[E0502]: cannot borrow" -> "E0502"
147/// - "some unknown error" -> "UNKNOWN"
148fn extract_error_code(message: &str) -> String {
149    // Pattern: E followed by 4 digits
150    if let Some(start) = message.find('E') {
151        let rest = &message[start..];
152        if rest.len() >= 5 && rest[1..5].chars().all(|c| c.is_ascii_digit()) {
153            return rest[..5].to_string();
154        }
155    }
156
157    // Try pattern with brackets: error[E0308]
158    if let Some(bracket_start) = message.find("[E") {
159        let rest = &message[bracket_start + 1..];
160        if let Some(bracket_end) = rest.find(']') {
161            return rest[..bracket_end].to_string();
162        }
163    }
164
165    "UNKNOWN".to_string()
166}
167
168/// Result of transpilation with verification.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct TranspilationResult {
171    /// Generated Rust code
172    pub rust_code: String,
173
174    /// Whether the generated code compiles
175    pub compiles: bool,
176
177    /// Compilation errors if any
178    pub errors: Vec<String>,
179
180    /// Clippy warnings count
181    pub warnings: usize,
182}
183
184impl TranspilationResult {
185    /// Create a successful result.
186    pub fn success(rust_code: String) -> Self {
187        Self {
188            rust_code,
189            compiles: true,
190            errors: Vec::new(),
191            warnings: 0,
192        }
193    }
194
195    /// Create a failed result with errors.
196    pub fn failure(rust_code: String, errors: Vec<String>) -> Self {
197        Self {
198            rust_code,
199            compiles: false,
200            errors,
201            warnings: 0,
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_extract_error_code_standard() {
212        assert_eq!(extract_error_code("E0308: mismatched types"), "E0308");
213    }
214
215    #[test]
216    fn test_extract_error_code_with_brackets() {
217        assert_eq!(extract_error_code("error[E0502]: cannot borrow"), "E0502");
218    }
219
220    #[test]
221    fn test_extract_error_code_unknown() {
222        assert_eq!(extract_error_code("some random error"), "UNKNOWN");
223    }
224
225    #[test]
226    fn test_extract_error_code_partial() {
227        assert_eq!(extract_error_code("E03"), "UNKNOWN");
228    }
229}