use crate::error::{OptimError, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerReviewSystem {
pub sessions: HashMap<String, ReviewSession>,
pub reviewers: HashMap<String, Reviewer>,
pub assignments: Vec<ReviewAssignment>,
pub quality_metrics: Vec<ReviewQualityMetric>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewSession {
pub id: String,
pub submission_id: String,
pub review_type: ReviewType,
pub status: ReviewSessionStatus,
pub criteria: Vec<ReviewCriterion>,
pub deadline: DateTime<Utc>,
pub reviews: Vec<PeerReview>,
pub meta_review: Option<MetaReview>,
pub discussion: Vec<ReviewDiscussion>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ReviewType {
SingleBlind,
DoubleBlind,
Open,
PostPublication,
Internal,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ReviewSessionStatus {
WaitingForReviewers,
InProgress,
ReviewsComplete,
MetaReviewInProgress,
Complete,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewCriterion {
pub name: String,
pub description: String,
pub score_range: (f64, f64),
pub weight: f64,
pub required: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerReview {
pub id: String,
pub reviewer_id: String,
pub recommendation: ReviewRecommendation,
pub criterion_scores: HashMap<String, f64>,
pub overall_score: f64,
pub confidence: f64,
pub written_review: WrittenReview,
pub status: ReviewStatus,
pub submitted_at: Option<DateTime<Utc>>,
pub time_spent_minutes: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum ReviewRecommendation {
StrongAccept,
Accept,
WeakAccept,
BorderlineAccept,
BorderlineReject,
WeakReject,
Reject,
StrongReject,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WrittenReview {
pub summary: String,
pub strengths: Vec<String>,
pub weaknesses: Vec<String>,
pub detailed_comments: String,
pub questions: Vec<String>,
pub minor_issues: Vec<String>,
pub suggestions: Vec<String>,
pub committee_comments: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ReviewStatus {
Assigned,
InProgress,
Draft,
Submitted,
RevisionRequested,
Declined,
Overdue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetaReview {
pub meta_reviewer_id: String,
pub review_summary: String,
pub final_recommendation: ReviewRecommendation,
pub justification: String,
pub review_quality: Vec<ReviewQualityAssessment>,
pub areas_of_agreement: Vec<String>,
pub areas_of_disagreement: Vec<String>,
pub decision_rationale: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewQualityAssessment {
pub review_id: String,
pub quality_scores: HashMap<String, f64>,
pub overall_quality: f64,
pub helpfulness: f64,
pub comments: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewDiscussion {
pub id: String,
pub author: String,
pub content: String,
pub reply_to: Option<String>,
pub posted_at: DateTime<Utc>,
pub post_type: DiscussionPostType,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DiscussionPostType {
Question,
Answer,
Clarification,
Disagreement,
Consensus,
ModeratorMessage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Reviewer {
pub id: String,
pub expertise_areas: Vec<String>,
pub experience_level: ExperienceLevel,
pub review_history: ReviewerHistory,
pub availability: ReviewerAvailability,
pub quality_metrics: ReviewerQualityMetrics,
pub preferences: ReviewerPreferences,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ExperienceLevel {
Expert,
Senior,
Experienced,
Junior,
Novice,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewerHistory {
pub total_reviews: usize,
pub reviews_last_year: usize,
pub avg_review_time_days: f64,
pub on_time_rate: f64,
pub avg_quality_score: f64,
pub review_acceptance_rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewerAvailability {
pub available: bool,
pub max_reviews_per_month: u32,
pub current_load: u32,
pub unavailable_periods: Vec<(DateTime<Utc>, DateTime<Utc>)>,
pub preferred_types: Vec<ReviewType>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewerQualityMetrics {
pub thoroughness: f64,
pub constructiveness: f64,
pub timeliness: f64,
pub expertise_match: f64,
pub overall_score: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewerPreferences {
pub preferred_paper_types: Vec<String>,
pub avoid_paper_types: Vec<String>,
pub max_review_length: Option<u32>,
pub anonymous_preference: bool,
pub notification_preferences: NotificationPreferences,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationPreferences {
pub email: bool,
pub reminder_frequency: u32,
pub deadline_notifications: bool,
pub discussion_notifications: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewAssignment {
pub id: String,
pub session_id: String,
pub reviewer_id: String,
pub assigned_at: DateTime<Utc>,
pub due_date: DateTime<Utc>,
pub status: AssignmentStatus,
pub assignment_method: AssignmentMethod,
pub expertise_match: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum AssignmentStatus {
Pending,
Accepted,
Declined,
Completed,
Overdue,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum AssignmentMethod {
Manual,
AutomaticExpertise,
AutomaticLoadBalancing,
Hybrid,
SelfAssignment,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewQualityMetric {
pub name: String,
pub description: String,
pub value_range: (f64, f64),
pub higher_is_better: bool,
pub calculation_method: String,
}
impl Default for PeerReviewSystem {
fn default() -> Self {
Self::new()
}
}
impl PeerReviewSystem {
pub fn new() -> Self {
Self {
sessions: HashMap::new(),
reviewers: HashMap::new(),
assignments: Vec::new(),
quality_metrics: Self::create_default_quality_metrics(),
}
}
pub fn create_review_session(
&mut self,
submission_id: &str,
review_type: ReviewType,
criteria: Vec<ReviewCriterion>,
deadline: DateTime<Utc>,
) -> String {
let session_id = uuid::Uuid::new_v4().to_string();
let session = ReviewSession {
id: session_id.clone(),
submission_id: submission_id.to_string(),
review_type,
status: ReviewSessionStatus::WaitingForReviewers,
criteria,
deadline,
reviews: Vec::new(),
meta_review: None,
discussion: Vec::new(),
};
self.sessions.insert(session_id.clone(), session);
session_id
}
pub fn assign_reviewers(
&mut self,
session_id: &str,
reviewer_ids: &[String],
assignment_method: AssignmentMethod,
) -> Result<Vec<String>> {
if !self.sessions.contains_key(session_id) {
return Err(OptimError::InvalidConfig("Session not found".to_string()));
}
let mut assignment_ids = Vec::new();
let now = Utc::now();
let session = self.sessions.get(session_id).expect("unwrap failed");
for reviewer_id in reviewer_ids {
if !self.reviewers.contains_key(reviewer_id) {
continue; }
let assignment_id = uuid::Uuid::new_v4().to_string();
let assignment = ReviewAssignment {
id: assignment_id.clone(),
session_id: session_id.to_string(),
reviewer_id: reviewer_id.clone(),
assigned_at: now,
due_date: session.deadline,
status: AssignmentStatus::Pending,
assignment_method: assignment_method.clone(),
expertise_match: self.calculate_expertise_match(reviewer_id, session_id),
};
self.assignments.push(assignment);
assignment_ids.push(assignment_id);
}
if let Some(session) = self.sessions.get_mut(session_id) {
session.status = ReviewSessionStatus::InProgress;
}
Ok(assignment_ids)
}
pub fn submit_review(
&mut self,
session_id: &str,
reviewer_id: &str,
review: PeerReview,
) -> Result<()> {
let session = self
.sessions
.get_mut(session_id)
.ok_or_else(|| OptimError::InvalidConfig("Session not found".to_string()))?;
let assignment = self
.assignments
.iter_mut()
.find(|a| a.session_id == session_id && a.reviewer_id == reviewer_id)
.ok_or_else(|| {
OptimError::InvalidConfig("Reviewer not assigned to this session".to_string())
})?;
assignment.status = AssignmentStatus::Completed;
session.reviews.push(review);
let total_assignments = self
.assignments
.iter()
.filter(|a| a.session_id == session_id)
.count();
if session.reviews.len() == total_assignments {
session.status = ReviewSessionStatus::ReviewsComplete;
}
Ok(())
}
pub fn generate_meta_review(&mut self, session_id: &str, meta_reviewer_id: &str) -> Result<()> {
let meta_review = {
let session = self
.sessions
.get(session_id)
.ok_or_else(|| OptimError::InvalidConfig("Session not found".to_string()))?;
if session.status != ReviewSessionStatus::ReviewsComplete {
return Err(OptimError::InvalidConfig(
"Not all reviews are complete".to_string(),
));
}
self.create_meta_review(session, meta_reviewer_id)
};
let session = self.sessions.get_mut(session_id).expect("unwrap failed"); session.meta_review = Some(meta_review);
session.status = ReviewSessionStatus::Complete;
Ok(())
}
pub fn calculate_reviewer_workload(&self, reviewer_id: &str) -> u32 {
self.assignments
.iter()
.filter(|a| {
a.reviewer_id == reviewer_id
&& matches!(
a.status,
AssignmentStatus::Pending | AssignmentStatus::Accepted
)
})
.count() as u32
}
pub fn get_available_reviewers(&self, expertise_area: &str) -> Vec<&Reviewer> {
self.reviewers
.values()
.filter(|r| {
r.availability.available
&& r.expertise_areas
.iter()
.any(|area| area.to_lowercase().contains(&expertise_area.to_lowercase()))
&& r.availability.current_load < r.availability.max_reviews_per_month
})
.collect()
}
pub fn calculate_review_quality(&self, review: &PeerReview) -> f64 {
let mut quality_score = 0.0;
let mut total_weight = 0.0;
let review_length = review.written_review.detailed_comments.len()
+ review
.written_review
.strengths
.iter()
.map(|s| s.len())
.sum::<usize>()
+ review
.written_review
.weaknesses
.iter()
.map(|s| s.len())
.sum::<usize>();
let length_score = ((review_length as f64).ln() / 10.0).min(1.0);
quality_score += length_score * 0.3;
total_weight += 0.3;
let specific_points = review.written_review.strengths.len()
+ review.written_review.weaknesses.len()
+ review.written_review.suggestions.len();
let specificity_score = (specific_points as f64 / 10.0).min(1.0);
quality_score += specificity_score * 0.4;
total_weight += 0.4;
quality_score += review.confidence * 0.3;
total_weight += 0.3;
quality_score / total_weight
}
fn calculate_expertise_match(&self, reviewer_id: &str, sessionid: &str) -> f64 {
if let Some(reviewer) = self.reviewers.get(reviewer_id) {
if reviewer.expertise_areas.is_empty() {
0.5 } else {
0.8 }
} else {
0.0
}
}
fn create_meta_review(&self, session: &ReviewSession, meta_reviewer_id: &str) -> MetaReview {
let review_summary = format!("Meta-review of {} reviews", session.reviews.len());
let recommendations: Vec<_> = session.reviews.iter().map(|r| &r.recommendation).collect();
let final_recommendation = self.determine_consensus_recommendation(&recommendations);
let review_quality: Vec<_> = session
.reviews
.iter()
.map(|review| {
let quality_score = self.calculate_review_quality(review);
ReviewQualityAssessment {
review_id: review.id.clone(),
quality_scores: HashMap::new(),
overall_quality: quality_score,
helpfulness: quality_score * 0.9, comments: if quality_score > 0.7 {
"High quality review".to_string()
} else {
"Review could be more detailed".to_string()
},
}
})
.collect();
MetaReview {
meta_reviewer_id: meta_reviewer_id.to_string(),
review_summary,
final_recommendation,
justification: "Based on consensus of reviewer recommendations".to_string(),
review_quality,
areas_of_agreement: vec!["Technical quality assessment".to_string()],
areas_of_disagreement: vec!["Significance of contribution".to_string()],
decision_rationale: "Decision based on majority reviewer consensus".to_string(),
}
}
fn determine_consensus_recommendation(
&self,
recommendations: &[&ReviewRecommendation],
) -> ReviewRecommendation {
let mut counts = HashMap::new();
for rec in recommendations {
*counts.entry(rec).or_insert(0) += 1;
}
counts
.into_iter()
.max_by_key(|(_, count)| *count)
.map(|(rec, _)| (*rec).clone())
.unwrap_or(ReviewRecommendation::BorderlineReject)
}
fn create_default_quality_metrics() -> Vec<ReviewQualityMetric> {
vec![
ReviewQualityMetric {
name: "Thoroughness".to_string(),
description: "How comprehensive and detailed the review is".to_string(),
value_range: (0.0, 1.0),
higher_is_better: true,
calculation_method: "Based on review length and number of specific points"
.to_string(),
},
ReviewQualityMetric {
name: "Constructiveness".to_string(),
description: "How helpful the review is for improving the work".to_string(),
value_range: (0.0, 1.0),
higher_is_better: true,
calculation_method: "Based on number of suggestions and actionable feedback"
.to_string(),
},
ReviewQualityMetric {
name: "Timeliness".to_string(),
description: "How promptly the review was submitted".to_string(),
value_range: (0.0, 1.0),
higher_is_better: true,
calculation_method: "Based on submission time relative to deadline".to_string(),
},
]
}
}
impl Default for ReviewerHistory {
fn default() -> Self {
Self {
total_reviews: 0,
reviews_last_year: 0,
avg_review_time_days: 14.0,
on_time_rate: 1.0,
avg_quality_score: 0.7,
review_acceptance_rate: 0.9,
}
}
}
impl Default for ReviewerAvailability {
fn default() -> Self {
Self {
available: true,
max_reviews_per_month: 5,
current_load: 0,
unavailable_periods: Vec::new(),
preferred_types: vec![ReviewType::DoubleBlind],
}
}
}
impl Default for ReviewerQualityMetrics {
fn default() -> Self {
Self {
thoroughness: 0.7,
constructiveness: 0.7,
timeliness: 0.8,
expertise_match: 0.7,
overall_score: 0.7,
}
}
}
impl Default for NotificationPreferences {
fn default() -> Self {
Self {
email: true,
reminder_frequency: 7,
deadline_notifications: true,
discussion_notifications: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_peer_review_system_creation() {
let system = PeerReviewSystem::new();
assert!(system.sessions.is_empty());
assert!(system.reviewers.is_empty());
assert!(!system.quality_metrics.is_empty());
}
#[test]
fn test_create_review_session() {
let mut system = PeerReviewSystem::new();
let criteria = vec![ReviewCriterion {
name: "Technical Quality".to_string(),
description: "Assessment of technical merit".to_string(),
score_range: (1.0, 5.0),
weight: 0.4,
required: true,
}];
let deadline = Utc::now() + chrono::Duration::days(14);
let session_id =
system.create_review_session("paper123", ReviewType::DoubleBlind, criteria, deadline);
assert!(system.sessions.contains_key(&session_id));
let session = &system.sessions[&session_id];
assert_eq!(session.submission_id, "paper123");
assert_eq!(session.review_type, ReviewType::DoubleBlind);
}
#[test]
fn test_reviewer_workload_calculation() {
let mut system = PeerReviewSystem::new();
system.assignments.push(ReviewAssignment {
id: "assign1".to_string(),
session_id: "session1".to_string(),
reviewer_id: "reviewer1".to_string(),
assigned_at: Utc::now(),
due_date: Utc::now() + chrono::Duration::days(14),
status: AssignmentStatus::Pending,
assignment_method: AssignmentMethod::Manual,
expertise_match: 0.8,
});
let workload = system.calculate_reviewer_workload("reviewer1");
assert_eq!(workload, 1);
let workload = system.calculate_reviewer_workload("reviewer2");
assert_eq!(workload, 0);
}
}