Skip to main content

decy_ownership/
error_tracking.rs

1//! CITL-based error tracking for ownership inference (DECY-ML-015).
2//!
3//! Integrates with entrenar's CITL module to track and analyze
4//! ownership inference errors using Tarantula fault localization.
5//!
6//! # Architecture
7//!
8//! ```text
9//! ┌─────────────────────────────────────────────────────────────────┐
10//! │                   ERROR TRACKING PIPELINE                       │
11//! │                                                                 │
12//! │  Ownership        ┌──────────────┐     ┌────────────────┐      │
13//! │  Inference  ─────►│ Error        │────►│ CITL           │      │
14//! │  Result           │ Collector    │     │ Analysis       │      │
15//! │                   └──────────────┘     └────────────────┘      │
16//! │                          │                    │                 │
17//! │                          ▼                    ▼                 │
18//! │                   ┌──────────────┐     ┌────────────────┐      │
19//! │                   │ Error        │     │ Suspiciousness │      │
20//! │                   │ Database     │     │ Scores         │      │
21//! │                   └──────────────┘     └────────────────┘      │
22//! │                                               │                 │
23//! │                                               ▼                 │
24//! │                                        ┌────────────────┐      │
25//! │                                        │ Improvement    │      │
26//! │                                        │ Suggestions    │      │
27//! │                                        └────────────────┘      │
28//! └─────────────────────────────────────────────────────────────────┘
29//! ```
30//!
31//! # Toyota Way: Hansei (Reflection)
32//!
33//! Error tracking embodies Hansei by:
34//! - Reflecting on every inference failure
35//! - Identifying root causes through CITL analysis
36//! - Using insights to improve the model
37
38use crate::ml_features::{InferredOwnership, OwnershipDefect};
39use serde::{Deserialize, Serialize};
40use std::collections::HashMap;
41
42/// An ownership inference error record.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct InferenceError {
45    /// Unique error ID
46    pub id: u64,
47    /// Variable name
48    pub variable: String,
49    /// Source file
50    pub source_file: String,
51    /// Source line
52    pub source_line: u32,
53    /// Predicted ownership
54    pub predicted: InferredOwnership,
55    /// Expected ownership (ground truth)
56    pub expected: InferredOwnership,
57    /// Confidence of prediction
58    pub confidence: f64,
59    /// C features present in context
60    pub c_features: Vec<String>,
61    /// Rust error code (if compilation failed)
62    pub rust_error: Option<String>,
63    /// Defect category
64    pub defect: OwnershipDefect,
65    /// Timestamp
66    pub timestamp: u64,
67}
68
69impl InferenceError {
70    /// Create a new inference error.
71    pub fn new(
72        variable: impl Into<String>,
73        source_file: impl Into<String>,
74        source_line: u32,
75        predicted: InferredOwnership,
76        expected: InferredOwnership,
77        confidence: f64,
78        defect: OwnershipDefect,
79    ) -> Self {
80        let now = std::time::SystemTime::now()
81            .duration_since(std::time::UNIX_EPOCH)
82            .unwrap_or_default()
83            .as_millis() as u64;
84
85        Self {
86            id: 0,
87            variable: variable.into(),
88            source_file: source_file.into(),
89            source_line,
90            predicted,
91            expected,
92            confidence: confidence.clamp(0.0, 1.0),
93            c_features: Vec::new(),
94            rust_error: None,
95            defect,
96            timestamp: now,
97        }
98    }
99
100    /// Add C features to the error.
101    pub fn with_features(mut self, features: Vec<String>) -> Self {
102        self.c_features = features;
103        self
104    }
105
106    /// Add Rust error code.
107    pub fn with_rust_error(mut self, error: impl Into<String>) -> Self {
108        self.rust_error = Some(error.into());
109        self
110    }
111}
112
113/// Statistics for a specific error pattern.
114#[derive(Debug, Clone, Default, Serialize, Deserialize)]
115pub struct PatternStats {
116    /// Number of occurrences
117    pub count: u64,
118    /// Number of times pattern led to failure
119    pub failure_count: u64,
120    /// Number of times pattern led to success
121    pub success_count: u64,
122    /// Tarantula suspiciousness score
123    pub suspiciousness: f64,
124}
125
126impl PatternStats {
127    /// Calculate failure rate.
128    pub fn failure_rate(&self) -> f64 {
129        if self.count == 0 {
130            0.0
131        } else {
132            self.failure_count as f64 / self.count as f64
133        }
134    }
135
136    /// Update with new outcome.
137    pub fn record(&mut self, is_failure: bool) {
138        self.count += 1;
139        if is_failure {
140            self.failure_count += 1;
141        } else {
142            self.success_count += 1;
143        }
144    }
145}
146
147/// Suspiciousness score for a C feature.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct FeatureSuspiciousness {
150    /// Feature name
151    pub feature: String,
152    /// Tarantula suspiciousness score (0.0 - 1.0)
153    pub score: f64,
154    /// Total occurrences
155    pub total_count: u64,
156    /// Failure count
157    pub failure_count: u64,
158    /// Success count
159    pub success_count: u64,
160}
161
162impl FeatureSuspiciousness {
163    /// Check if feature is suspicious (score > 0.5).
164    pub fn is_suspicious(&self) -> bool {
165        self.score > 0.5
166    }
167
168    /// Check if feature is highly suspicious (score > 0.7).
169    pub fn is_highly_suspicious(&self) -> bool {
170        self.score > 0.7
171    }
172}
173
174/// Error tracker using CITL-style analysis.
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct ErrorTracker {
177    /// All recorded errors
178    errors: Vec<InferenceError>,
179    /// Pattern statistics by C feature
180    feature_stats: HashMap<String, PatternStats>,
181    /// Pattern statistics by defect category
182    defect_stats: HashMap<String, PatternStats>,
183    /// Pattern statistics by (feature, defect) combination
184    feature_defect_stats: HashMap<(String, String), PatternStats>,
185    /// Total successes (for Tarantula calculation)
186    total_successes: u64,
187    /// Total failures (for Tarantula calculation)
188    total_failures: u64,
189    /// Next error ID
190    next_id: u64,
191}
192
193impl Default for ErrorTracker {
194    fn default() -> Self {
195        Self::new()
196    }
197}
198
199impl ErrorTracker {
200    /// Create a new error tracker.
201    pub fn new() -> Self {
202        Self {
203            errors: Vec::new(),
204            feature_stats: HashMap::new(),
205            defect_stats: HashMap::new(),
206            feature_defect_stats: HashMap::new(),
207            total_successes: 0,
208            total_failures: 0,
209            next_id: 1,
210        }
211    }
212
213    /// Record an inference error.
214    pub fn record_error(&mut self, mut error: InferenceError) {
215        error.id = self.next_id;
216        self.next_id += 1;
217        self.total_failures += 1;
218
219        // Update feature stats
220        for feature in &error.c_features {
221            self.feature_stats
222                .entry(feature.clone())
223                .or_default()
224                .record(true);
225        }
226
227        // Update defect stats
228        let defect_key = format!("{:?}", error.defect);
229        self.defect_stats
230            .entry(defect_key.clone())
231            .or_default()
232            .record(true);
233
234        // Update feature-defect combination stats
235        for feature in &error.c_features {
236            self.feature_defect_stats
237                .entry((feature.clone(), defect_key.clone()))
238                .or_default()
239                .record(true);
240        }
241
242        self.errors.push(error);
243    }
244
245    /// Record a successful inference (no error).
246    pub fn record_success(&mut self, c_features: &[String]) {
247        self.total_successes += 1;
248
249        for feature in c_features {
250            self.feature_stats
251                .entry(feature.clone())
252                .or_default()
253                .record(false);
254        }
255    }
256
257    /// Get all errors.
258    pub fn errors(&self) -> &[InferenceError] {
259        &self.errors
260    }
261
262    /// Get error count.
263    pub fn error_count(&self) -> usize {
264        self.errors.len()
265    }
266
267    /// Get total successes.
268    pub fn success_count(&self) -> u64 {
269        self.total_successes
270    }
271
272    /// Calculate Tarantula suspiciousness for all features.
273    ///
274    /// Tarantula formula:
275    /// suspiciousness = (failed(e) / total_failed) / ((failed(e) / total_failed) + (passed(e) / total_passed))
276    pub fn calculate_suspiciousness(&mut self) -> Vec<FeatureSuspiciousness> {
277        let total_failed = self.total_failures.max(1) as f64;
278        let total_passed = self.total_successes.max(1) as f64;
279
280        let mut results = Vec::new();
281
282        for (feature, stats) in &mut self.feature_stats {
283            let failed_ratio = stats.failure_count as f64 / total_failed;
284            let passed_ratio = stats.success_count as f64 / total_passed;
285
286            let suspiciousness = if failed_ratio + passed_ratio > 0.0 {
287                failed_ratio / (failed_ratio + passed_ratio)
288            } else {
289                0.0
290            };
291
292            stats.suspiciousness = suspiciousness;
293
294            results.push(FeatureSuspiciousness {
295                feature: feature.clone(),
296                score: suspiciousness,
297                total_count: stats.count,
298                failure_count: stats.failure_count,
299                success_count: stats.success_count,
300            });
301        }
302
303        // Sort by suspiciousness (highest first)
304        results.sort_by(|a, b| {
305            b.score
306                .partial_cmp(&a.score)
307                .unwrap_or(std::cmp::Ordering::Equal)
308        });
309
310        results
311    }
312
313    /// Get top N most suspicious features.
314    pub fn top_suspicious(&mut self, n: usize) -> Vec<FeatureSuspiciousness> {
315        let all = self.calculate_suspiciousness();
316        all.into_iter().take(n).collect()
317    }
318
319    /// Get errors by defect category.
320    pub fn errors_by_defect(&self, defect: &OwnershipDefect) -> Vec<&InferenceError> {
321        let defect_key = format!("{:?}", defect);
322        self.errors
323            .iter()
324            .filter(|e| format!("{:?}", e.defect) == defect_key)
325            .collect()
326    }
327
328    /// Get errors by C feature.
329    pub fn errors_by_feature(&self, feature: &str) -> Vec<&InferenceError> {
330        self.errors
331            .iter()
332            .filter(|e| e.c_features.contains(&feature.to_string()))
333            .collect()
334    }
335
336    /// Get defect distribution.
337    pub fn defect_distribution(&self) -> HashMap<String, u64> {
338        let mut dist = HashMap::new();
339        for error in &self.errors {
340            *dist.entry(format!("{:?}", error.defect)).or_insert(0) += 1;
341        }
342        dist
343    }
344
345    /// Get feature distribution among errors.
346    pub fn feature_distribution(&self) -> HashMap<String, u64> {
347        let mut dist = HashMap::new();
348        for error in &self.errors {
349            for feature in &error.c_features {
350                *dist.entry(feature.clone()).or_insert(0) += 1;
351            }
352        }
353        dist
354    }
355
356    /// Get correlation between features and defects.
357    pub fn feature_defect_correlation(&self) -> Vec<(String, String, u64)> {
358        self.feature_defect_stats
359            .iter()
360            .map(|((f, d), stats)| (f.clone(), d.clone(), stats.failure_count))
361            .collect()
362    }
363
364    /// Generate improvement suggestions based on analysis.
365    pub fn generate_suggestions(&mut self) -> Vec<ImprovementSuggestion> {
366        let suspicious = self.top_suspicious(5);
367        let defect_dist = self.defect_distribution();
368
369        let mut suggestions = Vec::new();
370
371        // Suggest improvements for highly suspicious features
372        for fs in suspicious {
373            if fs.is_highly_suspicious() {
374                suggestions.push(ImprovementSuggestion {
375                    priority: SuggestionPriority::High,
376                    category: SuggestionCategory::FeatureHandling,
377                    description: format!(
378                        "Improve handling of '{}' (suspiciousness: {:.2}, {} failures)",
379                        fs.feature, fs.score, fs.failure_count
380                    ),
381                    affected_feature: Some(fs.feature),
382                    affected_defect: None,
383                });
384            }
385        }
386
387        // Suggest improvements for common defects
388        let mut defects: Vec<_> = defect_dist.into_iter().collect();
389        defects.sort_by(|a, b| b.1.cmp(&a.1));
390
391        for (defect, count) in defects.iter().take(3) {
392            if *count > 5 {
393                suggestions.push(ImprovementSuggestion {
394                    priority: if *count > 20 {
395                        SuggestionPriority::High
396                    } else {
397                        SuggestionPriority::Medium
398                    },
399                    category: SuggestionCategory::DefectPrevention,
400                    description: format!(
401                        "Address {} defect category ({} occurrences)",
402                        defect, count
403                    ),
404                    affected_feature: None,
405                    affected_defect: Some(defect.clone()),
406                });
407            }
408        }
409
410        suggestions
411    }
412
413    /// Generate markdown report.
414    pub fn generate_markdown_report(&mut self) -> String {
415        let suspicious = self.top_suspicious(10);
416        let defect_dist = self.defect_distribution();
417        let suggestions = self.generate_suggestions();
418
419        let mut report = String::from("## Error Tracking Report (CITL Analysis)\n\n");
420
421        // Summary
422        report.push_str(&format!(
423            "### Summary\n\n\
424            | Metric | Value |\n\
425            |--------|-------|\n\
426            | Total Errors | {} |\n\
427            | Total Successes | {} |\n\
428            | Error Rate | {:.1}% |\n\n",
429            self.error_count(),
430            self.success_count(),
431            if self.error_count() + self.success_count() as usize > 0 {
432                (self.error_count() as f64
433                    / (self.error_count() + self.success_count() as usize) as f64)
434                    * 100.0
435            } else {
436                0.0
437            }
438        ));
439
440        // Suspicious features
441        report.push_str("### Top Suspicious Features (Tarantula)\n\n");
442        report.push_str("| Feature | Score | Failures | Successes |\n");
443        report.push_str("|---------|-------|----------|----------|\n");
444        for fs in suspicious.iter().take(5) {
445            report.push_str(&format!(
446                "| {} | {:.2} | {} | {} |\n",
447                fs.feature, fs.score, fs.failure_count, fs.success_count
448            ));
449        }
450        report.push('\n');
451
452        // Defect distribution
453        report.push_str("### Defect Distribution\n\n");
454        let mut defects: Vec<_> = defect_dist.into_iter().collect();
455        defects.sort_by(|a, b| b.1.cmp(&a.1));
456        for (defect, count) in defects.iter().take(5) {
457            report.push_str(&format!(
458                "- {}: {} ({:.1}%)\n",
459                defect,
460                count,
461                (*count as f64 / self.error_count().max(1) as f64) * 100.0
462            ));
463        }
464        report.push('\n');
465
466        // Suggestions
467        if !suggestions.is_empty() {
468            report.push_str("### Improvement Suggestions\n\n");
469            for (i, s) in suggestions.iter().enumerate() {
470                report.push_str(&format!(
471                    "{}. **[{:?}]** {}\n",
472                    i + 1,
473                    s.priority,
474                    s.description
475                ));
476            }
477        }
478
479        report
480    }
481}
482
483/// Priority level for improvement suggestions.
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
485pub enum SuggestionPriority {
486    /// Critical - fix immediately
487    Critical,
488    /// High - fix soon
489    High,
490    /// Medium - plan to fix
491    Medium,
492    /// Low - nice to have
493    Low,
494}
495
496/// Category of improvement suggestion.
497#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
498pub enum SuggestionCategory {
499    /// Improve handling of specific C feature
500    FeatureHandling,
501    /// Prevent specific defect category
502    DefectPrevention,
503    /// Add training data for pattern
504    TrainingData,
505    /// Adjust model configuration
506    Configuration,
507}
508
509/// An improvement suggestion based on error analysis.
510#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct ImprovementSuggestion {
512    /// Priority level
513    pub priority: SuggestionPriority,
514    /// Category
515    pub category: SuggestionCategory,
516    /// Description
517    pub description: String,
518    /// Affected C feature (if any)
519    pub affected_feature: Option<String>,
520    /// Affected defect category (if any)
521    pub affected_defect: Option<String>,
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527
528    // ========================================================================
529    // InferenceError tests
530    // ========================================================================
531
532    #[test]
533    fn inference_error_new() {
534        let error = InferenceError::new(
535            "ptr",
536            "test.c",
537            42,
538            InferredOwnership::Borrowed,
539            InferredOwnership::Owned,
540            0.6,
541            OwnershipDefect::PointerMisclassification,
542        );
543
544        assert_eq!(error.variable, "ptr");
545        assert_eq!(error.source_file, "test.c");
546        assert_eq!(error.source_line, 42);
547    }
548
549    #[test]
550    fn inference_error_with_features() {
551        let error = InferenceError::new(
552            "ptr",
553            "test.c",
554            42,
555            InferredOwnership::Borrowed,
556            InferredOwnership::Owned,
557            0.6,
558            OwnershipDefect::PointerMisclassification,
559        )
560        .with_features(vec![
561            "malloc_free".to_string(),
562            "pointer_arithmetic".to_string(),
563        ]);
564
565        assert_eq!(error.c_features.len(), 2);
566    }
567
568    #[test]
569    fn inference_error_with_rust_error() {
570        let error = InferenceError::new(
571            "ptr",
572            "test.c",
573            42,
574            InferredOwnership::Borrowed,
575            InferredOwnership::Owned,
576            0.6,
577            OwnershipDefect::PointerMisclassification,
578        )
579        .with_rust_error("E0382");
580
581        assert_eq!(error.rust_error, Some("E0382".to_string()));
582    }
583
584    // ========================================================================
585    // PatternStats tests
586    // ========================================================================
587
588    #[test]
589    fn pattern_stats_default() {
590        let stats = PatternStats::default();
591        assert_eq!(stats.count, 0);
592        assert_eq!(stats.failure_rate(), 0.0);
593    }
594
595    #[test]
596    fn pattern_stats_record() {
597        let mut stats = PatternStats::default();
598        stats.record(true); // failure
599        stats.record(false); // success
600        stats.record(true); // failure
601
602        assert_eq!(stats.count, 3);
603        assert_eq!(stats.failure_count, 2);
604        assert_eq!(stats.success_count, 1);
605        assert!((stats.failure_rate() - 0.666).abs() < 0.01);
606    }
607
608    // ========================================================================
609    // FeatureSuspiciousness tests
610    // ========================================================================
611
612    #[test]
613    fn feature_suspiciousness_thresholds() {
614        let low = FeatureSuspiciousness {
615            feature: "test".to_string(),
616            score: 0.3,
617            total_count: 10,
618            failure_count: 3,
619            success_count: 7,
620        };
621        assert!(!low.is_suspicious());
622        assert!(!low.is_highly_suspicious());
623
624        let medium = FeatureSuspiciousness {
625            feature: "test".to_string(),
626            score: 0.6,
627            total_count: 10,
628            failure_count: 6,
629            success_count: 4,
630        };
631        assert!(medium.is_suspicious());
632        assert!(!medium.is_highly_suspicious());
633
634        let high = FeatureSuspiciousness {
635            feature: "test".to_string(),
636            score: 0.8,
637            total_count: 10,
638            failure_count: 8,
639            success_count: 2,
640        };
641        assert!(high.is_suspicious());
642        assert!(high.is_highly_suspicious());
643    }
644
645    // ========================================================================
646    // ErrorTracker tests
647    // ========================================================================
648
649    #[test]
650    fn error_tracker_new() {
651        let tracker = ErrorTracker::new();
652        assert_eq!(tracker.error_count(), 0);
653        assert_eq!(tracker.success_count(), 0);
654    }
655
656    #[test]
657    fn error_tracker_record_error() {
658        let mut tracker = ErrorTracker::new();
659
660        let error = InferenceError::new(
661            "ptr",
662            "test.c",
663            42,
664            InferredOwnership::Borrowed,
665            InferredOwnership::Owned,
666            0.6,
667            OwnershipDefect::PointerMisclassification,
668        )
669        .with_features(vec!["malloc_free".to_string()]);
670
671        tracker.record_error(error);
672
673        assert_eq!(tracker.error_count(), 1);
674        assert_eq!(tracker.errors()[0].id, 1);
675    }
676
677    #[test]
678    fn error_tracker_record_success() {
679        let mut tracker = ErrorTracker::new();
680
681        tracker.record_success(&["malloc_free".to_string()]);
682
683        assert_eq!(tracker.success_count(), 1);
684        assert!(tracker.feature_stats.contains_key("malloc_free"));
685    }
686
687    #[test]
688    fn error_tracker_calculate_suspiciousness() {
689        let mut tracker = ErrorTracker::new();
690
691        // Feature A: 8 failures, 2 successes (high suspiciousness)
692        for _ in 0..8 {
693            let error = InferenceError::new(
694                "ptr",
695                "test.c",
696                42,
697                InferredOwnership::Borrowed,
698                InferredOwnership::Owned,
699                0.6,
700                OwnershipDefect::PointerMisclassification,
701            )
702            .with_features(vec!["feature_a".to_string()]);
703            tracker.record_error(error);
704        }
705        for _ in 0..2 {
706            tracker.record_success(&["feature_a".to_string()]);
707        }
708
709        // Feature B: 2 failures, 8 successes (low suspiciousness)
710        for _ in 0..2 {
711            let error = InferenceError::new(
712                "ptr",
713                "test.c",
714                42,
715                InferredOwnership::Borrowed,
716                InferredOwnership::Owned,
717                0.6,
718                OwnershipDefect::PointerMisclassification,
719            )
720            .with_features(vec!["feature_b".to_string()]);
721            tracker.record_error(error);
722        }
723        for _ in 0..8 {
724            tracker.record_success(&["feature_b".to_string()]);
725        }
726
727        let suspicious = tracker.calculate_suspiciousness();
728
729        // Feature A should be more suspicious than Feature B
730        let feature_a = suspicious
731            .iter()
732            .find(|s| s.feature == "feature_a")
733            .unwrap();
734        let feature_b = suspicious
735            .iter()
736            .find(|s| s.feature == "feature_b")
737            .unwrap();
738
739        assert!(feature_a.score > feature_b.score);
740    }
741
742    #[test]
743    fn error_tracker_top_suspicious() {
744        let mut tracker = ErrorTracker::new();
745
746        // Add errors with different features
747        for (i, feature) in ["a", "b", "c"].iter().enumerate() {
748            for _ in 0..(i + 1) * 3 {
749                let error = InferenceError::new(
750                    "ptr",
751                    "test.c",
752                    42,
753                    InferredOwnership::Borrowed,
754                    InferredOwnership::Owned,
755                    0.6,
756                    OwnershipDefect::PointerMisclassification,
757                )
758                .with_features(vec![feature.to_string()]);
759                tracker.record_error(error);
760            }
761        }
762
763        let top = tracker.top_suspicious(2);
764        assert_eq!(top.len(), 2);
765    }
766
767    #[test]
768    fn error_tracker_errors_by_defect() {
769        let mut tracker = ErrorTracker::new();
770
771        // Add two different defect types
772        let error1 = InferenceError::new(
773            "ptr1",
774            "test.c",
775            1,
776            InferredOwnership::Borrowed,
777            InferredOwnership::Owned,
778            0.6,
779            OwnershipDefect::PointerMisclassification,
780        );
781        let error2 = InferenceError::new(
782            "ptr2",
783            "test.c",
784            2,
785            InferredOwnership::Borrowed,
786            InferredOwnership::Owned,
787            0.6,
788            OwnershipDefect::LifetimeInferenceGap,
789        );
790        let error3 = InferenceError::new(
791            "ptr3",
792            "test.c",
793            3,
794            InferredOwnership::Borrowed,
795            InferredOwnership::Owned,
796            0.6,
797            OwnershipDefect::PointerMisclassification,
798        );
799
800        tracker.record_error(error1);
801        tracker.record_error(error2);
802        tracker.record_error(error3);
803
804        let pointer_errors = tracker.errors_by_defect(&OwnershipDefect::PointerMisclassification);
805        assert_eq!(pointer_errors.len(), 2);
806    }
807
808    #[test]
809    fn error_tracker_errors_by_feature() {
810        let mut tracker = ErrorTracker::new();
811
812        let error1 = InferenceError::new(
813            "ptr1",
814            "test.c",
815            1,
816            InferredOwnership::Borrowed,
817            InferredOwnership::Owned,
818            0.6,
819            OwnershipDefect::PointerMisclassification,
820        )
821        .with_features(vec!["malloc_free".to_string()]);
822
823        let error2 = InferenceError::new(
824            "ptr2",
825            "test.c",
826            2,
827            InferredOwnership::Borrowed,
828            InferredOwnership::Owned,
829            0.6,
830            OwnershipDefect::PointerMisclassification,
831        )
832        .with_features(vec!["pointer_arithmetic".to_string()]);
833
834        tracker.record_error(error1);
835        tracker.record_error(error2);
836
837        let malloc_errors = tracker.errors_by_feature("malloc_free");
838        assert_eq!(malloc_errors.len(), 1);
839    }
840
841    #[test]
842    fn error_tracker_defect_distribution() {
843        let mut tracker = ErrorTracker::new();
844
845        for _ in 0..3 {
846            tracker.record_error(InferenceError::new(
847                "ptr",
848                "test.c",
849                1,
850                InferredOwnership::Borrowed,
851                InferredOwnership::Owned,
852                0.6,
853                OwnershipDefect::PointerMisclassification,
854            ));
855        }
856        for _ in 0..2 {
857            tracker.record_error(InferenceError::new(
858                "ptr",
859                "test.c",
860                1,
861                InferredOwnership::Borrowed,
862                InferredOwnership::Owned,
863                0.6,
864                OwnershipDefect::LifetimeInferenceGap,
865            ));
866        }
867
868        let dist = tracker.defect_distribution();
869        assert_eq!(dist.get("PointerMisclassification"), Some(&3));
870        assert_eq!(dist.get("LifetimeInferenceGap"), Some(&2));
871    }
872
873    #[test]
874    fn error_tracker_generate_suggestions() {
875        let mut tracker = ErrorTracker::new();
876
877        // Add many errors for a highly suspicious feature
878        for _ in 0..25 {
879            let error = InferenceError::new(
880                "ptr",
881                "test.c",
882                42,
883                InferredOwnership::Borrowed,
884                InferredOwnership::Owned,
885                0.6,
886                OwnershipDefect::PointerMisclassification,
887            )
888            .with_features(vec!["problematic_feature".to_string()]);
889            tracker.record_error(error);
890        }
891
892        // Add a few successes
893        for _ in 0..5 {
894            tracker.record_success(&["problematic_feature".to_string()]);
895        }
896
897        let suggestions = tracker.generate_suggestions();
898        assert!(!suggestions.is_empty());
899    }
900
901    #[test]
902    fn error_tracker_generate_markdown_report() {
903        let mut tracker = ErrorTracker::new();
904
905        let error = InferenceError::new(
906            "ptr",
907            "test.c",
908            42,
909            InferredOwnership::Borrowed,
910            InferredOwnership::Owned,
911            0.6,
912            OwnershipDefect::PointerMisclassification,
913        )
914        .with_features(vec!["malloc_free".to_string()]);
915
916        tracker.record_error(error);
917        tracker.record_success(&["other_feature".to_string()]);
918
919        let md = tracker.generate_markdown_report();
920        assert!(md.contains("Error Tracking Report"));
921        assert!(md.contains("CITL Analysis"));
922        assert!(md.contains("Tarantula"));
923    }
924
925    // ========================================================================
926    // ImprovementSuggestion tests
927    // ========================================================================
928
929    #[test]
930    fn improvement_suggestion_structure() {
931        let suggestion = ImprovementSuggestion {
932            priority: SuggestionPriority::High,
933            category: SuggestionCategory::FeatureHandling,
934            description: "Test suggestion".to_string(),
935            affected_feature: Some("malloc_free".to_string()),
936            affected_defect: None,
937        };
938
939        assert_eq!(suggestion.priority, SuggestionPriority::High);
940        assert_eq!(suggestion.category, SuggestionCategory::FeatureHandling);
941    }
942
943    // ========================================================================
944    // Additional coverage tests
945    // ========================================================================
946
947    #[test]
948    fn feature_distribution_multiple_features() {
949        let mut tracker = ErrorTracker::new();
950        let error1 = InferenceError::new(
951            "ptr1", "test.c", 1,
952            InferredOwnership::Borrowed, InferredOwnership::Owned,
953            0.6, OwnershipDefect::PointerMisclassification,
954        ).with_features(vec!["malloc_free".into(), "pointer_arithmetic".into()]);
955        let error2 = InferenceError::new(
956            "ptr2", "test.c", 2,
957            InferredOwnership::Borrowed, InferredOwnership::Owned,
958            0.6, OwnershipDefect::PointerMisclassification,
959        ).with_features(vec!["malloc_free".into(), "struct_field".into()]);
960        tracker.record_error(error1);
961        tracker.record_error(error2);
962
963        let dist = tracker.feature_distribution();
964        assert_eq!(dist.get("malloc_free"), Some(&2));
965        assert_eq!(dist.get("pointer_arithmetic"), Some(&1));
966        assert_eq!(dist.get("struct_field"), Some(&1));
967    }
968
969    #[test]
970    fn feature_defect_correlation_test() {
971        let mut tracker = ErrorTracker::new();
972        let error = InferenceError::new(
973            "ptr", "test.c", 1,
974            InferredOwnership::Borrowed, InferredOwnership::Owned,
975            0.6, OwnershipDefect::PointerMisclassification,
976        ).with_features(vec!["malloc_free".into()]);
977        tracker.record_error(error);
978
979        let correlations = tracker.feature_defect_correlation();
980        assert!(!correlations.is_empty());
981        let (feature, defect, count) = &correlations[0];
982        assert_eq!(feature, "malloc_free");
983        assert!(defect.contains("PointerMisclassification"));
984        assert_eq!(*count, 1);
985    }
986
987    #[test]
988    fn calculate_suspiciousness_zero_ratio_edge() {
989        let mut tracker = ErrorTracker::new();
990        // Feature with zero successes and zero failures shouldn't panic
991        // (impossible via normal API, but tests the formula edge case)
992        let suspicious = tracker.calculate_suspiciousness();
993        assert!(suspicious.is_empty());
994    }
995
996    #[test]
997    fn generate_suggestions_medium_priority() {
998        let mut tracker = ErrorTracker::new();
999        // Add 10 errors for the same defect (count > 5 but <= 20 = Medium)
1000        for i in 0..10 {
1001            tracker.record_error(InferenceError::new(
1002                format!("ptr{}", i), "test.c", i as u32,
1003                InferredOwnership::Borrowed, InferredOwnership::Owned,
1004                0.6, OwnershipDefect::LifetimeInferenceGap,
1005            ));
1006        }
1007        // Also add a few successes so suspiciousness isn't max
1008        for _ in 0..20 {
1009            tracker.record_success(&["some_feature".into()]);
1010        }
1011
1012        let suggestions = tracker.generate_suggestions();
1013        let defect_suggestions: Vec<_> = suggestions.iter()
1014            .filter(|s| s.category == SuggestionCategory::DefectPrevention)
1015            .collect();
1016        assert!(!defect_suggestions.is_empty());
1017        assert_eq!(defect_suggestions[0].priority, SuggestionPriority::Medium);
1018    }
1019
1020    #[test]
1021    fn generate_suggestions_high_priority_defect() {
1022        let mut tracker = ErrorTracker::new();
1023        // Add 25 errors for the same defect (count > 20 = High)
1024        for i in 0..25 {
1025            tracker.record_error(InferenceError::new(
1026                format!("ptr{}", i), "test.c", i as u32,
1027                InferredOwnership::Borrowed, InferredOwnership::Owned,
1028                0.6, OwnershipDefect::PointerMisclassification,
1029            ));
1030        }
1031        for _ in 0..5 {
1032            tracker.record_success(&["x".into()]);
1033        }
1034
1035        let suggestions = tracker.generate_suggestions();
1036        let defect_suggestions: Vec<_> = suggestions.iter()
1037            .filter(|s| s.category == SuggestionCategory::DefectPrevention)
1038            .collect();
1039        assert!(!defect_suggestions.is_empty());
1040        assert_eq!(defect_suggestions[0].priority, SuggestionPriority::High);
1041    }
1042
1043    #[test]
1044    fn suggestion_priority_all_variants() {
1045        let _c = SuggestionPriority::Critical;
1046        let _h = SuggestionPriority::High;
1047        let _m = SuggestionPriority::Medium;
1048        let _l = SuggestionPriority::Low;
1049        assert_ne!(SuggestionPriority::Critical, SuggestionPriority::Low);
1050    }
1051
1052    #[test]
1053    fn suggestion_category_all_variants() {
1054        let _f = SuggestionCategory::FeatureHandling;
1055        let _d = SuggestionCategory::DefectPrevention;
1056        let _t = SuggestionCategory::TrainingData;
1057        let _c = SuggestionCategory::Configuration;
1058        assert_ne!(SuggestionCategory::TrainingData, SuggestionCategory::Configuration);
1059    }
1060
1061    #[test]
1062    fn error_tracker_default_trait() {
1063        let tracker = ErrorTracker::default();
1064        assert_eq!(tracker.error_count(), 0);
1065        assert_eq!(tracker.success_count(), 0);
1066    }
1067
1068    #[test]
1069    fn generate_markdown_report_empty() {
1070        let mut tracker = ErrorTracker::new();
1071        let md = tracker.generate_markdown_report();
1072        assert!(md.contains("Total Errors | 0"));
1073        assert!(md.contains("Total Successes | 0"));
1074    }
1075
1076    #[test]
1077    fn generate_markdown_report_with_suggestions() {
1078        let mut tracker = ErrorTracker::new();
1079        // Add many errors to trigger suggestions
1080        for i in 0..30 {
1081            let error = InferenceError::new(
1082                format!("ptr{}", i), "test.c", i as u32,
1083                InferredOwnership::Borrowed, InferredOwnership::Owned,
1084                0.6, OwnershipDefect::PointerMisclassification,
1085            ).with_features(vec!["problematic".into()]);
1086            tracker.record_error(error);
1087        }
1088        for _ in 0..2 {
1089            tracker.record_success(&["problematic".into()]);
1090        }
1091
1092        let md = tracker.generate_markdown_report();
1093        assert!(md.contains("Improvement Suggestions"));
1094        assert!(md.contains("problematic"));
1095    }
1096
1097    #[test]
1098    fn inference_error_confidence_clamp() {
1099        let error = InferenceError::new(
1100            "ptr", "test.c", 1,
1101            InferredOwnership::Borrowed, InferredOwnership::Owned,
1102            1.5, // above 1.0
1103            OwnershipDefect::PointerMisclassification,
1104        );
1105        assert_eq!(error.confidence, 1.0);
1106
1107        let error2 = InferenceError::new(
1108            "ptr", "test.c", 1,
1109            InferredOwnership::Borrowed, InferredOwnership::Owned,
1110            -0.5, // below 0.0
1111            OwnershipDefect::PointerMisclassification,
1112        );
1113        assert_eq!(error2.confidence, 0.0);
1114    }
1115
1116    #[test]
1117    fn serialization_round_trip_inference_error() {
1118        let error = InferenceError::new(
1119            "ptr", "test.c", 1,
1120            InferredOwnership::Borrowed, InferredOwnership::Owned,
1121            0.6, OwnershipDefect::PointerMisclassification,
1122        ).with_features(vec!["malloc_free".into()]).with_rust_error("E0382");
1123
1124        let json = serde_json::to_string(&error).unwrap();
1125        let restored: InferenceError = serde_json::from_str(&json).unwrap();
1126        assert_eq!(restored.variable, "ptr");
1127        assert_eq!(restored.source_file, "test.c");
1128        assert_eq!(restored.rust_error, Some("E0382".into()));
1129        assert_eq!(restored.c_features.len(), 1);
1130    }
1131
1132    #[test]
1133    fn serialization_round_trip_suggestion() {
1134        let suggestion = ImprovementSuggestion {
1135            priority: SuggestionPriority::Critical,
1136            category: SuggestionCategory::TrainingData,
1137            description: "Add more training data".into(),
1138            affected_feature: None,
1139            affected_defect: Some("LifetimeInferenceGap".into()),
1140        };
1141        let json = serde_json::to_string(&suggestion).unwrap();
1142        let restored: ImprovementSuggestion = serde_json::from_str(&json).unwrap();
1143        assert_eq!(restored.priority, SuggestionPriority::Critical);
1144        assert_eq!(restored.category, SuggestionCategory::TrainingData);
1145    }
1146
1147    #[test]
1148    fn multiple_errors_auto_increments_ids() {
1149        let mut tracker = ErrorTracker::new();
1150        for i in 0..5 {
1151            tracker.record_error(InferenceError::new(
1152                format!("ptr{}", i), "test.c", i as u32,
1153                InferredOwnership::Borrowed, InferredOwnership::Owned,
1154                0.6, OwnershipDefect::PointerMisclassification,
1155            ));
1156        }
1157        let ids: Vec<u64> = tracker.errors().iter().map(|e| e.id).collect();
1158        assert_eq!(ids, vec![1, 2, 3, 4, 5]);
1159    }
1160
1161    // ========================================================================
1162    // Deep coverage: generate_markdown_report all branches
1163    // ========================================================================
1164
1165    #[test]
1166    fn generate_markdown_report_error_rate_percentage() {
1167        let mut tracker = ErrorTracker::new();
1168        // 3 errors, 7 successes => 30% error rate
1169        for i in 0..3 {
1170            tracker.record_error(InferenceError::new(
1171                format!("ptr{}", i), "test.c", i as u32,
1172                InferredOwnership::Borrowed, InferredOwnership::Owned,
1173                0.6, OwnershipDefect::PointerMisclassification,
1174            ).with_features(vec!["feat_a".into()]));
1175        }
1176        for _ in 0..7 {
1177            tracker.record_success(&["feat_a".into()]);
1178        }
1179
1180        let md = tracker.generate_markdown_report();
1181        // Check summary section
1182        assert!(md.contains("Total Errors | 3"));
1183        assert!(md.contains("Total Successes | 7"));
1184        assert!(md.contains("Error Rate | 30.0%"));
1185    }
1186
1187    #[test]
1188    fn generate_markdown_report_suspicious_features_table() {
1189        let mut tracker = ErrorTracker::new();
1190        // Create distinct features with varying suspiciousness
1191        for i in 0..10 {
1192            let feature = format!("feature_{}", i % 3);
1193            tracker.record_error(InferenceError::new(
1194                format!("ptr{}", i), "test.c", i as u32,
1195                InferredOwnership::Borrowed, InferredOwnership::Owned,
1196                0.5, OwnershipDefect::PointerMisclassification,
1197            ).with_features(vec![feature.clone()]));
1198        }
1199        for i in 0..5 {
1200            let feature = format!("feature_{}", i % 3);
1201            tracker.record_success(&[feature]);
1202        }
1203
1204        let md = tracker.generate_markdown_report();
1205        // Should contain suspicious features table headers
1206        assert!(md.contains("Top Suspicious Features (Tarantula)"));
1207        assert!(md.contains("| Feature | Score | Failures | Successes |"));
1208        // At least one feature should appear in the table
1209        assert!(md.contains("feature_"));
1210    }
1211
1212    #[test]
1213    fn generate_markdown_report_defect_distribution_section() {
1214        let mut tracker = ErrorTracker::new();
1215        // Multiple defect types
1216        for _ in 0..5 {
1217            tracker.record_error(InferenceError::new(
1218                "ptr", "test.c", 1,
1219                InferredOwnership::Borrowed, InferredOwnership::Owned,
1220                0.6, OwnershipDefect::PointerMisclassification,
1221            ));
1222        }
1223        for _ in 0..3 {
1224            tracker.record_error(InferenceError::new(
1225                "ptr", "test.c", 2,
1226                InferredOwnership::Borrowed, InferredOwnership::Owned,
1227                0.6, OwnershipDefect::LifetimeInferenceGap,
1228            ));
1229        }
1230        tracker.record_error(InferenceError::new(
1231            "ptr", "test.c", 3,
1232            InferredOwnership::Borrowed, InferredOwnership::Owned,
1233            0.6, OwnershipDefect::DanglingPointerRisk,
1234        ));
1235
1236        let md = tracker.generate_markdown_report();
1237        assert!(md.contains("Defect Distribution"));
1238        // Sorted by count descending
1239        assert!(md.contains("PointerMisclassification"));
1240        assert!(md.contains("LifetimeInferenceGap"));
1241        assert!(md.contains("DanglingPointerRisk"));
1242        // Each defect has percentage
1243        assert!(md.contains("%"));
1244    }
1245
1246    #[test]
1247    fn generate_markdown_report_no_suggestions_section_when_few_errors() {
1248        let mut tracker = ErrorTracker::new();
1249        // Only 2 errors - not enough to trigger suggestions (need > 5)
1250        for i in 0..2 {
1251            tracker.record_error(InferenceError::new(
1252                format!("ptr{}", i), "test.c", i as u32,
1253                InferredOwnership::Borrowed, InferredOwnership::Owned,
1254                0.6, OwnershipDefect::PointerMisclassification,
1255            ));
1256        }
1257        for _ in 0..20 {
1258            tracker.record_success(&["safe_feature".into()]);
1259        }
1260
1261        let md = tracker.generate_markdown_report();
1262        // With only 2 errors total for one defect, suggestions may or may not appear
1263        // but the report should still be valid
1264        assert!(md.contains("Error Tracking Report"));
1265    }
1266
1267    #[test]
1268    fn generate_markdown_report_improvement_suggestions_numbered() {
1269        let mut tracker = ErrorTracker::new();
1270        // Create enough errors to trigger both feature and defect suggestions
1271        for i in 0..30 {
1272            tracker.record_error(InferenceError::new(
1273                format!("ptr{}", i), "test.c", i as u32,
1274                InferredOwnership::Borrowed, InferredOwnership::Owned,
1275                0.6, OwnershipDefect::PointerMisclassification,
1276            ).with_features(vec!["dangerous_pattern".into()]));
1277        }
1278        for _ in 0..3 {
1279            tracker.record_success(&["dangerous_pattern".into()]);
1280        }
1281
1282        let md = tracker.generate_markdown_report();
1283        assert!(md.contains("Improvement Suggestions"));
1284        // Should have numbered suggestions
1285        assert!(md.contains("1. **["));
1286    }
1287
1288    // ========================================================================
1289    // Deep coverage: calculate_suspiciousness all branches
1290    // ========================================================================
1291
1292    #[test]
1293    fn calculate_suspiciousness_only_failures() {
1294        let mut tracker = ErrorTracker::new();
1295        // Feature with only failures (0 successes)
1296        for _ in 0..5 {
1297            tracker.record_error(InferenceError::new(
1298                "ptr", "test.c", 1,
1299                InferredOwnership::Borrowed, InferredOwnership::Owned,
1300                0.6, OwnershipDefect::PointerMisclassification,
1301            ).with_features(vec!["only_failures".into()]));
1302        }
1303
1304        let suspicious = tracker.calculate_suspiciousness();
1305        let feat = suspicious.iter().find(|s| s.feature == "only_failures").unwrap();
1306        // With 0 passed ratio, suspiciousness = failed_ratio / (failed_ratio + 0) = 1.0
1307        assert!((feat.score - 1.0).abs() < 0.001);
1308        assert_eq!(feat.failure_count, 5);
1309        assert_eq!(feat.success_count, 0);
1310        assert!(feat.is_highly_suspicious());
1311    }
1312
1313    #[test]
1314    fn calculate_suspiciousness_only_successes() {
1315        let mut tracker = ErrorTracker::new();
1316        // We need at least one failure to have total_failures > 0
1317        tracker.record_error(InferenceError::new(
1318            "ptr", "test.c", 1,
1319            InferredOwnership::Borrowed, InferredOwnership::Owned,
1320            0.6, OwnershipDefect::PointerMisclassification,
1321        ).with_features(vec!["has_failure".into()]));
1322
1323        // Feature with only successes
1324        for _ in 0..5 {
1325            tracker.record_success(&["only_successes".into()]);
1326        }
1327
1328        let suspicious = tracker.calculate_suspiciousness();
1329        let feat = suspicious.iter().find(|s| s.feature == "only_successes").unwrap();
1330        // With 0 failed_ratio, suspiciousness = 0 / (0 + passed_ratio) = 0.0
1331        assert!((feat.score - 0.0).abs() < 0.001);
1332        assert_eq!(feat.failure_count, 0);
1333        assert_eq!(feat.success_count, 5);
1334        assert!(!feat.is_suspicious());
1335    }
1336
1337    #[test]
1338    fn calculate_suspiciousness_mixed_features_sorted() {
1339        let mut tracker = ErrorTracker::new();
1340        // Feature A: high suspiciousness (mostly failures)
1341        for _ in 0..9 {
1342            tracker.record_error(InferenceError::new(
1343                "ptr", "test.c", 1,
1344                InferredOwnership::Borrowed, InferredOwnership::Owned,
1345                0.6, OwnershipDefect::PointerMisclassification,
1346            ).with_features(vec!["high_susp".into()]));
1347        }
1348        tracker.record_success(&["high_susp".into()]);
1349
1350        // Feature B: low suspiciousness (mostly successes)
1351        tracker.record_error(InferenceError::new(
1352            "ptr", "test.c", 1,
1353            InferredOwnership::Borrowed, InferredOwnership::Owned,
1354            0.6, OwnershipDefect::PointerMisclassification,
1355        ).with_features(vec!["low_susp".into()]));
1356        for _ in 0..9 {
1357            tracker.record_success(&["low_susp".into()]);
1358        }
1359
1360        let suspicious = tracker.calculate_suspiciousness();
1361        // Results should be sorted by score descending
1362        assert!(suspicious.len() >= 2);
1363        assert!(suspicious[0].score >= suspicious[1].score);
1364        // First should be high_susp
1365        assert_eq!(suspicious[0].feature, "high_susp");
1366    }
1367
1368    #[test]
1369    fn calculate_suspiciousness_updates_stats_in_place() {
1370        let mut tracker = ErrorTracker::new();
1371        for _ in 0..3 {
1372            tracker.record_error(InferenceError::new(
1373                "ptr", "test.c", 1,
1374                InferredOwnership::Borrowed, InferredOwnership::Owned,
1375                0.6, OwnershipDefect::PointerMisclassification,
1376            ).with_features(vec!["feat".into()]));
1377        }
1378        for _ in 0..2 {
1379            tracker.record_success(&["feat".into()]);
1380        }
1381
1382        // First call calculates and stores suspiciousness
1383        let results1 = tracker.calculate_suspiciousness();
1384        // Second call should give same results (idempotent)
1385        let results2 = tracker.calculate_suspiciousness();
1386
1387        assert_eq!(results1.len(), results2.len());
1388        assert!((results1[0].score - results2[0].score).abs() < 0.001);
1389    }
1390
1391    #[test]
1392    fn calculate_suspiciousness_multiple_features_per_error() {
1393        let mut tracker = ErrorTracker::new();
1394        // Error with two features
1395        tracker.record_error(InferenceError::new(
1396            "ptr", "test.c", 1,
1397            InferredOwnership::Borrowed, InferredOwnership::Owned,
1398            0.6, OwnershipDefect::PointerMisclassification,
1399        ).with_features(vec!["feat_x".into(), "feat_y".into()]));
1400
1401        tracker.record_success(&["feat_y".into()]);
1402
1403        let suspicious = tracker.calculate_suspiciousness();
1404        // Both features should be tracked
1405        assert!(suspicious.iter().any(|s| s.feature == "feat_x"));
1406        assert!(suspicious.iter().any(|s| s.feature == "feat_y"));
1407
1408        // feat_x has 1 failure 0 successes, feat_y has 1 failure 1 success
1409        let feat_x = suspicious.iter().find(|s| s.feature == "feat_x").unwrap();
1410        let feat_y = suspicious.iter().find(|s| s.feature == "feat_y").unwrap();
1411        assert!(feat_x.score > feat_y.score, "feat_x should be more suspicious");
1412    }
1413
1414    // ========================================================================
1415    // generate_suggestions: cover all branches exhaustively
1416    // ========================================================================
1417
1418    #[test]
1419    fn generate_suggestions_no_highly_suspicious_no_common_defects() {
1420        let mut tracker = ErrorTracker::new();
1421        // Just a few errors, not enough for defect suggestions
1422        for i in 0..3 {
1423            tracker.record_error(InferenceError::new(
1424                format!("ptr{}", i), "test.c", i as u32,
1425                InferredOwnership::Borrowed, InferredOwnership::Owned,
1426                0.6, OwnershipDefect::PointerMisclassification,
1427            ).with_features(vec![format!("unique_feat_{}", i)]));
1428        }
1429        // Many successes for those features (makes them not suspicious)
1430        for i in 0..3 {
1431            for _ in 0..20 {
1432                tracker.record_success(&[format!("unique_feat_{}", i)]);
1433            }
1434        }
1435
1436        let suggestions = tracker.generate_suggestions();
1437        // 3 errors for PointerMisclassification but <= 5, no defect suggestion
1438        // Features have low suspiciousness due to many successes
1439        // So either empty or only has low-suspiciousness feature suggestions
1440        let feature_suggestions: Vec<_> = suggestions.iter()
1441            .filter(|s| s.category == SuggestionCategory::FeatureHandling)
1442            .collect();
1443        assert!(feature_suggestions.is_empty(), "Features should not be highly suspicious");
1444    }
1445
1446    #[test]
1447    fn generate_suggestions_multiple_defect_categories() {
1448        let mut tracker = ErrorTracker::new();
1449        // Three defect categories with different counts
1450        for _ in 0..25 {
1451            tracker.record_error(InferenceError::new(
1452                "ptr", "test.c", 1,
1453                InferredOwnership::Borrowed, InferredOwnership::Owned,
1454                0.6, OwnershipDefect::PointerMisclassification,
1455            ));
1456        }
1457        for _ in 0..10 {
1458            tracker.record_error(InferenceError::new(
1459                "ptr", "test.c", 2,
1460                InferredOwnership::Borrowed, InferredOwnership::Owned,
1461                0.6, OwnershipDefect::LifetimeInferenceGap,
1462            ));
1463        }
1464        for _ in 0..8 {
1465            tracker.record_error(InferenceError::new(
1466                "ptr", "test.c", 3,
1467                InferredOwnership::Borrowed, InferredOwnership::Owned,
1468                0.6, OwnershipDefect::DanglingPointerRisk,
1469            ));
1470        }
1471        for _ in 0..5 {
1472            tracker.record_success(&["x".into()]);
1473        }
1474
1475        let suggestions = tracker.generate_suggestions();
1476        let defect_suggestions: Vec<_> = suggestions.iter()
1477            .filter(|s| s.category == SuggestionCategory::DefectPrevention)
1478            .collect();
1479
1480        // All three defects have > 5 count, should produce up to 3 suggestions
1481        assert!(defect_suggestions.len() >= 2);
1482        // PointerMisclassification (25) should be High priority
1483        assert!(defect_suggestions.iter().any(|s| s.priority == SuggestionPriority::High));
1484        // LifetimeInferenceGap (10) should be Medium priority
1485        assert!(defect_suggestions.iter().any(|s| s.priority == SuggestionPriority::Medium));
1486    }
1487
1488    // ========================================================================
1489    // ErrorTracker: edge cases for defect stats and feature-defect combos
1490    // ========================================================================
1491
1492    #[test]
1493    fn record_error_updates_feature_defect_stats() {
1494        let mut tracker = ErrorTracker::new();
1495        let error = InferenceError::new(
1496            "ptr", "test.c", 1,
1497            InferredOwnership::Borrowed, InferredOwnership::Owned,
1498            0.6, OwnershipDefect::AliasViolation,
1499        ).with_features(vec!["feature_alpha".into(), "feature_beta".into()]);
1500
1501        tracker.record_error(error);
1502
1503        // Check defect stats
1504        let defect_dist = tracker.defect_distribution();
1505        assert_eq!(defect_dist.get("AliasViolation"), Some(&1));
1506
1507        // Check feature-defect correlation
1508        let correlations = tracker.feature_defect_correlation();
1509        assert!(correlations.iter().any(|(f, d, c)| f == "feature_alpha" && d.contains("AliasViolation") && *c == 1));
1510        assert!(correlations.iter().any(|(f, d, c)| f == "feature_beta" && d.contains("AliasViolation") && *c == 1));
1511    }
1512
1513    #[test]
1514    fn record_multiple_errors_same_feature_different_defects() {
1515        let mut tracker = ErrorTracker::new();
1516        tracker.record_error(InferenceError::new(
1517            "ptr1", "test.c", 1,
1518            InferredOwnership::Borrowed, InferredOwnership::Owned,
1519            0.6, OwnershipDefect::PointerMisclassification,
1520        ).with_features(vec!["shared_feature".into()]));
1521
1522        tracker.record_error(InferenceError::new(
1523            "ptr2", "test.c", 2,
1524            InferredOwnership::Borrowed, InferredOwnership::Owned,
1525            0.6, OwnershipDefect::DanglingPointerRisk,
1526        ).with_features(vec!["shared_feature".into()]));
1527
1528        // Feature stats should have 2 failures
1529        let suspicious = tracker.calculate_suspiciousness();
1530        let feat = suspicious.iter().find(|s| s.feature == "shared_feature").unwrap();
1531        assert_eq!(feat.failure_count, 2);
1532
1533        // Feature-defect correlation should have 2 entries
1534        let correlations = tracker.feature_defect_correlation();
1535        let shared_correlations: Vec<_> = correlations.iter()
1536            .filter(|(f, _, _)| f == "shared_feature")
1537            .collect();
1538        assert_eq!(shared_correlations.len(), 2);
1539    }
1540}