Coverage Report

Created: 2025-09-05 15: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
1
    pub fn new() -> Self {
198
1
        Self {
199
1
            replay_validator: ReplayValidator::new(true),
200
1
            plagiarism_detector: PlagiarismDetector::new(),
201
1
            secure_sandbox: SecureSandbox::new(),
202
1
        }
203
1
    }
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
2
    pub fn new() -> Self {
523
2
        Self {
524
2
            known_submissions: Vec::new(),
525
2
        }
526
2
    }
527
    
528
1
    pub fn analyze(&self, submission: &ReplSession) -> f32 {
529
        // Generate fingerprint for submission
530
1
        let fingerprint = self.generate_fingerprint(submission);
531
        
532
        // Compare against known submissions
533
1
        for 
known0
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
1
        100.0 // Full originality score
541
1
    }
542
    
543
1
    fn generate_fingerprint(&self, session: &ReplSession) -> AstFingerprint {
544
1
        let mut hasher = Sha256::new();
545
1
        let mut structure = Vec::new();
546
        
547
        // Extract structural patterns from code inputs
548
1
        for 
event0
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
1
        AstFingerprint {
556
1
            hash: format!("{:x}", hasher.finalize()),
557
1
            structure,
558
1
            complexity: session.timeline.len(),
559
1
        }
560
1
    }
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
1
    pub fn new() -> Self {
627
1
        Self {
628
1
            resource_limits: ResourceLimits {
629
1
                max_heap_mb: 100,
630
1
                max_stack_kb: 8192,
631
1
                max_cpu_ms: 5000,
632
1
            },
633
1
        }
634
1
    }
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
1
    pub fn new(assignment_id: String) -> Self {
664
1
        Self {
665
1
            assignment_id,
666
1
            submission_time: chrono::Utc::now().to_rfc3339(),
667
1
            task_grades: Vec::new(),
668
1
            rubric_score: 0.0,
669
1
            performance_score: 100.0,
670
1
            originality_score: 100.0,
671
1
            final_grade: 0.0,
672
1
            feedback: Vec::new(),
673
1
            violations: Vec::new(),
674
1
            is_valid: true,
675
1
        }
676
1
    }
677
    
678
1
    pub fn mark_invalid(&mut self, reason: &str) {
679
1
        self.is_valid = false;
680
1
        self.violations.push(reason.to_string());
681
1
        self.final_grade = 0.0;
682
1
    }
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
1
    fn test_grading_engine_creation() {
787
1
        let engine = GradingEngine::new();
788
1
        assert!(engine.replay_validator.strict_mode);
789
1
    }
790
    
791
    #[test]
792
1
    fn test_grade_report() {
793
1
        let mut report = GradeReport::new("test_assignment".to_string());
794
1
        assert!(report.is_valid);
795
1
        assert_eq!(report.final_grade, 0.0);
796
        
797
1
        report.mark_invalid("Test violation");
798
1
        assert!(!report.is_valid);
799
1
        assert_eq!(report.violations.len(), 1);
800
1
    }
801
    
802
    #[test]
803
1
    fn test_plagiarism_detection() {
804
1
        let detector = PlagiarismDetector::new();
805
        
806
        // Create mock session
807
1
        let session = ReplSession {
808
1
            version: crate::runtime::replay::SemVer::new(1, 0, 0),
809
1
            metadata: crate::runtime::replay::SessionMetadata {
810
1
                session_id: "test".to_string(),
811
1
                created_at: "2025-08-28T10:00:00Z".to_string(),
812
1
                ruchy_version: "1.23.0".to_string(),
813
1
                student_id: Some("student1".to_string()),
814
1
                assignment_id: Some("hw1".to_string()),
815
1
                tags: vec![],
816
1
            },
817
1
            environment: crate::runtime::replay::Environment {
818
1
                seed: 42,
819
1
                feature_flags: vec![],
820
1
                resource_limits: crate::runtime::replay::ResourceLimits {
821
1
                    heap_mb: 100,
822
1
                    stack_kb: 8192,
823
1
                    cpu_ms: 5000,
824
1
                },
825
1
            },
826
1
            timeline: vec![],
827
1
            checkpoints: std::collections::BTreeMap::new(),
828
1
        };
829
        
830
1
        let score = detector.analyze(&session);
831
1
        assert_eq!(score, 100.0); // Empty session should have full originality
832
1
    }
833
}