1use crate::error::{OptimError, Result};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct PeerReviewSystem {
14 pub sessions: HashMap<String, ReviewSession>,
16 pub reviewers: HashMap<String, Reviewer>,
18 pub assignments: Vec<ReviewAssignment>,
20 pub quality_metrics: Vec<ReviewQualityMetric>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ReviewSession {
27 pub id: String,
29 pub submission_id: String,
31 pub review_type: ReviewType,
33 pub status: ReviewSessionStatus,
35 pub criteria: Vec<ReviewCriterion>,
37 pub deadline: DateTime<Utc>,
39 pub reviews: Vec<PeerReview>,
41 pub meta_review: Option<MetaReview>,
43 pub discussion: Vec<ReviewDiscussion>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49pub enum ReviewType {
50 SingleBlind,
52 DoubleBlind,
54 Open,
56 PostPublication,
58 Internal,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64pub enum ReviewSessionStatus {
65 WaitingForReviewers,
67 InProgress,
69 ReviewsComplete,
71 MetaReviewInProgress,
73 Complete,
75 Cancelled,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ReviewCriterion {
82 pub name: String,
84 pub description: String,
86 pub score_range: (f64, f64),
88 pub weight: f64,
90 pub required: bool,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct PeerReview {
97 pub id: String,
99 pub reviewer_id: String,
101 pub recommendation: ReviewRecommendation,
103 pub criterion_scores: HashMap<String, f64>,
105 pub overall_score: f64,
107 pub confidence: f64,
109 pub written_review: WrittenReview,
111 pub status: ReviewStatus,
113 pub submitted_at: Option<DateTime<Utc>>,
115 pub time_spent_minutes: Option<u32>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
121pub enum ReviewRecommendation {
122 StrongAccept,
124 Accept,
126 WeakAccept,
128 BorderlineAccept,
130 BorderlineReject,
132 WeakReject,
134 Reject,
136 StrongReject,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct WrittenReview {
143 pub summary: String,
145 pub strengths: Vec<String>,
147 pub weaknesses: Vec<String>,
149 pub detailed_comments: String,
151 pub questions: Vec<String>,
153 pub minor_issues: Vec<String>,
155 pub suggestions: Vec<String>,
157 pub committee_comments: Option<String>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
163pub enum ReviewStatus {
164 Assigned,
166 InProgress,
168 Draft,
170 Submitted,
172 RevisionRequested,
174 Declined,
176 Overdue,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct MetaReview {
183 pub meta_reviewer_id: String,
185 pub review_summary: String,
187 pub final_recommendation: ReviewRecommendation,
189 pub justification: String,
191 pub review_quality: Vec<ReviewQualityAssessment>,
193 pub areas_of_agreement: Vec<String>,
195 pub areas_of_disagreement: Vec<String>,
197 pub decision_rationale: String,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ReviewQualityAssessment {
204 pub review_id: String,
206 pub quality_scores: HashMap<String, f64>,
208 pub overall_quality: f64,
210 pub helpfulness: f64,
212 pub comments: String,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct ReviewDiscussion {
219 pub id: String,
221 pub author: String,
223 pub content: String,
225 pub reply_to: Option<String>,
227 pub posted_at: DateTime<Utc>,
229 pub post_type: DiscussionPostType,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
235pub enum DiscussionPostType {
236 Question,
238 Answer,
240 Clarification,
242 Disagreement,
244 Consensus,
246 ModeratorMessage,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct Reviewer {
253 pub id: String,
255 pub expertise_areas: Vec<String>,
257 pub experience_level: ExperienceLevel,
259 pub review_history: ReviewerHistory,
261 pub availability: ReviewerAvailability,
263 pub quality_metrics: ReviewerQualityMetrics,
265 pub preferences: ReviewerPreferences,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
271pub enum ExperienceLevel {
272 Expert,
274 Senior,
276 Experienced,
278 Junior,
280 Novice,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct ReviewerHistory {
287 pub total_reviews: usize,
289 pub reviews_last_year: usize,
291 pub avg_review_time_days: f64,
293 pub on_time_rate: f64,
295 pub avg_quality_score: f64,
297 pub review_acceptance_rate: f64,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct ReviewerAvailability {
304 pub available: bool,
306 pub max_reviews_per_month: u32,
308 pub current_load: u32,
310 pub unavailable_periods: Vec<(DateTime<Utc>, DateTime<Utc>)>,
312 pub preferred_types: Vec<ReviewType>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct ReviewerQualityMetrics {
319 pub thoroughness: f64,
321 pub constructiveness: f64,
323 pub timeliness: f64,
325 pub expertise_match: f64,
327 pub overall_score: f64,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct ReviewerPreferences {
334 pub preferred_paper_types: Vec<String>,
336 pub avoid_paper_types: Vec<String>,
338 pub max_review_length: Option<u32>,
340 pub anonymous_preference: bool,
342 pub notification_preferences: NotificationPreferences,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct NotificationPreferences {
349 pub email: bool,
351 pub reminder_frequency: u32,
353 pub deadline_notifications: bool,
355 pub discussion_notifications: bool,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct ReviewAssignment {
362 pub id: String,
364 pub session_id: String,
366 pub reviewer_id: String,
368 pub assigned_at: DateTime<Utc>,
370 pub due_date: DateTime<Utc>,
372 pub status: AssignmentStatus,
374 pub assignment_method: AssignmentMethod,
376 pub expertise_match: f64,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
382pub enum AssignmentStatus {
383 Pending,
385 Accepted,
387 Declined,
389 Completed,
391 Overdue,
393 Cancelled,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
399pub enum AssignmentMethod {
400 Manual,
402 AutomaticExpertise,
404 AutomaticLoadBalancing,
406 Hybrid,
408 SelfAssignment,
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct ReviewQualityMetric {
415 pub name: String,
417 pub description: String,
419 pub value_range: (f64, f64),
421 pub higher_is_better: bool,
423 pub calculation_method: String,
425}
426
427impl Default for PeerReviewSystem {
428 fn default() -> Self {
429 Self::new()
430 }
431}
432
433impl PeerReviewSystem {
434 pub fn new() -> Self {
436 Self {
437 sessions: HashMap::new(),
438 reviewers: HashMap::new(),
439 assignments: Vec::new(),
440 quality_metrics: Self::create_default_quality_metrics(),
441 }
442 }
443
444 pub fn create_review_session(
446 &mut self,
447 submission_id: &str,
448 review_type: ReviewType,
449 criteria: Vec<ReviewCriterion>,
450 deadline: DateTime<Utc>,
451 ) -> String {
452 let session_id = uuid::Uuid::new_v4().to_string();
453 let session = ReviewSession {
454 id: session_id.clone(),
455 submission_id: submission_id.to_string(),
456 review_type,
457 status: ReviewSessionStatus::WaitingForReviewers,
458 criteria,
459 deadline,
460 reviews: Vec::new(),
461 meta_review: None,
462 discussion: Vec::new(),
463 };
464
465 self.sessions.insert(session_id.clone(), session);
466 session_id
467 }
468
469 pub fn assign_reviewers(
471 &mut self,
472 session_id: &str,
473 reviewer_ids: &[String],
474 assignment_method: AssignmentMethod,
475 ) -> Result<Vec<String>> {
476 if !self.sessions.contains_key(session_id) {
477 return Err(OptimError::InvalidConfig("Session not found".to_string()));
478 }
479
480 let mut assignment_ids = Vec::new();
481 let now = Utc::now();
482 let session = self.sessions.get(session_id).unwrap();
483
484 for reviewer_id in reviewer_ids {
485 if !self.reviewers.contains_key(reviewer_id) {
486 continue; }
488
489 let assignment_id = uuid::Uuid::new_v4().to_string();
490 let assignment = ReviewAssignment {
491 id: assignment_id.clone(),
492 session_id: session_id.to_string(),
493 reviewer_id: reviewer_id.clone(),
494 assigned_at: now,
495 due_date: session.deadline,
496 status: AssignmentStatus::Pending,
497 assignment_method: assignment_method.clone(),
498 expertise_match: self.calculate_expertise_match(reviewer_id, session_id),
499 };
500
501 self.assignments.push(assignment);
502 assignment_ids.push(assignment_id);
503 }
504
505 if let Some(session) = self.sessions.get_mut(session_id) {
507 session.status = ReviewSessionStatus::InProgress;
508 }
509
510 Ok(assignment_ids)
511 }
512
513 pub fn submit_review(
515 &mut self,
516 session_id: &str,
517 reviewer_id: &str,
518 review: PeerReview,
519 ) -> Result<()> {
520 let session = self
521 .sessions
522 .get_mut(session_id)
523 .ok_or_else(|| OptimError::InvalidConfig("Session not found".to_string()))?;
524
525 let assignment = self
527 .assignments
528 .iter_mut()
529 .find(|a| a.session_id == session_id && a.reviewer_id == reviewer_id)
530 .ok_or_else(|| {
531 OptimError::InvalidConfig("Reviewer not assigned to this session".to_string())
532 })?;
533
534 assignment.status = AssignmentStatus::Completed;
536
537 session.reviews.push(review);
539
540 let total_assignments = self
542 .assignments
543 .iter()
544 .filter(|a| a.session_id == session_id)
545 .count();
546
547 if session.reviews.len() == total_assignments {
548 session.status = ReviewSessionStatus::ReviewsComplete;
549 }
550
551 Ok(())
552 }
553
554 pub fn generate_meta_review(&mut self, session_id: &str, meta_reviewer_id: &str) -> Result<()> {
556 let meta_review = {
558 let session = self
559 .sessions
560 .get(session_id)
561 .ok_or_else(|| OptimError::InvalidConfig("Session not found".to_string()))?;
562
563 if session.status != ReviewSessionStatus::ReviewsComplete {
564 return Err(OptimError::InvalidConfig(
565 "Not all reviews are complete".to_string(),
566 ));
567 }
568
569 self.create_meta_review(session, meta_reviewer_id)
570 };
571
572 let session = self.sessions.get_mut(session_id).unwrap(); session.meta_review = Some(meta_review);
575 session.status = ReviewSessionStatus::Complete;
576
577 Ok(())
578 }
579
580 pub fn calculate_reviewer_workload(&self, reviewer_id: &str) -> u32 {
582 self.assignments
583 .iter()
584 .filter(|a| {
585 a.reviewer_id == reviewer_id
586 && matches!(
587 a.status,
588 AssignmentStatus::Pending | AssignmentStatus::Accepted
589 )
590 })
591 .count() as u32
592 }
593
594 pub fn get_available_reviewers(&self, expertise_area: &str) -> Vec<&Reviewer> {
596 self.reviewers
597 .values()
598 .filter(|r| {
599 r.availability.available
600 && r.expertise_areas
601 .iter()
602 .any(|area| area.to_lowercase().contains(&expertise_area.to_lowercase()))
603 && r.availability.current_load < r.availability.max_reviews_per_month
604 })
605 .collect()
606 }
607
608 pub fn calculate_review_quality(&self, review: &PeerReview) -> f64 {
610 let mut quality_score = 0.0;
611 let mut total_weight = 0.0;
612
613 let review_length = review.written_review.detailed_comments.len()
615 + review
616 .written_review
617 .strengths
618 .iter()
619 .map(|s| s.len())
620 .sum::<usize>()
621 + review
622 .written_review
623 .weaknesses
624 .iter()
625 .map(|s| s.len())
626 .sum::<usize>();
627
628 let length_score = ((review_length as f64).ln() / 10.0).min(1.0);
629 quality_score += length_score * 0.3;
630 total_weight += 0.3;
631
632 let specific_points = review.written_review.strengths.len()
634 + review.written_review.weaknesses.len()
635 + review.written_review.suggestions.len();
636
637 let specificity_score = (specific_points as f64 / 10.0).min(1.0);
638 quality_score += specificity_score * 0.4;
639 total_weight += 0.4;
640
641 quality_score += review.confidence * 0.3;
643 total_weight += 0.3;
644
645 quality_score / total_weight
646 }
647
648 fn calculate_expertise_match(&self, reviewer_id: &str, sessionid: &str) -> f64 {
649 if let Some(reviewer) = self.reviewers.get(reviewer_id) {
652 if reviewer.expertise_areas.is_empty() {
653 0.5 } else {
655 0.8 }
657 } else {
658 0.0
659 }
660 }
661
662 fn create_meta_review(&self, session: &ReviewSession, meta_reviewer_id: &str) -> MetaReview {
663 let review_summary = format!("Meta-review of {} reviews", session.reviews.len());
664
665 let recommendations: Vec<_> = session.reviews.iter().map(|r| &r.recommendation).collect();
667
668 let final_recommendation = self.determine_consensus_recommendation(&recommendations);
669
670 let review_quality: Vec<_> = session
672 .reviews
673 .iter()
674 .map(|review| {
675 let quality_score = self.calculate_review_quality(review);
676 ReviewQualityAssessment {
677 review_id: review.id.clone(),
678 quality_scores: HashMap::new(),
679 overall_quality: quality_score,
680 helpfulness: quality_score * 0.9, comments: if quality_score > 0.7 {
682 "High quality review".to_string()
683 } else {
684 "Review could be more detailed".to_string()
685 },
686 }
687 })
688 .collect();
689
690 MetaReview {
691 meta_reviewer_id: meta_reviewer_id.to_string(),
692 review_summary,
693 final_recommendation,
694 justification: "Based on consensus of reviewer recommendations".to_string(),
695 review_quality,
696 areas_of_agreement: vec!["Technical quality assessment".to_string()],
697 areas_of_disagreement: vec!["Significance of contribution".to_string()],
698 decision_rationale: "Decision based on majority reviewer consensus".to_string(),
699 }
700 }
701
702 fn determine_consensus_recommendation(
703 &self,
704 recommendations: &[&ReviewRecommendation],
705 ) -> ReviewRecommendation {
706 let mut counts = HashMap::new();
708 for rec in recommendations {
709 *counts.entry(rec).or_insert(0) += 1;
710 }
711
712 counts
713 .into_iter()
714 .max_by_key(|(_, count)| *count)
715 .map(|(rec, _)| (*rec).clone())
716 .unwrap_or(ReviewRecommendation::BorderlineReject)
717 }
718
719 fn create_default_quality_metrics() -> Vec<ReviewQualityMetric> {
720 vec![
721 ReviewQualityMetric {
722 name: "Thoroughness".to_string(),
723 description: "How comprehensive and detailed the review is".to_string(),
724 value_range: (0.0, 1.0),
725 higher_is_better: true,
726 calculation_method: "Based on review length and number of specific points"
727 .to_string(),
728 },
729 ReviewQualityMetric {
730 name: "Constructiveness".to_string(),
731 description: "How helpful the review is for improving the work".to_string(),
732 value_range: (0.0, 1.0),
733 higher_is_better: true,
734 calculation_method: "Based on number of suggestions and actionable feedback"
735 .to_string(),
736 },
737 ReviewQualityMetric {
738 name: "Timeliness".to_string(),
739 description: "How promptly the review was submitted".to_string(),
740 value_range: (0.0, 1.0),
741 higher_is_better: true,
742 calculation_method: "Based on submission time relative to deadline".to_string(),
743 },
744 ]
745 }
746}
747
748impl Default for ReviewerHistory {
749 fn default() -> Self {
750 Self {
751 total_reviews: 0,
752 reviews_last_year: 0,
753 avg_review_time_days: 14.0,
754 on_time_rate: 1.0,
755 avg_quality_score: 0.7,
756 review_acceptance_rate: 0.9,
757 }
758 }
759}
760
761impl Default for ReviewerAvailability {
762 fn default() -> Self {
763 Self {
764 available: true,
765 max_reviews_per_month: 5,
766 current_load: 0,
767 unavailable_periods: Vec::new(),
768 preferred_types: vec![ReviewType::DoubleBlind],
769 }
770 }
771}
772
773impl Default for ReviewerQualityMetrics {
774 fn default() -> Self {
775 Self {
776 thoroughness: 0.7,
777 constructiveness: 0.7,
778 timeliness: 0.8,
779 expertise_match: 0.7,
780 overall_score: 0.7,
781 }
782 }
783}
784
785impl Default for NotificationPreferences {
786 fn default() -> Self {
787 Self {
788 email: true,
789 reminder_frequency: 7,
790 deadline_notifications: true,
791 discussion_notifications: false,
792 }
793 }
794}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799
800 #[test]
801 fn test_peer_review_system_creation() {
802 let system = PeerReviewSystem::new();
803 assert!(system.sessions.is_empty());
804 assert!(system.reviewers.is_empty());
805 assert!(!system.quality_metrics.is_empty());
806 }
807
808 #[test]
809 fn test_create_review_session() {
810 let mut system = PeerReviewSystem::new();
811
812 let criteria = vec![ReviewCriterion {
813 name: "Technical Quality".to_string(),
814 description: "Assessment of technical merit".to_string(),
815 score_range: (1.0, 5.0),
816 weight: 0.4,
817 required: true,
818 }];
819
820 let deadline = Utc::now() + chrono::Duration::days(14);
821 let session_id =
822 system.create_review_session("paper123", ReviewType::DoubleBlind, criteria, deadline);
823
824 assert!(system.sessions.contains_key(&session_id));
825 let session = &system.sessions[&session_id];
826 assert_eq!(session.submission_id, "paper123");
827 assert_eq!(session.review_type, ReviewType::DoubleBlind);
828 }
829
830 #[test]
831 fn test_reviewer_workload_calculation() {
832 let mut system = PeerReviewSystem::new();
833
834 system.assignments.push(ReviewAssignment {
836 id: "assign1".to_string(),
837 session_id: "session1".to_string(),
838 reviewer_id: "reviewer1".to_string(),
839 assigned_at: Utc::now(),
840 due_date: Utc::now() + chrono::Duration::days(14),
841 status: AssignmentStatus::Pending,
842 assignment_method: AssignmentMethod::Manual,
843 expertise_match: 0.8,
844 });
845
846 let workload = system.calculate_reviewer_workload("reviewer1");
847 assert_eq!(workload, 1);
848
849 let workload = system.calculate_reviewer_workload("reviewer2");
850 assert_eq!(workload, 0);
851 }
852}