Skip to main content

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) { "✅ PASS" } else { "❌ FAIL" };
103
104        let mut report = format!(
105            "## Compile Success Rate Metrics\n\n\
106             | Metric | Value |\n\
107             |--------|-------|\n\
108             | Total Attempts | {} |\n\
109             | Successes | {} |\n\
110             | Failures | {} |\n\
111             | Success Rate | {:.1}% |\n\
112             | Target (80%) | {} |\n",
113            self.total_attempts, self.successes, self.failures, rate_pct, target_status
114        );
115
116        if !self.error_counts.is_empty() {
117            report.push_str("\n### Error Breakdown\n\n");
118            report.push_str("| Error Code | Count |\n");
119            report.push_str("|------------|-------|\n");
120
121            let mut sorted_errors: Vec<_> = self.error_counts.iter().collect();
122            sorted_errors.sort_by(|a, b| b.1.cmp(a.1));
123
124            for (code, count) in sorted_errors {
125                report.push_str(&format!("| {} | {} |\n", code, count));
126            }
127        }
128
129        report
130    }
131
132    /// Serialize metrics to JSON for CI integration.
133    pub fn to_json(&self) -> String {
134        serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
135    }
136}
137
138/// Extract error code from Rust compiler error message.
139///
140/// # Examples
141/// - "E0308: mismatched types" -> "E0308"
142/// - "error[E0502]: cannot borrow" -> "E0502"
143/// - "some unknown error" -> "UNKNOWN"
144fn extract_error_code(message: &str) -> String {
145    // Pattern: E followed by 4 digits
146    if let Some(start) = message.find('E') {
147        let rest = &message[start..];
148        if rest.len() >= 5 && rest[1..5].chars().all(|c| c.is_ascii_digit()) {
149            return rest[..5].to_string();
150        }
151    }
152
153    // Try pattern with brackets: error[E0308]
154    if let Some(bracket_start) = message.find("[E") {
155        let rest = &message[bracket_start + 1..];
156        if let Some(bracket_end) = rest.find(']') {
157            return rest[..bracket_end].to_string();
158        }
159    }
160
161    "UNKNOWN".to_string()
162}
163
164/// Result of transpilation with verification.
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct TranspilationResult {
167    /// Generated Rust code
168    pub rust_code: String,
169
170    /// Whether the generated code compiles
171    pub compiles: bool,
172
173    /// Compilation errors if any
174    pub errors: Vec<String>,
175
176    /// Clippy warnings count
177    pub warnings: usize,
178}
179
180impl TranspilationResult {
181    /// Create a successful result.
182    pub fn success(rust_code: String) -> Self {
183        Self { rust_code, compiles: true, errors: Vec::new(), warnings: 0 }
184    }
185
186    /// Create a failed result with errors.
187    pub fn failure(rust_code: String, errors: Vec<String>) -> Self {
188        Self { rust_code, compiles: false, errors, warnings: 0 }
189    }
190}
191
192/// DECY-191: Per-tier metrics for corpus convergence measurement.
193#[derive(Debug, Clone, Default, Serialize, Deserialize)]
194pub struct TierMetrics {
195    /// Tier name (e.g., "chapter-1", "P0", "P1")
196    pub tier_name: String,
197    /// Number of C files in this tier
198    pub total_files: u64,
199    /// Files that transpiled successfully
200    pub transpile_success: u64,
201    /// Files that compiled after transpilation
202    pub compile_success: u64,
203    /// Files that failed transpilation
204    pub transpile_failures: u64,
205    /// Files that transpiled but failed compilation
206    pub compile_failures: u64,
207}
208
209impl TierMetrics {
210    /// Create new tier metrics for a named tier.
211    pub fn new(name: &str) -> Self {
212        Self { tier_name: name.to_string(), ..Default::default() }
213    }
214
215    /// Transpilation success rate (0.0 to 1.0).
216    pub fn transpile_rate(&self) -> f64 {
217        contract_pre_configuration!();
218        if self.total_files == 0 {
219            0.0
220        } else {
221            self.transpile_success as f64 / self.total_files as f64
222        }
223    }
224
225    /// Compile success rate (0.0 to 1.0).
226    pub fn compile_rate(&self) -> f64 {
227        if self.total_files == 0 {
228            0.0
229        } else {
230            self.compile_success as f64 / self.total_files as f64
231        }
232    }
233}
234
235/// DECY-191: Convergence report across all tiers.
236#[derive(Debug, Clone, Default, Serialize, Deserialize)]
237pub struct ConvergenceReport {
238    /// Per-tier metrics
239    pub tiers: Vec<TierMetrics>,
240}
241
242impl ConvergenceReport {
243    /// Create a new empty convergence report.
244    pub fn new() -> Self {
245        Self::default()
246    }
247
248    /// Add tier metrics to the report.
249    pub fn add_tier(&mut self, tier: TierMetrics) {
250        self.tiers.push(tier);
251    }
252
253    /// Overall transpilation rate across all tiers.
254    pub fn overall_transpile_rate(&self) -> f64 {
255        contract_pre_configuration!();
256        let total: u64 = self.tiers.iter().map(|t| t.total_files).sum();
257        let success: u64 = self.tiers.iter().map(|t| t.transpile_success).sum();
258        if total == 0 {
259            0.0
260        } else {
261            success as f64 / total as f64
262        }
263    }
264
265    /// Overall compile rate across all tiers.
266    pub fn overall_compile_rate(&self) -> f64 {
267        let total: u64 = self.tiers.iter().map(|t| t.total_files).sum();
268        let success: u64 = self.tiers.iter().map(|t| t.compile_success).sum();
269        if total == 0 {
270            0.0
271        } else {
272            success as f64 / total as f64
273        }
274    }
275
276    /// Generate a markdown table of convergence results.
277    pub fn to_markdown(&self) -> String {
278        let mut report = String::new();
279        report.push_str("## Corpus Convergence Report\n\n");
280        report.push_str("| Tier | Files | Transpile | Compile | Transpile Rate | Compile Rate |\n");
281        report.push_str("|------|-------|-----------|---------|----------------|-------------|\n");
282
283        for tier in &self.tiers {
284            report.push_str(&format!(
285                "| {} | {} | {} | {} | {:.1}% | {:.1}% |\n",
286                tier.tier_name,
287                tier.total_files,
288                tier.transpile_success,
289                tier.compile_success,
290                tier.transpile_rate() * 100.0,
291                tier.compile_rate() * 100.0,
292            ));
293        }
294
295        let total_files: u64 = self.tiers.iter().map(|t| t.total_files).sum();
296        let total_transpile: u64 = self.tiers.iter().map(|t| t.transpile_success).sum();
297        let total_compile: u64 = self.tiers.iter().map(|t| t.compile_success).sum();
298
299        report.push_str(&format!(
300            "| **Total** | **{}** | **{}** | **{}** | **{:.1}%** | **{:.1}%** |\n",
301            total_files,
302            total_transpile,
303            total_compile,
304            self.overall_transpile_rate() * 100.0,
305            self.overall_compile_rate() * 100.0,
306        ));
307
308        report
309    }
310}
311
312/// DECY-195: Record of a semantic equivalence divergence.
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct EquivalenceDivergence {
315    /// Path to the C file
316    pub file: String,
317    /// Expected output (from gcc)
318    pub expected_stdout: String,
319    /// Actual output (from transpiled Rust)
320    pub actual_stdout: String,
321    /// Expected exit code (from gcc)
322    pub expected_exit: i32,
323    /// Actual exit code (from transpiled Rust)
324    pub actual_exit: i32,
325}
326
327/// DECY-195: Metrics for semantic equivalence validation.
328#[derive(Debug, Clone, Default, Serialize, Deserialize)]
329pub struct EquivalenceMetrics {
330    /// Number of files tested
331    pub total_files: u64,
332    /// Files with matching output and exit code
333    pub equivalent: u64,
334    /// Files with divergent behavior
335    pub divergent: u64,
336    /// Files that could not be compiled (either C or Rust)
337    pub errors: u64,
338    /// Detailed divergence records
339    pub divergences: Vec<EquivalenceDivergence>,
340}
341
342impl EquivalenceMetrics {
343    /// Create new empty equivalence metrics.
344    pub fn new() -> Self {
345        Self::default()
346    }
347
348    /// Equivalence rate (0.0 to 1.0).
349    pub fn equivalence_rate(&self) -> f64 {
350        if self.total_files == 0 {
351            0.0
352        } else {
353            self.equivalent as f64 / self.total_files as f64
354        }
355    }
356
357    /// Generate a markdown report.
358    pub fn to_markdown(&self) -> String {
359        let mut report = String::new();
360        report.push_str("## Semantic Equivalence Report\n\n");
361        report.push_str(&format!(
362            "| Metric | Value |\n\
363             |--------|-------|\n\
364             | Total Files | {} |\n\
365             | Equivalent | {} |\n\
366             | Divergent | {} |\n\
367             | Errors | {} |\n\
368             | Equivalence Rate | {:.1}% |\n",
369            self.total_files,
370            self.equivalent,
371            self.divergent,
372            self.errors,
373            self.equivalence_rate() * 100.0,
374        ));
375
376        if !self.divergences.is_empty() {
377            report.push_str("\n### Divergences\n\n");
378            for d in &self.divergences {
379                report.push_str(&format!(
380                    "- **{}**: exit {} vs {} | stdout differs: {}\n",
381                    d.file,
382                    d.expected_exit,
383                    d.actual_exit,
384                    d.expected_stdout != d.actual_stdout,
385                ));
386            }
387        }
388
389        report
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    #[test]
398    fn test_extract_error_code_standard() {
399        assert_eq!(extract_error_code("E0308: mismatched types"), "E0308");
400    }
401
402    #[test]
403    fn test_extract_error_code_with_brackets() {
404        assert_eq!(extract_error_code("error[E0502]: cannot borrow"), "E0502");
405    }
406
407    #[test]
408    fn test_extract_error_code_unknown() {
409        assert_eq!(extract_error_code("some random error"), "UNKNOWN");
410    }
411
412    #[test]
413    fn test_extract_error_code_partial() {
414        assert_eq!(extract_error_code("E03"), "UNKNOWN");
415    }
416
417    // TierMetrics tests
418
419    #[test]
420    fn test_tier_metrics_new() {
421        let tier = TierMetrics::new("chapter-1");
422        assert_eq!(tier.tier_name, "chapter-1");
423        assert_eq!(tier.total_files, 0);
424        assert_eq!(tier.transpile_success, 0);
425    }
426
427    #[test]
428    fn test_tier_metrics_transpile_rate_empty() {
429        let tier = TierMetrics::new("empty");
430        assert_eq!(tier.transpile_rate(), 0.0);
431    }
432
433    #[test]
434    fn test_tier_metrics_transpile_rate() {
435        let mut tier = TierMetrics::new("test");
436        tier.total_files = 10;
437        tier.transpile_success = 8;
438        assert!((tier.transpile_rate() - 0.8).abs() < 0.001);
439    }
440
441    #[test]
442    fn test_tier_metrics_compile_rate_empty() {
443        let tier = TierMetrics::new("empty");
444        assert_eq!(tier.compile_rate(), 0.0);
445    }
446
447    #[test]
448    fn test_tier_metrics_compile_rate() {
449        let mut tier = TierMetrics::new("test");
450        tier.total_files = 10;
451        tier.compile_success = 7;
452        assert!((tier.compile_rate() - 0.7).abs() < 0.001);
453    }
454
455    // ConvergenceReport tests
456
457    #[test]
458    fn test_convergence_report_new_empty() {
459        let report = ConvergenceReport::new();
460        assert!(report.tiers.is_empty());
461        assert_eq!(report.overall_transpile_rate(), 0.0);
462        assert_eq!(report.overall_compile_rate(), 0.0);
463    }
464
465    #[test]
466    fn test_convergence_report_add_tier() {
467        let mut report = ConvergenceReport::new();
468        let mut tier = TierMetrics::new("tier1");
469        tier.total_files = 10;
470        tier.transpile_success = 9;
471        tier.compile_success = 7;
472        report.add_tier(tier);
473        assert_eq!(report.tiers.len(), 1);
474    }
475
476    #[test]
477    fn test_convergence_report_overall_rates() {
478        let mut report = ConvergenceReport::new();
479        let mut t1 = TierMetrics::new("t1");
480        t1.total_files = 10;
481        t1.transpile_success = 8;
482        t1.compile_success = 6;
483        let mut t2 = TierMetrics::new("t2");
484        t2.total_files = 10;
485        t2.transpile_success = 10;
486        t2.compile_success = 8;
487        report.add_tier(t1);
488        report.add_tier(t2);
489
490        assert!((report.overall_transpile_rate() - 0.9).abs() < 0.001);
491        assert!((report.overall_compile_rate() - 0.7).abs() < 0.001);
492    }
493
494    #[test]
495    fn test_convergence_report_to_markdown() {
496        let mut report = ConvergenceReport::new();
497        let mut tier = TierMetrics::new("chapter-1");
498        tier.total_files = 20;
499        tier.transpile_success = 18;
500        tier.compile_success = 15;
501        report.add_tier(tier);
502
503        let md = report.to_markdown();
504        assert!(md.contains("Corpus Convergence Report"));
505        assert!(md.contains("chapter-1"));
506        assert!(md.contains("20"));
507        assert!(md.contains("18"));
508        assert!(md.contains("15"));
509        assert!(md.contains("Total"));
510    }
511
512    // EquivalenceMetrics tests
513
514    #[test]
515    fn test_equivalence_metrics_new_empty() {
516        let metrics = EquivalenceMetrics::new();
517        assert_eq!(metrics.total_files, 0);
518        assert_eq!(metrics.equivalence_rate(), 0.0);
519    }
520
521    #[test]
522    fn test_equivalence_metrics_rate() {
523        let mut metrics = EquivalenceMetrics::new();
524        metrics.total_files = 20;
525        metrics.equivalent = 18;
526        metrics.divergent = 2;
527        assert!((metrics.equivalence_rate() - 0.9).abs() < 0.001);
528    }
529
530    #[test]
531    fn test_equivalence_metrics_to_markdown() {
532        let mut metrics = EquivalenceMetrics::new();
533        metrics.total_files = 10;
534        metrics.equivalent = 8;
535        metrics.divergent = 1;
536        metrics.errors = 1;
537
538        let md = metrics.to_markdown();
539        assert!(md.contains("Semantic Equivalence Report"));
540        assert!(md.contains("10"));
541        assert!(md.contains("80.0%"));
542    }
543
544    #[test]
545    fn test_equivalence_metrics_to_markdown_with_divergences() {
546        let mut metrics = EquivalenceMetrics::new();
547        metrics.total_files = 5;
548        metrics.equivalent = 4;
549        metrics.divergent = 1;
550        metrics.errors = 0;
551        metrics.divergences.push(EquivalenceDivergence {
552            file: "test.c".to_string(),
553            expected_stdout: "hello".to_string(),
554            actual_stdout: "world".to_string(),
555            expected_exit: 0,
556            actual_exit: 1,
557        });
558
559        let md = metrics.to_markdown();
560        assert!(md.contains("Divergences"));
561        assert!(md.contains("test.c"));
562        assert!(md.contains("exit 0 vs 1"));
563    }
564
565    // CompileMetrics tests for uncovered paths
566
567    #[test]
568    fn test_compile_metrics_new_empty() {
569        let metrics = CompileMetrics::new();
570        assert_eq!(metrics.success_rate(), 0.0);
571        assert_eq!(metrics.total_attempts(), 0);
572        assert!(metrics.error_histogram().is_empty());
573    }
574
575    #[test]
576    fn test_compile_metrics_record_success() {
577        let mut metrics = CompileMetrics::new();
578        metrics.record_success();
579        metrics.record_success();
580        assert!((metrics.success_rate() - 1.0).abs() < 0.001);
581        assert_eq!(metrics.successes(), 2);
582    }
583
584    #[test]
585    fn test_compile_metrics_record_failure() {
586        let mut metrics = CompileMetrics::new();
587        metrics.record_success();
588        metrics.record_failure("E0308: mismatched types");
589        metrics.record_failure("E0502: cannot borrow");
590        assert!((metrics.success_rate() - (1.0 / 3.0)).abs() < 0.01);
591        assert_eq!(metrics.failures(), 2);
592    }
593
594    #[test]
595    fn test_compile_metrics_error_histogram() {
596        let mut metrics = CompileMetrics::new();
597        metrics.record_failure("E0308: mismatched");
598        metrics.record_failure("E0308: mismatched again");
599        metrics.record_failure("E0502: borrow");
600        let hist = metrics.error_histogram();
601        assert_eq!(hist.get("E0308"), Some(&2));
602        assert_eq!(hist.get("E0502"), Some(&1));
603    }
604
605    #[test]
606    fn test_compile_metrics_meets_target() {
607        let mut metrics = CompileMetrics::new();
608        for _ in 0..8 {
609            metrics.record_success();
610        }
611        for _ in 0..2 {
612            metrics.record_failure("E0308: test");
613        }
614        assert!(metrics.meets_target(0.80));
615        assert!(!metrics.meets_target(0.90));
616    }
617
618    #[test]
619    fn test_compile_metrics_to_markdown() {
620        let mut metrics = CompileMetrics::new();
621        metrics.record_success();
622        metrics.record_failure("E0308: test");
623        let md = metrics.to_markdown();
624        assert!(md.contains("Compile Success Rate"));
625        assert!(md.contains("50.0%"));
626        assert!(md.contains("E0308"));
627    }
628
629    #[test]
630    fn test_compile_metrics_to_markdown_passing() {
631        let mut metrics = CompileMetrics::new();
632        for _ in 0..10 {
633            metrics.record_success();
634        }
635        let md = metrics.to_markdown();
636        assert!(md.contains("PASS"));
637        assert!(md.contains("100.0%"));
638    }
639
640    #[test]
641    fn test_compile_metrics_reset() {
642        let mut metrics = CompileMetrics::new();
643        metrics.record_success();
644        metrics.record_failure("E0308: test");
645        metrics.reset();
646        assert_eq!(metrics.total_attempts(), 0);
647        assert_eq!(metrics.successes(), 0);
648        assert_eq!(metrics.failures(), 0);
649        assert!(metrics.error_histogram().is_empty());
650    }
651
652    #[test]
653    fn test_compile_metrics_to_json() {
654        let mut metrics = CompileMetrics::new();
655        metrics.record_success();
656        metrics.record_failure("E0308: test");
657        let json = metrics.to_json();
658        assert!(json.contains("\"total_attempts\": 2"));
659        assert!(json.contains("\"successes\": 1"));
660        assert!(json.contains("\"failures\": 1"));
661        assert!(json.contains("E0308"));
662    }
663
664    #[test]
665    fn test_transpilation_result_success() {
666        let result = TranspilationResult::success("fn main() {}".to_string());
667        assert!(result.compiles);
668        assert!(result.errors.is_empty());
669        assert_eq!(result.warnings, 0);
670    }
671
672    #[test]
673    fn test_transpilation_result_failure() {
674        let result = TranspilationResult::failure(
675            "fn main() {}".to_string(),
676            vec!["E0308: mismatched types".to_string()],
677        );
678        assert!(!result.compiles);
679        assert_eq!(result.errors.len(), 1);
680    }
681
682    #[test]
683    fn test_extract_error_code_bracket_fallback() {
684        // First path fails (E99 is only 2 digits after E), falls to bracket pattern
685        assert_eq!(extract_error_code("error[E99]: short code"), "E99");
686    }
687
688    #[test]
689    fn test_extract_error_code_bracket_first_e_not_code() {
690        // First E in "EXPECTED" isn't followed by 4 digits; bracket fallback finds [E55]
691        assert_eq!(extract_error_code("EXPECTED [E55]: issue"), "E55");
692    }
693
694    #[test]
695    fn test_compile_metrics_to_markdown_no_errors() {
696        let metrics = CompileMetrics::new();
697        let md = metrics.to_markdown();
698        assert!(md.contains("0.0%"));
699        assert!(md.contains("FAIL")); // 0% < 80%
700                                      // Should NOT contain error breakdown section
701        assert!(!md.contains("Error Breakdown"));
702    }
703}