Skip to main content

agentic_evolve_core/matching/
semantic.rs

1//! SemanticMatcher — matches based on semantic meaning.
2
3use crate::types::error::EvolveResult;
4use crate::types::match_result::{MatchContext, MatchResult, MatchScore};
5use crate::types::pattern::{FunctionSignature, Pattern};
6
7/// Matches patterns based on semantic meaning of names and types.
8#[derive(Debug, Default)]
9pub struct SemanticMatcher;
10
11impl SemanticMatcher {
12    pub fn new() -> Self {
13        Self
14    }
15
16    pub fn find_matches(
17        &self,
18        signature: &FunctionSignature,
19        patterns: &[&Pattern],
20        _context: &MatchContext,
21        limit: usize,
22    ) -> EvolveResult<Vec<MatchResult>> {
23        let sig_tokens = tokenize_name(&signature.name);
24
25        let mut results: Vec<MatchResult> = patterns
26            .iter()
27            .map(|p| {
28                let score = self.score_semantic(p, &sig_tokens);
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 > 0.0)
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_semantic(&self, pattern: &Pattern, query_tokens: &[String]) -> f64 {
50        let pattern_tokens = tokenize_name(&pattern.name);
51        if query_tokens.is_empty() || pattern_tokens.is_empty() {
52            return 0.0;
53        }
54        let matches = query_tokens
55            .iter()
56            .filter(|qt| {
57                pattern_tokens
58                    .iter()
59                    .any(|pt| pt == *qt || is_semantic_match(pt, qt))
60            })
61            .count();
62        let max_len = query_tokens.len().max(pattern_tokens.len());
63        matches as f64 / max_len as f64
64    }
65}
66
67fn tokenize_name(name: &str) -> Vec<String> {
68    let mut tokens = Vec::new();
69    let mut current = String::new();
70
71    for ch in name.chars() {
72        if ch == '_' || ch == '-' || ch == '.' || ch == ' ' {
73            if !current.is_empty() {
74                tokens.push(current.to_lowercase());
75                current.clear();
76            }
77        } else if ch.is_uppercase() && !current.is_empty() {
78            tokens.push(current.to_lowercase());
79            current.clear();
80            current.push(ch);
81        } else {
82            current.push(ch);
83        }
84    }
85    if !current.is_empty() {
86        tokens.push(current.to_lowercase());
87    }
88    tokens
89}
90
91fn is_semantic_match(a: &str, b: &str) -> bool {
92    const SYNONYMS: &[&[&str]] = &[
93        &["get", "fetch", "retrieve", "read", "load", "find", "query"],
94        &["set", "put", "write", "store", "save", "update"],
95        &["delete", "remove", "drop", "destroy", "clear"],
96        &["create", "new", "build", "make", "init", "construct"],
97        &["list", "all", "enumerate", "iter"],
98        &["check", "validate", "verify", "test", "is", "has"],
99        &["parse", "decode", "deserialize", "from"],
100        &["format", "encode", "serialize", "to"],
101        &["send", "emit", "dispatch", "publish", "notify"],
102        &["receive", "handle", "process", "consume", "subscribe"],
103    ];
104
105    for group in SYNONYMS {
106        if group.contains(&a) && group.contains(&b) {
107            return true;
108        }
109    }
110    false
111}