Skip to main content

entrenar/citl/trainer/
stats.rs

1//! Decision statistics types for CITL trainer
2
3#![allow(clippy::field_reassign_with_default)]
4
5use super::{CompilationOutcome, DecisionTrace};
6
7/// Session data for a single compilation
8#[derive(Debug, Clone)]
9#[allow(dead_code)] // Fields used for future session replay/analysis
10pub(crate) struct Session {
11    /// Session ID
12    pub(crate) id: String,
13    /// Decision traces
14    pub(crate) decisions: Vec<DecisionTrace>,
15    /// Compilation outcome
16    pub(crate) outcome: CompilationOutcome,
17    /// Optional fix diff (if error was fixed)
18    pub(crate) fix_diff: Option<String>,
19}
20
21/// Statistics for a decision type across sessions
22#[derive(Debug, Clone, Default)]
23pub struct DecisionStats {
24    /// Times seen in successful sessions
25    pub success_count: u32,
26    /// Times seen in failed sessions
27    pub fail_count: u32,
28    /// Total successful sessions
29    pub total_success: u32,
30    /// Total failed sessions
31    pub total_fail: u32,
32}
33
34impl DecisionStats {
35    /// Calculate Tarantula suspiciousness score
36    ///
37    /// Suspiciousness = (fail_freq) / (fail_freq + success_freq)
38    /// where fail_freq = fail_count / total_fail
39    /// and success_freq = success_count / total_success
40    #[must_use]
41    pub fn tarantula_score(&self) -> f32 {
42        if self.total_fail == 0 || self.fail_count == 0 {
43            return 0.0;
44        }
45
46        let fail_freq = self.fail_count as f32 / self.total_fail as f32;
47        let success_freq = if self.total_success > 0 {
48            self.success_count as f32 / self.total_success as f32
49        } else {
50            0.0
51        };
52
53        if fail_freq + success_freq < f32::EPSILON {
54            0.0
55        } else {
56            fail_freq / (fail_freq + success_freq)
57        }
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn test_decision_stats_tarantula() {
67        let mut stats = DecisionStats::default();
68        stats.success_count = 2;
69        stats.fail_count = 8;
70        stats.total_success = 10;
71        stats.total_fail = 10;
72
73        // fail_freq = 8/10 = 0.8
74        // success_freq = 2/10 = 0.2
75        // suspiciousness = 0.8 / (0.8 + 0.2) = 0.8
76        assert!((stats.tarantula_score() - 0.8).abs() < 0.01);
77    }
78
79    #[test]
80    fn test_decision_stats_tarantula_no_failures() {
81        let stats =
82            DecisionStats { success_count: 5, fail_count: 0, total_success: 5, total_fail: 0 };
83        assert_eq!(stats.tarantula_score(), 0.0);
84    }
85
86    #[test]
87    fn test_decision_stats_tarantula_only_failures() {
88        let stats =
89            DecisionStats { success_count: 0, fail_count: 5, total_success: 0, total_fail: 5 };
90        // fail_freq = 1.0, success_freq = 0.0
91        // suspiciousness = 1.0 / 1.0 = 1.0
92        assert_eq!(stats.tarantula_score(), 1.0);
93    }
94}
95
96#[cfg(test)]
97mod prop_tests {
98    use super::*;
99    use proptest::prelude::*;
100
101    proptest! {
102        #[test]
103        fn prop_tarantula_score_bounded(
104            success in 0u32..100,
105            fail in 0u32..100,
106            total_success in 1u32..100,
107            total_fail in 1u32..100
108        ) {
109            let stats = DecisionStats {
110                success_count: success.min(total_success),
111                fail_count: fail.min(total_fail),
112                total_success,
113                total_fail,
114            };
115
116            let score = stats.tarantula_score();
117            prop_assert!(score >= 0.0);
118            prop_assert!(score <= 1.0);
119        }
120    }
121}