use crate::classifier::ErrorCategory;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DifficultyLevel {
Easy,
Medium,
Hard,
Expert,
}
impl DifficultyLevel {
#[must_use]
pub fn score(&self) -> f32 {
match self {
Self::Easy => 0.25,
Self::Medium => 0.50,
Self::Hard => 0.75,
Self::Expert => 1.00,
}
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Easy => "EASY",
Self::Medium => "MEDIUM",
Self::Hard => "HARD",
Self::Expert => "EXPERT",
}
}
#[must_use]
pub fn all() -> &'static [Self] {
&[Self::Easy, Self::Medium, Self::Hard, Self::Expert]
}
#[must_use]
pub fn priority(&self) -> u8 {
match self {
Self::Easy => 0,
Self::Medium => 1,
Self::Hard => 2,
Self::Expert => 3,
}
}
}
impl PartialOrd for DifficultyLevel {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DifficultyLevel {
fn cmp(&self, other: &Self) -> Ordering {
self.priority().cmp(&other.priority())
}
}
impl std::fmt::Display for DifficultyLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct CurriculumEntry {
pub id: String,
pub file: String,
pub error_code: String,
pub error_message: String,
pub difficulty: DifficultyLevel,
pub complexity: f32,
}
impl CurriculumEntry {
#[must_use]
pub fn new(
id: impl Into<String>,
file: impl Into<String>,
error_code: impl Into<String>,
error_message: impl Into<String>,
difficulty: DifficultyLevel,
) -> Self {
Self {
id: id.into(),
file: file.into(),
error_code: error_code.into(),
error_message: error_message.into(),
difficulty,
complexity: difficulty.score(),
}
}
#[must_use]
pub fn with_complexity(mut self, complexity: f32) -> Self {
self.complexity = complexity;
self
}
}
#[derive(Debug)]
struct PrioritizedEntry {
entry: CurriculumEntry,
}
impl PartialEq for PrioritizedEntry {
fn eq(&self, other: &Self) -> bool {
self.entry.difficulty == other.entry.difficulty
&& (self.entry.complexity - other.entry.complexity).abs() < f32::EPSILON
}
}
impl Eq for PrioritizedEntry {}
impl PartialOrd for PrioritizedEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PrioritizedEntry {
fn cmp(&self, other: &Self) -> Ordering {
match other.entry.difficulty.cmp(&self.entry.difficulty) {
Ordering::Equal => {
other
.entry
.complexity
.partial_cmp(&self.entry.complexity)
.unwrap_or(Ordering::Equal)
}
ord => ord,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CurriculumStats {
pub total_added: usize,
pub processed: usize,
pub successes: HashMap<DifficultyLevel, usize>,
pub failures: HashMap<DifficultyLevel, usize>,
pub current_level: Option<DifficultyLevel>,
}
impl CurriculumStats {
#[must_use]
pub fn success_rate(&self, level: DifficultyLevel) -> f64 {
let successes = *self.successes.get(&level).unwrap_or(&0);
let failures = *self.failures.get(&level).unwrap_or(&0);
let total = successes + failures;
if total == 0 {
0.0
} else {
successes as f64 / total as f64
}
}
#[must_use]
pub fn overall_success_rate(&self) -> f64 {
let total_successes: usize = self.successes.values().sum();
let total_failures: usize = self.failures.values().sum();
let total = total_successes + total_failures;
if total == 0 {
0.0
} else {
total_successes as f64 / total as f64
}
}
#[must_use]
pub fn progress(&self) -> f64 {
if self.total_added == 0 {
0.0
} else {
self.processed as f64 / self.total_added as f64
}
}
}
pub struct CurriculumScheduler {
queue: BinaryHeap<PrioritizedEntry>,
stats: CurriculumStats,
adaptive: bool,
advance_threshold: f64,
}
impl CurriculumScheduler {
#[must_use]
pub fn new() -> Self {
Self {
queue: BinaryHeap::new(),
stats: CurriculumStats::default(),
adaptive: true,
advance_threshold: 0.8,
}
}
#[must_use]
pub fn with_adaptive(mut self, adaptive: bool) -> Self {
self.adaptive = adaptive;
self
}
#[must_use]
pub fn with_advance_threshold(mut self, threshold: f64) -> Self {
self.advance_threshold = threshold;
self
}
pub fn add(&mut self, entry: CurriculumEntry) {
self.stats.total_added += 1;
self.queue.push(PrioritizedEntry { entry });
}
pub fn add_classified(
&mut self,
id: impl Into<String>,
file: impl Into<String>,
error_code: impl Into<String>,
error_message: impl Into<String>,
) {
let error_code_str = error_code.into();
let error_message_str = error_message.into();
let difficulty = classify_error_difficulty(&error_code_str, &error_message_str);
let entry = CurriculumEntry::new(id, file, error_code_str, error_message_str, difficulty);
self.add(entry);
}
pub fn pop_next(&mut self) -> Option<CurriculumEntry> {
let entry = self.queue.pop().map(|p| p.entry)?;
self.stats.current_level = Some(entry.difficulty);
Some(entry)
}
#[must_use]
pub fn peek(&self) -> Option<&CurriculumEntry> {
self.queue.peek().map(|p| &p.entry)
}
pub fn record_success(&mut self, difficulty: DifficultyLevel) {
self.stats.processed += 1;
*self.stats.successes.entry(difficulty).or_insert(0) += 1;
}
pub fn record_failure(&mut self, difficulty: DifficultyLevel) {
self.stats.processed += 1;
*self.stats.failures.entry(difficulty).or_insert(0) += 1;
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.queue.len()
}
#[must_use]
pub fn stats(&self) -> &CurriculumStats {
&self.stats
}
#[must_use]
pub fn count_by_level(&self) -> HashMap<DifficultyLevel, usize> {
let mut counts = HashMap::new();
for entry in self.queue.iter() {
*counts.entry(entry.entry.difficulty).or_insert(0) += 1;
}
counts
}
#[must_use]
pub fn summary(&self) -> String {
let mut report = String::new();
report.push_str("# Curriculum Summary\n\n");
report.push_str(&format!(
"Total: {} | Processed: {} | Remaining: {}\n\n",
self.stats.total_added,
self.stats.processed,
self.queue.len()
));
report.push_str("## By Difficulty\n\n");
report.push_str("| Level | Count | Success | Failure | Rate |\n");
report.push_str("|-------|-------|---------|---------|------|\n");
let counts = self.count_by_level();
for level in DifficultyLevel::all() {
let count = counts.get(level).unwrap_or(&0);
let successes = *self.stats.successes.get(level).unwrap_or(&0);
let failures = *self.stats.failures.get(level).unwrap_or(&0);
let rate = self.stats.success_rate(*level);
report.push_str(&format!(
"| {} | {} | {} | {} | {:.1}% |\n",
level,
count,
successes,
failures,
rate * 100.0
));
}
report.push_str(&format!(
"\nOverall success rate: {:.1}%\n",
self.stats.overall_success_rate() * 100.0
));
report
}
}
impl Default for CurriculumScheduler {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for CurriculumScheduler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CurriculumScheduler")
.field("pending", &self.queue.len())
.field("processed", &self.stats.processed)
.finish()
}
}
#[must_use]
pub fn classify_error_difficulty(error_code: &str, error_message: &str) -> DifficultyLevel {
let msg_lower = error_message.to_lowercase();
match error_code {
"E0433" | "E0412" | "E0405" | "E0425" => return DifficultyLevel::Easy, "E0601" | "E0602" => return DifficultyLevel::Easy, _ => {}
}
if msg_lower.contains("cannot find") && msg_lower.contains("in this scope") {
return DifficultyLevel::Easy;
}
if msg_lower.contains("unresolved import") {
return DifficultyLevel::Easy;
}
match error_code {
"E0308" => return DifficultyLevel::Medium, "E0599" => return DifficultyLevel::Medium, "E0609" => return DifficultyLevel::Medium, "E0061" | "E0060" => return DifficultyLevel::Medium, _ => {}
}
if msg_lower.contains("mismatched types") {
return DifficultyLevel::Medium;
}
if msg_lower.contains("method not found") {
return DifficultyLevel::Medium;
}
match error_code {
"E0277" => return DifficultyLevel::Hard, "E0382" | "E0505" | "E0507" => return DifficultyLevel::Hard, "E0502" | "E0499" => return DifficultyLevel::Hard, _ => {}
}
if msg_lower.contains("trait") && msg_lower.contains("not implemented") {
return DifficultyLevel::Hard;
}
if msg_lower.contains("moved") || msg_lower.contains("borrow") {
return DifficultyLevel::Hard;
}
match error_code {
"E0106" | "E0495" | "E0621" => return DifficultyLevel::Expert, "E0597" | "E0716" => return DifficultyLevel::Expert, "E0728" | "E0746" => return DifficultyLevel::Expert, _ => {}
}
if msg_lower.contains("lifetime") || msg_lower.contains("'a") {
return DifficultyLevel::Expert;
}
if msg_lower.contains("async") || msg_lower.contains("future") {
return DifficultyLevel::Expert;
}
DifficultyLevel::Medium
}
#[must_use]
pub fn classify_from_category(category: ErrorCategory) -> DifficultyLevel {
match category {
ErrorCategory::SyntaxError => DifficultyLevel::Easy,
ErrorCategory::MissingImport => DifficultyLevel::Easy,
ErrorCategory::TypeMismatch => DifficultyLevel::Medium,
ErrorCategory::BorrowChecker => DifficultyLevel::Hard,
ErrorCategory::LifetimeError => DifficultyLevel::Expert,
ErrorCategory::TraitBound => DifficultyLevel::Hard,
ErrorCategory::Other => DifficultyLevel::Medium,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_difficulty_level_score() {
assert!((DifficultyLevel::Easy.score() - 0.25).abs() < 0.001);
assert!((DifficultyLevel::Medium.score() - 0.50).abs() < 0.001);
assert!((DifficultyLevel::Hard.score() - 0.75).abs() < 0.001);
assert!((DifficultyLevel::Expert.score() - 1.00).abs() < 0.001);
}
#[test]
fn test_difficulty_level_ordering() {
assert!(DifficultyLevel::Easy < DifficultyLevel::Medium);
assert!(DifficultyLevel::Medium < DifficultyLevel::Hard);
assert!(DifficultyLevel::Hard < DifficultyLevel::Expert);
}
#[test]
fn test_curriculum_entry_creation() {
let entry = CurriculumEntry::new(
"1",
"test.rs",
"E0308",
"type mismatch",
DifficultyLevel::Medium,
);
assert_eq!(entry.error_code, "E0308");
assert_eq!(entry.difficulty, DifficultyLevel::Medium);
}
#[test]
fn test_curriculum_scheduler_ordering() {
let mut scheduler = CurriculumScheduler::new();
scheduler.add(CurriculumEntry::new(
"1",
"test.rs",
"E0277",
"trait bound",
DifficultyLevel::Hard,
));
scheduler.add(CurriculumEntry::new(
"2",
"test.rs",
"E0433",
"unresolved",
DifficultyLevel::Easy,
));
scheduler.add(CurriculumEntry::new(
"3",
"test.rs",
"E0106",
"lifetime",
DifficultyLevel::Expert,
));
scheduler.add(CurriculumEntry::new(
"4",
"test.rs",
"E0308",
"mismatch",
DifficultyLevel::Medium,
));
assert_eq!(
scheduler.pop_next().unwrap().difficulty,
DifficultyLevel::Easy
);
assert_eq!(
scheduler.pop_next().unwrap().difficulty,
DifficultyLevel::Medium
);
assert_eq!(
scheduler.pop_next().unwrap().difficulty,
DifficultyLevel::Hard
);
assert_eq!(
scheduler.pop_next().unwrap().difficulty,
DifficultyLevel::Expert
);
}
#[test]
fn test_curriculum_scheduler_stats() {
let mut scheduler = CurriculumScheduler::new();
scheduler.add(CurriculumEntry::new(
"1",
"test.rs",
"E0433",
"easy",
DifficultyLevel::Easy,
));
scheduler.add(CurriculumEntry::new(
"2",
"test.rs",
"E0308",
"medium",
DifficultyLevel::Medium,
));
assert_eq!(scheduler.stats().total_added, 2);
assert_eq!(scheduler.len(), 2);
scheduler.pop_next();
scheduler.record_success(DifficultyLevel::Easy);
assert_eq!(scheduler.stats().processed, 1);
assert!((scheduler.stats().success_rate(DifficultyLevel::Easy) - 1.0).abs() < 0.001);
}
#[test]
fn test_classify_error_difficulty_easy() {
assert_eq!(
classify_error_difficulty("E0433", "unresolved import"),
DifficultyLevel::Easy
);
assert_eq!(
classify_error_difficulty("E0425", "cannot find value"),
DifficultyLevel::Easy
);
}
#[test]
fn test_classify_error_difficulty_medium() {
assert_eq!(
classify_error_difficulty("E0308", "mismatched types"),
DifficultyLevel::Medium
);
assert_eq!(
classify_error_difficulty("E0599", "method not found"),
DifficultyLevel::Medium
);
}
#[test]
fn test_classify_error_difficulty_hard() {
assert_eq!(
classify_error_difficulty("E0277", "trait not implemented"),
DifficultyLevel::Hard
);
assert_eq!(
classify_error_difficulty("E0382", "use of moved value"),
DifficultyLevel::Hard
);
}
#[test]
fn test_classify_error_difficulty_expert() {
assert_eq!(
classify_error_difficulty("E0106", "missing lifetime"),
DifficultyLevel::Expert
);
assert_eq!(
classify_error_difficulty("E0728", "async fn"),
DifficultyLevel::Expert
);
}
#[test]
fn test_classify_from_category() {
assert_eq!(
classify_from_category(ErrorCategory::SyntaxError),
DifficultyLevel::Easy
);
assert_eq!(
classify_from_category(ErrorCategory::TypeMismatch),
DifficultyLevel::Medium
);
assert_eq!(
classify_from_category(ErrorCategory::BorrowChecker),
DifficultyLevel::Hard
);
assert_eq!(
classify_from_category(ErrorCategory::LifetimeError),
DifficultyLevel::Expert
);
}
#[test]
fn test_curriculum_stats_overall_success_rate() {
let mut scheduler = CurriculumScheduler::new();
scheduler.record_success(DifficultyLevel::Easy);
scheduler.record_success(DifficultyLevel::Easy);
scheduler.record_failure(DifficultyLevel::Easy);
scheduler.record_success(DifficultyLevel::Medium);
assert!((scheduler.stats().overall_success_rate() - 0.75).abs() < 0.001);
}
#[test]
fn test_curriculum_scheduler_add_classified() {
let mut scheduler = CurriculumScheduler::new();
scheduler.add_classified("1", "test.rs", "E0433", "unresolved import");
scheduler.add_classified("2", "test.rs", "E0106", "lifetime annotation");
let first = scheduler.pop_next().unwrap();
assert_eq!(first.difficulty, DifficultyLevel::Easy);
let second = scheduler.pop_next().unwrap();
assert_eq!(second.difficulty, DifficultyLevel::Expert);
}
#[test]
fn test_curriculum_summary() {
let mut scheduler = CurriculumScheduler::new();
scheduler.add(CurriculumEntry::new(
"1",
"test.rs",
"E0433",
"easy",
DifficultyLevel::Easy,
));
let summary = scheduler.summary();
assert!(summary.contains("Curriculum Summary"));
assert!(summary.contains("EASY"));
}
}