## Coverage Report

#### Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/ruchy/src/runtime/assessment.rs

    Line
    Count
    Source
    1
    //! Educational Assessment System for REPL Replay Testing
    2
    //!
    3
    //! Provides automated grading, rubric evaluation, and academic integrity checking
    4
    //! for educational use of the Ruchy REPL.
    5
    6
    use anyhow::Result;
    7
    use serde::{Serialize, Deserialize};
    8
    use std::collections::{HashMap, HashSet};
    9
    use sha2::{Sha256, Digest};
    10
    use regex::Regex;
    11
    12
    use crate::runtime::replay::{
    13
        ReplSession, Event, ReplayValidator
    14
    };
    15
    use crate::runtime::repl::Repl;
    16
    17
    // ============================================================================
    18
    // Assignment Specification
    19
    // ============================================================================
    20
    21
    /// Complete assignment specification for automated grading
    22
    #[derive(Debug, Clone, Serialize, Deserialize)]
    23
    pub struct Assignment {
    24
        pub id: String,
    25
        pub title: String,
    26
        pub description: String,
    27
        pub setup: AssignmentSetup,
    28
        pub tasks: Vec<Task>,
    29
        pub constraints: AssignmentConstraints,
    30
        pub rubric: GradingRubric,
    31
    }
    32
    33
    /// Initial setup for assignment environment
    34
    #[derive(Debug, Clone, Serialize, Deserialize)]
    35
    pub struct AssignmentSetup {
    36
        pub prelude_code: Vec<String>,
    37
        pub provided_functions: HashMap<String, String>,
    38
        pub immutable_bindings: HashSet<String>,
    39
    }
    40
    41
    /// Individual task within an assignment
    42
    #[derive(Debug, Clone, Serialize, Deserialize)]
    43
    pub struct Task {
    44
        pub id: String,
    45
        pub description: String,
    46
        pub points: u32,
    47
        pub test_cases: Vec<TestCase>,
    48
        pub hidden_cases: Vec<TestCase>,
    49
        pub requirements: Vec<Requirement>,
    50
    }
    51
    52
    /// Test case for validation
    53
    #[derive(Debug, Clone, Serialize, Deserialize)]
    54
    pub struct TestCase {
    55
        pub input: String,
    56
        pub expected: ExpectedBehavior,
    57
        pub points: u32,
    58
        pub timeout_ms: u64,
    59
    }
    60
    61
    /// Expected behavior patterns for test validation
    62
    #[derive(Debug, Clone, Serialize, Deserialize)]
    63
    pub enum ExpectedBehavior {
    64
        ExactOutput(String),
    65
        Pattern(String), // Regex pattern
    66
        TypeSignature(String),
    67
        Predicate(PredicateCheck),
    68
        PerformanceBound {
    69
            max_ns: u64,
    70
            max_bytes: usize,
    71
        },
    72
    }
    73
    74
    /// Predicate-based checking for complex validation
    75
    #[derive(Debug, Clone, Serialize, Deserialize)]
    76
    pub struct PredicateCheck {
    77
        pub name: String,
    78
        pub check_fn: String, // Code to evaluate
    79
    }
    80
    81
    /// Requirements that must be met
    82
    #[derive(Debug, Clone, Serialize, Deserialize)]
    83
    pub enum Requirement {
    84
        UseRecursion,
    85
        NoLoops,
    86
        UseHigherOrderFunctions,
    87
        TypeSafe,
    88
        PureFunction,
    89
        TailRecursive,
    90
    }
    91
    92
    /// Constraints on the assignment
    93
    #[derive(Debug, Clone, Serialize, Deserialize)]
    94
    pub struct AssignmentConstraints {
    95
        pub max_time_ms: u64,
    96
        pub max_memory_mb: usize,
    97
        pub allowed_imports: Vec<String>,
    98
        pub forbidden_keywords: Vec<String>,
    99
        pub performance: Option<PerformanceConstraints>,
    100
    }
    101
    102
    /// Performance requirements
    103
    #[derive(Debug, Clone, Serialize, Deserialize)]
    104
    pub struct PerformanceConstraints {
    105
        pub max_cpu_ms: u64,
    106
        pub max_heap_mb: usize,
    107
        pub complexity_bound: String, // e.g., "O(n log n)"
    108
    }
    109
    110
    // ============================================================================
    111
    // Grading Rubric
    112
    // ============================================================================
    113
    114
    /// Grading rubric with weighted categories
    115
    #[derive(Debug, Clone, Serialize, Deserialize)]
    116
    pub struct GradingRubric {
    117
        pub categories: Vec<RubricCategory>,
    118
        pub late_penalty: Option<LatePenalty>,
    119
        pub bonus_criteria: Vec<BonusCriterion>,
    120
    }
    121
    122
    /// Category in the grading rubric
    123
    #[derive(Debug, Clone, Serialize, Deserialize)]
    124
    pub struct RubricCategory {
    125
        pub name: String,
    126
        pub weight: f32,
    127
        pub criteria: Vec<Criterion>,
    128
    }
    129
    130
    /// Individual grading criterion
    131
    #[derive(Debug, Clone, Serialize, Deserialize)]
    132
    pub struct Criterion {
    133
        pub description: String,
    134
        pub max_points: u32,
    135
        pub evaluation: CriterionEvaluation,
    136
    }
    137
    138
    /// How to evaluate a criterion
    139
    #[derive(Debug, Clone, Serialize, Deserialize)]
    140
    pub enum CriterionEvaluation {
    141
        Automatic(AutomaticCheck),
    142
        Manual(String), // Instructions for manual grading
    143
        Hybrid { auto_weight: f32, manual_weight: f32 },
    144
    }
    145
    146
    /// Automatic checking methods
    147
    #[derive(Debug, Clone, Serialize, Deserialize)]
    148
    pub enum AutomaticCheck {
    149
        TestsPassed,
    150
        CodeQuality { min_score: f32 },
    151
        Documentation { required_sections: Vec<String> },
    152
        Performance { metric: String, threshold: f64 },
    153
    }
    154
    155
    /// Late submission penalty
    156
    #[derive(Debug, Clone, Serialize, Deserialize)]
    157
    pub struct LatePenalty {
    158
        pub grace_hours: u32,
    159
        pub penalty_per_day: f32,
    160
        pub max_days_late: u32,
    161
    }
    162
    163
    /// Bonus points criteria
    164
    #[derive(Debug, Clone, Serialize, Deserialize)]
    165
    pub struct BonusCriterion {
    166
        pub description: String,
    167
        pub points: u32,
    168
        pub check: BonusCheck,
    169
    }
    170
    171
    /// Bonus checking methods
    172
    #[derive(Debug, Clone, Serialize, Deserialize)]
    173
    pub enum BonusCheck {
    174
        ExtraFeature(String),
    175
        Optimization { improvement_percent: f32 },
    176
        CreativeSolution,
    177
    }
    178
    179
    // ============================================================================
    180
    // Grading Engine
    181
    // ============================================================================
    182
    183
    /// Main grading engine for automated assessment
    184
    pub struct GradingEngine {
    185
        pub replay_validator: ReplayValidator,
    186
        pub plagiarism_detector: PlagiarismDetector,
    187
        pub secure_sandbox: SecureSandbox,
    188
    }
    189
    190
    impl Default for GradingEngine {
    191
    0
        fn default() -> Self {
    192
    0
            Self::new()
    193
    0
        }
    194
    }
    195
    196
    impl GradingEngine {
    197
    0
        pub fn new() -> Self {
    198
    0
            Self {
    199
    0
                replay_validator: ReplayValidator::new(true),
    200
    0
                plagiarism_detector: PlagiarismDetector::new(),
    201
    0
                secure_sandbox: SecureSandbox::new(),
    202
    0
            }
    203
    0
        }
    204
    205
        /// Grade a student submission against an assignment
    206
    0
        pub fn grade_submission(
    207
    0
            &mut self,
    208
    0
            assignment: &Assignment,
    209
    0
            submission: &ReplSession,
    210
    0
        ) -> GradeReport {
    211
    0
            let mut report = GradeReport::new(assignment.id.clone());
    212
    213
            // Verify submission integrity
    214
    0
            if !self.verify_no_tampering(submission) {
    215
    0
                report.mark_invalid("Session integrity check failed");
    216
    0
                return report;
    217
    0
            }
    218
    219
            // Setup assignment environment
    220
    0
            let mut repl = match self.secure_sandbox.create_isolated_repl() {
    221
    0
                Ok(r) => r,
    222
    0
                Err(e) => {
    223
    0
                    report.mark_invalid(&format!("Failed to create sandbox: {e}"));
    224
    0
                    return report;
    225
                }
    226
            };
    227
    228
            // Load assignment setup
    229
    0
            if let Err(e) = self.load_setup(&mut repl, &assignment.setup) {
    230
    0
                report.mark_invalid(&format!("Failed to load setup: {e}"));
    231
    0
                return report;
    232
    0
            }
    233
    234
            // Grade each task
    235
    0
            for task in &assignment.tasks {
    236
    0
                let task_grade = self.grade_task(&mut repl, task, submission);
    237
    0
                report.add_task_grade(task_grade);
    238
    0
            }
    239
    240
            // Evaluate rubric
    241
    0
            report.rubric_score = self.evaluate_rubric(&assignment.rubric, submission);
    242
    243
            // Check performance requirements
    244
    0
            if let Some(perf) = &assignment.constraints.performance {
    245
    0
                report.performance_score = self.measure_performance(submission, perf);
    246
    0
            }
    247
    248
            // Detect plagiarism
    249
    0
            report.originality_score = self.plagiarism_detector.analyze(submission);
    250
    251
            // Calculate final grade
    252
    0
            report.calculate_final_grade();
    253
    254
    0
            report
    255
    0
        }
    256
    257
    0
        fn verify_no_tampering(&self, session: &ReplSession) -> bool {
    258
            // Verify event sequence integrity
    259
    0
            let mut prev_timestamp = 0u64;
    260
    0
            for event in &session.timeline {
    261
    0
                if event.timestamp_ns < prev_timestamp {
    262
    0
                    return false; // Time went backwards
    263
    0
                }
    264
    0
                prev_timestamp = event.timestamp_ns;
    265
            }
    266
    267
            // Verify state hashes are consistent
    268
            // In production, would replay and verify each hash
    269
    0
            true
    270
    0
        }
    271
    272
    0
        fn load_setup(&self, repl: &mut Repl, setup: &AssignmentSetup) -> Result<()> {
    273
            // Load prelude code
    274
    0
            for code in &setup.prelude_code {
    275
    0
                repl.eval(code)?;
    276
            }
    277
    278
            // Load provided functions
    279
    0
            for (name, code) in &setup.provided_functions {
    280
    0
                repl.eval(&format!("let {name} = {code}"))?;
    281
            }
    282
    283
    0
            Ok(())
    284
    0
        }
    285
    286
    0
        fn grade_task(
    287
    0
            &mut self,
    288
    0
            repl: &mut Repl,
    289
    0
            task: &Task,
    290
    0
            _submission: &ReplSession,
    291
    0
        ) -> TaskGrade {
    292
    0
            let mut grade = TaskGrade::new(task.id.clone());
    293
    294
            // Test visible cases
    295
    0
            for test in &task.test_cases {
    296
    0
                let result = self.run_test_case(repl, test);
    297
    0
                grade.add_test_result(test.input.clone(), result);
    298
    0
            }
    299
    300
            // Test hidden cases (for academic integrity)
    301
    0
            for test in &task.hidden_cases {
    302
    0
                let result = self.run_test_case(repl, test);
    303
    0
                grade.add_hidden_result(test.input.clone(), result);
    304
    0
            }
    305
    306
            // Check requirements
    307
    0
            for req in &task.requirements {
    308
    0
                if self.check_requirement(repl, req) {
    309
    0
                    grade.requirements_met.insert(format!("{req:?}"));
    310
    0
                }
    311
            }
    312
    313
    0
            grade.calculate_score(task.points);
    314
    0
            grade
    315
    0
        }
    316
    317
    0
        fn run_test_case(&self, repl: &mut Repl, test: &TestCase) -> TestResult {
    318
            // Execute with timeout
    319
    0
            let start = std::time::Instant::now();
    320
    0
            let output = match repl.eval(&test.input) {
    321
    0
                Ok(out) => out,
    322
    0
                Err(e) => {
    323
    0
                    return TestResult {
    324
    0
                        passed: false,
    325
    0
                        points_earned: 0,
    326
    0
                        feedback: format!("Error: {e}"),
    327
    0
                        execution_time_ms: start.elapsed().as_millis() as u64,
    328
    0
                    };
    329
                }
    330
            };
    331
    332
    0
            let execution_time_ms = start.elapsed().as_millis() as u64;
    333
    334
            // Check timeout
    335
    0
            if execution_time_ms > test.timeout_ms {
    336
    0
                return TestResult {
    337
    0
                    passed: false,
    338
    0
                    points_earned: 0,
    339
    0
                    feedback: format!("Timeout: {}ms > {}ms", execution_time_ms, test.timeout_ms),
    340
    0
                    execution_time_ms,
    341
    0
                };
    342
    0
            }
    343
    344
            // Check expected behavior
    345
    0
            let (passed, feedback) = match &test.expected {
    346
    0
                ExpectedBehavior::ExactOutput(expected) => {
    347
    0
                    let passed = output == *expected;
    348
    0
                    let feedback = if passed {
    349
    0
                        "Correct output".to_string()
    350
                    } else {
    351
    0
                        format!("Expected '{expected}', got '{output}'")
    352
                    };
    353
    0
                    (passed, feedback)
    354
                }
    355
    0
                ExpectedBehavior::Pattern(pattern) => {
    356
    0
                    let regex = Regex::new(pattern).unwrap_or_else(|_| Regex::new(".*").unwrap());
    357
    0
                    let passed = regex.is_match(&output);
    358
    0
                    let feedback = if passed {
    359
    0
                        "Output matches pattern".to_string()
    360
                    } else {
    361
    0
                        format!("Output doesn't match pattern: {pattern}")
    362
                    };
    363
    0
                    (passed, feedback)
    364
                }
    365
    0
                ExpectedBehavior::TypeSignature(expected_type) => {
    366
                    // In production, would check actual type
    367
    0
                    let passed = output.contains(expected_type);
    368
    0
                    let feedback = if passed {
    369
    0
                        "Type signature correct".to_string()
    370
                    } else {
    371
    0
                        format!("Expected type {expected_type}")
    372
                    };
    373
    0
                    (passed, feedback)
    374
                }
    375
    0
                _ => (false, "Unsupported check".to_string()),
    376
            };
    377
    378
            TestResult {
    379
    0
                passed,
    380
    0
                points_earned: if passed { test.points } else { 0 },
    381
    0
                feedback,
    382
    0
                execution_time_ms,
    383
            }
    384
    0
        }
    385
    386
    0
        fn check_requirement(&self, _repl: &Repl, req: &Requirement) -> bool {
    387
            // In production, would analyze AST to check requirements
    388
    0
            match req {
    389
    0
                Requirement::UseRecursion => true, // Would check for recursive calls
    390
    0
                Requirement::NoLoops => true,      // Would check for loop constructs
    391
    0
                Requirement::UseHigherOrderFunctions => true, // Would check for HOF usage
    392
    0
                Requirement::TypeSafe => true,     // Would verify type safety
    393
    0
                Requirement::PureFunction => true, // Would check for side effects
    394
    0
                Requirement::TailRecursive => true, // Would verify tail recursion
    395
            }
    396
    0
        }
    397
    398
    0
        fn evaluate_rubric(&self, rubric: &GradingRubric, _submission: &ReplSession) -> f32 {
    399
    0
            let mut total_score = 0.0;
    400
    0
            let mut total_weight = 0.0;
    401
    402
    0
            for category in &rubric.categories {
    403
    0
                let category_score = self.evaluate_category(category);
    404
    0
                total_score += category_score * category.weight;
    405
    0
                total_weight += category.weight;
    406
    0
            }
    407
    408
    0
            if total_weight > 0.0 {
    409
    0
                (total_score / total_weight) * 100.0
    410
            } else {
    411
    0
                0.0
    412
            }
    413
    0
        }
    414
    415
    0
        fn evaluate_category(&self, category: &RubricCategory) -> f32 {
    416
    0
            let mut earned = 0u32;
    417
    0
            let mut possible = 0u32;
    418
    419
    0
            for criterion in &category.criteria {
    420
    0
                possible += criterion.max_points;
    421
    0
                earned += self.evaluate_criterion(criterion);
    422
    0
            }
    423
    424
    0
            if possible > 0 {
    425
    0
                earned as f32 / possible as f32
    426
            } else {
    427
    0
                0.0
    428
            }
    429
    0
        }
    430
    431
    0
        fn evaluate_criterion(&self, criterion: &Criterion) -> u32 {
    432
    0
            match &criterion.evaluation {
    433
    0
                CriterionEvaluation::Automatic(check) => {
    434
    0
                    match check {
    435
    0
                        AutomaticCheck::TestsPassed => criterion.max_points,
    436
    0
                        AutomaticCheck::CodeQuality { min_score } => {
    437
                            // In production, would run quality analysis
    438
    0
                            if *min_score <= 0.8 {
    439
    0
                                criterion.max_points
    440
                            } else {
    441
    0
                                0
    442
                            }
    443
                        }
    444
    0
                        _ => 0,
    445
                    }
    446
                }
    447
    0
                CriterionEvaluation::Manual(_) => 0, // Requires manual grading
    448
    0
                CriterionEvaluation::Hybrid { auto_weight, .. } => {
    449
    0
                    (criterion.max_points as f32 * auto_weight) as u32
    450
                }
    451
            }
    452
    0
        }
    453
    454
    0
        fn measure_performance(
    455
    0
            &self,
    456
    0
            session: &ReplSession,
    457
    0
            constraints: &PerformanceConstraints,
    458
    0
        ) -> f32 {
    459
    0
            let mut score: f32 = 100.0;
    460
    461
            // Check CPU time
    462
    0
            let total_cpu_ns: u64 = session.timeline.iter()
    463
    0
                .filter_map(|e| {
    464
    0
                    if let Event::ResourceUsage { cpu_ns, .. } = &e.event {
    465
    0
                        Some(*cpu_ns)
    466
                    } else {
    467
    0
                        None
    468
                    }
    469
    0
                })
    470
    0
                .sum();
    471
    472
    0
            let cpu_ms = total_cpu_ns / 1_000_000;
    473
    0
            if cpu_ms > constraints.max_cpu_ms {
    474
    0
                score -= 20.0;
    475
    0
            }
    476
    477
            // Check heap usage
    478
    0
            let max_heap: usize = session.timeline.iter()
    479
    0
                .filter_map(|e| {
    480
    0
                    if let Event::ResourceUsage { heap_bytes, .. } = &e.event {
    481
    0
                        Some(*heap_bytes)
    482
                    } else {
    483
    0
                        None
    484
                    }
    485
    0
                })
    486
    0
                .max()
    487
    0
                .unwrap_or(0);
    488
    489
    0
            let heap_mb = max_heap / (1024 * 1024);
    490
    0
            if heap_mb > constraints.max_heap_mb {
    491
    0
                score -= 20.0;
    492
    0
            }
    493
    494
    0
            score.max(0.0).min(100.0)
    495
    0
        }
    496
    }
    497
    498
    // ============================================================================
    499
    // Plagiarism Detection
    500
    // ============================================================================
    501
    502
    /// AST-based plagiarism detection system
    503
    pub struct PlagiarismDetector {
    504
        known_submissions: Vec<AstFingerprint>,
    505
    }
    506
    507
    /// Structural fingerprint of AST for comparison
    508
    #[derive(Debug, Clone)]
    509
    pub struct AstFingerprint {
    510
        pub hash: String,
    511
        pub structure: Vec<String>,
    512
        pub complexity: usize,
    513
    }
    514
    515
    impl Default for PlagiarismDetector {
    516
    0
        fn default() -> Self {
    517
    0
            Self::new()
    518
    0
        }
    519
    }
    520
    521
    impl PlagiarismDetector {
    522
    0
        pub fn new() -> Self {
    523
    0
            Self {
    524
    0
                known_submissions: Vec::new(),
    525
    0
            }
    526
    0
        }
    527
    528
    0
        pub fn analyze(&self, submission: &ReplSession) -> f32 {
    529
            // Generate fingerprint for submission
    530
    0
            let fingerprint = self.generate_fingerprint(submission);
    531
    532
            // Compare against known submissions
    533
    0
            for known in &self.known_submissions {
    534
    0
                let similarity = self.compute_similarity(&fingerprint, known);
    535
    0
                if similarity > 0.85 {
    536
    0
                    return 100.0 * (1.0 - similarity); // High similarity = low originality
    537
    0
                }
    538
            }
    539
    540
    0
            100.0 // Full originality score
    541
    0
        }
    542
    543
    0
        fn generate_fingerprint(&self, session: &ReplSession) -> AstFingerprint {
    544
    0
            let mut hasher = Sha256::new();
    545
    0
            let mut structure = Vec::new();
    546
    547
            // Extract structural patterns from code inputs
    548
    0
            for event in &session.timeline {
    549
    0
                if let Event::Input { text, .. } = &event.event {
    550
    0
                    hasher.update(text.as_bytes());
    551
    0
                    structure.push(self.extract_structure(text));
    552
    0
                }
    553
            }
    554
    555
    0
            AstFingerprint {
    556
    0
                hash: format!("{:x}", hasher.finalize()),
    557
    0
                structure,
    558
    0
                complexity: session.timeline.len(),
    559
    0
            }
    560
    0
        }
    561
    562
    0
        fn extract_structure(&self, code: &str) -> String {
    563
            // Simplified: extract function definitions and control flow
    564
    0
            let mut patterns = Vec::new();
    565
    566
    0
            if code.contains("fn ") || code.contains("fun ") {
    567
    0
                patterns.push("FN");
    568
    0
            }
    569
    0
            if code.contains("if ") {
    570
    0
                patterns.push("IF");
    571
    0
            }
    572
    0
            if code.contains("for ") || code.contains("while ") {
    573
    0
                patterns.push("LOOP");
    574
    0
            }
    575
    0
            if code.contains("match ") {
    576
    0
                patterns.push("MATCH");
    577
    0
            }
    578
    579
    0
            patterns.join("-")
    580
    0
        }
    581
    582
    0
        fn compute_similarity(&self, fp1: &AstFingerprint, fp2: &AstFingerprint) -> f32 {
    583
    0
            if fp1.hash == fp2.hash {
    584
    0
                return 1.0; // Identical
    585
    0
            }
    586
    587
            // Compare structural patterns
    588
    0
            let common: usize = fp1.structure.iter()
    589
    0
                .zip(fp2.structure.iter())
    590
    0
                .filter(|(a, b)| a == b)
    591
    0
                .count();
    592
    593
    0
            let total = fp1.structure.len().max(fp2.structure.len());
    594
    0
            if total > 0 {
    595
    0
                common as f32 / total as f32
    596
            } else {
    597
    0
                0.0
    598
            }
    599
    0
        }
    600
    }
    601
    602
    // ============================================================================
    603
    // Secure Sandbox
    604
    // ============================================================================
    605
    606
    /// Secure execution environment for untrusted code
    607
    pub struct SecureSandbox {
    608
        #[allow(dead_code)]
    609
        resource_limits: ResourceLimits,
    610
    }
    611
    612
    #[derive(Debug, Clone)]
    613
    pub struct ResourceLimits {
    614
        pub max_heap_mb: usize,
    615
        pub max_stack_kb: usize,
    616
        pub max_cpu_ms: u64,
    617
    }
    618
    619
    impl Default for SecureSandbox {
    620
    0
        fn default() -> Self {
    621
    0
            Self::new()
    622
    0
        }
    623
    }
    624
    625
    impl SecureSandbox {
    626
    0
        pub fn new() -> Self {
    627
    0
            Self {
    628
    0
                resource_limits: ResourceLimits {
    629
    0
                    max_heap_mb: 100,
    630
    0
                    max_stack_kb: 8192,
    631
    0
                    max_cpu_ms: 5000,
    632
    0
                },
    633
    0
            }
    634
    0
        }
    635
    636
    0
        pub fn create_isolated_repl(&self) -> Result<Repl> {
    637
            // In production, would create actual sandboxed environment
    638
            // For now, create regular REPL with resource tracking
    639
    0
            Repl::new()
    640
    0
        }
    641
    }
    642
    643
    // ============================================================================
    644
    // Grade Report
    645
    // ============================================================================
    646
    647
    /// Complete grading report for a submission
    648
    #[derive(Debug, Clone, Serialize, Deserialize)]
    649
    pub struct GradeReport {
    650
        pub assignment_id: String,
    651
        pub submission_time: String,
    652
        pub task_grades: Vec<TaskGrade>,
    653
        pub rubric_score: f32,
    654
        pub performance_score: f32,
    655
        pub originality_score: f32,
    656
        pub final_grade: f32,
    657
        pub feedback: Vec<String>,
    658
        pub violations: Vec<String>,
    659
        pub is_valid: bool,
    660
    }
    661
    662
    impl GradeReport {
    663
    0
        pub fn new(assignment_id: String) -> Self {
    664
    0
            Self {
    665
    0
                assignment_id,
    666
    0
                submission_time: chrono::Utc::now().to_rfc3339(),
    667
    0
                task_grades: Vec::new(),
    668
    0
                rubric_score: 0.0,
    669
    0
                performance_score: 100.0,
    670
    0
                originality_score: 100.0,
    671
    0
                final_grade: 0.0,
    672
    0
                feedback: Vec::new(),
    673
    0
                violations: Vec::new(),
    674
    0
                is_valid: true,
    675
    0
            }
    676
    0
        }
    677
    678
    0
        pub fn mark_invalid(&mut self, reason: &str) {
    679
    0
            self.is_valid = false;
    680
    0
            self.violations.push(reason.to_string());
    681
    0
            self.final_grade = 0.0;
    682
    0
        }
    683
    684
    0
        pub fn add_task_grade(&mut self, grade: TaskGrade) {
    685
    0
            self.task_grades.push(grade);
    686
    0
        }
    687
    688
    0
        pub fn calculate_final_grade(&mut self) {
    689
    0
            if !self.is_valid {
    690
    0
                self.final_grade = 0.0;
    691
    0
                return;
    692
    0
            }
    693
    694
            // Calculate task score
    695
    0
            let task_score: f32 = if self.task_grades.is_empty() {
    696
    0
                0.0
    697
            } else {
    698
    0
                let earned: u32 = self.task_grades.iter().map(|g| g.points_earned).sum();
    699
    0
                let possible: u32 = self.task_grades.iter().map(|g| g.points_possible).sum();
    700
    0
                if possible > 0 {
    701
    0
                    (earned as f32 / possible as f32) * 100.0
    702
                } else {
    703
    0
                    0.0
    704
                }
    705
            };
    706
    707
            // Weighted average: 60% tasks, 20% rubric, 10% performance, 10% originality
    708
    0
            self.final_grade = task_score * 0.6
    709
    0
                + self.rubric_score * 0.2
    710
    0
                + self.performance_score * 0.1
    711
    0
                + self.originality_score * 0.1;
    712
    713
            // Add feedback based on grade
    714
    0
            if self.final_grade >= 90.0 {
    715
    0
                self.feedback.push("Excellent work!".to_string());
    716
    0
            } else if self.final_grade >= 80.0 {
    717
    0
                self.feedback.push("Good job!".to_string());
    718
    0
            } else if self.final_grade >= 70.0 {
    719
    0
                self.feedback.push("Satisfactory work.".to_string());
    720
    0
            } else {
    721
    0
                self.feedback.push("Needs improvement.".to_string());
    722
    0
            }
    723
    0
        }
    724
    }
    725
    726
    /// Grade for an individual task
    727
    #[derive(Debug, Clone, Serialize, Deserialize)]
    728
    pub struct TaskGrade {
    729
        pub task_id: String,
    730
        pub points_earned: u32,
    731
        pub points_possible: u32,
    732
        pub test_results: Vec<(String, TestResult)>,
    733
        pub hidden_results: Vec<(String, TestResult)>,
    734
        pub requirements_met: HashSet<String>,
    735
    }
    736
    737
    impl TaskGrade {
    738
    0
        pub fn new(task_id: String) -> Self {
    739
    0
            Self {
    740
    0
                task_id,
    741
    0
                points_earned: 0,
    742
    0
                points_possible: 0,
    743
    0
                test_results: Vec::new(),
    744
    0
                hidden_results: Vec::new(),
    745
    0
                requirements_met: HashSet::new(),
    746
    0
            }
    747
    0
        }
    748
    749
    0
        pub fn add_test_result(&mut self, input: String, result: TestResult) {
    750
    0
            self.test_results.push((input, result));
    751
    0
        }
    752
    753
    0
        pub fn add_hidden_result(&mut self, input: String, result: TestResult) {
    754
    0
            self.hidden_results.push((input, result));
    755
    0
        }
    756
    757
    0
        pub fn calculate_score(&mut self, max_points: u32) {
    758
    0
            self.points_possible = max_points;
    759
    760
            // Sum points from test results
    761
    0
            let test_points: u32 = self.test_results.iter()
    762
    0
                .map(|(_, r)| r.points_earned)
    763
    0
                .sum();
    764
    0
            let hidden_points: u32 = self.hidden_results.iter()
    765
    0
                .map(|(_, r)| r.points_earned)
    766
    0
                .sum();
    767
    768
    0
            self.points_earned = (test_points + hidden_points).min(max_points);
    769
    0
        }
    770
    }
    771
    772
    /// Result of running a single test case
    773
    #[derive(Debug, Clone, Serialize, Deserialize)]
    774
    pub struct TestResult {
    775
        pub passed: bool,
    776
        pub points_earned: u32,
    777
        pub feedback: String,
    778
        pub execution_time_ms: u64,
    779
    }
    780
    781
    #[cfg(test)]
    782
    mod tests {
    783
        use super::*;
    784
    785
        #[test]
    786
        fn test_grading_engine_creation() {
    787
            let engine = GradingEngine::new();
    788
            assert!(engine.replay_validator.strict_mode);
    789
        }
    790
    791
        #[test]
    792
        fn test_grade_report() {
    793
            let mut report = GradeReport::new("test_assignment".to_string());
    794
            assert!(report.is_valid);
    795
            assert_eq!(report.final_grade, 0.0);
    796
    797
            report.mark_invalid("Test violation");
    798
            assert!(!report.is_valid);
    799
            assert_eq!(report.violations.len(), 1);
    800
        }
    801
    802
        #[test]
    803
        fn test_plagiarism_detection() {
    804
            let detector = PlagiarismDetector::new();
    805
    806
            // Create mock session
    807
            let session = ReplSession {
    808
                version: crate::runtime::replay::SemVer::new(1, 0, 0),
    809
                metadata: crate::runtime::replay::SessionMetadata {
    810
                    session_id: "test".to_string(),
    811
                    created_at: "2025-08-28T10:00:00Z".to_string(),
    812
                    ruchy_version: "1.23.0".to_string(),
    813
                    student_id: Some("student1".to_string()),
    814
                    assignment_id: Some("hw1".to_string()),
    815
                    tags: vec![],
    816
                },
    817
                environment: crate::runtime::replay::Environment {
    818
                    seed: 42,
    819
                    feature_flags: vec![],
    820
                    resource_limits: crate::runtime::replay::ResourceLimits {
    821
                        heap_mb: 100,
    822
                        stack_kb: 8192,
    823
                        cpu_ms: 5000,
    824
                    },
    825
                },
    826
                timeline: vec![],
    827
                checkpoints: std::collections::BTreeMap::new(),
    828
            };
    829
    830
            let score = detector.analyze(&session);
    831
            assert_eq!(score, 100.0); // Empty session should have full originality
    832
        }
    833
    }