1use serde::{Deserialize, Serialize};
11use std::time::SystemTime;
12
13#[derive(Debug, Clone, PartialEq)]
15pub enum RetirementDecision {
16 Keep,
18 Retire(RetirementReason),
20 Archive(RetirementReason),
22}
23
24impl RetirementDecision {
25 pub fn should_remove(&self) -> bool {
27 matches!(self, RetirementDecision::Retire(_) | RetirementDecision::Archive(_))
28 }
29
30 pub fn reason(&self) -> Option<&RetirementReason> {
32 match self {
33 RetirementDecision::Retire(r) | RetirementDecision::Archive(r) => Some(r),
34 RetirementDecision::Keep => None,
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub enum RetirementReason {
42 LowUsage { uses: usize, threshold: usize, window_days: u32 },
44 HighFailureRate { success_rate: f32, threshold: f32 },
46 Superseded { better_pattern_id: String, improvement: f32 },
48 ManualDeprecation { reason: String },
50}
51
52impl std::fmt::Display for RetirementReason {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 RetirementReason::LowUsage { uses, threshold, window_days } => {
56 write!(
57 f,
58 "Low usage: {} uses in {} days (threshold: {})",
59 uses, window_days, threshold
60 )
61 }
62 RetirementReason::HighFailureRate { success_rate, threshold } => {
63 write!(
64 f,
65 "High failure rate: {:.1}% (threshold: {:.1}%)",
66 success_rate * 100.0,
67 threshold * 100.0
68 )
69 }
70 RetirementReason::Superseded { better_pattern_id, improvement } => {
71 write!(
72 f,
73 "Superseded by {} (+{:.1}% success)",
74 better_pattern_id,
75 improvement * 100.0
76 )
77 }
78 RetirementReason::ManualDeprecation { reason } => {
79 write!(f, "Manually deprecated: {}", reason)
80 }
81 }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct PatternStats {
88 pub pattern_id: String,
90 pub error_code: String,
92 pub total_uses: usize,
94 pub uses_in_window: usize,
96 pub successes: usize,
98 pub failures: usize,
100 pub last_used: Option<SystemTime>,
102 pub superseded_by: Option<String>,
104}
105
106impl PatternStats {
107 pub fn new(pattern_id: impl Into<String>, error_code: impl Into<String>) -> Self {
109 Self {
110 pattern_id: pattern_id.into(),
111 error_code: error_code.into(),
112 total_uses: 0,
113 uses_in_window: 0,
114 successes: 0,
115 failures: 0,
116 last_used: None,
117 superseded_by: None,
118 }
119 }
120
121 pub fn record_use(&mut self, success: bool) {
123 self.total_uses += 1;
124 self.uses_in_window += 1;
125 self.last_used = Some(SystemTime::now());
126
127 if success {
128 self.successes += 1;
129 } else {
130 self.failures += 1;
131 }
132 }
133
134 pub fn success_rate(&self) -> f32 {
136 let total = self.successes + self.failures;
137 if total == 0 {
138 0.0
139 } else {
140 self.successes as f32 / total as f32
141 }
142 }
143
144 pub fn reset_window(&mut self) {
146 self.uses_in_window = 0;
147 }
148
149 pub fn mark_superseded(&mut self, better_pattern_id: impl Into<String>) {
151 self.superseded_by = Some(better_pattern_id.into());
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct RetirementConfig {
158 pub min_usage_threshold: usize,
160 pub min_success_rate: f32,
162 pub evaluation_window_days: u32,
164 pub supersede_improvement_threshold: f32,
166 pub archive_instead_of_delete: bool,
168}
169
170impl Default for RetirementConfig {
171 fn default() -> Self {
172 Self {
173 min_usage_threshold: 5,
174 min_success_rate: 0.3,
175 evaluation_window_days: 30,
176 supersede_improvement_threshold: 0.1,
177 archive_instead_of_delete: true,
178 }
179 }
180}
181
182pub struct PatternRetirementPolicy {
184 config: RetirementConfig,
185}
186
187impl PatternRetirementPolicy {
188 pub fn new() -> Self {
190 Self { config: RetirementConfig::default() }
191 }
192
193 pub fn with_config(config: RetirementConfig) -> Self {
195 Self { config }
196 }
197
198 pub fn config(&self) -> &RetirementConfig {
200 &self.config
201 }
202
203 pub fn evaluate(&self, stats: &PatternStats) -> RetirementDecision {
205 if stats.uses_in_window < self.config.min_usage_threshold {
207 let reason = RetirementReason::LowUsage {
208 uses: stats.uses_in_window,
209 threshold: self.config.min_usage_threshold,
210 window_days: self.config.evaluation_window_days,
211 };
212 return if self.config.archive_instead_of_delete {
213 RetirementDecision::Archive(reason)
214 } else {
215 RetirementDecision::Retire(reason)
216 };
217 }
218
219 if stats.success_rate() < self.config.min_success_rate && stats.total_uses >= 5 {
221 let reason = RetirementReason::HighFailureRate {
222 success_rate: stats.success_rate(),
223 threshold: self.config.min_success_rate,
224 };
225 return if self.config.archive_instead_of_delete {
226 RetirementDecision::Archive(reason)
227 } else {
228 RetirementDecision::Retire(reason)
229 };
230 }
231
232 if let Some(ref better_id) = stats.superseded_by {
234 let reason = RetirementReason::Superseded {
235 better_pattern_id: better_id.clone(),
236 improvement: self.config.supersede_improvement_threshold,
237 };
238 return RetirementDecision::Archive(reason);
239 }
240
241 RetirementDecision::Keep
242 }
243
244 pub fn evaluate_batch(&self, stats_list: &[PatternStats]) -> Vec<(String, RetirementDecision)> {
246 stats_list.iter().map(|stats| (stats.pattern_id.clone(), self.evaluate(stats))).collect()
247 }
248
249 pub fn find_retireable<'a>(&self, stats_list: &'a [PatternStats]) -> Vec<&'a PatternStats> {
251 stats_list.iter().filter(|stats| self.evaluate(stats).should_remove()).collect()
252 }
253}
254
255impl Default for PatternRetirementPolicy {
256 fn default() -> Self {
257 Self::new()
258 }
259}
260
261#[derive(Debug, Clone, Default, Serialize, Deserialize)]
263pub struct RetirementSweepResult {
264 pub total_evaluated: usize,
266 pub kept: usize,
268 pub retired_low_usage: usize,
270 pub retired_high_failure: usize,
272 pub retired_superseded: usize,
274 pub archived: usize,
276 pub retired_ids: Vec<String>,
278}
279
280impl RetirementSweepResult {
281 pub fn new() -> Self {
283 Self::default()
284 }
285
286 pub fn record(&mut self, pattern_id: &str, decision: &RetirementDecision) {
288 self.total_evaluated += 1;
289 match decision {
290 RetirementDecision::Keep => {
291 self.kept += 1;
292 }
293 RetirementDecision::Retire(reason) | RetirementDecision::Archive(reason) => {
294 self.retired_ids.push(pattern_id.to_string());
295 if matches!(decision, RetirementDecision::Archive(_)) {
296 self.archived += 1;
297 }
298 match reason {
299 RetirementReason::LowUsage { .. } => self.retired_low_usage += 1,
300 RetirementReason::HighFailureRate { .. } => self.retired_high_failure += 1,
301 RetirementReason::Superseded { .. } => self.retired_superseded += 1,
302 RetirementReason::ManualDeprecation { .. } => {}
303 }
304 }
305 }
306 }
307
308 pub fn total_retired(&self) -> usize {
310 self.retired_low_usage + self.retired_high_failure + self.retired_superseded
311 }
312
313 pub fn retirement_rate(&self) -> f32 {
315 if self.total_evaluated == 0 {
316 0.0
317 } else {
318 self.total_retired() as f32 / self.total_evaluated as f32
319 }
320 }
321}
322
323pub fn run_retirement_sweep(
325 stats_list: &[PatternStats],
326 policy: &PatternRetirementPolicy,
327) -> RetirementSweepResult {
328 let mut result = RetirementSweepResult::new();
329
330 for stats in stats_list {
331 let decision = policy.evaluate(stats);
332 result.record(&stats.pattern_id, &decision);
333 }
334
335 result
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
347 fn test_retirement_reason_display_low_usage() {
348 let reason = RetirementReason::LowUsage { uses: 2, threshold: 5, window_days: 30 };
349 let display = format!("{}", reason);
350 assert!(display.contains("Low usage"));
351 assert!(display.contains("2 uses"));
352 }
353
354 #[test]
355 fn test_retirement_reason_display_high_failure() {
356 let reason = RetirementReason::HighFailureRate { success_rate: 0.2, threshold: 0.3 };
357 let display = format!("{}", reason);
358 assert!(display.contains("High failure rate"));
359 assert!(display.contains("20.0%"));
360 }
361
362 #[test]
363 fn test_retirement_reason_display_superseded() {
364 let reason = RetirementReason::Superseded {
365 better_pattern_id: "pattern-123".into(),
366 improvement: 0.15,
367 };
368 let display = format!("{}", reason);
369 assert!(display.contains("Superseded"));
370 assert!(display.contains("pattern-123"));
371 }
372
373 #[test]
378 fn test_retirement_decision_should_remove() {
379 assert!(!RetirementDecision::Keep.should_remove());
380 assert!(RetirementDecision::Retire(RetirementReason::LowUsage {
381 uses: 0,
382 threshold: 5,
383 window_days: 30
384 })
385 .should_remove());
386 assert!(RetirementDecision::Archive(RetirementReason::LowUsage {
387 uses: 0,
388 threshold: 5,
389 window_days: 30
390 })
391 .should_remove());
392 }
393
394 #[test]
395 fn test_retirement_decision_reason() {
396 assert!(RetirementDecision::Keep.reason().is_none());
397
398 let reason = RetirementReason::LowUsage { uses: 0, threshold: 5, window_days: 30 };
399 let decision = RetirementDecision::Retire(reason.clone());
400 assert!(decision.reason().is_some());
401 }
402
403 #[test]
408 fn test_pattern_stats_new() {
409 let stats = PatternStats::new("pat-1", "E0382");
410 assert_eq!(stats.pattern_id, "pat-1");
411 assert_eq!(stats.error_code, "E0382");
412 assert_eq!(stats.total_uses, 0);
413 assert_eq!(stats.success_rate(), 0.0);
414 }
415
416 #[test]
417 fn test_pattern_stats_record_use() {
418 let mut stats = PatternStats::new("pat-1", "E0382");
419
420 stats.record_use(true);
421 stats.record_use(true);
422 stats.record_use(false);
423
424 assert_eq!(stats.total_uses, 3);
425 assert_eq!(stats.successes, 2);
426 assert_eq!(stats.failures, 1);
427 assert!((stats.success_rate() - 0.666).abs() < 0.01);
428 }
429
430 #[test]
431 fn test_pattern_stats_reset_window() {
432 let mut stats = PatternStats::new("pat-1", "E0382");
433 stats.record_use(true);
434 stats.record_use(true);
435
436 assert_eq!(stats.uses_in_window, 2);
437 stats.reset_window();
438 assert_eq!(stats.uses_in_window, 0);
439 assert_eq!(stats.total_uses, 2); }
441
442 #[test]
443 fn test_pattern_stats_mark_superseded() {
444 let mut stats = PatternStats::new("pat-1", "E0382");
445 assert!(stats.superseded_by.is_none());
446
447 stats.mark_superseded("pat-2");
448 assert_eq!(stats.superseded_by, Some("pat-2".into()));
449 }
450
451 #[test]
456 fn test_retirement_config_default() {
457 let config = RetirementConfig::default();
458 assert_eq!(config.min_usage_threshold, 5);
459 assert!((config.min_success_rate - 0.3).abs() < f32::EPSILON);
460 assert_eq!(config.evaluation_window_days, 30);
461 assert!(config.archive_instead_of_delete);
462 }
463
464 #[test]
469 fn test_policy_keeps_active_pattern() {
470 let policy = PatternRetirementPolicy::new();
471 let mut stats = PatternStats::new("pat-1", "E0382");
472
473 for _ in 0..10 {
475 stats.record_use(true);
476 }
477
478 let decision = policy.evaluate(&stats);
479 assert_eq!(decision, RetirementDecision::Keep);
480 }
481
482 #[test]
483 fn test_policy_retires_low_usage() {
484 let policy = PatternRetirementPolicy::new();
485 let mut stats = PatternStats::new("pat-1", "E0382");
486
487 stats.record_use(true);
489 stats.record_use(true);
490
491 let decision = policy.evaluate(&stats);
492 assert!(decision.should_remove());
493 assert!(matches!(decision.reason(), Some(RetirementReason::LowUsage { .. })));
494 }
495
496 #[test]
497 fn test_policy_retires_high_failure() {
498 let policy = PatternRetirementPolicy::new();
499 let mut stats = PatternStats::new("pat-1", "E0382");
500
501 stats.record_use(true);
503 stats.record_use(true);
504 for _ in 0..8 {
505 stats.record_use(false);
506 }
507 stats.uses_in_window = 10;
509
510 let decision = policy.evaluate(&stats);
511 assert!(decision.should_remove());
512 assert!(matches!(decision.reason(), Some(RetirementReason::HighFailureRate { .. })));
513 }
514
515 #[test]
516 fn test_policy_retires_superseded() {
517 let policy = PatternRetirementPolicy::new();
518 let mut stats = PatternStats::new("pat-1", "E0382");
519
520 for _ in 0..10 {
522 stats.record_use(true);
523 }
524 stats.mark_superseded("pat-2");
525
526 let decision = policy.evaluate(&stats);
527 assert!(decision.should_remove());
528 assert!(matches!(decision.reason(), Some(RetirementReason::Superseded { .. })));
529 }
530
531 #[test]
532 fn test_policy_evaluate_batch() {
533 let policy = PatternRetirementPolicy::new();
534
535 let mut stats1 = PatternStats::new("pat-1", "E0382");
536 for _ in 0..10 {
537 stats1.record_use(true);
538 }
539
540 let mut stats2 = PatternStats::new("pat-2", "E0382");
541 stats2.record_use(true);
542
543 let batch = vec![stats1, stats2];
544 let decisions = policy.evaluate_batch(&batch);
545
546 assert_eq!(decisions.len(), 2);
547 assert_eq!(decisions[0].1, RetirementDecision::Keep);
548 assert!(decisions[1].1.should_remove());
549 }
550
551 #[test]
552 fn test_policy_find_retireable() {
553 let policy = PatternRetirementPolicy::new();
554
555 let mut stats1 = PatternStats::new("pat-1", "E0382");
556 for _ in 0..10 {
557 stats1.record_use(true);
558 }
559
560 let mut stats2 = PatternStats::new("pat-2", "E0382");
561 stats2.record_use(true);
562
563 let batch = vec![stats1, stats2];
564 let retireable = policy.find_retireable(&batch);
565
566 assert_eq!(retireable.len(), 1);
567 assert_eq!(retireable[0].pattern_id, "pat-2");
568 }
569
570 #[test]
575 fn test_sweep_result_new() {
576 let result = RetirementSweepResult::new();
577 assert_eq!(result.total_evaluated, 0);
578 assert_eq!(result.kept, 0);
579 }
580
581 #[test]
582 fn test_sweep_result_record() {
583 let mut result = RetirementSweepResult::new();
584
585 result.record("pat-1", &RetirementDecision::Keep);
586 result.record(
587 "pat-2",
588 &RetirementDecision::Retire(RetirementReason::LowUsage {
589 uses: 0,
590 threshold: 5,
591 window_days: 30,
592 }),
593 );
594 result.record(
595 "pat-3",
596 &RetirementDecision::Archive(RetirementReason::HighFailureRate {
597 success_rate: 0.1,
598 threshold: 0.3,
599 }),
600 );
601
602 assert_eq!(result.total_evaluated, 3);
603 assert_eq!(result.kept, 1);
604 assert_eq!(result.retired_low_usage, 1);
605 assert_eq!(result.retired_high_failure, 1);
606 assert_eq!(result.archived, 1);
607 assert_eq!(result.retired_ids.len(), 2);
608 }
609
610 #[test]
611 fn test_sweep_result_total_retired() {
612 let mut result = RetirementSweepResult::new();
613 result.retired_low_usage = 3;
614 result.retired_high_failure = 2;
615 result.retired_superseded = 1;
616
617 assert_eq!(result.total_retired(), 6);
618 }
619
620 #[test]
621 fn test_sweep_result_retirement_rate() {
622 let mut result = RetirementSweepResult::new();
623 result.total_evaluated = 10;
624 result.retired_low_usage = 2;
625 result.retired_high_failure = 1;
626
627 assert!((result.retirement_rate() - 0.3).abs() < 0.01);
628 }
629
630 #[test]
635 fn test_run_retirement_sweep() {
636 let policy = PatternRetirementPolicy::new();
637
638 let mut stats1 = PatternStats::new("pat-1", "E0382");
639 for _ in 0..10 {
640 stats1.record_use(true);
641 }
642
643 let mut stats2 = PatternStats::new("pat-2", "E0382");
644 stats2.record_use(true);
645
646 let mut stats3 = PatternStats::new("pat-3", "E0382");
647 for _ in 0..10 {
648 stats3.record_use(false);
649 }
650 stats3.uses_in_window = 10;
651
652 let batch = vec![stats1, stats2, stats3];
653 let result = run_retirement_sweep(&batch, &policy);
654
655 assert_eq!(result.total_evaluated, 3);
656 assert_eq!(result.kept, 1);
657 assert_eq!(result.total_retired(), 2);
658 }
659
660 #[test]
665 fn test_spec_low_usage_threshold() {
666 let config = RetirementConfig::default();
668 assert_eq!(config.min_usage_threshold, 5);
669 assert_eq!(config.evaluation_window_days, 30);
670 }
671
672 #[test]
673 fn test_spec_high_failure_threshold() {
674 let config = RetirementConfig::default();
676 assert!((config.min_success_rate - 0.3).abs() < f32::EPSILON);
677 }
678
679 #[test]
680 fn test_spec_superseded_archived() {
681 let policy = PatternRetirementPolicy::new();
683 let mut stats = PatternStats::new("pat-1", "E0382");
684
685 for _ in 0..10 {
686 stats.record_use(true);
687 }
688 stats.mark_superseded("pat-2");
689
690 let decision = policy.evaluate(&stats);
691 assert!(matches!(decision, RetirementDecision::Archive(_)));
692 }
693
694 #[test]
699 fn test_manual_deprecation_display() {
700 let reason = RetirementReason::ManualDeprecation { reason: "API changed".to_string() };
701 let display = format!("{}", reason);
702 assert!(display.contains("Manually deprecated"), "Got: {}", display);
703 assert!(display.contains("API changed"), "Got: {}", display);
704 }
705
706 #[test]
707 fn test_policy_with_config() {
708 let config = RetirementConfig {
709 min_usage_threshold: 10,
710 min_success_rate: 0.5,
711 evaluation_window_days: 60,
712 supersede_improvement_threshold: 0.2,
713 archive_instead_of_delete: false,
714 };
715 let policy = PatternRetirementPolicy::with_config(config);
716 assert_eq!(policy.config().min_usage_threshold, 10);
717 assert_eq!(policy.config().evaluation_window_days, 60);
718 assert!(!policy.config().archive_instead_of_delete);
719 }
720
721 #[test]
722 fn test_policy_config_accessor() {
723 let policy = PatternRetirementPolicy::new();
724 let config = policy.config();
725 assert_eq!(config.min_usage_threshold, 5);
726 assert!((config.min_success_rate - 0.3).abs() < 0.01);
727 assert!(config.archive_instead_of_delete);
728 }
729
730 #[test]
731 fn test_policy_default_impl() {
732 let policy = PatternRetirementPolicy::default();
733 assert_eq!(policy.config().min_usage_threshold, 5);
734 }
735
736 #[test]
737 fn test_evaluate_low_usage_retire_not_archive() {
738 let config = RetirementConfig { archive_instead_of_delete: false, ..Default::default() };
739 let policy = PatternRetirementPolicy::with_config(config);
740 let stats = PatternStats::new("pat-1", "E0382");
741 let decision = policy.evaluate(&stats);
743 assert!(
744 matches!(decision, RetirementDecision::Retire(RetirementReason::LowUsage { .. })),
745 "Expected Retire(LowUsage), got: {:?}",
746 decision
747 );
748 }
749
750 #[test]
751 fn test_evaluate_high_failure_retire_not_archive() {
752 let config = RetirementConfig { archive_instead_of_delete: false, ..Default::default() };
753 let policy = PatternRetirementPolicy::with_config(config);
754 let mut stats = PatternStats::new("pat-1", "E0382");
755 for _ in 0..5 {
757 stats.record_use(true);
758 }
759 stats.uses_in_window = 10; stats.successes = 1;
761 stats.failures = 9;
762 stats.total_uses = 10;
763 let decision = policy.evaluate(&stats);
764 assert!(
765 matches!(
766 decision,
767 RetirementDecision::Retire(RetirementReason::HighFailureRate { .. })
768 ),
769 "Expected Retire(HighFailureRate), got: {:?}",
770 decision
771 );
772 }
773
774 #[test]
775 fn test_sweep_result_manual_deprecation() {
776 let mut result = RetirementSweepResult::default();
777 let decision = RetirementDecision::Retire(RetirementReason::ManualDeprecation {
778 reason: "Obsolete".to_string(),
779 });
780 result.record("pat-1", &decision);
781 assert_eq!(result.total_evaluated, 1);
782 assert_eq!(result.retired_ids.len(), 1);
783 assert_eq!(result.retired_ids[0], "pat-1");
784 assert_eq!(result.retired_low_usage, 0);
786 assert_eq!(result.retired_high_failure, 0);
787 assert_eq!(result.retired_superseded, 0);
788 }
789
790 #[test]
791 fn test_sweep_result_retirement_rate_zero() {
792 let result = RetirementSweepResult::default();
793 assert_eq!(result.retirement_rate(), 0.0);
794 }
795
796 #[test]
797 fn test_sweep_result_record_superseded() {
798 let mut result = RetirementSweepResult::default();
799 let decision = RetirementDecision::Archive(RetirementReason::Superseded {
800 better_pattern_id: "pat-new".to_string(),
801 improvement: 0.15,
802 });
803 result.record("pat-old", &decision);
804 assert_eq!(result.total_evaluated, 1);
805 assert_eq!(result.retired_superseded, 1);
806 assert_eq!(result.archived, 1);
807 assert_eq!(result.retired_ids.len(), 1);
808 }
809
810 #[test]
811 fn test_sweep_result_record_archive_low_usage() {
812 let mut result = RetirementSweepResult::default();
813 let decision = RetirementDecision::Archive(RetirementReason::LowUsage {
814 uses: 1,
815 threshold: 5,
816 window_days: 30,
817 });
818 result.record("pat-stale", &decision);
819 assert_eq!(result.retired_low_usage, 1);
820 assert_eq!(result.archived, 1);
821 }
822}