Skip to main content

agentic_evolve_core/matching/
fuzzy.rs

1//! FuzzyMatcher — handles approximate matches with edit distance.
2
3use crate::types::error::EvolveResult;
4use crate::types::match_result::{MatchContext, MatchResult, MatchScore};
5use crate::types::pattern::{FunctionSignature, Pattern};
6
7/// Matches patterns using fuzzy/approximate matching.
8#[derive(Debug)]
9pub struct FuzzyMatcher {
10    threshold: f64,
11}
12
13impl FuzzyMatcher {
14    pub fn new(threshold: f64) -> Self {
15        Self { threshold }
16    }
17
18    pub fn find_matches(
19        &self,
20        signature: &FunctionSignature,
21        patterns: &[&Pattern],
22        _context: &MatchContext,
23        limit: usize,
24    ) -> EvolveResult<Vec<MatchResult>> {
25        let mut results: Vec<MatchResult> = patterns
26            .iter()
27            .map(|p| {
28                let score = self.score_fuzzy(p, signature);
29                MatchResult {
30                    pattern_id: p.id.clone(),
31                    pattern: (*p).clone(),
32                    score: MatchScore::from_single(score),
33                    suggested_bindings: std::collections::HashMap::new(),
34                }
35            })
36            .filter(|r| r.score.combined >= self.threshold)
37            .collect();
38
39        results.sort_by(|a, b| {
40            b.score
41                .combined
42                .partial_cmp(&a.score.combined)
43                .unwrap_or(std::cmp::Ordering::Equal)
44        });
45        results.truncate(limit);
46        Ok(results)
47    }
48
49    fn score_fuzzy(&self, pattern: &Pattern, signature: &FunctionSignature) -> f64 {
50        let name_sim = fuzzy_similarity(&pattern.signature.name, &signature.name);
51        let lang_match = if pattern.signature.language == signature.language {
52            1.0
53        } else {
54            0.5
55        };
56
57        let param_sim = if pattern.signature.params.is_empty() && signature.params.is_empty() {
58            1.0
59        } else {
60            let max_params = pattern.signature.params.len().max(signature.params.len());
61            let matching = pattern
62                .signature
63                .params
64                .iter()
65                .zip(signature.params.iter())
66                .filter(|(a, b)| fuzzy_similarity(&a.param_type, &b.param_type) > 0.6)
67                .count();
68            if max_params == 0 {
69                1.0
70            } else {
71                matching as f64 / max_params as f64
72            }
73        };
74
75        name_sim * 0.5 + lang_match * 0.2 + param_sim * 0.3
76    }
77}
78
79impl Default for FuzzyMatcher {
80    fn default() -> Self {
81        Self::new(0.3)
82    }
83}
84
85fn fuzzy_similarity(a: &str, b: &str) -> f64 {
86    if a == b {
87        return 1.0;
88    }
89    let a_lower = a.to_lowercase();
90    let b_lower = b.to_lowercase();
91    if a_lower == b_lower {
92        return 0.95;
93    }
94
95    // Trigram similarity
96    let a_trigrams = trigrams(&a_lower);
97    let b_trigrams = trigrams(&b_lower);
98    if a_trigrams.is_empty() && b_trigrams.is_empty() {
99        return 1.0;
100    }
101    if a_trigrams.is_empty() || b_trigrams.is_empty() {
102        return 0.0;
103    }
104
105    let intersection = a_trigrams.iter().filter(|t| b_trigrams.contains(t)).count();
106    let union = a_trigrams.len() + b_trigrams.len() - intersection;
107    if union == 0 {
108        return 0.0;
109    }
110    intersection as f64 / union as f64
111}
112
113fn trigrams(s: &str) -> Vec<String> {
114    let chars: Vec<char> = s.chars().collect();
115    if chars.len() < 3 {
116        return vec![s.to_string()];
117    }
118    chars.windows(3).map(|w| w.iter().collect()).collect()
119}