Skip to main content

do_memory_core/patterns/
similarity.rs

1//! Similarity calculation utilities for patterns
2
3use crate::types::TaskContext;
4
5/// Calculate similarity between two sequences using normalized edit distance
6pub(super) fn sequence_similarity(seq1: &[String], seq2: &[String]) -> f32 {
7    if seq1.is_empty() && seq2.is_empty() {
8        return 1.0;
9    }
10    if seq1.is_empty() || seq2.is_empty() {
11        return 0.0;
12    }
13
14    let distance = edit_distance(seq1, seq2);
15    let max_len = seq1.len().max(seq2.len());
16
17    1.0 - (distance as f32 / max_len as f32)
18}
19
20/// Calculate edit distance (Levenshtein) between two sequences
21///
22/// Optimization:
23/// 1. Uses a rolling buffer to reduce space complexity from O(N*M) to O(min(N, M)).
24/// 2. Swaps buffers instead of copying each iteration for O(1) row transitions.
25/// 3. Ensures the shorter sequence is used for buffer sizing.
26fn edit_distance(seq1: &[String], seq2: &[String]) -> usize {
27    // Ensure s1 is the shorter sequence for O(min(N, M)) space
28    let (s1, s2) = if seq1.len() < seq2.len() {
29        (seq1, seq2)
30    } else {
31        (seq2, seq1)
32    };
33
34    let len1 = s1.len();
35    let len2 = s2.len();
36
37    // After swapping, len1 <= len2, so len1 == 0 implies len2 == 0 too.
38    if len1 == 0 {
39        return len2;
40    }
41
42    let mut prev_row: Vec<usize> = (0..=len1).collect();
43    let mut curr_row = vec![0; len1 + 1];
44
45    for j in 1..=len2 {
46        curr_row[0] = j;
47        for i in 1..=len1 {
48            let cost = usize::from(s1[i - 1] != s2[j - 1]);
49            curr_row[i] = (prev_row[i] + 1)
50                .min(curr_row[i - 1] + 1)
51                .min(prev_row[i - 1] + cost);
52        }
53        std::mem::swap(&mut prev_row, &mut curr_row);
54    }
55
56    prev_row[len1]
57}
58
59/// Calculate similarity between two strings using normalized edit distance
60pub(super) fn string_similarity(s1: &str, s2: &str) -> f32 {
61    if s1.is_empty() && s2.is_empty() {
62        return 1.0;
63    }
64    if s1.is_empty() || s2.is_empty() {
65        return 0.0;
66    }
67
68    let chars1: Vec<char> = s1.chars().collect();
69    let chars2: Vec<char> = s2.chars().collect();
70
71    let distance = char_edit_distance(&chars1, &chars2);
72    let max_len = chars1.len().max(chars2.len());
73
74    1.0 - (distance as f32 / max_len as f32)
75}
76
77/// Calculate edit distance for character sequences
78///
79/// Optimization:
80/// 1. Uses a rolling buffer to reduce space complexity from O(N*M) to O(min(N, M)).
81/// 2. Swaps buffers instead of copying each iteration for O(1) row transitions.
82/// 3. Ensures the shorter sequence is used for buffer sizing.
83fn char_edit_distance(chars1: &[char], chars2: &[char]) -> usize {
84    // Ensure s1 is the shorter sequence for O(min(N, M)) space
85    let (s1, s2) = if chars1.len() < chars2.len() {
86        (chars1, chars2)
87    } else {
88        (chars2, chars1)
89    };
90
91    let len1 = s1.len();
92    let len2 = s2.len();
93
94    // After swapping, len1 <= len2, so len1 == 0 implies len2 == 0 too.
95    if len1 == 0 {
96        return len2;
97    }
98
99    let mut prev_row: Vec<usize> = (0..=len1).collect();
100    let mut curr_row = vec![0; len1 + 1];
101
102    for j in 1..=len2 {
103        curr_row[0] = j;
104        for i in 1..=len1 {
105            let cost = usize::from(s1[i - 1] != s2[j - 1]);
106            curr_row[i] = (prev_row[i] + 1)
107                .min(curr_row[i - 1] + 1)
108                .min(prev_row[i - 1] + cost);
109        }
110        std::mem::swap(&mut prev_row, &mut curr_row);
111    }
112
113    prev_row[len1]
114}
115
116/// Calculate similarity between two ToolSequence patterns
117pub(super) fn tool_sequence_similarity(
118    tools1: &[String],
119    ctx1: &TaskContext,
120    tools2: &[String],
121    ctx2: &TaskContext,
122) -> f32 {
123    let sequence_similarity = sequence_similarity(tools1, tools2);
124    let context_similarity = context_similarity(ctx1, ctx2);
125    sequence_similarity * 0.7 + context_similarity * 0.3
126}
127
128/// Calculate similarity between two DecisionPoint patterns
129pub(super) fn decision_point_similarity(
130    cond1: &str,
131    act1: &str,
132    ctx1: &TaskContext,
133    cond2: &str,
134    act2: &str,
135    ctx2: &TaskContext,
136) -> f32 {
137    let condition_sim = string_similarity(cond1, cond2);
138    let action_sim = string_similarity(act1, act2);
139    let context_sim = context_similarity(ctx1, ctx2);
140    condition_sim * 0.4 + action_sim * 0.4 + context_sim * 0.2
141}
142
143/// Calculate similarity between two ErrorRecovery patterns
144pub(super) fn error_recovery_similarity(
145    err1: &str,
146    steps1: &[String],
147    ctx1: &TaskContext,
148    err2: &str,
149    steps2: &[String],
150    ctx2: &TaskContext,
151) -> f32 {
152    let error_sim = string_similarity(err1, err2);
153    let steps_sim = sequence_similarity(steps1, steps2);
154    let context_sim = context_similarity(ctx1, ctx2);
155    error_sim * 0.4 + steps_sim * 0.4 + context_sim * 0.2
156}
157
158/// Calculate similarity between two ContextPattern patterns
159pub(super) fn context_pattern_similarity(
160    feat1: &[String],
161    rec1: &str,
162    feat2: &[String],
163    rec2: &str,
164) -> f32 {
165    let features_sim = sequence_similarity(feat1, feat2);
166    let approach_sim = string_similarity(rec1, rec2);
167    features_sim * 0.6 + approach_sim * 0.4
168}
169
170/// Calculate context similarity between two task contexts
171pub(super) fn context_similarity(ctx1: &TaskContext, ctx2: &TaskContext) -> f32 {
172    let mut score = 0.0;
173    let mut weight_sum = 0.0;
174
175    // Domain match (weight: 0.4)
176    if ctx1.domain == ctx2.domain {
177        score += 0.4;
178    }
179    weight_sum += 0.4;
180
181    // Language match (weight: 0.3)
182    match (&ctx1.language, &ctx2.language) {
183        (Some(l1), Some(l2)) if l1 == l2 => score += 0.3,
184        (None, None) => score += 0.15, // Partial credit for both being None
185        _ => {}
186    }
187    weight_sum += 0.3;
188
189    // Tags overlap (weight: 0.3)
190    if !ctx1.tags.is_empty() || !ctx2.tags.is_empty() {
191        let common_tags: Vec<_> = ctx1.tags.iter().filter(|t| ctx2.tags.contains(t)).collect();
192
193        let total_unique_tags = ctx1
194            .tags
195            .iter()
196            .chain(ctx2.tags.iter())
197            .collect::<std::collections::HashSet<_>>()
198            .len();
199
200        if total_unique_tags > 0 {
201            let jaccard = common_tags.len() as f32 / total_unique_tags as f32;
202            score += jaccard * 0.3;
203        }
204    }
205    weight_sum += 0.3;
206
207    if weight_sum > 0.0 {
208        score / weight_sum
209    } else {
210        0.0
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_sequence_similarity() {
220        let seq1 = vec!["a".to_string(), "b".to_string(), "c".to_string()];
221        let seq2 = vec!["a".to_string(), "b".to_string(), "c".to_string()];
222
223        assert_eq!(sequence_similarity(&seq1, &seq2), 1.0);
224
225        let seq3 = vec!["a".to_string(), "b".to_string(), "d".to_string()];
226        let sim = sequence_similarity(&seq1, &seq3);
227        // 2 out of 3 match
228        assert!(sim > 0.6 && sim < 0.7);
229    }
230
231    #[test]
232    fn test_string_similarity() {
233        assert_eq!(string_similarity("hello", "hello"), 1.0);
234        assert_eq!(string_similarity("", ""), 1.0);
235        assert_eq!(string_similarity("abc", ""), 0.0);
236
237        // "hello" vs "hallo" - one character different
238        let sim = string_similarity("hello", "hallo");
239        assert!(sim > 0.7 && sim < 0.9);
240    }
241
242    #[test]
243    fn test_context_similarity() {
244        let ctx1 = TaskContext {
245            domain: "web-api".to_string(),
246            language: Some("rust".to_string()),
247            tags: vec!["async".to_string(), "http".to_string()],
248            ..Default::default()
249        };
250
251        let ctx2 = TaskContext {
252            domain: "web-api".to_string(),
253            language: Some("rust".to_string()),
254            tags: vec!["async".to_string(), "rest".to_string()],
255            ..Default::default()
256        };
257
258        let similarity = context_similarity(&ctx1, &ctx2);
259
260        // Same domain, same language, some tag overlap
261        assert!(similarity > 0.7);
262    }
263}