1use crate::models::Rule;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct RuleComparison {
9 pub original: Rule,
11 pub updated: Rule,
13 pub changed_fields: Vec<String>,
15 pub changes: ComparisonDetails,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ComparisonDetails {
22 pub pattern_changed: bool,
24 pub old_pattern: Option<String>,
26 pub new_pattern: Option<String>,
28 pub action_changed: bool,
30 pub old_action: Option<String>,
32 pub new_action: Option<String>,
34 pub confidence_changed: bool,
36 pub old_confidence: Option<f32>,
38 pub new_confidence: Option<f32>,
40 pub metadata_changed: bool,
42 pub old_metadata: Option<serde_json::Value>,
44 pub new_metadata: Option<serde_json::Value>,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub enum ReviewStatus {
51 Pending,
53 Approved,
55 Rejected,
57 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#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ReviewComment {
75 pub id: String,
77 pub author: String,
79 pub text: String,
81 pub created_at: DateTime<Utc>,
83 pub is_critical: bool,
85}
86
87impl ReviewComment {
88 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#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ReviewInfo {
103 pub rule: Rule,
105 pub status: ReviewStatus,
107 pub started_at: DateTime<Utc>,
109 pub completed_at: Option<DateTime<Utc>>,
111 pub reviewer: Option<String>,
113 pub comments: Vec<ReviewComment>,
115 pub comparison: Option<RuleComparison>,
117 pub review_score: Option<f32>,
119}
120
121impl ReviewInfo {
122 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 pub fn add_comment(&mut self, comment: ReviewComment) {
138 self.comments.push(comment);
139 }
140
141 pub fn set_comparison(&mut self, comparison: RuleComparison) {
143 self.comparison = Some(comparison);
144 }
145
146 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 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 pub fn request_revision(&mut self, reviewer: String) {
164 self.status = ReviewStatus::NeedsRevision;
165 self.reviewer = Some(reviewer);
166 }
167
168 pub fn is_complete(&self) -> bool {
170 self.status != ReviewStatus::Pending && self.completed_at.is_some()
171 }
172
173 pub fn get_critical_comments(&self) -> Vec<&ReviewComment> {
175 self.comments.iter().filter(|c| c.is_critical).collect()
176 }
177
178 pub fn get_comments(&self) -> &[ReviewComment] {
180 &self.comments
181 }
182
183 pub fn comment_count(&self) -> usize {
185 self.comments.len()
186 }
187
188 pub fn critical_comment_count(&self) -> usize {
190 self.comments.iter().filter(|c| c.is_critical).count()
191 }
192}
193
194pub struct RuleReviewManager {
196 reviews: std::collections::HashMap<String, ReviewInfo>,
198}
199
200impl RuleReviewManager {
201 pub fn new() -> Self {
203 Self {
204 reviews: std::collections::HashMap::new(),
205 }
206 }
207
208 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 pub fn get_review(&self, rule_id: &str) -> Option<&ReviewInfo> {
218 self.reviews.get(rule_id)
219 }
220
221 pub fn get_review_mut(&mut self, rule_id: &str) -> Option<&mut ReviewInfo> {
223 self.reviews.get_mut(rule_id)
224 }
225
226 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 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 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 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 pub fn get_all_reviews(&self) -> Vec<&ReviewInfo> {
313 self.reviews.values().collect()
314 }
315
316 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 pub fn get_pending_reviews(&self) -> Vec<&ReviewInfo> {
326 self.get_reviews_by_status(ReviewStatus::Pending)
327 }
328
329 pub fn get_approved_reviews(&self) -> Vec<&ReviewInfo> {
331 self.get_reviews_by_status(ReviewStatus::Approved)
332 }
333
334 pub fn get_rejected_reviews(&self) -> Vec<&ReviewInfo> {
336 self.get_reviews_by_status(ReviewStatus::Rejected)
337 }
338
339 pub fn get_reviews_needing_revision(&self) -> Vec<&ReviewInfo> {
341 self.get_reviews_by_status(ReviewStatus::NeedsRevision)
342 }
343
344 pub fn remove_review(&mut self, rule_id: &str) -> Option<ReviewInfo> {
346 self.reviews.remove(rule_id)
347 }
348
349 pub fn clear_reviews(&mut self) {
351 self.reviews.clear();
352 }
353
354 pub fn review_count(&self) -> usize {
356 self.reviews.len()
357 }
358
359 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
371pub 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}