ricecoder_learning/
rule_review.rs

1/// Rule review interface for managing rule promotion reviews
2use crate::models::Rule;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// Detailed comparison between two rule versions
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct RuleComparison {
9    /// Original rule
10    pub original: Rule,
11    /// Updated rule
12    pub updated: Rule,
13    /// Fields that changed
14    pub changed_fields: Vec<String>,
15    /// Detailed changes
16    pub changes: ComparisonDetails,
17}
18
19/// Detailed comparison information
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ComparisonDetails {
22    /// Pattern changed
23    pub pattern_changed: bool,
24    /// Old pattern
25    pub old_pattern: Option<String>,
26    /// New pattern
27    pub new_pattern: Option<String>,
28    /// Action changed
29    pub action_changed: bool,
30    /// Old action
31    pub old_action: Option<String>,
32    /// New action
33    pub new_action: Option<String>,
34    /// Confidence changed
35    pub confidence_changed: bool,
36    /// Old confidence
37    pub old_confidence: Option<f32>,
38    /// New confidence
39    pub new_confidence: Option<f32>,
40    /// Metadata changed
41    pub metadata_changed: bool,
42    /// Old metadata
43    pub old_metadata: Option<serde_json::Value>,
44    /// New metadata
45    pub new_metadata: Option<serde_json::Value>,
46}
47
48/// Review status for a rule
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub enum ReviewStatus {
51    /// Pending review
52    Pending,
53    /// Approved
54    Approved,
55    /// Rejected
56    Rejected,
57    /// Needs revision
58    NeedsRevision,
59}
60
61impl std::fmt::Display for ReviewStatus {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            ReviewStatus::Pending => write!(f, "pending"),
65            ReviewStatus::Approved => write!(f, "approved"),
66            ReviewStatus::Rejected => write!(f, "rejected"),
67            ReviewStatus::NeedsRevision => write!(f, "needs_revision"),
68        }
69    }
70}
71
72/// Review comment on a rule
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ReviewComment {
75    /// Unique identifier
76    pub id: String,
77    /// Author of the comment
78    pub author: String,
79    /// Comment text
80    pub text: String,
81    /// When the comment was created
82    pub created_at: DateTime<Utc>,
83    /// Whether this is a critical comment
84    pub is_critical: bool,
85}
86
87impl ReviewComment {
88    /// Create a new review comment
89    pub fn new(author: String, text: String, is_critical: bool) -> Self {
90        Self {
91            id: uuid::Uuid::new_v4().to_string(),
92            author,
93            text,
94            created_at: Utc::now(),
95            is_critical,
96        }
97    }
98}
99
100/// Complete review information for a rule
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ReviewInfo {
103    /// Rule being reviewed
104    pub rule: Rule,
105    /// Current review status
106    pub status: ReviewStatus,
107    /// When the review was started
108    pub started_at: DateTime<Utc>,
109    /// When the review was completed
110    pub completed_at: Option<DateTime<Utc>>,
111    /// Reviewer name
112    pub reviewer: Option<String>,
113    /// Review comments
114    pub comments: Vec<ReviewComment>,
115    /// Comparison with previous version
116    pub comparison: Option<RuleComparison>,
117    /// Overall review score (0.0 to 1.0)
118    pub review_score: Option<f32>,
119}
120
121impl ReviewInfo {
122    /// Create a new review
123    pub fn new(rule: Rule) -> Self {
124        Self {
125            rule,
126            status: ReviewStatus::Pending,
127            started_at: Utc::now(),
128            completed_at: None,
129            reviewer: None,
130            comments: Vec::new(),
131            comparison: None,
132            review_score: None,
133        }
134    }
135
136    /// Add a comment to the review
137    pub fn add_comment(&mut self, comment: ReviewComment) {
138        self.comments.push(comment);
139    }
140
141    /// Set the comparison
142    pub fn set_comparison(&mut self, comparison: RuleComparison) {
143        self.comparison = Some(comparison);
144    }
145
146    /// Approve the review
147    pub fn approve(&mut self, reviewer: String, score: f32) {
148        self.status = ReviewStatus::Approved;
149        self.reviewer = Some(reviewer);
150        self.completed_at = Some(Utc::now());
151        self.review_score = Some(score);
152    }
153
154    /// Reject the review
155    pub fn reject(&mut self, reviewer: String, score: f32) {
156        self.status = ReviewStatus::Rejected;
157        self.reviewer = Some(reviewer);
158        self.completed_at = Some(Utc::now());
159        self.review_score = Some(score);
160    }
161
162    /// Mark as needing revision
163    pub fn request_revision(&mut self, reviewer: String) {
164        self.status = ReviewStatus::NeedsRevision;
165        self.reviewer = Some(reviewer);
166    }
167
168    /// Check if review is complete
169    pub fn is_complete(&self) -> bool {
170        self.status != ReviewStatus::Pending && self.completed_at.is_some()
171    }
172
173    /// Get critical comments
174    pub fn get_critical_comments(&self) -> Vec<&ReviewComment> {
175        self.comments.iter().filter(|c| c.is_critical).collect()
176    }
177
178    /// Get all comments
179    pub fn get_comments(&self) -> &[ReviewComment] {
180        &self.comments
181    }
182
183    /// Get comment count
184    pub fn comment_count(&self) -> usize {
185        self.comments.len()
186    }
187
188    /// Get critical comment count
189    pub fn critical_comment_count(&self) -> usize {
190        self.comments.iter().filter(|c| c.is_critical).count()
191    }
192}
193
194/// Rule review manager
195pub struct RuleReviewManager {
196    /// Active reviews
197    reviews: std::collections::HashMap<String, ReviewInfo>,
198}
199
200impl RuleReviewManager {
201    /// Create a new review manager
202    pub fn new() -> Self {
203        Self {
204            reviews: std::collections::HashMap::new(),
205        }
206    }
207
208    /// Start a new review
209    pub fn start_review(&mut self, rule: Rule) -> String {
210        let review = ReviewInfo::new(rule.clone());
211        let rule_id = rule.id.clone();
212        self.reviews.insert(rule_id.clone(), review);
213        rule_id
214    }
215
216    /// Get a review
217    pub fn get_review(&self, rule_id: &str) -> Option<&ReviewInfo> {
218        self.reviews.get(rule_id)
219    }
220
221    /// Get a mutable review
222    pub fn get_review_mut(&mut self, rule_id: &str) -> Option<&mut ReviewInfo> {
223        self.reviews.get_mut(rule_id)
224    }
225
226    /// Add a comment to a review
227    pub fn add_comment(
228        &mut self,
229        rule_id: &str,
230        author: String,
231        text: String,
232        is_critical: bool,
233    ) -> crate::error::Result<()> {
234        let review = self.reviews.get_mut(rule_id).ok_or_else(|| {
235            crate::error::LearningError::RulePromotionFailed(format!(
236                "Review not found for rule '{}'",
237                rule_id
238            ))
239        })?;
240
241        let comment = ReviewComment::new(author, text, is_critical);
242        review.add_comment(comment);
243        Ok(())
244    }
245
246    /// Approve a review
247    pub fn approve_review(
248        &mut self,
249        rule_id: &str,
250        reviewer: String,
251        score: f32,
252    ) -> crate::error::Result<()> {
253        if !(0.0..=1.0).contains(&score) {
254            return Err(crate::error::LearningError::RulePromotionFailed(
255                "Review score must be between 0.0 and 1.0".to_string(),
256            ));
257        }
258
259        let review = self.reviews.get_mut(rule_id).ok_or_else(|| {
260            crate::error::LearningError::RulePromotionFailed(format!(
261                "Review not found for rule '{}'",
262                rule_id
263            ))
264        })?;
265
266        review.approve(reviewer, score);
267        Ok(())
268    }
269
270    /// Reject a review
271    pub fn reject_review(
272        &mut self,
273        rule_id: &str,
274        reviewer: String,
275        score: f32,
276    ) -> crate::error::Result<()> {
277        if !(0.0..=1.0).contains(&score) {
278            return Err(crate::error::LearningError::RulePromotionFailed(
279                "Review score must be between 0.0 and 1.0".to_string(),
280            ));
281        }
282
283        let review = self.reviews.get_mut(rule_id).ok_or_else(|| {
284            crate::error::LearningError::RulePromotionFailed(format!(
285                "Review not found for rule '{}'",
286                rule_id
287            ))
288        })?;
289
290        review.reject(reviewer, score);
291        Ok(())
292    }
293
294    /// Request revision
295    pub fn request_revision(
296        &mut self,
297        rule_id: &str,
298        reviewer: String,
299    ) -> crate::error::Result<()> {
300        let review = self.reviews.get_mut(rule_id).ok_or_else(|| {
301            crate::error::LearningError::RulePromotionFailed(format!(
302                "Review not found for rule '{}'",
303                rule_id
304            ))
305        })?;
306
307        review.request_revision(reviewer);
308        Ok(())
309    }
310
311    /// Get all reviews
312    pub fn get_all_reviews(&self) -> Vec<&ReviewInfo> {
313        self.reviews.values().collect()
314    }
315
316    /// Get reviews by status
317    pub fn get_reviews_by_status(&self, status: ReviewStatus) -> Vec<&ReviewInfo> {
318        self.reviews
319            .values()
320            .filter(|r| r.status == status)
321            .collect()
322    }
323
324    /// Get pending reviews
325    pub fn get_pending_reviews(&self) -> Vec<&ReviewInfo> {
326        self.get_reviews_by_status(ReviewStatus::Pending)
327    }
328
329    /// Get approved reviews
330    pub fn get_approved_reviews(&self) -> Vec<&ReviewInfo> {
331        self.get_reviews_by_status(ReviewStatus::Approved)
332    }
333
334    /// Get rejected reviews
335    pub fn get_rejected_reviews(&self) -> Vec<&ReviewInfo> {
336        self.get_reviews_by_status(ReviewStatus::Rejected)
337    }
338
339    /// Get reviews needing revision
340    pub fn get_reviews_needing_revision(&self) -> Vec<&ReviewInfo> {
341        self.get_reviews_by_status(ReviewStatus::NeedsRevision)
342    }
343
344    /// Remove a review
345    pub fn remove_review(&mut self, rule_id: &str) -> Option<ReviewInfo> {
346        self.reviews.remove(rule_id)
347    }
348
349    /// Clear all reviews
350    pub fn clear_reviews(&mut self) {
351        self.reviews.clear();
352    }
353
354    /// Get review count
355    pub fn review_count(&self) -> usize {
356        self.reviews.len()
357    }
358
359    /// Get pending review count
360    pub fn pending_review_count(&self) -> usize {
361        self.get_pending_reviews().len()
362    }
363}
364
365impl Default for RuleReviewManager {
366    fn default() -> Self {
367        Self::new()
368    }
369}
370
371/// Compare two rules
372pub fn compare_rules(original: &Rule, updated: &Rule) -> RuleComparison {
373    let mut changed_fields = Vec::new();
374    let mut details = ComparisonDetails {
375        pattern_changed: false,
376        old_pattern: None,
377        new_pattern: None,
378        action_changed: false,
379        old_action: None,
380        new_action: None,
381        confidence_changed: false,
382        old_confidence: None,
383        new_confidence: None,
384        metadata_changed: false,
385        old_metadata: None,
386        new_metadata: None,
387    };
388
389    if original.pattern != updated.pattern {
390        changed_fields.push("pattern".to_string());
391        details.pattern_changed = true;
392        details.old_pattern = Some(original.pattern.clone());
393        details.new_pattern = Some(updated.pattern.clone());
394    }
395
396    if original.action != updated.action {
397        changed_fields.push("action".to_string());
398        details.action_changed = true;
399        details.old_action = Some(original.action.clone());
400        details.new_action = Some(updated.action.clone());
401    }
402
403    if (original.confidence - updated.confidence).abs() > f32::EPSILON {
404        changed_fields.push("confidence".to_string());
405        details.confidence_changed = true;
406        details.old_confidence = Some(original.confidence);
407        details.new_confidence = Some(updated.confidence);
408    }
409
410    if original.metadata != updated.metadata {
411        changed_fields.push("metadata".to_string());
412        details.metadata_changed = true;
413        details.old_metadata = Some(original.metadata.clone());
414        details.new_metadata = Some(updated.metadata.clone());
415    }
416
417    RuleComparison {
418        original: original.clone(),
419        updated: updated.clone(),
420        changed_fields,
421        changes: details,
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428    use crate::models::{Rule, RuleScope, RuleSource};
429
430    #[test]
431    fn test_review_info_creation() {
432        let rule = Rule::new(
433            RuleScope::Project,
434            "pattern".to_string(),
435            "action".to_string(),
436            RuleSource::Learned,
437        );
438
439        let review = ReviewInfo::new(rule.clone());
440        assert_eq!(review.status, ReviewStatus::Pending);
441        assert_eq!(review.rule.id, rule.id);
442        assert!(!review.is_complete());
443    }
444
445    #[test]
446    fn test_add_comment() {
447        let rule = Rule::new(
448            RuleScope::Project,
449            "pattern".to_string(),
450            "action".to_string(),
451            RuleSource::Learned,
452        );
453
454        let mut review = ReviewInfo::new(rule);
455        let comment = ReviewComment::new(
456            "reviewer".to_string(),
457            "Looks good".to_string(),
458            false,
459        );
460
461        review.add_comment(comment);
462        assert_eq!(review.comment_count(), 1);
463    }
464
465    #[test]
466    fn test_approve_review() {
467        let rule = Rule::new(
468            RuleScope::Project,
469            "pattern".to_string(),
470            "action".to_string(),
471            RuleSource::Learned,
472        );
473
474        let mut review = ReviewInfo::new(rule);
475        review.approve("reviewer".to_string(), 0.9);
476
477        assert_eq!(review.status, ReviewStatus::Approved);
478        assert_eq!(review.reviewer, Some("reviewer".to_string()));
479        assert_eq!(review.review_score, Some(0.9));
480        assert!(review.is_complete());
481    }
482
483    #[test]
484    fn test_reject_review() {
485        let rule = Rule::new(
486            RuleScope::Project,
487            "pattern".to_string(),
488            "action".to_string(),
489            RuleSource::Learned,
490        );
491
492        let mut review = ReviewInfo::new(rule);
493        review.reject("reviewer".to_string(), 0.2);
494
495        assert_eq!(review.status, ReviewStatus::Rejected);
496        assert_eq!(review.reviewer, Some("reviewer".to_string()));
497        assert_eq!(review.review_score, Some(0.2));
498        assert!(review.is_complete());
499    }
500
501    #[test]
502    fn test_request_revision() {
503        let rule = Rule::new(
504            RuleScope::Project,
505            "pattern".to_string(),
506            "action".to_string(),
507            RuleSource::Learned,
508        );
509
510        let mut review = ReviewInfo::new(rule);
511        review.request_revision("reviewer".to_string());
512
513        assert_eq!(review.status, ReviewStatus::NeedsRevision);
514        assert_eq!(review.reviewer, Some("reviewer".to_string()));
515    }
516
517    #[test]
518    fn test_critical_comments() {
519        let rule = Rule::new(
520            RuleScope::Project,
521            "pattern".to_string(),
522            "action".to_string(),
523            RuleSource::Learned,
524        );
525
526        let mut review = ReviewInfo::new(rule);
527
528        let comment1 = ReviewComment::new(
529            "reviewer1".to_string(),
530            "Critical issue".to_string(),
531            true,
532        );
533        let comment2 = ReviewComment::new(
534            "reviewer2".to_string(),
535            "Minor suggestion".to_string(),
536            false,
537        );
538
539        review.add_comment(comment1);
540        review.add_comment(comment2);
541
542        assert_eq!(review.comment_count(), 2);
543        assert_eq!(review.critical_comment_count(), 1);
544        assert_eq!(review.get_critical_comments().len(), 1);
545    }
546
547    #[test]
548    fn test_review_manager_creation() {
549        let manager = RuleReviewManager::new();
550        assert_eq!(manager.review_count(), 0);
551    }
552
553    #[test]
554    fn test_start_review() {
555        let mut manager = RuleReviewManager::new();
556
557        let rule = Rule::new(
558            RuleScope::Project,
559            "pattern".to_string(),
560            "action".to_string(),
561            RuleSource::Learned,
562        );
563
564        let rule_id = rule.id.clone();
565        manager.start_review(rule);
566
567        assert_eq!(manager.review_count(), 1);
568        assert!(manager.get_review(&rule_id).is_some());
569    }
570
571    #[test]
572    fn test_approve_review_manager() {
573        let mut manager = RuleReviewManager::new();
574
575        let rule = Rule::new(
576            RuleScope::Project,
577            "pattern".to_string(),
578            "action".to_string(),
579            RuleSource::Learned,
580        );
581
582        let rule_id = rule.id.clone();
583        manager.start_review(rule);
584        manager
585            .approve_review(&rule_id, "reviewer".to_string(), 0.9)
586            .unwrap();
587
588        let review = manager.get_review(&rule_id).unwrap();
589        assert_eq!(review.status, ReviewStatus::Approved);
590    }
591
592    #[test]
593    fn test_get_reviews_by_status() {
594        let mut manager = RuleReviewManager::new();
595
596        let rule1 = Rule::new(
597            RuleScope::Project,
598            "pattern1".to_string(),
599            "action1".to_string(),
600            RuleSource::Learned,
601        );
602
603        let rule2 = Rule::new(
604            RuleScope::Project,
605            "pattern2".to_string(),
606            "action2".to_string(),
607            RuleSource::Learned,
608        );
609
610        let rule1_id = rule1.id.clone();
611        let rule2_id = rule2.id.clone();
612
613        manager.start_review(rule1);
614        manager.start_review(rule2);
615
616        manager
617            .approve_review(&rule1_id, "reviewer".to_string(), 0.9)
618            .unwrap();
619
620        let pending = manager.get_pending_reviews();
621        assert_eq!(pending.len(), 1);
622
623        let approved = manager.get_approved_reviews();
624        assert_eq!(approved.len(), 1);
625    }
626
627    #[test]
628    fn test_compare_rules() {
629        let mut original = Rule::new(
630            RuleScope::Project,
631            "old_pattern".to_string(),
632            "old_action".to_string(),
633            RuleSource::Learned,
634        );
635        original.confidence = 0.5;
636
637        let mut updated = Rule::new(
638            RuleScope::Project,
639            "new_pattern".to_string(),
640            "new_action".to_string(),
641            RuleSource::Learned,
642        );
643        updated.confidence = 0.8;
644
645        let comparison = compare_rules(&original, &updated);
646
647        assert!(comparison.changes.pattern_changed);
648        assert!(comparison.changes.action_changed);
649        assert!(comparison.changes.confidence_changed);
650        assert_eq!(comparison.changed_fields.len(), 3);
651    }
652
653    #[test]
654    fn test_invalid_review_score() {
655        let mut manager = RuleReviewManager::new();
656
657        let rule = Rule::new(
658            RuleScope::Project,
659            "pattern".to_string(),
660            "action".to_string(),
661            RuleSource::Learned,
662        );
663
664        let rule_id = rule.id.clone();
665        manager.start_review(rule);
666
667        let result = manager.approve_review(&rule_id, "reviewer".to_string(), 1.5);
668        assert!(result.is_err());
669    }
670}