Skip to main content

decy_oracle/
retirement.rs

1//! Pattern retirement policy for oracle hygiene
2//!
3//! Prunes obsolete patterns that are rarely used or superseded.
4//! Implements Kaizen principle - continuous improvement.
5//!
6//! # References
7//! - training-oracle-spec.md §3.3.3: Pattern Retirement Policy (Kaizen Enhancement)
8//! - Gemini Review: "define a Retirement Policy for patterns"
9
10use serde::{Deserialize, Serialize};
11use std::time::SystemTime;
12
13/// Retirement decision for a pattern
14#[derive(Debug, Clone, PartialEq)]
15pub enum RetirementDecision {
16    /// Keep the pattern active
17    Keep,
18    /// Retire the pattern with reason
19    Retire(RetirementReason),
20    /// Archive the pattern (keep for analysis, don't use for suggestions)
21    Archive(RetirementReason),
22}
23
24impl RetirementDecision {
25    /// Check if pattern should be removed from active use
26    pub fn should_remove(&self) -> bool {
27        matches!(self, RetirementDecision::Retire(_) | RetirementDecision::Archive(_))
28    }
29
30    /// Get the reason if retiring
31    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/// Reason for retiring a pattern
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub enum RetirementReason {
42    /// Pattern rarely used (< threshold uses in evaluation window)
43    LowUsage { uses: usize, threshold: usize, window_days: u32 },
44    /// Pattern has high failure rate
45    HighFailureRate { success_rate: f32, threshold: f32 },
46    /// Pattern superseded by better alternative
47    Superseded { better_pattern_id: String, improvement: f32 },
48    /// Manually deprecated by maintainer
49    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/// Usage statistics for a pattern
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct PatternStats {
88    /// Pattern identifier
89    pub pattern_id: String,
90    /// Error code this pattern addresses
91    pub error_code: String,
92    /// Total number of times pattern was used
93    pub total_uses: usize,
94    /// Uses within the evaluation window
95    pub uses_in_window: usize,
96    /// Successful applications
97    pub successes: usize,
98    /// Failed applications
99    pub failures: usize,
100    /// Timestamp of last use
101    pub last_used: Option<SystemTime>,
102    /// ID of a better pattern if one exists
103    pub superseded_by: Option<String>,
104}
105
106impl PatternStats {
107    /// Create new pattern stats
108    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    /// Record a use of the pattern
122    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    /// Get success rate
135    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    /// Reset window usage (called at window boundary)
145    pub fn reset_window(&mut self) {
146        self.uses_in_window = 0;
147    }
148
149    /// Mark as superseded by another pattern
150    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/// Configuration for pattern retirement policy
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct RetirementConfig {
158    /// Minimum uses to keep pattern (default: 5)
159    pub min_usage_threshold: usize,
160    /// Minimum success rate to keep pattern (default: 0.3)
161    pub min_success_rate: f32,
162    /// Evaluation window in days (default: 30)
163    pub evaluation_window_days: u32,
164    /// Improvement threshold to consider pattern superseded (default: 0.1)
165    pub supersede_improvement_threshold: f32,
166    /// Whether to archive instead of delete
167    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
182/// Pattern retirement policy
183pub struct PatternRetirementPolicy {
184    config: RetirementConfig,
185}
186
187impl PatternRetirementPolicy {
188    /// Create new policy with default configuration
189    pub fn new() -> Self {
190        Self { config: RetirementConfig::default() }
191    }
192
193    /// Create policy with custom configuration
194    pub fn with_config(config: RetirementConfig) -> Self {
195        Self { config }
196    }
197
198    /// Get configuration
199    pub fn config(&self) -> &RetirementConfig {
200        &self.config
201    }
202
203    /// Evaluate whether a pattern should be retired
204    pub fn evaluate(&self, stats: &PatternStats) -> RetirementDecision {
205        // Criterion 1: Low usage
206        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        // Criterion 2: High failure rate
220        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        // Criterion 3: Superseded by better pattern
233        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    /// Evaluate multiple patterns and return retirement decisions
245    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    /// Find patterns that should be retired
250    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/// Results of a retirement sweep
262#[derive(Debug, Clone, Default, Serialize, Deserialize)]
263pub struct RetirementSweepResult {
264    /// Total patterns evaluated
265    pub total_evaluated: usize,
266    /// Patterns kept
267    pub kept: usize,
268    /// Patterns retired due to low usage
269    pub retired_low_usage: usize,
270    /// Patterns retired due to high failure rate
271    pub retired_high_failure: usize,
272    /// Patterns retired due to being superseded
273    pub retired_superseded: usize,
274    /// Patterns archived (instead of deleted)
275    pub archived: usize,
276    /// Pattern IDs that were retired
277    pub retired_ids: Vec<String>,
278}
279
280impl RetirementSweepResult {
281    /// Create new empty result
282    pub fn new() -> Self {
283        Self::default()
284    }
285
286    /// Record a decision
287    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    /// Get total retired count
309    pub fn total_retired(&self) -> usize {
310        self.retired_low_usage + self.retired_high_failure + self.retired_superseded
311    }
312
313    /// Get retirement rate
314    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
323/// Run a retirement sweep on pattern statistics
324pub 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    // ============================================================================
343    // RetirementReason Tests
344    // ============================================================================
345
346    #[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    // ============================================================================
374    // RetirementDecision Tests
375    // ============================================================================
376
377    #[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    // ============================================================================
404    // PatternStats Tests
405    // ============================================================================
406
407    #[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); // Total unchanged
440    }
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    // ============================================================================
452    // RetirementConfig Tests
453    // ============================================================================
454
455    #[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    // ============================================================================
465    // PatternRetirementPolicy Tests
466    // ============================================================================
467
468    #[test]
469    fn test_policy_keeps_active_pattern() {
470        let policy = PatternRetirementPolicy::new();
471        let mut stats = PatternStats::new("pat-1", "E0382");
472
473        // Add enough uses with good success rate
474        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        // Only 2 uses (below threshold of 5)
488        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        // 10 uses, 20% success rate (below threshold of 30%)
502        stats.record_use(true);
503        stats.record_use(true);
504        for _ in 0..8 {
505            stats.record_use(false);
506        }
507        // Force uses_in_window to be above threshold
508        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        // Good stats but superseded
521        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    // ============================================================================
571    // RetirementSweepResult Tests
572    // ============================================================================
573
574    #[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    // ============================================================================
631    // run_retirement_sweep Tests
632    // ============================================================================
633
634    #[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    // ============================================================================
661    // Spec Compliance Tests
662    // ============================================================================
663
664    #[test]
665    fn test_spec_low_usage_threshold() {
666        // Spec: Retire patterns with < 5 uses in 30 days
667        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        // Spec: Retire patterns with success_rate < 0.3
675        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        // Spec: Superseded patterns should be archived (not deleted)
682        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    // ============================================================================
695    // COVERAGE: ManualDeprecation Display, with_config, config(), Retire branches
696    // ============================================================================
697
698    #[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        // 0 uses in window < threshold 5 → Retire (not Archive)
742        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        // Need >= 5 total uses and success_rate < 0.3
756        for _ in 0..5 {
757            stats.record_use(true);
758        }
759        stats.uses_in_window = 10; // above threshold
760        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        // ManualDeprecation doesn't increment specific counters
785        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}