Skip to main content

do_memory_core/extraction/
utils.rs

1//! Utility functions for pattern processing
2
3use crate::patterns::Pattern;
4use crate::types::TaskContext;
5use chrono::{DateTime, Utc};
6use std::collections::HashSet;
7
8/// Remove duplicate patterns from a list
9#[must_use]
10pub fn deduplicate_patterns(patterns: Vec<Pattern>) -> Vec<Pattern> {
11    let mut seen = HashSet::new();
12    let mut deduplicated = Vec::new();
13
14    for pattern in patterns {
15        let key = pattern.similarity_key();
16
17        if seen.insert(key) {
18            deduplicated.push(pattern);
19        }
20    }
21
22    deduplicated
23}
24
25/// Rank patterns by relevance/quality
26///
27/// Optimization: Uses Schwartzian Transform (decorate-sort-undecorate) to calculate
28/// pattern scores exactly once per item, reducing complexity from O(N log N) to O(N).
29/// Also pre-calculates the query context tag set to avoid redundant HashSet creation.
30#[must_use]
31pub fn rank_patterns(patterns: Vec<Pattern>, context: &TaskContext) -> Vec<Pattern> {
32    if patterns.is_empty() {
33        return patterns;
34    }
35
36    let query_tags: HashSet<_> = context.tags.iter().collect();
37    let now = Utc::now();
38
39    // Decorate: Calculate scores once
40    let mut decorated: Vec<(f64, Pattern)> = patterns
41        .into_iter()
42        .map(|p| {
43            let score = calculate_pattern_score(&p, context, &query_tags, now);
44            (score, p)
45        })
46        .collect();
47
48    // Sort: Use pre-calculated scores
49    // Sort in descending order (higher score first)
50    decorated.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
51
52    // Undecorate: Remove scores
53    decorated.into_iter().map(|(_, p)| p).collect()
54}
55
56/// Calculate a relevance score for a pattern given the current context
57///
58/// Scoring system (max ~400+ points):
59/// - Base success rate: 0-100 points
60/// - Sample size: 0-50 points
61/// - Context relevance: 0-100 points
62/// - Pattern type bonuses: 0-50 points
63/// - **Effectiveness tracking: 0-200 points** (NEW)
64fn calculate_pattern_score(
65    pattern: &Pattern,
66    current_context: &TaskContext,
67    query_tags: &HashSet<&String>,
68    now: DateTime<Utc>,
69) -> f64 {
70    let mut score = 0.0;
71
72    // Base score from success rate (0-100 points)
73    score += f64::from(pattern.success_rate()) * 100.0;
74
75    // Sample size bonus (0-50 points, diminishing returns)
76    let sample_size = pattern.sample_size() as f64;
77    score += (sample_size.min(10.0) / 10.0) * 50.0;
78
79    // Context relevance bonus (0-100 points)
80    if let Some(pattern_context) = pattern.context() {
81        score += calculate_context_similarity(pattern_context, current_context, query_tags) * 100.0;
82    }
83
84    // Pattern type specific bonuses
85    match pattern {
86        Pattern::ToolSequence { tools, .. } => {
87            // Prefer patterns with diverse tool usage
88            // Optimization: Avoid HashSet for small tool sequences (max 5)
89            let mut unique_tools: usize = 0;
90            let mut seen: Vec<&String> = Vec::with_capacity(tools.len());
91            for tool in tools {
92                if !seen.contains(&tool) {
93                    unique_tools += 1;
94                    seen.push(tool);
95                }
96            }
97            score += (unique_tools as f64 / tools.len() as f64) * 20.0;
98        }
99        Pattern::ErrorRecovery { .. } => {
100            // Error recovery patterns are valuable for robustness
101            score += 30.0;
102        }
103        Pattern::DecisionPoint { outcome_stats, .. } => {
104            // Decision points with clear outcomes are more valuable
105            if outcome_stats.total_count > 5 {
106                score += 25.0;
107            }
108        }
109        Pattern::ContextPattern { evidence, .. } => {
110            // Context patterns with more evidence are better
111            score += (evidence.len() as f64).min(5.0) * 10.0;
112        }
113    }
114
115    // **NEW: Effectiveness-based scoring (0-200 points)**
116    // This is the key metric for self-learning!
117    let effectiveness = pattern.effectiveness();
118
119    // 1. Effectiveness score boost (0-100 points)
120    // Combines success rate, usage confidence, and reward impact
121    score += f64::from(effectiveness.effectiveness_score()) * 100.0;
122
123    // 2. Proven usage bonus (0-50 points)
124    // Patterns that have been successfully applied get priority
125    if effectiveness.times_applied > 0 {
126        let success_rate = effectiveness.application_success_rate();
127        let usage_confidence = (effectiveness.times_applied as f64).ln().min(3.0) / 3.0;
128        score += f64::from(success_rate) * usage_confidence * 50.0;
129    }
130
131    // 3. Reward delta bonus (0-50 points)
132    // Patterns that improve outcomes get strong preference
133    if effectiveness.avg_reward_delta > 0.0 {
134        let capped_delta = effectiveness.avg_reward_delta.min(0.5); // Cap at +0.5
135        score += f64::from(capped_delta) * 100.0; // 0.5 delta = 50 points
136    } else if effectiveness.avg_reward_delta < 0.0 {
137        // Penalize patterns that hurt performance
138        let capped_penalty = effectiveness.avg_reward_delta.max(-0.5); // Cap at -0.5
139        score += f64::from(capped_penalty) * 100.0; // Can subtract up to 50 points
140    }
141
142    // 4. Recency bonus (0-10 points)
143    // Recently used patterns are more likely to be relevant
144    if effectiveness.times_applied > 0 {
145        let days_since_use = (now - effectiveness.last_used).num_days();
146        if days_since_use < 30 {
147            score += (30.0 - days_since_use as f64) / 30.0 * 10.0;
148        }
149    }
150
151    score
152}
153
154/// Calculate similarity between two task contexts (0.0 to 1.0)
155///
156/// Optimization: query_tags is pre-calculated to avoid O(N) allocations in O(N log N) context.
157fn calculate_context_similarity(
158    a: &TaskContext,
159    b: &TaskContext,
160    query_tags: &HashSet<&String>,
161) -> f64 {
162    let mut similarity = 0.0;
163    let mut factors = 0.0;
164
165    // Language match (high weight)
166    if a.language == b.language {
167        similarity += 1.0;
168    }
169    factors += 1.0;
170
171    // Framework match (high weight)
172    if a.framework == b.framework {
173        similarity += 1.0;
174    }
175    factors += 1.0;
176
177    // Domain match (medium weight)
178    if a.domain == b.domain {
179        similarity += 0.8;
180    }
181    factors += 1.0;
182
183    // Complexity level match (medium weight)
184    if a.complexity == b.complexity {
185        similarity += 0.6;
186    }
187    factors += 1.0;
188
189    // Tag overlap (variable weight based on overlap)
190    // Optimization: Avoid re-allocating HashSet for 'a.tags'.
191    // We use the pre-calculated query_tags HashSet for efficient lookups.
192    if !a.tags.is_empty() || !query_tags.is_empty() {
193        let mut intersection_count = 0;
194        let mut a_unique_count = 0;
195        let mut seen_in_a = Vec::with_capacity(a.tags.len());
196
197        for tag in &a.tags {
198            if !seen_in_a.contains(&tag) {
199                a_unique_count += 1;
200                seen_in_a.push(tag);
201                if query_tags.contains(tag) {
202                    intersection_count += 1;
203                }
204            }
205        }
206
207        let union_count = query_tags.len() + a_unique_count - intersection_count;
208
209        if union_count > 0 {
210            similarity += (intersection_count as f64 / union_count as f64) * 0.7;
211        }
212        factors += 1.0;
213    }
214
215    if factors > 0.0 {
216        similarity / factors
217    } else {
218        0.0
219    }
220}