Skip to main content

depyler_analysis/
scoring.rs

1//! DEPYLER-SCORE-001: 100-Point Single-Shot Compile Score
2//!
3//! Quantifies transpilation quality across multiple orthogonal dimensions:
4//! - A. Compilation Success (40 points)
5//! - B. Type Inference Quality (25 points)
6//! - C. Test Coverage (15 points)
7//! - D. Code Quality (10 points)
8//! - E. Semantic Equivalence (10 points)
9//!
10//! Academic Foundation:
11//! - Jia & Harman (2011): Mutation testing for quality assessment
12//! - Pierce (2002): Type systems and Hindley-Milner inference
13//! - Leroy (2009): CompCert formal verification
14//! - Chidamber & Kemerer (1994): CK metrics suite
15//! - Sculley et al. (2015): ML feedback loops
16
17use std::collections::HashMap;
18use std::path::PathBuf;
19
20/// Scoring mode determines which checks are performed
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum ScoringMode {
23    /// Quick mode: <10s, A1-A3 only (filesystem + rustc)
24    Quick,
25    /// Fast mode: <60s, A + B + D1 (compile + clippy)
26    #[default]
27    Fast,
28    /// Full mode: <5m, all categories (complete verification)
29    Full,
30}
31
32/// The 100-point score with category breakdown
33#[derive(Debug, Clone, Default)]
34pub struct SingleShotScore {
35    /// Total score (0-100)
36    pub total: u8,
37    /// Category A: Compilation Success (0-40)
38    pub compilation: u8,
39    /// Category B: Type Inference Quality (0-25)
40    pub type_inference: u8,
41    /// Category C: Test Coverage (0-15)
42    pub test_coverage: u8,
43    /// Category D: Code Quality (0-10)
44    pub code_quality: u8,
45    /// Category E: Semantic Equivalence (0-10)
46    pub semantic_equivalence: u8,
47    /// Whether the gateway (A >= 24) passed
48    pub gateway_passed: bool,
49    /// Which scoring mode was used
50    pub mode: ScoringMode,
51}
52
53/// Detailed breakdown of each subcategory
54#[derive(Debug, Clone, Default)]
55pub struct CategoryBreakdown {
56    // Category A: Compilation Success
57    /// A1: Parse success (0-10)
58    pub a1_parse: u8,
59    /// A2: Type check success (0-15)
60    pub a2_type_check: u8,
61    /// A3: Cargo build success (0-15)
62    pub a3_cargo_build: u8,
63
64    // Category B: Type Inference Quality
65    /// B1: No E0308 errors (0-10)
66    pub b1_no_e0308: u8,
67    /// B2: No E0599 errors (0-8)
68    pub b2_no_e0599: u8,
69    /// B3: No E0425 errors (0-7)
70    pub b3_no_e0425: u8,
71
72    // Category C: Test Coverage
73    /// C1: Doctest pass (0-5)
74    pub c1_doctest: u8,
75    /// C2: Unit test pass (0-5)
76    pub c2_unit_test: u8,
77    /// C3: Property test pass (0-5)
78    pub c3_property_test: u8,
79
80    // Category D: Code Quality
81    /// D1: Clippy clean (0-5)
82    pub d1_clippy: u8,
83    /// D2: TDG score >= B (0-3)
84    pub d2_tdg: u8,
85    /// D3: Complexity <= 10 (0-2)
86    pub d3_complexity: u8,
87
88    // Category E: Semantic Equivalence
89    /// E1: Golden trace match (0-5)
90    pub e1_trace_match: u8,
91    /// E2: Output equivalence (0-5)
92    pub e2_output_equiv: u8,
93}
94
95/// Compilation error details for training
96#[derive(Debug, Clone)]
97pub struct CompilationError {
98    /// Error code (e.g., "E0308")
99    pub code: String,
100    /// Error message
101    pub message: String,
102    /// File location
103    pub location: Option<String>,
104    /// Line number
105    pub line: Option<u32>,
106}
107
108/// A transpiler decision that can be correlated with outcomes
109#[derive(Debug, Clone, PartialEq, Eq, Hash)]
110pub enum TranspilerDecision {
111    /// Type inference decision
112    TypeInference {
113        variable: String,
114        inferred_type: String,
115    },
116    /// Method translation decision
117    MethodTranslation {
118        python_method: String,
119        rust_method: String,
120    },
121    /// Import mapping decision
122    ImportMapping {
123        python_import: String,
124        rust_import: String,
125    },
126    /// Fallback to serde_json::Value
127    ValueFallback { context: String },
128    /// Other decision
129    Other(String),
130}
131
132/// Single file transpilation result with comprehensive scoring
133#[derive(Debug, Clone)]
134pub struct SingleShotResult {
135    /// Path to the file
136    pub file_path: PathBuf,
137    /// The computed score
138    pub score: SingleShotScore,
139    /// Detailed category breakdown
140    pub category_breakdown: CategoryBreakdown,
141    /// List of compilation errors
142    pub error_details: Vec<CompilationError>,
143    /// Transpiler decisions made for this file
144    pub transpiler_decisions: Vec<TranspilerDecision>,
145}
146
147/// Configuration for scoring
148#[derive(Debug, Clone)]
149pub struct ScoringConfig {
150    /// Gateway threshold (default: 0.6 = 60%)
151    pub gateway_threshold: f32,
152    /// Category weights
153    pub weights: CategoryWeights,
154    /// Enable semantic check (requires Renacer)
155    pub enable_semantic_check: bool,
156    /// Send results to oracle for training
157    pub oracle_feedback: bool,
158}
159
160impl Default for ScoringConfig {
161    fn default() -> Self {
162        Self {
163            gateway_threshold: 0.6,
164            weights: CategoryWeights::default(),
165            enable_semantic_check: true,
166            oracle_feedback: true,
167        }
168    }
169}
170
171/// Category weights (must sum to 1.0)
172#[derive(Debug, Clone)]
173pub struct CategoryWeights {
174    /// Compilation weight (default: 0.40)
175    pub compilation: f32,
176    /// Type inference weight (default: 0.25)
177    pub type_inference: f32,
178    /// Test coverage weight (default: 0.15)
179    pub test_coverage: f32,
180    /// Code quality weight (default: 0.10)
181    pub code_quality: f32,
182    /// Semantic equivalence weight (default: 0.10)
183    pub semantic_equiv: f32,
184}
185
186impl Default for CategoryWeights {
187    fn default() -> Self {
188        Self {
189            compilation: 0.40,
190            type_inference: 0.25,
191            test_coverage: 0.15,
192            code_quality: 0.10,
193            semantic_equiv: 0.10,
194        }
195    }
196}
197
198/// Output format for score reports
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
200pub enum OutputFormat {
201    /// Terminal-friendly table
202    #[default]
203    Human,
204    /// Machine-readable JSON
205    Json,
206    /// Analytics/ML training Parquet
207    Parquet,
208    /// Documentation Markdown
209    Markdown,
210}
211
212/// Corpus-level score report
213#[derive(Debug, Clone)]
214pub struct CorpusScoreReport {
215    /// Individual file results
216    pub results: Vec<SingleShotResult>,
217    /// Aggregate score
218    pub aggregate_score: f32,
219    /// Letter grade
220    pub grade: Grade,
221    /// Category aggregates
222    pub category_averages: CategoryBreakdown,
223    /// Top blockers (Pareto analysis)
224    pub top_blockers: Vec<Blocker>,
225}
226
227/// Letter grade mapping
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub enum Grade {
230    APlus,  // 95-100
231    A,      // 90-94
232    AMinus, // 85-89
233    BPlus,  // 80-84
234    B,      // 70-79
235    C,      // 60-69
236    D,      // 50-59
237    F,      // 0-49
238}
239
240impl Grade {
241    /// Convert score to grade
242    pub fn from_score(score: f32) -> Self {
243        match score as u8 {
244            95..=100 => Grade::APlus,
245            90..=94 => Grade::A,
246            85..=89 => Grade::AMinus,
247            80..=84 => Grade::BPlus,
248            70..=79 => Grade::B,
249            60..=69 => Grade::C,
250            50..=59 => Grade::D,
251            _ => Grade::F,
252        }
253    }
254
255    /// Get display string
256    pub fn as_str(&self) -> &'static str {
257        match self {
258            Grade::APlus => "A+",
259            Grade::A => "A",
260            Grade::AMinus => "A-",
261            Grade::BPlus => "B+",
262            Grade::B => "B",
263            Grade::C => "C",
264            Grade::D => "D",
265            Grade::F => "F",
266        }
267    }
268}
269
270/// A blocker identified by Pareto analysis
271#[derive(Debug, Clone)]
272pub struct Blocker {
273    /// Error pattern or issue
274    pub pattern: String,
275    /// Number of files affected
276    pub affected_files: usize,
277    /// Average points lost
278    pub avg_points_lost: f32,
279}
280
281/// Input data for calculating score breakdown from error analysis
282#[derive(Debug, Clone, Default)]
283pub struct BreakdownInput<'a> {
284    /// Parse succeeded
285    pub parse_ok: bool,
286    /// Type check succeeded
287    pub type_check_ok: bool,
288    /// Cargo build succeeded
289    pub build_ok: bool,
290    /// Compilation errors
291    pub errors: &'a [CompilationError],
292    /// Doctests passed
293    pub doctest_pass: bool,
294    /// Unit tests passed
295    pub unit_test_pass: bool,
296    /// Property tests passed
297    pub property_test_pass: bool,
298    /// Clippy clean
299    pub clippy_clean: bool,
300    /// TDG grade B or better
301    pub tdg_grade_b_or_better: bool,
302    /// Complexity <= 10
303    pub complexity_ok: bool,
304    /// Trace match (Renacer)
305    pub trace_match: bool,
306    /// Output equivalence
307    pub output_equiv: bool,
308}
309
310/// Score calculator
311pub struct ScoreCalculator {
312    config: ScoringConfig,
313}
314
315impl ScoreCalculator {
316    /// Create a new score calculator with default config
317    pub fn new() -> Self {
318        Self {
319            config: ScoringConfig::default(),
320        }
321    }
322
323    /// Create with custom config
324    pub fn with_config(config: ScoringConfig) -> Self {
325        Self { config }
326    }
327
328    /// Calculate score for a single file
329    pub fn calculate(&self, breakdown: &CategoryBreakdown, mode: ScoringMode) -> SingleShotScore {
330        // Calculate category totals
331        let compilation = breakdown.a1_parse + breakdown.a2_type_check + breakdown.a3_cargo_build;
332        let type_inference = breakdown.b1_no_e0308 + breakdown.b2_no_e0599 + breakdown.b3_no_e0425;
333        let test_coverage =
334            breakdown.c1_doctest + breakdown.c2_unit_test + breakdown.c3_property_test;
335        let code_quality = breakdown.d1_clippy + breakdown.d2_tdg + breakdown.d3_complexity;
336        let semantic_equivalence = breakdown.e1_trace_match + breakdown.e2_output_equiv;
337
338        // Check gateway (Popper-inspired falsifiability)
339        let gateway_threshold = (40.0 * self.config.gateway_threshold) as u8; // 24 by default
340        let gateway_passed = compilation >= gateway_threshold;
341
342        // Calculate total (0 if gateway failed)
343        let total = if gateway_passed {
344            compilation + type_inference + test_coverage + code_quality + semantic_equivalence
345        } else {
346            0
347        };
348
349        SingleShotScore {
350            total,
351            compilation,
352            type_inference,
353            test_coverage,
354            code_quality,
355            semantic_equivalence,
356            gateway_passed,
357            mode,
358        }
359    }
360
361    /// Calculate breakdown from error analysis
362    pub fn breakdown_from_errors(&self, input: &BreakdownInput<'_>) -> CategoryBreakdown {
363        // Count error types
364        let e0308_count = input.errors.iter().filter(|e| e.code == "E0308").count();
365        let e0599_count = input.errors.iter().filter(|e| e.code == "E0599").count();
366        let e0425_count = input.errors.iter().filter(|e| e.code == "E0425").count();
367        let total_errors = input.errors.len().max(1); // Avoid division by zero
368
369        // Calculate B subcategories based on error ratios
370        let e0308_ratio = e0308_count as f32 / total_errors as f32;
371        let e0599_ratio = e0599_count as f32 / total_errors as f32;
372        let e0425_ratio = e0425_count as f32 / total_errors as f32;
373
374        CategoryBreakdown {
375            // Category A
376            a1_parse: if input.parse_ok { 10 } else { 0 },
377            a2_type_check: if input.type_check_ok { 15 } else { 0 },
378            a3_cargo_build: if input.build_ok { 15 } else { 0 },
379
380            // Category B (inversely proportional to error ratio)
381            b1_no_e0308: ((1.0 - e0308_ratio) * 10.0) as u8,
382            b2_no_e0599: ((1.0 - e0599_ratio) * 8.0) as u8,
383            b3_no_e0425: ((1.0 - e0425_ratio) * 7.0) as u8,
384
385            // Category C
386            c1_doctest: if input.doctest_pass { 5 } else { 0 },
387            c2_unit_test: if input.unit_test_pass { 5 } else { 0 },
388            c3_property_test: if input.property_test_pass { 5 } else { 0 },
389
390            // Category D
391            d1_clippy: if input.clippy_clean { 5 } else { 0 },
392            d2_tdg: if input.tdg_grade_b_or_better { 3 } else { 0 },
393            d3_complexity: if input.complexity_ok { 2 } else { 0 },
394
395            // Category E
396            e1_trace_match: if input.trace_match { 5 } else { 0 },
397            e2_output_equiv: if input.output_equiv { 5 } else { 0 },
398        }
399    }
400
401    /// Aggregate corpus results
402    pub fn aggregate(&self, results: &[SingleShotResult]) -> CorpusScoreReport {
403        if results.is_empty() {
404            return CorpusScoreReport {
405                results: vec![],
406                aggregate_score: 0.0,
407                grade: Grade::F,
408                category_averages: CategoryBreakdown::default(),
409                top_blockers: vec![],
410            };
411        }
412
413        let n = results.len() as f32;
414
415        // Calculate averages
416        let aggregate_score: f32 = results.iter().map(|r| r.score.total as f32).sum::<f32>() / n;
417
418        let category_averages = CategoryBreakdown {
419            a1_parse: (results
420                .iter()
421                .map(|r| r.category_breakdown.a1_parse as f32)
422                .sum::<f32>()
423                / n) as u8,
424            a2_type_check: (results
425                .iter()
426                .map(|r| r.category_breakdown.a2_type_check as f32)
427                .sum::<f32>()
428                / n) as u8,
429            a3_cargo_build: (results
430                .iter()
431                .map(|r| r.category_breakdown.a3_cargo_build as f32)
432                .sum::<f32>()
433                / n) as u8,
434            b1_no_e0308: (results
435                .iter()
436                .map(|r| r.category_breakdown.b1_no_e0308 as f32)
437                .sum::<f32>()
438                / n) as u8,
439            b2_no_e0599: (results
440                .iter()
441                .map(|r| r.category_breakdown.b2_no_e0599 as f32)
442                .sum::<f32>()
443                / n) as u8,
444            b3_no_e0425: (results
445                .iter()
446                .map(|r| r.category_breakdown.b3_no_e0425 as f32)
447                .sum::<f32>()
448                / n) as u8,
449            c1_doctest: (results
450                .iter()
451                .map(|r| r.category_breakdown.c1_doctest as f32)
452                .sum::<f32>()
453                / n) as u8,
454            c2_unit_test: (results
455                .iter()
456                .map(|r| r.category_breakdown.c2_unit_test as f32)
457                .sum::<f32>()
458                / n) as u8,
459            c3_property_test: (results
460                .iter()
461                .map(|r| r.category_breakdown.c3_property_test as f32)
462                .sum::<f32>()
463                / n) as u8,
464            d1_clippy: (results
465                .iter()
466                .map(|r| r.category_breakdown.d1_clippy as f32)
467                .sum::<f32>()
468                / n) as u8,
469            d2_tdg: (results
470                .iter()
471                .map(|r| r.category_breakdown.d2_tdg as f32)
472                .sum::<f32>()
473                / n) as u8,
474            d3_complexity: (results
475                .iter()
476                .map(|r| r.category_breakdown.d3_complexity as f32)
477                .sum::<f32>()
478                / n) as u8,
479            e1_trace_match: (results
480                .iter()
481                .map(|r| r.category_breakdown.e1_trace_match as f32)
482                .sum::<f32>()
483                / n) as u8,
484            e2_output_equiv: (results
485                .iter()
486                .map(|r| r.category_breakdown.e2_output_equiv as f32)
487                .sum::<f32>()
488                / n) as u8,
489        };
490
491        // Pareto analysis for blockers
492        let mut error_counts: HashMap<String, (usize, f32)> = HashMap::new();
493        for result in results {
494            for error in &result.error_details {
495                let entry = error_counts.entry(error.code.clone()).or_insert((0, 0.0));
496                entry.0 += 1;
497                entry.1 += 100.0 - result.score.total as f32;
498            }
499        }
500
501        let mut top_blockers: Vec<Blocker> = error_counts
502            .into_iter()
503            .map(|(code, (count, total_lost))| Blocker {
504                pattern: code,
505                affected_files: count,
506                avg_points_lost: total_lost / count as f32,
507            })
508            .collect();
509
510        top_blockers.sort_by(|a, b| b.affected_files.cmp(&a.affected_files));
511        top_blockers.truncate(5);
512
513        CorpusScoreReport {
514            results: results.to_vec(),
515            aggregate_score,
516            grade: Grade::from_score(aggregate_score),
517            category_averages,
518            top_blockers,
519        }
520    }
521}
522
523impl Default for ScoreCalculator {
524    fn default() -> Self {
525        Self::new()
526    }
527}
528
529/// Tarantula fault localization score
530#[derive(Debug, Clone)]
531pub struct TarantulaScore {
532    /// Suspiciousness score (0.0 - 1.0)
533    pub suspiciousness: f32,
534    /// Number of failed tests with this decision
535    pub failed_count: usize,
536    /// Number of passed tests with this decision
537    pub passed_count: usize,
538}
539
540/// Statistics for a transpiler decision
541#[derive(Debug, Clone, Default)]
542pub struct DecisionStats {
543    pub failed_count: usize,
544    pub passed_count: usize,
545}
546
547impl DecisionStats {
548    /// Calculate Tarantula suspiciousness score
549    pub fn tarantula_score(&self, total_failed: usize, total_passed: usize) -> TarantulaScore {
550        let failed_ratio = if total_failed > 0 {
551            self.failed_count as f32 / total_failed as f32
552        } else {
553            0.0
554        };
555
556        let passed_ratio = if total_passed > 0 {
557            self.passed_count as f32 / total_passed as f32
558        } else {
559            0.0
560        };
561
562        let suspiciousness = if failed_ratio + passed_ratio > 0.0 {
563            failed_ratio / (failed_ratio + passed_ratio)
564        } else {
565            0.0
566        };
567
568        TarantulaScore {
569            suspiciousness,
570            failed_count: self.failed_count,
571            passed_count: self.passed_count,
572        }
573    }
574}
575
576/// Analyze score failures using Tarantula fault localization
577pub fn analyze_score_failures(
578    results: &[SingleShotResult],
579) -> HashMap<TranspilerDecision, TarantulaScore> {
580    let mut stats: HashMap<TranspilerDecision, DecisionStats> = HashMap::new();
581    let mut total_failed = 0;
582    let mut total_passed = 0;
583
584    for result in results {
585        let failed = result.score.total < 80;
586        if failed {
587            total_failed += 1;
588        } else {
589            total_passed += 1;
590        }
591
592        for decision in &result.transpiler_decisions {
593            let entry = stats.entry(decision.clone()).or_default();
594            if failed {
595                entry.failed_count += 1;
596            } else {
597                entry.passed_count += 1;
598            }
599        }
600    }
601
602    stats
603        .into_iter()
604        .map(|(d, s)| (d, s.tarantula_score(total_failed, total_passed)))
605        .collect()
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611
612    // === ScoringMode tests ===
613
614    #[test]
615    fn test_scoring_mode_default() {
616        let mode = ScoringMode::default();
617        assert_eq!(mode, ScoringMode::Fast);
618    }
619
620    #[test]
621    fn test_scoring_mode_quick() {
622        let mode = ScoringMode::Quick;
623        assert_eq!(mode, ScoringMode::Quick);
624    }
625
626    #[test]
627    fn test_scoring_mode_full() {
628        let mode = ScoringMode::Full;
629        assert_eq!(mode, ScoringMode::Full);
630    }
631
632    #[test]
633    fn test_scoring_mode_clone() {
634        let mode = ScoringMode::Fast;
635        let cloned = mode;
636        assert_eq!(cloned, ScoringMode::Fast);
637    }
638
639    #[test]
640    fn test_scoring_mode_debug() {
641        let debug = format!("{:?}", ScoringMode::Quick);
642        assert!(debug.contains("Quick"));
643    }
644
645    // === SingleShotScore tests ===
646
647    #[test]
648    fn test_single_shot_score_default() {
649        let score = SingleShotScore::default();
650        assert_eq!(score.total, 0);
651        assert_eq!(score.compilation, 0);
652        assert!(!score.gateway_passed);
653    }
654
655    #[test]
656    fn test_single_shot_score_clone() {
657        let score = SingleShotScore {
658            total: 75,
659            compilation: 35,
660            type_inference: 20,
661            test_coverage: 10,
662            code_quality: 5,
663            semantic_equivalence: 5,
664            gateway_passed: true,
665            mode: ScoringMode::Fast,
666        };
667        let cloned = score.clone();
668        assert_eq!(cloned.total, 75);
669        assert_eq!(cloned.compilation, 35);
670    }
671
672    #[test]
673    fn test_single_shot_score_debug() {
674        let score = SingleShotScore::default();
675        let debug = format!("{:?}", score);
676        assert!(debug.contains("SingleShotScore"));
677    }
678
679    // === CategoryBreakdown tests ===
680
681    #[test]
682    fn test_category_breakdown_default() {
683        let breakdown = CategoryBreakdown::default();
684        assert_eq!(breakdown.a1_parse, 0);
685        assert_eq!(breakdown.a2_type_check, 0);
686        assert_eq!(breakdown.b1_no_e0308, 0);
687    }
688
689    #[test]
690    fn test_category_breakdown_clone() {
691        let breakdown = CategoryBreakdown {
692            a1_parse: 10,
693            a2_type_check: 15,
694            ..Default::default()
695        };
696        let cloned = breakdown.clone();
697        assert_eq!(cloned.a1_parse, 10);
698        assert_eq!(cloned.a2_type_check, 15);
699    }
700
701    #[test]
702    fn test_category_breakdown_debug() {
703        let breakdown = CategoryBreakdown::default();
704        let debug = format!("{:?}", breakdown);
705        assert!(debug.contains("CategoryBreakdown"));
706    }
707
708    // === CompilationError tests ===
709
710    #[test]
711    fn test_compilation_error_fields() {
712        let error = CompilationError {
713            code: "E0308".to_string(),
714            message: "mismatched types".to_string(),
715            location: Some("src/lib.rs".to_string()),
716            line: Some(42),
717        };
718        assert_eq!(error.code, "E0308");
719        assert_eq!(error.message, "mismatched types");
720        assert_eq!(error.location, Some("src/lib.rs".to_string()));
721        assert_eq!(error.line, Some(42));
722    }
723
724    #[test]
725    fn test_compilation_error_none_fields() {
726        let error = CompilationError {
727            code: "E0001".to_string(),
728            message: "error".to_string(),
729            location: None,
730            line: None,
731        };
732        assert!(error.location.is_none());
733        assert!(error.line.is_none());
734    }
735
736    #[test]
737    fn test_compilation_error_clone() {
738        let error = CompilationError {
739            code: "E0308".to_string(),
740            message: "test".to_string(),
741            location: None,
742            line: None,
743        };
744        let cloned = error.clone();
745        assert_eq!(cloned.code, error.code);
746    }
747
748    #[test]
749    fn test_compilation_error_debug() {
750        let error = CompilationError {
751            code: "E0308".to_string(),
752            message: "test".to_string(),
753            location: None,
754            line: None,
755        };
756        let debug = format!("{:?}", error);
757        assert!(debug.contains("CompilationError"));
758    }
759
760    // === TranspilerDecision tests ===
761
762    #[test]
763    fn test_transpiler_decision_type_inference() {
764        let decision = TranspilerDecision::TypeInference {
765            variable: "x".to_string(),
766            inferred_type: "i32".to_string(),
767        };
768        assert!(matches!(decision, TranspilerDecision::TypeInference { .. }));
769    }
770
771    #[test]
772    fn test_transpiler_decision_method_translation() {
773        let decision = TranspilerDecision::MethodTranslation {
774            python_method: "append".to_string(),
775            rust_method: "push".to_string(),
776        };
777        assert!(matches!(
778            decision,
779            TranspilerDecision::MethodTranslation { .. }
780        ));
781    }
782
783    #[test]
784    fn test_transpiler_decision_import_mapping() {
785        let decision = TranspilerDecision::ImportMapping {
786            python_import: "json".to_string(),
787            rust_import: "serde_json".to_string(),
788        };
789        assert!(matches!(decision, TranspilerDecision::ImportMapping { .. }));
790    }
791
792    #[test]
793    fn test_transpiler_decision_value_fallback() {
794        let decision = TranspilerDecision::ValueFallback {
795            context: "unknown type".to_string(),
796        };
797        assert!(matches!(decision, TranspilerDecision::ValueFallback { .. }));
798    }
799
800    #[test]
801    fn test_transpiler_decision_other() {
802        let decision = TranspilerDecision::Other("custom decision".to_string());
803        assert!(matches!(decision, TranspilerDecision::Other(_)));
804    }
805
806    #[test]
807    fn test_transpiler_decision_clone() {
808        let decision = TranspilerDecision::TypeInference {
809            variable: "x".to_string(),
810            inferred_type: "i32".to_string(),
811        };
812        let cloned = decision.clone();
813        assert_eq!(cloned, decision);
814    }
815
816    #[test]
817    fn test_transpiler_decision_hash() {
818        use std::collections::HashSet;
819        let mut set = HashSet::new();
820        let d1 = TranspilerDecision::Other("a".to_string());
821        let d2 = TranspilerDecision::Other("b".to_string());
822        set.insert(d1.clone());
823        set.insert(d2.clone());
824        assert_eq!(set.len(), 2);
825        assert!(set.contains(&d1));
826    }
827
828    // === ScoringConfig tests ===
829
830    #[test]
831    fn test_scoring_config_default() {
832        let config = ScoringConfig::default();
833        assert!((config.gateway_threshold - 0.6).abs() < f32::EPSILON);
834        assert!(config.enable_semantic_check);
835        assert!(config.oracle_feedback);
836    }
837
838    #[test]
839    fn test_scoring_config_clone() {
840        let config = ScoringConfig::default();
841        let cloned = config.clone();
842        assert_eq!(cloned.gateway_threshold, config.gateway_threshold);
843    }
844
845    #[test]
846    fn test_scoring_config_debug() {
847        let config = ScoringConfig::default();
848        let debug = format!("{:?}", config);
849        assert!(debug.contains("ScoringConfig"));
850    }
851
852    // === CategoryWeights tests ===
853
854    #[test]
855    fn test_category_weights_default() {
856        let weights = CategoryWeights::default();
857        assert!((weights.compilation - 0.40).abs() < f32::EPSILON);
858        assert!((weights.type_inference - 0.25).abs() < f32::EPSILON);
859        assert!((weights.test_coverage - 0.15).abs() < f32::EPSILON);
860        assert!((weights.code_quality - 0.10).abs() < f32::EPSILON);
861        assert!((weights.semantic_equiv - 0.10).abs() < f32::EPSILON);
862    }
863
864    #[test]
865    fn test_category_weights_sum_to_one() {
866        let weights = CategoryWeights::default();
867        let sum = weights.compilation
868            + weights.type_inference
869            + weights.test_coverage
870            + weights.code_quality
871            + weights.semantic_equiv;
872        assert!((sum - 1.0).abs() < f32::EPSILON);
873    }
874
875    #[test]
876    fn test_category_weights_clone() {
877        let weights = CategoryWeights::default();
878        let cloned = weights.clone();
879        assert_eq!(cloned.compilation, weights.compilation);
880    }
881
882    // === OutputFormat tests ===
883
884    #[test]
885    fn test_output_format_default() {
886        let format = OutputFormat::default();
887        assert_eq!(format, OutputFormat::Human);
888    }
889
890    #[test]
891    fn test_output_format_json() {
892        let format = OutputFormat::Json;
893        assert_eq!(format, OutputFormat::Json);
894    }
895
896    #[test]
897    fn test_output_format_parquet() {
898        let format = OutputFormat::Parquet;
899        assert_eq!(format, OutputFormat::Parquet);
900    }
901
902    #[test]
903    fn test_output_format_markdown() {
904        let format = OutputFormat::Markdown;
905        assert_eq!(format, OutputFormat::Markdown);
906    }
907
908    #[test]
909    fn test_output_format_debug() {
910        let debug = format!("{:?}", OutputFormat::Json);
911        assert!(debug.contains("Json"));
912    }
913
914    // === Grade tests ===
915
916    #[test]
917    fn test_grade_as_str() {
918        assert_eq!(Grade::APlus.as_str(), "A+");
919        assert_eq!(Grade::A.as_str(), "A");
920        assert_eq!(Grade::AMinus.as_str(), "A-");
921        assert_eq!(Grade::BPlus.as_str(), "B+");
922        assert_eq!(Grade::B.as_str(), "B");
923        assert_eq!(Grade::C.as_str(), "C");
924        assert_eq!(Grade::D.as_str(), "D");
925        assert_eq!(Grade::F.as_str(), "F");
926    }
927
928    #[test]
929    fn test_grade_clone() {
930        let grade = Grade::APlus;
931        let cloned = grade;
932        assert_eq!(cloned, Grade::APlus);
933    }
934
935    #[test]
936    fn test_grade_debug() {
937        let debug = format!("{:?}", Grade::APlus);
938        assert!(debug.contains("APlus"));
939    }
940
941    #[test]
942    fn test_grade_eq() {
943        assert_eq!(Grade::A, Grade::A);
944        assert_ne!(Grade::A, Grade::B);
945    }
946
947    // === Blocker tests ===
948
949    #[test]
950    fn test_blocker_fields() {
951        let blocker = Blocker {
952            pattern: "E0308".to_string(),
953            affected_files: 5,
954            avg_points_lost: 15.5,
955        };
956        assert_eq!(blocker.pattern, "E0308");
957        assert_eq!(blocker.affected_files, 5);
958        assert!((blocker.avg_points_lost - 15.5).abs() < f32::EPSILON);
959    }
960
961    #[test]
962    fn test_blocker_clone() {
963        let blocker = Blocker {
964            pattern: "test".to_string(),
965            affected_files: 3,
966            avg_points_lost: 10.0,
967        };
968        let cloned = blocker.clone();
969        assert_eq!(cloned.pattern, blocker.pattern);
970    }
971
972    #[test]
973    fn test_blocker_debug() {
974        let blocker = Blocker {
975            pattern: "test".to_string(),
976            affected_files: 1,
977            avg_points_lost: 5.0,
978        };
979        let debug = format!("{:?}", blocker);
980        assert!(debug.contains("Blocker"));
981    }
982
983    // === BreakdownInput tests ===
984
985    #[test]
986    fn test_breakdown_input_default() {
987        let input = BreakdownInput::default();
988        assert!(!input.parse_ok);
989        assert!(!input.type_check_ok);
990        assert!(!input.build_ok);
991    }
992
993    #[test]
994    fn test_breakdown_input_clone() {
995        let empty: Vec<CompilationError> = vec![];
996        let input = BreakdownInput {
997            parse_ok: true,
998            type_check_ok: true,
999            build_ok: false,
1000            errors: &empty,
1001            ..Default::default()
1002        };
1003        let cloned = input.clone();
1004        assert_eq!(cloned.parse_ok, input.parse_ok);
1005    }
1006
1007    #[test]
1008    fn test_breakdown_input_debug() {
1009        let input = BreakdownInput::default();
1010        let debug = format!("{:?}", input);
1011        assert!(debug.contains("BreakdownInput"));
1012    }
1013
1014    // === ScoreCalculator tests ===
1015
1016    #[test]
1017    fn test_score_calculator_new() {
1018        let calc = ScoreCalculator::new();
1019        assert!((calc.config.gateway_threshold - 0.6).abs() < f32::EPSILON);
1020    }
1021
1022    #[test]
1023    fn test_score_calculator_default() {
1024        let calc = ScoreCalculator::default();
1025        assert!((calc.config.gateway_threshold - 0.6).abs() < f32::EPSILON);
1026    }
1027
1028    #[test]
1029    fn test_score_calculator_with_config() {
1030        let config = ScoringConfig {
1031            gateway_threshold: 0.8,
1032            ..Default::default()
1033        };
1034        let calc = ScoreCalculator::with_config(config);
1035        assert!((calc.config.gateway_threshold - 0.8).abs() < f32::EPSILON);
1036    }
1037
1038    #[test]
1039    fn test_score_calculation_perfect() {
1040        let calculator = ScoreCalculator::new();
1041        let breakdown = CategoryBreakdown {
1042            a1_parse: 10,
1043            a2_type_check: 15,
1044            a3_cargo_build: 15,
1045            b1_no_e0308: 10,
1046            b2_no_e0599: 8,
1047            b3_no_e0425: 7,
1048            c1_doctest: 5,
1049            c2_unit_test: 5,
1050            c3_property_test: 5,
1051            d1_clippy: 5,
1052            d2_tdg: 3,
1053            d3_complexity: 2,
1054            e1_trace_match: 5,
1055            e2_output_equiv: 5,
1056        };
1057
1058        let score = calculator.calculate(&breakdown, ScoringMode::Full);
1059
1060        assert_eq!(score.total, 100);
1061        assert_eq!(score.compilation, 40);
1062        assert_eq!(score.type_inference, 25);
1063        assert_eq!(score.test_coverage, 15);
1064        assert_eq!(score.code_quality, 10);
1065        assert_eq!(score.semantic_equivalence, 10);
1066        assert!(score.gateway_passed);
1067    }
1068
1069    #[test]
1070    fn test_gateway_blocks_when_compilation_fails() {
1071        let calculator = ScoreCalculator::new();
1072        let breakdown = CategoryBreakdown {
1073            a1_parse: 10,
1074            a2_type_check: 5,  // Partial failure
1075            a3_cargo_build: 0, // Failed
1076            b1_no_e0308: 10,
1077            b2_no_e0599: 8,
1078            b3_no_e0425: 7,
1079            c1_doctest: 5,
1080            c2_unit_test: 5,
1081            c3_property_test: 5,
1082            d1_clippy: 5,
1083            d2_tdg: 3,
1084            d3_complexity: 2,
1085            e1_trace_match: 5,
1086            e2_output_equiv: 5,
1087        };
1088
1089        let score = calculator.calculate(&breakdown, ScoringMode::Full);
1090
1091        // Gateway threshold is 24 (60% of 40)
1092        // compilation = 10 + 5 + 0 = 15 < 24
1093        assert_eq!(score.compilation, 15);
1094        assert!(!score.gateway_passed);
1095        assert_eq!(score.total, 0); // Total is 0 when gateway fails
1096    }
1097
1098    #[test]
1099    fn test_gateway_passes_at_threshold() {
1100        let calculator = ScoreCalculator::new();
1101        let breakdown = CategoryBreakdown {
1102            a1_parse: 10,
1103            a2_type_check: 14, // Just enough
1104            a3_cargo_build: 0, // Failed
1105            ..Default::default()
1106        };
1107
1108        let score = calculator.calculate(&breakdown, ScoringMode::Quick);
1109
1110        // compilation = 10 + 14 + 0 = 24 >= 24
1111        assert_eq!(score.compilation, 24);
1112        assert!(score.gateway_passed);
1113    }
1114
1115    #[test]
1116    fn test_grade_mapping() {
1117        assert_eq!(Grade::from_score(100.0), Grade::APlus);
1118        assert_eq!(Grade::from_score(95.0), Grade::APlus);
1119        assert_eq!(Grade::from_score(94.0), Grade::A);
1120        assert_eq!(Grade::from_score(90.0), Grade::A);
1121        assert_eq!(Grade::from_score(89.0), Grade::AMinus);
1122        assert_eq!(Grade::from_score(85.0), Grade::AMinus);
1123        assert_eq!(Grade::from_score(84.0), Grade::BPlus);
1124        assert_eq!(Grade::from_score(80.0), Grade::BPlus);
1125        assert_eq!(Grade::from_score(79.0), Grade::B);
1126        assert_eq!(Grade::from_score(70.0), Grade::B);
1127        assert_eq!(Grade::from_score(69.0), Grade::C);
1128        assert_eq!(Grade::from_score(60.0), Grade::C);
1129        assert_eq!(Grade::from_score(59.0), Grade::D);
1130        assert_eq!(Grade::from_score(50.0), Grade::D);
1131        assert_eq!(Grade::from_score(49.0), Grade::F);
1132        assert_eq!(Grade::from_score(0.0), Grade::F);
1133    }
1134
1135    #[test]
1136    fn test_breakdown_from_errors() {
1137        let calculator = ScoreCalculator::new();
1138        let errors = vec![
1139            CompilationError {
1140                code: "E0308".to_string(),
1141                message: "type mismatch".to_string(),
1142                location: None,
1143                line: None,
1144            },
1145            CompilationError {
1146                code: "E0308".to_string(),
1147                message: "type mismatch 2".to_string(),
1148                location: None,
1149                line: None,
1150            },
1151            CompilationError {
1152                code: "E0599".to_string(),
1153                message: "method not found".to_string(),
1154                location: None,
1155                line: None,
1156            },
1157        ];
1158
1159        let breakdown = calculator.breakdown_from_errors(&BreakdownInput {
1160            parse_ok: true,
1161            type_check_ok: false,
1162            build_ok: false,
1163            errors: &errors,
1164            doctest_pass: false,
1165            unit_test_pass: false,
1166            property_test_pass: false,
1167            clippy_clean: false,
1168            tdg_grade_b_or_better: false,
1169            complexity_ok: true,
1170            trace_match: false,
1171            output_equiv: false,
1172        });
1173
1174        assert_eq!(breakdown.a1_parse, 10);
1175        assert_eq!(breakdown.a2_type_check, 0);
1176        assert_eq!(breakdown.a3_cargo_build, 0);
1177
1178        // E0308 ratio = 2/3 ≈ 0.67, so b1 = (1 - 0.67) * 10 ≈ 3
1179        assert!(breakdown.b1_no_e0308 <= 4);
1180
1181        // E0599 ratio = 1/3 ≈ 0.33, so b2 = (1 - 0.33) * 8 ≈ 5
1182        assert!(breakdown.b2_no_e0599 >= 5);
1183
1184        // E0425 ratio = 0/3 = 0, so b3 = (1 - 0) * 7 = 7
1185        assert_eq!(breakdown.b3_no_e0425, 7);
1186
1187        assert_eq!(breakdown.d3_complexity, 2);
1188    }
1189
1190    #[test]
1191    fn test_breakdown_from_errors_all_pass() {
1192        let calculator = ScoreCalculator::new();
1193        let empty: Vec<CompilationError> = vec![];
1194
1195        let breakdown = calculator.breakdown_from_errors(&BreakdownInput {
1196            parse_ok: true,
1197            type_check_ok: true,
1198            build_ok: true,
1199            errors: &empty,
1200            doctest_pass: true,
1201            unit_test_pass: true,
1202            property_test_pass: true,
1203            clippy_clean: true,
1204            tdg_grade_b_or_better: true,
1205            complexity_ok: true,
1206            trace_match: true,
1207            output_equiv: true,
1208        });
1209
1210        assert_eq!(breakdown.a1_parse, 10);
1211        assert_eq!(breakdown.a2_type_check, 15);
1212        assert_eq!(breakdown.a3_cargo_build, 15);
1213        assert_eq!(breakdown.c1_doctest, 5);
1214        assert_eq!(breakdown.c2_unit_test, 5);
1215        assert_eq!(breakdown.c3_property_test, 5);
1216        assert_eq!(breakdown.d1_clippy, 5);
1217        assert_eq!(breakdown.d2_tdg, 3);
1218        assert_eq!(breakdown.d3_complexity, 2);
1219        assert_eq!(breakdown.e1_trace_match, 5);
1220        assert_eq!(breakdown.e2_output_equiv, 5);
1221    }
1222
1223    // === TarantulaScore tests ===
1224
1225    #[test]
1226    fn test_tarantula_score() {
1227        let stats = DecisionStats {
1228            failed_count: 8,
1229            passed_count: 2,
1230        };
1231
1232        let tarantula = stats.tarantula_score(10, 10);
1233
1234        // failed_ratio = 8/10 = 0.8
1235        // passed_ratio = 2/10 = 0.2
1236        // suspiciousness = 0.8 / (0.8 + 0.2) = 0.8
1237        assert!((tarantula.suspiciousness - 0.8).abs() < 0.01);
1238    }
1239
1240    #[test]
1241    fn test_tarantula_score_zero_failed() {
1242        let stats = DecisionStats {
1243            failed_count: 0,
1244            passed_count: 5,
1245        };
1246
1247        let tarantula = stats.tarantula_score(0, 10);
1248        assert_eq!(tarantula.suspiciousness, 0.0);
1249    }
1250
1251    #[test]
1252    fn test_tarantula_score_zero_passed() {
1253        let stats = DecisionStats {
1254            failed_count: 5,
1255            passed_count: 0,
1256        };
1257
1258        let tarantula = stats.tarantula_score(10, 0);
1259        // failed_ratio = 5/10 = 0.5
1260        // passed_ratio = 0 (total_passed = 0)
1261        // suspiciousness = 0.5 / (0.5 + 0) = 1.0
1262        assert!((tarantula.suspiciousness - 1.0).abs() < 0.01);
1263    }
1264
1265    #[test]
1266    fn test_tarantula_score_both_zero() {
1267        let stats = DecisionStats {
1268            failed_count: 0,
1269            passed_count: 0,
1270        };
1271
1272        let tarantula = stats.tarantula_score(0, 0);
1273        assert_eq!(tarantula.suspiciousness, 0.0);
1274    }
1275
1276    #[test]
1277    fn test_tarantula_score_clone() {
1278        let score = TarantulaScore {
1279            suspiciousness: 0.5,
1280            failed_count: 3,
1281            passed_count: 7,
1282        };
1283        let cloned = score.clone();
1284        assert_eq!(cloned.suspiciousness, score.suspiciousness);
1285    }
1286
1287    #[test]
1288    fn test_tarantula_score_debug() {
1289        let score = TarantulaScore {
1290            suspiciousness: 0.5,
1291            failed_count: 3,
1292            passed_count: 7,
1293        };
1294        let debug = format!("{:?}", score);
1295        assert!(debug.contains("TarantulaScore"));
1296    }
1297
1298    // === DecisionStats tests ===
1299
1300    #[test]
1301    fn test_decision_stats_default() {
1302        let stats = DecisionStats::default();
1303        assert_eq!(stats.failed_count, 0);
1304        assert_eq!(stats.passed_count, 0);
1305    }
1306
1307    #[test]
1308    fn test_decision_stats_clone() {
1309        let stats = DecisionStats {
1310            failed_count: 5,
1311            passed_count: 10,
1312        };
1313        let cloned = stats.clone();
1314        assert_eq!(cloned.failed_count, stats.failed_count);
1315    }
1316
1317    #[test]
1318    fn test_decision_stats_debug() {
1319        let stats = DecisionStats::default();
1320        let debug = format!("{:?}", stats);
1321        assert!(debug.contains("DecisionStats"));
1322    }
1323
1324    // === Corpus aggregation tests ===
1325
1326    #[test]
1327    fn test_corpus_aggregation() {
1328        let calculator = ScoreCalculator::new();
1329
1330        let results = vec![
1331            SingleShotResult {
1332                file_path: PathBuf::from("a.py"),
1333                score: SingleShotScore {
1334                    total: 80,
1335                    compilation: 40,
1336                    type_inference: 20,
1337                    test_coverage: 10,
1338                    code_quality: 5,
1339                    semantic_equivalence: 5,
1340                    gateway_passed: true,
1341                    mode: ScoringMode::Fast,
1342                },
1343                category_breakdown: CategoryBreakdown::default(),
1344                error_details: vec![],
1345                transpiler_decisions: vec![],
1346            },
1347            SingleShotResult {
1348                file_path: PathBuf::from("b.py"),
1349                score: SingleShotScore {
1350                    total: 60,
1351                    compilation: 30,
1352                    type_inference: 15,
1353                    test_coverage: 10,
1354                    code_quality: 3,
1355                    semantic_equivalence: 2,
1356                    gateway_passed: true,
1357                    mode: ScoringMode::Fast,
1358                },
1359                category_breakdown: CategoryBreakdown::default(),
1360                error_details: vec![],
1361                transpiler_decisions: vec![],
1362            },
1363        ];
1364
1365        let report = calculator.aggregate(&results);
1366
1367        assert!((report.aggregate_score - 70.0).abs() < 0.01);
1368        assert_eq!(report.grade, Grade::B);
1369    }
1370
1371    #[test]
1372    fn test_corpus_aggregation_empty() {
1373        let calculator = ScoreCalculator::new();
1374        let results: Vec<SingleShotResult> = vec![];
1375
1376        let report = calculator.aggregate(&results);
1377
1378        assert_eq!(report.aggregate_score, 0.0);
1379        assert_eq!(report.grade, Grade::F);
1380        assert!(report.results.is_empty());
1381        assert!(report.top_blockers.is_empty());
1382    }
1383
1384    #[test]
1385    fn test_corpus_aggregation_with_errors() {
1386        let calculator = ScoreCalculator::new();
1387
1388        let results = vec![
1389            SingleShotResult {
1390                file_path: PathBuf::from("a.py"),
1391                score: SingleShotScore {
1392                    total: 50,
1393                    ..Default::default()
1394                },
1395                category_breakdown: CategoryBreakdown::default(),
1396                error_details: vec![CompilationError {
1397                    code: "E0308".to_string(),
1398                    message: "type mismatch".to_string(),
1399                    location: None,
1400                    line: None,
1401                }],
1402                transpiler_decisions: vec![],
1403            },
1404            SingleShotResult {
1405                file_path: PathBuf::from("b.py"),
1406                score: SingleShotScore {
1407                    total: 50,
1408                    ..Default::default()
1409                },
1410                category_breakdown: CategoryBreakdown::default(),
1411                error_details: vec![CompilationError {
1412                    code: "E0308".to_string(),
1413                    message: "type mismatch".to_string(),
1414                    location: None,
1415                    line: None,
1416                }],
1417                transpiler_decisions: vec![],
1418            },
1419        ];
1420
1421        let report = calculator.aggregate(&results);
1422
1423        // E0308 should be identified as a top blocker
1424        assert!(!report.top_blockers.is_empty());
1425        assert_eq!(report.top_blockers[0].pattern, "E0308");
1426        assert_eq!(report.top_blockers[0].affected_files, 2);
1427    }
1428
1429    #[test]
1430    fn test_corpus_score_report_clone() {
1431        let report = CorpusScoreReport {
1432            results: vec![],
1433            aggregate_score: 75.0,
1434            grade: Grade::B,
1435            category_averages: CategoryBreakdown::default(),
1436            top_blockers: vec![],
1437        };
1438        let cloned = report.clone();
1439        assert_eq!(cloned.aggregate_score, report.aggregate_score);
1440    }
1441
1442    #[test]
1443    fn test_corpus_score_report_debug() {
1444        let report = CorpusScoreReport {
1445            results: vec![],
1446            aggregate_score: 0.0,
1447            grade: Grade::F,
1448            category_averages: CategoryBreakdown::default(),
1449            top_blockers: vec![],
1450        };
1451        let debug = format!("{:?}", report);
1452        assert!(debug.contains("CorpusScoreReport"));
1453    }
1454
1455    // === analyze_score_failures tests ===
1456
1457    #[test]
1458    fn test_analyze_score_failures_empty() {
1459        let results: Vec<SingleShotResult> = vec![];
1460        let analysis = analyze_score_failures(&results);
1461        assert!(analysis.is_empty());
1462    }
1463
1464    #[test]
1465    fn test_analyze_score_failures_with_decisions() {
1466        let results = vec![
1467            SingleShotResult {
1468                file_path: PathBuf::from("a.py"),
1469                score: SingleShotScore {
1470                    total: 50, // Failed (< 80)
1471                    ..Default::default()
1472                },
1473                category_breakdown: CategoryBreakdown::default(),
1474                error_details: vec![],
1475                transpiler_decisions: vec![TranspilerDecision::TypeInference {
1476                    variable: "x".to_string(),
1477                    inferred_type: "Value".to_string(),
1478                }],
1479            },
1480            SingleShotResult {
1481                file_path: PathBuf::from("b.py"),
1482                score: SingleShotScore {
1483                    total: 90, // Passed (>= 80)
1484                    ..Default::default()
1485                },
1486                category_breakdown: CategoryBreakdown::default(),
1487                error_details: vec![],
1488                transpiler_decisions: vec![TranspilerDecision::TypeInference {
1489                    variable: "x".to_string(),
1490                    inferred_type: "Value".to_string(),
1491                }],
1492            },
1493        ];
1494
1495        let analysis = analyze_score_failures(&results);
1496
1497        assert_eq!(analysis.len(), 1);
1498        let decision = TranspilerDecision::TypeInference {
1499            variable: "x".to_string(),
1500            inferred_type: "Value".to_string(),
1501        };
1502        let score = analysis.get(&decision).unwrap();
1503        // 1 failed, 1 passed => suspiciousness = 0.5
1504        assert!((score.suspiciousness - 0.5).abs() < 0.01);
1505    }
1506
1507    // === SingleShotResult tests ===
1508
1509    #[test]
1510    fn test_single_shot_result_clone() {
1511        let result = SingleShotResult {
1512            file_path: PathBuf::from("test.py"),
1513            score: SingleShotScore::default(),
1514            category_breakdown: CategoryBreakdown::default(),
1515            error_details: vec![],
1516            transpiler_decisions: vec![],
1517        };
1518        let cloned = result.clone();
1519        assert_eq!(cloned.file_path, result.file_path);
1520    }
1521
1522    #[test]
1523    fn test_single_shot_result_debug() {
1524        let result = SingleShotResult {
1525            file_path: PathBuf::from("test.py"),
1526            score: SingleShotScore::default(),
1527            category_breakdown: CategoryBreakdown::default(),
1528            error_details: vec![],
1529            transpiler_decisions: vec![],
1530        };
1531        let debug = format!("{:?}", result);
1532        assert!(debug.contains("SingleShotResult"));
1533    }
1534}