Skip to main content

decy_ownership/
hybrid_classifier.rs

1//! Hybrid ownership classification combining rules and ML (DECY-ML-012).
2//!
3//! Implements the hybrid classification system from the ML-enhanced ownership
4//! inference specification. Combines rule-based heuristics with ML predictions
5//! and falls back to rules when ML confidence is below threshold.
6//!
7//! # Architecture
8//!
9//! ```text
10//! ┌─────────────────────────────────────────────────────────────┐
11//! │                  HYBRID CLASSIFIER                          │
12//! │  ┌───────────────────────────┐                              │
13//! │  │ Phase 1: Rule-Based       │──────────────────────┐       │
14//! │  │ • malloc/free → Box       │                      │       │
15//! │  │ • array alloc → Vec       │                      ▼       │
16//! │  │ • single deref → &T       │            ┌─────────────┐   │
17//! │  └───────────────────────────┘            │  Fallback   │   │
18//! │              │                            │  Logic      │   │
19//! │              ▼                            │             │   │
20//! │  ┌───────────────────────────┐            │  if conf    │   │
21//! │  │ Phase 2: ML Enhancement   │            │  < 0.65:    │   │
22//! │  │ • Feature extraction      │───────────►│  use rules  │   │
23//! │  │ • Pattern classification  │            └─────────────┘   │
24//! │  │ • Confidence scoring      │                      │       │
25//! │  └───────────────────────────┘                      │       │
26//! │                                                     ▼       │
27//! │                                          ┌─────────────┐    │
28//! │                                          │   Result    │    │
29//! │                                          └─────────────┘    │
30//! └─────────────────────────────────────────────────────────────┘
31//! ```
32
33use crate::inference::{OwnershipInference, OwnershipKind};
34use crate::ml_features::{InferredOwnership, OwnershipFeatures, OwnershipPrediction};
35use serde::{Deserialize, Serialize};
36
37/// Default confidence threshold for ML predictions.
38///
39/// If ML confidence is below this, fall back to rule-based classification.
40pub const DEFAULT_CONFIDENCE_THRESHOLD: f64 = 0.65;
41
42/// Classification method used for a prediction.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44pub enum ClassificationMethod {
45    /// Used rule-based heuristics only
46    RuleBased,
47    /// Used ML prediction with sufficient confidence
48    MachineLearning,
49    /// ML confidence was low, fell back to rules
50    Fallback,
51    /// Combined rules and ML (ensemble)
52    Hybrid,
53}
54
55impl std::fmt::Display for ClassificationMethod {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            ClassificationMethod::RuleBased => write!(f, "rule-based"),
59            ClassificationMethod::MachineLearning => write!(f, "ml"),
60            ClassificationMethod::Fallback => write!(f, "fallback"),
61            ClassificationMethod::Hybrid => write!(f, "hybrid"),
62        }
63    }
64}
65
66/// Result of hybrid classification.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct HybridResult {
69    /// Variable name
70    pub variable: String,
71    /// Final inferred ownership
72    pub ownership: InferredOwnership,
73    /// Confidence score (0.0 - 1.0)
74    pub confidence: f64,
75    /// Method used for classification
76    pub method: ClassificationMethod,
77    /// Rule-based result (always available)
78    pub rule_result: Option<InferredOwnership>,
79    /// ML result (if available)
80    pub ml_result: Option<OwnershipPrediction>,
81    /// Reasoning for final decision
82    pub reasoning: String,
83}
84
85impl HybridResult {
86    /// Check if this result used fallback logic.
87    pub fn used_fallback(&self) -> bool {
88        self.method == ClassificationMethod::Fallback
89    }
90
91    /// Check if ML was available but not used due to low confidence.
92    pub fn ml_rejected(&self) -> bool {
93        self.ml_result.is_some() && self.method == ClassificationMethod::Fallback
94    }
95}
96
97/// Trait for ML model inference.
98///
99/// Implement this trait to integrate a custom ML model (e.g., aprender RandomForest,
100/// CodeBERT fine-tuned, or external API).
101pub trait OwnershipModel: Send + Sync {
102    /// Predict ownership from features.
103    fn predict(&self, features: &OwnershipFeatures) -> OwnershipPrediction;
104
105    /// Batch predict for multiple feature vectors.
106    fn predict_batch(&self, features: &[OwnershipFeatures]) -> Vec<OwnershipPrediction> {
107        features.iter().map(|f| self.predict(f)).collect()
108    }
109
110    /// Get model name/version for logging.
111    fn name(&self) -> &str {
112        "unknown"
113    }
114}
115
116/// Null model that always returns low confidence.
117///
118/// Use as placeholder when no ML model is available.
119#[derive(Debug, Clone, Default)]
120pub struct NullModel;
121
122impl OwnershipModel for NullModel {
123    fn predict(&self, _features: &OwnershipFeatures) -> OwnershipPrediction {
124        OwnershipPrediction {
125            kind: InferredOwnership::RawPointer,
126            confidence: 0.0,
127            fallback: None,
128        }
129    }
130
131    fn name(&self) -> &str {
132        "null"
133    }
134}
135
136/// Hybrid ownership classifier combining rules and ML.
137#[derive(Debug)]
138pub struct HybridClassifier {
139    /// Confidence threshold for ML predictions
140    confidence_threshold: f64,
141    /// Whether ML is enabled
142    ml_enabled: bool,
143}
144
145impl Default for HybridClassifier {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151impl HybridClassifier {
152    /// Create a new hybrid classifier with default settings.
153    pub fn new() -> Self {
154        Self {
155            confidence_threshold: DEFAULT_CONFIDENCE_THRESHOLD,
156            ml_enabled: false,
157        }
158    }
159
160    /// Create with custom confidence threshold.
161    pub fn with_threshold(threshold: f64) -> Self {
162        Self {
163            confidence_threshold: threshold.clamp(0.0, 1.0),
164            ml_enabled: false,
165        }
166    }
167
168    /// Enable ML predictions.
169    pub fn enable_ml(&mut self) {
170        self.ml_enabled = true;
171    }
172
173    /// Disable ML predictions (use rules only).
174    pub fn disable_ml(&mut self) {
175        self.ml_enabled = false;
176    }
177
178    /// Check if ML is enabled.
179    pub fn ml_enabled(&self) -> bool {
180        self.ml_enabled
181    }
182
183    /// Get confidence threshold.
184    pub fn confidence_threshold(&self) -> f64 {
185        self.confidence_threshold
186    }
187
188    /// Set confidence threshold.
189    pub fn set_threshold(&mut self, threshold: f64) {
190        self.confidence_threshold = threshold.clamp(0.0, 1.0);
191    }
192
193    /// Classify using rules only.
194    pub fn classify_rule_based(&self, inference: &OwnershipInference) -> HybridResult {
195        let ownership = ownership_kind_to_inferred(&inference.kind);
196
197        HybridResult {
198            variable: inference.variable.clone(),
199            ownership,
200            confidence: inference.confidence as f64,
201            method: ClassificationMethod::RuleBased,
202            rule_result: Some(ownership),
203            ml_result: None,
204            reasoning: format!("Rule-based: {}", inference.reason),
205        }
206    }
207
208    /// Classify using hybrid approach with ML model.
209    pub fn classify_hybrid<M: OwnershipModel>(
210        &self,
211        inference: &OwnershipInference,
212        features: &OwnershipFeatures,
213        model: &M,
214    ) -> HybridResult {
215        // Always get rule-based result
216        let rule_ownership = ownership_kind_to_inferred(&inference.kind);
217
218        // If ML is disabled, use rules only
219        if !self.ml_enabled {
220            return self.classify_rule_based(inference);
221        }
222
223        // Get ML prediction
224        let ml_prediction = model.predict(features);
225        let ml_conf = ml_prediction.confidence as f64;
226        let ml_kind = ml_prediction.kind;
227
228        // Check confidence threshold
229        if ml_conf >= self.confidence_threshold {
230            // ML confidence is sufficient - use ML result
231            HybridResult {
232                variable: inference.variable.clone(),
233                ownership: ml_kind,
234                confidence: ml_conf,
235                method: ClassificationMethod::MachineLearning,
236                rule_result: Some(rule_ownership),
237                ml_result: Some(ml_prediction),
238                reasoning: format!("ML prediction (confidence {:.2}): {:?}", ml_conf, ml_kind),
239            }
240        } else {
241            // ML confidence too low - fall back to rules
242            HybridResult {
243                variable: inference.variable.clone(),
244                ownership: rule_ownership,
245                confidence: inference.confidence as f64,
246                method: ClassificationMethod::Fallback,
247                rule_result: Some(rule_ownership),
248                ml_result: Some(ml_prediction),
249                reasoning: format!(
250                    "Fallback to rules (ML confidence {:.2} < threshold {:.2}): {}",
251                    ml_conf, self.confidence_threshold, inference.reason
252                ),
253            }
254        }
255    }
256
257    /// Classify with ensemble (combine rules and ML).
258    ///
259    /// When both methods agree, boost confidence.
260    /// When they disagree, use the higher confidence result.
261    pub fn classify_ensemble<M: OwnershipModel>(
262        &self,
263        inference: &OwnershipInference,
264        features: &OwnershipFeatures,
265        model: &M,
266    ) -> HybridResult {
267        let rule_ownership = ownership_kind_to_inferred(&inference.kind);
268        let ml_prediction = model.predict(features);
269        let ml_conf = ml_prediction.confidence as f64;
270        let ml_kind = ml_prediction.kind;
271
272        // Check if they agree
273        let agree = rule_ownership == ml_kind;
274
275        if agree {
276            // Both agree - boost confidence
277            let combined_confidence = (inference.confidence as f64 + ml_conf) / 2.0 * 1.1;
278            let final_confidence = combined_confidence.min(1.0);
279
280            HybridResult {
281                variable: inference.variable.clone(),
282                ownership: rule_ownership,
283                confidence: final_confidence,
284                method: ClassificationMethod::Hybrid,
285                rule_result: Some(rule_ownership),
286                ml_result: Some(ml_prediction),
287                reasoning: format!(
288                    "Hybrid (rules + ML agree): boosted confidence {:.2}",
289                    final_confidence
290                ),
291            }
292        } else {
293            // Disagree - use higher confidence
294            if ml_conf > inference.confidence as f64 {
295                HybridResult {
296                    variable: inference.variable.clone(),
297                    ownership: ml_kind,
298                    confidence: ml_conf,
299                    method: ClassificationMethod::MachineLearning,
300                    rule_result: Some(rule_ownership),
301                    ml_result: Some(ml_prediction),
302                    reasoning: format!(
303                        "ML wins (conf {:.2} > rules {:.2}): {:?}",
304                        ml_conf, inference.confidence, ml_kind
305                    ),
306                }
307            } else {
308                HybridResult {
309                    variable: inference.variable.clone(),
310                    ownership: rule_ownership,
311                    confidence: inference.confidence as f64,
312                    method: ClassificationMethod::RuleBased,
313                    rule_result: Some(rule_ownership),
314                    ml_result: Some(ml_prediction),
315                    reasoning: format!(
316                        "Rules win (conf {:.2} > ML {:.2}): {}",
317                        inference.confidence, ml_conf, inference.reason
318                    ),
319                }
320            }
321        }
322    }
323}
324
325/// Convert OwnershipKind to InferredOwnership.
326fn ownership_kind_to_inferred(kind: &OwnershipKind) -> InferredOwnership {
327    match kind {
328        OwnershipKind::Owning => InferredOwnership::Owned,
329        OwnershipKind::ImmutableBorrow => InferredOwnership::Borrowed,
330        OwnershipKind::MutableBorrow => InferredOwnership::BorrowedMut,
331        OwnershipKind::ArrayPointer { .. } => InferredOwnership::Slice,
332        OwnershipKind::Unknown => InferredOwnership::RawPointer,
333    }
334}
335
336/// Metrics for hybrid classification.
337#[derive(Debug, Clone, Default, Serialize, Deserialize)]
338pub struct HybridMetrics {
339    /// Total classifications
340    pub total: u64,
341    /// Rule-based only classifications
342    pub rule_based: u64,
343    /// ML classifications with sufficient confidence
344    pub ml_used: u64,
345    /// Fallback to rules due to low ML confidence
346    pub fallback: u64,
347    /// Hybrid/ensemble classifications
348    pub hybrid: u64,
349    /// Cases where rules and ML agreed
350    pub agreements: u64,
351    /// Cases where rules and ML disagreed
352    pub disagreements: u64,
353}
354
355impl HybridMetrics {
356    /// Create new metrics tracker.
357    pub fn new() -> Self {
358        Self::default()
359    }
360
361    /// Record a classification result.
362    pub fn record(&mut self, result: &HybridResult) {
363        self.total += 1;
364        match result.method {
365            ClassificationMethod::RuleBased => self.rule_based += 1,
366            ClassificationMethod::MachineLearning => self.ml_used += 1,
367            ClassificationMethod::Fallback => self.fallback += 1,
368            ClassificationMethod::Hybrid => self.hybrid += 1,
369        }
370
371        // Track agreement/disagreement
372        if let (Some(rule), Some(ml)) = (&result.rule_result, &result.ml_result) {
373            if *rule == ml.kind {
374                self.agreements += 1;
375            } else {
376                self.disagreements += 1;
377            }
378        }
379    }
380
381    /// Get ML usage rate (0.0 - 1.0).
382    pub fn ml_usage_rate(&self) -> f64 {
383        if self.total == 0 {
384            0.0
385        } else {
386            self.ml_used as f64 / self.total as f64
387        }
388    }
389
390    /// Get fallback rate (0.0 - 1.0).
391    pub fn fallback_rate(&self) -> f64 {
392        if self.total == 0 {
393            0.0
394        } else {
395            self.fallback as f64 / self.total as f64
396        }
397    }
398
399    /// Get agreement rate when both methods were used (0.0 - 1.0).
400    pub fn agreement_rate(&self) -> f64 {
401        let both = self.agreements + self.disagreements;
402        if both == 0 {
403            1.0 // No comparisons = perfect agreement by default
404        } else {
405            self.agreements as f64 / both as f64
406        }
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    // ========================================================================
415    // ClassificationMethod tests
416    // ========================================================================
417
418    #[test]
419    fn classification_method_display() {
420        assert_eq!(ClassificationMethod::RuleBased.to_string(), "rule-based");
421        assert_eq!(ClassificationMethod::MachineLearning.to_string(), "ml");
422        assert_eq!(ClassificationMethod::Fallback.to_string(), "fallback");
423        assert_eq!(ClassificationMethod::Hybrid.to_string(), "hybrid");
424    }
425
426    // ========================================================================
427    // HybridClassifier tests
428    // ========================================================================
429
430    #[test]
431    fn hybrid_classifier_default() {
432        let classifier = HybridClassifier::new();
433        assert!(!classifier.ml_enabled());
434        assert!((classifier.confidence_threshold() - 0.65).abs() < 0.001);
435    }
436
437    #[test]
438    fn hybrid_classifier_with_threshold() {
439        let classifier = HybridClassifier::with_threshold(0.8);
440        assert!((classifier.confidence_threshold() - 0.8).abs() < 0.001);
441    }
442
443    #[test]
444    fn hybrid_classifier_threshold_clamp() {
445        let low = HybridClassifier::with_threshold(-0.5);
446        assert!((low.confidence_threshold() - 0.0).abs() < 0.001);
447
448        let high = HybridClassifier::with_threshold(1.5);
449        assert!((high.confidence_threshold() - 1.0).abs() < 0.001);
450    }
451
452    #[test]
453    fn hybrid_classifier_enable_disable_ml() {
454        let mut classifier = HybridClassifier::new();
455        assert!(!classifier.ml_enabled());
456
457        classifier.enable_ml();
458        assert!(classifier.ml_enabled());
459
460        classifier.disable_ml();
461        assert!(!classifier.ml_enabled());
462    }
463
464    #[test]
465    fn classify_rule_based_owning() {
466        let classifier = HybridClassifier::new();
467        let inference = OwnershipInference {
468            variable: "ptr".to_string(),
469            kind: OwnershipKind::Owning,
470            confidence: 0.9,
471            reason: "malloc detected".to_string(),
472        };
473
474        let result = classifier.classify_rule_based(&inference);
475
476        assert_eq!(result.variable, "ptr");
477        assert_eq!(result.ownership, InferredOwnership::Owned);
478        assert_eq!(result.method, ClassificationMethod::RuleBased);
479        assert!(result.ml_result.is_none());
480    }
481
482    #[test]
483    fn classify_rule_based_immutable_borrow() {
484        let classifier = HybridClassifier::new();
485        let inference = OwnershipInference {
486            variable: "ref".to_string(),
487            kind: OwnershipKind::ImmutableBorrow,
488            confidence: 0.85,
489            reason: "read-only access".to_string(),
490        };
491
492        let result = classifier.classify_rule_based(&inference);
493
494        assert_eq!(result.ownership, InferredOwnership::Borrowed);
495    }
496
497    // ========================================================================
498    // Hybrid classification with ML tests
499    // ========================================================================
500
501    /// Mock ML model for testing
502    struct MockModel {
503        ownership: InferredOwnership,
504        confidence: f64,
505    }
506
507    impl MockModel {
508        fn with_confidence(ownership: InferredOwnership, confidence: f64) -> Self {
509            Self {
510                ownership,
511                confidence,
512            }
513        }
514    }
515
516    impl OwnershipModel for MockModel {
517        fn predict(&self, _features: &OwnershipFeatures) -> OwnershipPrediction {
518            OwnershipPrediction {
519                kind: self.ownership,
520                confidence: self.confidence as f32,
521                fallback: None,
522            }
523        }
524
525        fn name(&self) -> &str {
526            "mock"
527        }
528    }
529
530    #[test]
531    fn classify_hybrid_ml_high_confidence() {
532        let mut classifier = HybridClassifier::new();
533        classifier.enable_ml();
534
535        let inference = OwnershipInference {
536            variable: "ptr".to_string(),
537            kind: OwnershipKind::Unknown,
538            confidence: 0.5,
539            reason: "uncertain".to_string(),
540        };
541
542        let features = OwnershipFeatures::default();
543        let model = MockModel::with_confidence(InferredOwnership::Vec, 0.9);
544
545        let result = classifier.classify_hybrid(&inference, &features, &model);
546
547        assert_eq!(result.ownership, InferredOwnership::Vec);
548        assert_eq!(result.method, ClassificationMethod::MachineLearning);
549        assert!(!result.used_fallback());
550    }
551
552    #[test]
553    fn classify_hybrid_ml_low_confidence_fallback() {
554        let mut classifier = HybridClassifier::new();
555        classifier.enable_ml();
556
557        let inference = OwnershipInference {
558            variable: "ptr".to_string(),
559            kind: OwnershipKind::Owning,
560            confidence: 0.8,
561            reason: "malloc detected".to_string(),
562        };
563
564        let features = OwnershipFeatures::default();
565        let model = MockModel::with_confidence(InferredOwnership::Borrowed, 0.3);
566
567        let result = classifier.classify_hybrid(&inference, &features, &model);
568
569        // Should fall back to rule-based (Owning → Owned)
570        assert_eq!(result.ownership, InferredOwnership::Owned);
571        assert_eq!(result.method, ClassificationMethod::Fallback);
572        assert!(result.used_fallback());
573        assert!(result.ml_rejected());
574    }
575
576    #[test]
577    fn classify_hybrid_ml_disabled() {
578        let classifier = HybridClassifier::new(); // ML disabled by default
579
580        let inference = OwnershipInference {
581            variable: "ptr".to_string(),
582            kind: OwnershipKind::MutableBorrow,
583            confidence: 0.7,
584            reason: "mutation detected".to_string(),
585        };
586
587        let features = OwnershipFeatures::default();
588        let model = MockModel::with_confidence(InferredOwnership::Owned, 0.99);
589
590        let result = classifier.classify_hybrid(&inference, &features, &model);
591
592        // Should use rules since ML is disabled
593        assert_eq!(result.ownership, InferredOwnership::BorrowedMut);
594        assert_eq!(result.method, ClassificationMethod::RuleBased);
595    }
596
597    // ========================================================================
598    // Ensemble classification tests
599    // ========================================================================
600
601    #[test]
602    fn classify_ensemble_agreement_boosts_confidence() {
603        let classifier = HybridClassifier::new();
604
605        let inference = OwnershipInference {
606            variable: "ptr".to_string(),
607            kind: OwnershipKind::Owning,
608            confidence: 0.7,
609            reason: "malloc".to_string(),
610        };
611
612        let features = OwnershipFeatures::default();
613        let model = MockModel::with_confidence(InferredOwnership::Owned, 0.8);
614
615        let result = classifier.classify_ensemble(&inference, &features, &model);
616
617        assert_eq!(result.method, ClassificationMethod::Hybrid);
618        // (0.7 + 0.8) / 2 * 1.1 = 0.825
619        assert!(result.confidence > 0.82);
620    }
621
622    #[test]
623    fn classify_ensemble_disagreement_ml_wins() {
624        let classifier = HybridClassifier::new();
625
626        let inference = OwnershipInference {
627            variable: "ptr".to_string(),
628            kind: OwnershipKind::Unknown,
629            confidence: 0.3,
630            reason: "unknown".to_string(),
631        };
632
633        let features = OwnershipFeatures::default();
634        let model = MockModel::with_confidence(InferredOwnership::Vec, 0.9);
635
636        let result = classifier.classify_ensemble(&inference, &features, &model);
637
638        assert_eq!(result.ownership, InferredOwnership::Vec);
639        assert_eq!(result.method, ClassificationMethod::MachineLearning);
640    }
641
642    #[test]
643    fn classify_ensemble_disagreement_rules_win() {
644        let classifier = HybridClassifier::new();
645
646        let inference = OwnershipInference {
647            variable: "ptr".to_string(),
648            kind: OwnershipKind::Owning,
649            confidence: 0.95,
650            reason: "malloc + free".to_string(),
651        };
652
653        let features = OwnershipFeatures::default();
654        let model = MockModel::with_confidence(InferredOwnership::Borrowed, 0.4);
655
656        let result = classifier.classify_ensemble(&inference, &features, &model);
657
658        assert_eq!(result.ownership, InferredOwnership::Owned);
659        assert_eq!(result.method, ClassificationMethod::RuleBased);
660    }
661
662    // ========================================================================
663    // NullModel tests
664    // ========================================================================
665
666    #[test]
667    fn null_model_returns_unknown() {
668        let model = NullModel;
669        let features = OwnershipFeatures::default();
670
671        let prediction = model.predict(&features);
672
673        assert_eq!(prediction.kind, InferredOwnership::RawPointer);
674        assert!((prediction.confidence as f64 - 0.0).abs() < 0.001);
675    }
676
677    #[test]
678    fn null_model_name() {
679        let model = NullModel;
680        assert_eq!(model.name(), "null");
681    }
682
683    // ========================================================================
684    // HybridResult tests
685    // ========================================================================
686
687    #[test]
688    fn hybrid_result_used_fallback() {
689        let result = HybridResult {
690            variable: "x".to_string(),
691            ownership: InferredOwnership::Owned,
692            confidence: 0.8,
693            method: ClassificationMethod::Fallback,
694            rule_result: Some(InferredOwnership::Owned),
695            ml_result: Some(OwnershipPrediction {
696                kind: InferredOwnership::Borrowed,
697                confidence: 0.3,
698                fallback: None,
699            }),
700            reasoning: "test".to_string(),
701        };
702
703        assert!(result.used_fallback());
704        assert!(result.ml_rejected());
705    }
706
707    #[test]
708    fn hybrid_result_ml_not_rejected() {
709        let result = HybridResult {
710            variable: "x".to_string(),
711            ownership: InferredOwnership::Owned,
712            confidence: 0.9,
713            method: ClassificationMethod::MachineLearning,
714            rule_result: Some(InferredOwnership::RawPointer),
715            ml_result: Some(OwnershipPrediction {
716                kind: InferredOwnership::Owned,
717                confidence: 0.9,
718                fallback: None,
719            }),
720            reasoning: "test".to_string(),
721        };
722
723        assert!(!result.used_fallback());
724        assert!(!result.ml_rejected());
725    }
726
727    // ========================================================================
728    // HybridMetrics tests
729    // ========================================================================
730
731    #[test]
732    fn hybrid_metrics_default() {
733        let metrics = HybridMetrics::new();
734        assert_eq!(metrics.total, 0);
735        assert_eq!(metrics.ml_usage_rate(), 0.0);
736    }
737
738    #[test]
739    fn hybrid_metrics_record() {
740        let mut metrics = HybridMetrics::new();
741
742        let result1 = HybridResult {
743            variable: "a".to_string(),
744            ownership: InferredOwnership::Owned,
745            confidence: 0.9,
746            method: ClassificationMethod::MachineLearning,
747            rule_result: Some(InferredOwnership::Owned),
748            ml_result: Some(OwnershipPrediction {
749                kind: InferredOwnership::Owned,
750                confidence: 0.9,
751                fallback: None,
752            }),
753            reasoning: "ml".to_string(),
754        };
755
756        let result2 = HybridResult {
757            variable: "b".to_string(),
758            ownership: InferredOwnership::Borrowed,
759            confidence: 0.7,
760            method: ClassificationMethod::RuleBased,
761            rule_result: Some(InferredOwnership::Borrowed),
762            ml_result: None,
763            reasoning: "rules".to_string(),
764        };
765
766        metrics.record(&result1);
767        metrics.record(&result2);
768
769        assert_eq!(metrics.total, 2);
770        assert_eq!(metrics.ml_used, 1);
771        assert_eq!(metrics.rule_based, 1);
772        assert!((metrics.ml_usage_rate() - 0.5).abs() < 0.001);
773    }
774
775    #[test]
776    fn hybrid_metrics_agreement_rate() {
777        let mut metrics = HybridMetrics::new();
778
779        // Agreement
780        let agree = HybridResult {
781            variable: "a".to_string(),
782            ownership: InferredOwnership::Owned,
783            confidence: 0.9,
784            method: ClassificationMethod::Hybrid,
785            rule_result: Some(InferredOwnership::Owned),
786            ml_result: Some(OwnershipPrediction {
787                kind: InferredOwnership::Owned,
788                confidence: 0.9,
789                fallback: None,
790            }),
791            reasoning: "agree".to_string(),
792        };
793
794        // Disagreement
795        let disagree = HybridResult {
796            variable: "b".to_string(),
797            ownership: InferredOwnership::Owned,
798            confidence: 0.9,
799            method: ClassificationMethod::Fallback,
800            rule_result: Some(InferredOwnership::Owned),
801            ml_result: Some(OwnershipPrediction {
802                kind: InferredOwnership::Borrowed,
803                confidence: 0.3,
804                fallback: None,
805            }),
806            reasoning: "disagree".to_string(),
807        };
808
809        metrics.record(&agree);
810        metrics.record(&disagree);
811
812        assert_eq!(metrics.agreements, 1);
813        assert_eq!(metrics.disagreements, 1);
814        assert!((metrics.agreement_rate() - 0.5).abs() < 0.001);
815    }
816
817    #[test]
818    fn hybrid_metrics_fallback_rate() {
819        let mut metrics = HybridMetrics::new();
820        metrics.total = 10;
821        metrics.fallback = 3;
822
823        assert!((metrics.fallback_rate() - 0.3).abs() < 0.001);
824    }
825
826    // ========================================================================
827    // ownership_kind_to_inferred tests
828    // ========================================================================
829
830    #[test]
831    fn convert_ownership_kinds() {
832        assert_eq!(
833            ownership_kind_to_inferred(&OwnershipKind::Owning),
834            InferredOwnership::Owned
835        );
836        assert_eq!(
837            ownership_kind_to_inferred(&OwnershipKind::ImmutableBorrow),
838            InferredOwnership::Borrowed
839        );
840        assert_eq!(
841            ownership_kind_to_inferred(&OwnershipKind::MutableBorrow),
842            InferredOwnership::BorrowedMut
843        );
844        assert_eq!(
845            ownership_kind_to_inferred(&OwnershipKind::Unknown),
846            InferredOwnership::RawPointer
847        );
848    }
849
850    #[test]
851    fn convert_array_pointer() {
852        let array_kind = OwnershipKind::ArrayPointer {
853            base_array: "arr".to_string(),
854            element_type: decy_hir::HirType::Int,
855            base_index: Some(0),
856        };
857        assert_eq!(
858            ownership_kind_to_inferred(&array_kind),
859            InferredOwnership::Slice
860        );
861    }
862
863    // ========================================================================
864    // Deep coverage: classify_ensemble all branches
865    // ========================================================================
866
867    #[test]
868    fn classify_ensemble_agreement_confidence_capped_at_one() {
869        // When both have very high confidence, boosted value should cap at 1.0
870        let classifier = HybridClassifier::new();
871
872        let inference = OwnershipInference {
873            variable: "ptr".to_string(),
874            kind: OwnershipKind::Owning,
875            confidence: 0.99, // Very high rule confidence
876            reason: "malloc + free".to_string(),
877        };
878
879        let features = OwnershipFeatures::default();
880        // ML agrees with high confidence
881        let model = MockModel::with_confidence(InferredOwnership::Owned, 0.99);
882
883        let result = classifier.classify_ensemble(&inference, &features, &model);
884
885        assert_eq!(result.method, ClassificationMethod::Hybrid);
886        assert_eq!(result.ownership, InferredOwnership::Owned);
887        // (0.99 + 0.99) / 2 * 1.1 = 1.089 -> capped at 1.0
888        assert!((result.confidence - 1.0).abs() < 0.001);
889        assert!(result.reasoning.contains("boosted"));
890    }
891
892    #[test]
893    fn classify_ensemble_agreement_low_confidence_boosted() {
894        let classifier = HybridClassifier::new();
895
896        let inference = OwnershipInference {
897            variable: "ptr".to_string(),
898            kind: OwnershipKind::ImmutableBorrow,
899            confidence: 0.4,
900            reason: "parameter read-only".to_string(),
901        };
902
903        let features = OwnershipFeatures::default();
904        let model = MockModel::with_confidence(InferredOwnership::Borrowed, 0.5);
905
906        let result = classifier.classify_ensemble(&inference, &features, &model);
907
908        assert_eq!(result.method, ClassificationMethod::Hybrid);
909        assert_eq!(result.ownership, InferredOwnership::Borrowed);
910        // (0.4 + 0.5) / 2 * 1.1 = 0.495
911        assert!(result.confidence > 0.49);
912        assert!(result.confidence < 0.51);
913    }
914
915    #[test]
916    fn classify_ensemble_disagreement_ml_wins_with_exact_equality() {
917        // Edge: ML confidence equals rules confidence => rules win (else branch)
918        let classifier = HybridClassifier::new();
919
920        let inference = OwnershipInference {
921            variable: "ptr".to_string(),
922            kind: OwnershipKind::Owning,
923            confidence: 0.7, // Same as ML
924            reason: "allocation".to_string(),
925        };
926
927        let features = OwnershipFeatures::default();
928        let model = MockModel::with_confidence(InferredOwnership::Borrowed, 0.7);
929
930        let result = classifier.classify_ensemble(&inference, &features, &model);
931
932        // They disagree, confidence is equal => rules win (ml_conf > inference.confidence is false)
933        assert_eq!(result.method, ClassificationMethod::RuleBased);
934        assert_eq!(result.ownership, InferredOwnership::Owned);
935        assert!(result.reasoning.contains("Rules win"));
936    }
937
938    #[test]
939    fn classify_ensemble_disagreement_ml_wins_clearly() {
940        let classifier = HybridClassifier::new();
941
942        let inference = OwnershipInference {
943            variable: "ptr".to_string(),
944            kind: OwnershipKind::Unknown, // Low confidence from rules
945            confidence: 0.3,
946            reason: "unknown pattern".to_string(),
947        };
948
949        let features = OwnershipFeatures::default();
950        let model = MockModel::with_confidence(InferredOwnership::Vec, 0.95);
951
952        let result = classifier.classify_ensemble(&inference, &features, &model);
953
954        assert_eq!(result.method, ClassificationMethod::MachineLearning);
955        assert_eq!(result.ownership, InferredOwnership::Vec);
956        assert!((result.confidence - 0.95).abs() < 0.001);
957        assert!(result.reasoning.contains("ML wins"));
958    }
959
960    #[test]
961    fn classify_ensemble_disagreement_rules_win_clearly() {
962        let classifier = HybridClassifier::new();
963
964        let inference = OwnershipInference {
965            variable: "ptr".to_string(),
966            kind: OwnershipKind::Owning,
967            confidence: 0.95,
968            reason: "malloc with free".to_string(),
969        };
970
971        let features = OwnershipFeatures::default();
972        let model = MockModel::with_confidence(InferredOwnership::Borrowed, 0.2);
973
974        let result = classifier.classify_ensemble(&inference, &features, &model);
975
976        assert_eq!(result.method, ClassificationMethod::RuleBased);
977        assert_eq!(result.ownership, InferredOwnership::Owned);
978        assert!((result.confidence - 0.95).abs() < 0.001);
979        assert!(result.reasoning.contains("Rules win"));
980        assert!(result.ml_result.is_some());
981        assert!(result.rule_result.is_some());
982    }
983
984    #[test]
985    fn classify_ensemble_all_ownership_kinds() {
986        // Test ensemble with different ownership kind conversions
987        let classifier = HybridClassifier::new();
988        let features = OwnershipFeatures::default();
989
990        // MutableBorrow
991        let inference_mut = OwnershipInference {
992            variable: "ptr".to_string(),
993            kind: OwnershipKind::MutableBorrow,
994            confidence: 0.7,
995            reason: "mutation detected".to_string(),
996        };
997        let model_agree = MockModel::with_confidence(InferredOwnership::BorrowedMut, 0.8);
998        let result = classifier.classify_ensemble(&inference_mut, &features, &model_agree);
999        assert_eq!(result.method, ClassificationMethod::Hybrid);
1000        assert_eq!(result.ownership, InferredOwnership::BorrowedMut);
1001
1002        // Unknown -> RawPointer
1003        let inference_unknown = OwnershipInference {
1004            variable: "ptr".to_string(),
1005            kind: OwnershipKind::Unknown,
1006            confidence: 0.3,
1007            reason: "uncertain".to_string(),
1008        };
1009        let model_agree_raw = MockModel::with_confidence(InferredOwnership::RawPointer, 0.4);
1010        let result = classifier.classify_ensemble(&inference_unknown, &features, &model_agree_raw);
1011        assert_eq!(result.method, ClassificationMethod::Hybrid);
1012        assert_eq!(result.ownership, InferredOwnership::RawPointer);
1013
1014        // ImmutableBorrow → Borrowed
1015        let inference_immut = OwnershipInference {
1016            variable: "data".to_string(),
1017            kind: OwnershipKind::ImmutableBorrow,
1018            confidence: 0.85,
1019            reason: "read-only access".to_string(),
1020        };
1021        let model_agree_borrow = MockModel::with_confidence(InferredOwnership::Borrowed, 0.9);
1022        let result =
1023            classifier.classify_ensemble(&inference_immut, &features, &model_agree_borrow);
1024        assert_eq!(result.method, ClassificationMethod::Hybrid);
1025        assert_eq!(result.ownership, InferredOwnership::Borrowed);
1026
1027        // ArrayPointer → Slice
1028        let inference_arr = OwnershipInference {
1029            variable: "arr".to_string(),
1030            kind: OwnershipKind::ArrayPointer {
1031                base_array: "arr".to_string(),
1032                element_type: decy_hir::HirType::Int,
1033                base_index: Some(0),
1034            },
1035            confidence: 0.75,
1036            reason: "array parameter".to_string(),
1037        };
1038        let model_agree_slice = MockModel::with_confidence(InferredOwnership::Slice, 0.8);
1039        let result =
1040            classifier.classify_ensemble(&inference_arr, &features, &model_agree_slice);
1041        assert_eq!(result.method, ClassificationMethod::Hybrid);
1042        assert_eq!(result.ownership, InferredOwnership::Slice);
1043    }
1044
1045    #[test]
1046    fn classify_ensemble_immutable_borrow_disagreement() {
1047        // Rules say ImmutableBorrow (Borrowed), ML says Owned → ML wins with higher confidence
1048        let classifier = HybridClassifier::new();
1049        let features = OwnershipFeatures::default();
1050        let inference = OwnershipInference {
1051            variable: "ptr".to_string(),
1052            kind: OwnershipKind::ImmutableBorrow,
1053            confidence: 0.5,
1054            reason: "read-only".to_string(),
1055        };
1056        let model = MockModel::with_confidence(InferredOwnership::Owned, 0.9);
1057        let result = classifier.classify_ensemble(&inference, &features, &model);
1058        assert_eq!(result.method, ClassificationMethod::MachineLearning);
1059        assert_eq!(result.ownership, InferredOwnership::Owned);
1060    }
1061
1062    #[test]
1063    fn classify_ensemble_array_pointer_disagree_rules_win() {
1064        // Rules say ArrayPointer (Slice), ML says BorrowedMut → rules win
1065        let classifier = HybridClassifier::new();
1066        let features = OwnershipFeatures::default();
1067        let inference = OwnershipInference {
1068            variable: "buf".to_string(),
1069            kind: OwnershipKind::ArrayPointer {
1070                base_array: "buf".to_string(),
1071                element_type: decy_hir::HirType::Char,
1072                base_index: None,
1073            },
1074            confidence: 0.9,
1075            reason: "array pattern".to_string(),
1076        };
1077        let model = MockModel::with_confidence(InferredOwnership::BorrowedMut, 0.3);
1078        let result = classifier.classify_ensemble(&inference, &features, &model);
1079        assert_eq!(result.method, ClassificationMethod::RuleBased);
1080        assert_eq!(result.ownership, InferredOwnership::Slice);
1081    }
1082
1083    // ========================================================================
1084    // classify_hybrid: additional branch coverage
1085    // ========================================================================
1086
1087    #[test]
1088    fn classify_hybrid_at_exact_threshold() {
1089        // ML confidence clearly above threshold => should use ML
1090        // Note: confidence is f32 in OwnershipPrediction, compared as f64
1091        // so we use a value clearly above to avoid f32 precision issues
1092        let mut classifier = HybridClassifier::with_threshold(0.5);
1093        classifier.enable_ml();
1094
1095        let inference = OwnershipInference {
1096            variable: "ptr".to_string(),
1097            kind: OwnershipKind::Unknown,
1098            confidence: 0.3,
1099            reason: "uncertain".to_string(),
1100        };
1101
1102        let features = OwnershipFeatures::default();
1103        let model = MockModel::with_confidence(InferredOwnership::Owned, 0.75);
1104
1105        let result = classifier.classify_hybrid(&inference, &features, &model);
1106
1107        // 0.75 >= 0.5 => use ML
1108        assert_eq!(result.method, ClassificationMethod::MachineLearning);
1109        assert_eq!(result.ownership, InferredOwnership::Owned);
1110        assert!(result.reasoning.contains("ML prediction"));
1111    }
1112
1113    #[test]
1114    fn classify_hybrid_just_below_threshold() {
1115        let mut classifier = HybridClassifier::with_threshold(0.65);
1116        classifier.enable_ml();
1117
1118        let inference = OwnershipInference {
1119            variable: "ptr".to_string(),
1120            kind: OwnershipKind::Owning,
1121            confidence: 0.9,
1122            reason: "malloc".to_string(),
1123        };
1124
1125        let features = OwnershipFeatures::default();
1126        let model = MockModel::with_confidence(InferredOwnership::Borrowed, 0.6499);
1127
1128        let result = classifier.classify_hybrid(&inference, &features, &model);
1129
1130        // 0.6499 < 0.65 => fallback to rules
1131        assert_eq!(result.method, ClassificationMethod::Fallback);
1132        assert_eq!(result.ownership, InferredOwnership::Owned);
1133        assert!(result.reasoning.contains("Fallback"));
1134    }
1135
1136    // ========================================================================
1137    // HybridClassifier: set_threshold coverage
1138    // ========================================================================
1139
1140    #[test]
1141    fn set_threshold_clamps() {
1142        let mut classifier = HybridClassifier::new();
1143        classifier.set_threshold(2.0);
1144        assert!((classifier.confidence_threshold() - 1.0).abs() < 0.001);
1145
1146        classifier.set_threshold(-1.0);
1147        assert!((classifier.confidence_threshold() - 0.0).abs() < 0.001);
1148
1149        classifier.set_threshold(0.42);
1150        assert!((classifier.confidence_threshold() - 0.42).abs() < 0.001);
1151    }
1152
1153    // ========================================================================
1154    // HybridMetrics: additional coverage
1155    // ========================================================================
1156
1157    #[test]
1158    fn hybrid_metrics_record_hybrid_method() {
1159        let mut metrics = HybridMetrics::new();
1160        let result = HybridResult {
1161            variable: "x".to_string(),
1162            ownership: InferredOwnership::Owned,
1163            confidence: 0.9,
1164            method: ClassificationMethod::Hybrid,
1165            rule_result: Some(InferredOwnership::Owned),
1166            ml_result: Some(OwnershipPrediction {
1167                kind: InferredOwnership::Owned,
1168                confidence: 0.9,
1169                fallback: None,
1170            }),
1171            reasoning: "hybrid agreement".to_string(),
1172        };
1173        metrics.record(&result);
1174
1175        assert_eq!(metrics.hybrid, 1);
1176        assert_eq!(metrics.total, 1);
1177        assert_eq!(metrics.agreements, 1);
1178        assert_eq!(metrics.disagreements, 0);
1179    }
1180
1181    #[test]
1182    fn hybrid_metrics_agreement_rate_no_comparisons() {
1183        let metrics = HybridMetrics::new();
1184        // No comparisons = perfect agreement by default
1185        assert!((metrics.agreement_rate() - 1.0).abs() < 0.001);
1186    }
1187
1188    #[test]
1189    fn hybrid_metrics_all_methods_tracked() {
1190        let mut metrics = HybridMetrics::new();
1191
1192        // Record one of each method
1193        for (method, rule_own, ml_own) in [
1194            (ClassificationMethod::RuleBased, InferredOwnership::Owned, InferredOwnership::Owned),
1195            (ClassificationMethod::MachineLearning, InferredOwnership::Borrowed, InferredOwnership::Borrowed),
1196            (ClassificationMethod::Fallback, InferredOwnership::Owned, InferredOwnership::Borrowed),
1197            (ClassificationMethod::Hybrid, InferredOwnership::Vec, InferredOwnership::Vec),
1198        ] {
1199            let result = HybridResult {
1200                variable: "x".to_string(),
1201                ownership: rule_own,
1202                confidence: 0.8,
1203                method,
1204                rule_result: Some(rule_own),
1205                ml_result: Some(OwnershipPrediction {
1206                    kind: ml_own,
1207                    confidence: 0.8,
1208                    fallback: None,
1209                }),
1210                reasoning: "test".to_string(),
1211            };
1212            metrics.record(&result);
1213        }
1214
1215        assert_eq!(metrics.total, 4);
1216        assert_eq!(metrics.rule_based, 1);
1217        assert_eq!(metrics.ml_used, 1);
1218        assert_eq!(metrics.fallback, 1);
1219        assert_eq!(metrics.hybrid, 1);
1220        // agreements: RuleBased (same), MachineLearning (same), Hybrid (same) = 3
1221        // disagreements: Fallback (Owned vs Borrowed) = 1
1222        assert_eq!(metrics.agreements, 3);
1223        assert_eq!(metrics.disagreements, 1);
1224    }
1225
1226    #[test]
1227    fn hybrid_result_ml_not_rejected_without_ml_result() {
1228        let result = HybridResult {
1229            variable: "x".to_string(),
1230            ownership: InferredOwnership::Owned,
1231            confidence: 0.8,
1232            method: ClassificationMethod::Fallback,
1233            rule_result: Some(InferredOwnership::Owned),
1234            ml_result: None, // No ML result
1235            reasoning: "rules only".to_string(),
1236        };
1237
1238        assert!(result.used_fallback());
1239        // ml_rejected requires ml_result.is_some() AND Fallback method
1240        assert!(!result.ml_rejected());
1241    }
1242
1243    // ========================================================================
1244    // NullModel: batch predict coverage
1245    // ========================================================================
1246
1247    #[test]
1248    fn null_model_batch_predict() {
1249        let model = NullModel;
1250        let features = vec![
1251            OwnershipFeatures::default(),
1252            OwnershipFeatures::default(),
1253            OwnershipFeatures::default(),
1254        ];
1255        let predictions = model.predict_batch(&features);
1256        assert_eq!(predictions.len(), 3);
1257        for pred in &predictions {
1258            assert_eq!(pred.kind, InferredOwnership::RawPointer);
1259            assert!((pred.confidence as f64).abs() < 0.001);
1260        }
1261    }
1262
1263    // ========================================================================
1264    // Default implementation
1265    // ========================================================================
1266
1267    /// Custom model that does NOT override name() - uses default "unknown"
1268    struct AnonymousModel;
1269
1270    impl OwnershipModel for AnonymousModel {
1271        fn predict(&self, _features: &OwnershipFeatures) -> OwnershipPrediction {
1272            OwnershipPrediction {
1273                kind: InferredOwnership::Owned,
1274                confidence: 0.5,
1275                fallback: None,
1276            }
1277        }
1278        // Note: does NOT override name() — uses default "unknown"
1279    }
1280
1281    #[test]
1282    fn ownership_model_default_name() {
1283        let model = AnonymousModel;
1284        assert_eq!(model.name(), "unknown");
1285    }
1286
1287    #[test]
1288    fn ownership_model_default_predict_batch() {
1289        let model = AnonymousModel;
1290        let features = vec![OwnershipFeatures::default(); 2];
1291        let predictions = model.predict_batch(&features);
1292        assert_eq!(predictions.len(), 2);
1293        assert_eq!(predictions[0].kind, InferredOwnership::Owned);
1294    }
1295
1296    #[test]
1297    fn hybrid_classifier_default_impl() {
1298        let classifier = HybridClassifier::default();
1299        assert!(!classifier.ml_enabled());
1300        assert!((classifier.confidence_threshold() - DEFAULT_CONFIDENCE_THRESHOLD).abs() < 0.001);
1301    }
1302}