Skip to main content

agentic_evolve_core/matching/
composite.rs

1//! CompositeMatcher — combines all matchers for best results.
2
3use crate::types::error::EvolveResult;
4use crate::types::match_result::{MatchContext, MatchResult, MatchScore};
5use crate::types::pattern::{FunctionSignature, Pattern};
6
7use super::{ContextMatcher, FuzzyMatcher, SemanticMatcher, SignatureMatcher};
8
9/// Combines all matchers with configurable weights.
10#[derive(Debug)]
11pub struct CompositeMatcher {
12    signature_matcher: SignatureMatcher,
13    context_matcher: ContextMatcher,
14    #[allow(dead_code)]
15    semantic_matcher: SemanticMatcher,
16    #[allow(dead_code)]
17    fuzzy_matcher: FuzzyMatcher,
18    weights: MatchWeights,
19}
20
21/// Weights for combining matcher scores.
22#[derive(Debug, Clone)]
23pub struct MatchWeights {
24    pub signature: f64,
25    pub context: f64,
26    pub semantic: f64,
27    pub fuzzy: f64,
28}
29
30impl Default for MatchWeights {
31    fn default() -> Self {
32        Self {
33            signature: 0.4,
34            context: 0.2,
35            semantic: 0.25,
36            fuzzy: 0.15,
37        }
38    }
39}
40
41impl CompositeMatcher {
42    pub fn new() -> Self {
43        Self {
44            signature_matcher: SignatureMatcher::new(),
45            context_matcher: ContextMatcher::new(),
46            semantic_matcher: SemanticMatcher::new(),
47            fuzzy_matcher: FuzzyMatcher::default(),
48            weights: MatchWeights::default(),
49        }
50    }
51
52    pub fn with_weights(weights: MatchWeights) -> Self {
53        Self {
54            signature_matcher: SignatureMatcher::new(),
55            context_matcher: ContextMatcher::new(),
56            semantic_matcher: SemanticMatcher::new(),
57            fuzzy_matcher: FuzzyMatcher::default(),
58            weights,
59        }
60    }
61
62    pub fn find_matches(
63        &self,
64        signature: &FunctionSignature,
65        patterns: &[&Pattern],
66        context: &MatchContext,
67        limit: usize,
68    ) -> EvolveResult<Vec<MatchResult>> {
69        let mut combined_scores: std::collections::HashMap<String, (f64, f64, f64, f64, &Pattern)> =
70            std::collections::HashMap::new();
71
72        // Score each pattern with each matcher
73        for pattern in patterns {
74            let sig_score = self.signature_matcher.score_match(pattern, signature);
75            let ctx_score = self.context_matcher.score_context(pattern, context);
76            let sem_score = {
77                let tokens = tokenize_camel_snake(&signature.name);
78                let pat_tokens = tokenize_camel_snake(&pattern.name);
79                token_overlap(&tokens, &pat_tokens)
80            };
81            let confidence_factor = pattern.confidence;
82
83            combined_scores.insert(
84                pattern.id.as_str().to_string(),
85                (sig_score, ctx_score, sem_score, confidence_factor, pattern),
86            );
87        }
88
89        let mut results: Vec<MatchResult> = combined_scores
90            .into_iter()
91            .map(|(_id, (sig, ctx, sem, conf, pattern))| {
92                let combined = sig * self.weights.signature
93                    + ctx * self.weights.context
94                    + sem * self.weights.semantic
95                    + conf * self.weights.fuzzy;
96                MatchResult {
97                    pattern_id: pattern.id.clone(),
98                    pattern: pattern.clone(),
99                    score: MatchScore::new(sig, ctx, sem, combined),
100                    suggested_bindings: std::collections::HashMap::new(),
101                }
102            })
103            .filter(|r| r.score.combined > 0.1)
104            .collect();
105
106        results.sort_by(|a, b| {
107            b.score
108                .combined
109                .partial_cmp(&a.score.combined)
110                .unwrap_or(std::cmp::Ordering::Equal)
111        });
112        results.truncate(limit);
113        Ok(results)
114    }
115}
116
117impl Default for CompositeMatcher {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123fn tokenize_camel_snake(name: &str) -> Vec<String> {
124    let mut tokens = Vec::new();
125    let mut current = String::new();
126    for ch in name.chars() {
127        if ch == '_' || ch == '-' || ch == ' ' {
128            if !current.is_empty() {
129                tokens.push(current.to_lowercase());
130                current.clear();
131            }
132        } else if ch.is_uppercase() && !current.is_empty() {
133            tokens.push(current.to_lowercase());
134            current.clear();
135            current.push(ch);
136        } else {
137            current.push(ch);
138        }
139    }
140    if !current.is_empty() {
141        tokens.push(current.to_lowercase());
142    }
143    tokens
144}
145
146fn token_overlap(a: &[String], b: &[String]) -> f64 {
147    if a.is_empty() && b.is_empty() {
148        return 1.0;
149    }
150    let max_len = a.len().max(b.len());
151    if max_len == 0 {
152        return 0.0;
153    }
154    let matches = a.iter().filter(|t| b.contains(t)).count();
155    matches as f64 / max_len as f64
156}