Skip to main content
Glama

In Memoria

relationships.rs19.8 kB
//! Relationship analysis and learning between semantic concepts #[cfg(feature = "napi-bindings")] use napi_derive::napi; use crate::types::SemanticConcept; use std::collections::HashMap; /// Analyzer for learning and discovering relationships between code concepts #[cfg_attr(feature = "napi-bindings", napi)] pub struct RelationshipLearner; #[cfg_attr(feature = "napi-bindings", napi)] impl RelationshipLearner { #[cfg_attr(feature = "napi-bindings", napi(constructor))] pub fn new() -> Self { RelationshipLearner } /// Learn relationships between concepts and update the relationships map pub fn learn_concept_relationships( concepts: &Vec<SemanticConcept>, relationships: &mut HashMap<String, Vec<String>>, ) { Self::analyze_spatial_relationships(concepts, relationships); Self::analyze_naming_relationships(concepts, relationships); Self::analyze_type_relationships(concepts, relationships); Self::analyze_file_relationships(concepts, relationships); } /// Analyze relationships based on proximity in source code fn analyze_spatial_relationships( concepts: &Vec<SemanticConcept>, relationships: &mut HashMap<String, Vec<String>>, ) { // Group concepts by file for spatial analysis let mut file_concepts: HashMap<String, Vec<&SemanticConcept>> = HashMap::new(); for concept in concepts { file_concepts .entry(concept.file_path.clone()) .or_default() .push(concept); } // Analyze spatial relationships within each file for concepts_in_file in file_concepts.values() { for (i, concept1) in concepts_in_file.iter().enumerate() { for concept2 in concepts_in_file.iter().skip(i + 1) { let distance = Self::calculate_line_distance(concept1, concept2); // Consider concepts close if within 10 lines if distance <= 10 { Self::add_relationship( relationships, &concept1.id, &concept2.id, "spatial_proximity", ); Self::add_relationship( relationships, &concept2.id, &concept1.id, "spatial_proximity", ); } } } } } /// Analyze relationships based on naming patterns fn analyze_naming_relationships( concepts: &Vec<SemanticConcept>, relationships: &mut HashMap<String, Vec<String>>, ) { for concept1 in concepts { for concept2 in concepts { if concept1.id == concept2.id { continue; } let similarity = Self::calculate_name_similarity(&concept1.name, &concept2.name); // Consider names similar if they share significant prefixes/suffixes if similarity > 0.6 { Self::add_relationship( relationships, &concept1.id, &concept2.id, "naming_similarity", ); } // Check for common naming patterns if Self::has_naming_relationship(&concept1.name, &concept2.name) { Self::add_relationship( relationships, &concept1.id, &concept2.id, "naming_pattern", ); } } } } /// Analyze relationships based on concept types fn analyze_type_relationships( concepts: &Vec<SemanticConcept>, relationships: &mut HashMap<String, Vec<String>>, ) { // Group concepts by type let mut type_groups: HashMap<String, Vec<&SemanticConcept>> = HashMap::new(); for concept in concepts { type_groups .entry(concept.concept_type.clone()) .or_default() .push(concept); } // Analyze relationships within type groups for (_concept_type, group_concepts) in type_groups { for (i, concept1) in group_concepts.iter().enumerate() { for concept2 in group_concepts.iter().skip(i + 1) { Self::add_relationship( relationships, &concept1.id, &concept2.id, "same_type", ); Self::add_relationship( relationships, &concept2.id, &concept1.id, "same_type", ); } } } // Analyze cross-type relationships (e.g., functions in classes) Self::analyze_cross_type_relationships(concepts, relationships); } /// Analyze relationships between different concept types fn analyze_cross_type_relationships( concepts: &Vec<SemanticConcept>, relationships: &mut HashMap<String, Vec<String>>, ) { for concept in concepts { match concept.concept_type.as_str() { "function" | "method" => { // Look for classes or interfaces this function might belong to for other_concept in concepts { if (other_concept.concept_type == "class" || other_concept.concept_type == "interface") && Self::is_function_in_class(concept, other_concept) { Self::add_relationship( relationships, &concept.id, &other_concept.id, "member_of", ); Self::add_relationship( relationships, &other_concept.id, &concept.id, "contains", ); } } } "variable" | "field" => { // Look for functions or classes this variable might belong to for other_concept in concepts { if (other_concept.concept_type == "function" || other_concept.concept_type == "class") && Self::is_variable_in_scope(concept, other_concept) { Self::add_relationship( relationships, &concept.id, &other_concept.id, "scoped_in", ); } } } _ => {} } } } /// Analyze relationships based on file organization fn analyze_file_relationships( concepts: &Vec<SemanticConcept>, relationships: &mut HashMap<String, Vec<String>>, ) { // Group concepts by file let mut file_groups: HashMap<String, Vec<&SemanticConcept>> = HashMap::new(); for concept in concepts { file_groups .entry(concept.file_path.clone()) .or_default() .push(concept); } // Analyze relationships within files for concepts_in_file in file_groups.values() { for (i, concept1) in concepts_in_file.iter().enumerate() { for concept2 in concepts_in_file.iter().skip(i + 1) { Self::add_relationship( relationships, &concept1.id, &concept2.id, "same_file", ); } } } // Analyze cross-file relationships based on import/export patterns Self::analyze_import_relationships(&file_groups, relationships); } /// Analyze import/export relationships between files fn analyze_import_relationships( file_groups: &HashMap<String, Vec<&SemanticConcept>>, relationships: &mut HashMap<String, Vec<String>>, ) { // This is a simplified analysis - in practice, you'd parse import statements for (file1, concepts1) in file_groups { for (file2, concepts2) in file_groups { if file1 == file2 { continue; } // Look for potential import relationships based on naming for concept1 in concepts1 { for concept2 in concepts2 { if (concept1.concept_type == "class" || concept1.concept_type == "interface") && concept2.metadata.contains_key("imports") { if let Some(imports) = concept2.metadata.get("imports") { if imports.contains(&concept1.name) { Self::add_relationship( relationships, &concept2.id, &concept1.id, "imports", ); Self::add_relationship( relationships, &concept1.id, &concept2.id, "imported_by", ); } } } } } } } } /// Calculate distance between concepts in lines fn calculate_line_distance(concept1: &SemanticConcept, concept2: &SemanticConcept) -> u32 { if concept1.file_path != concept2.file_path { return u32::MAX; // Different files have infinite distance } let start1 = concept1.line_range.start; let end1 = concept1.line_range.end; let start2 = concept2.line_range.start; let end2 = concept2.line_range.end; if end1 < start2 { start2 - end1 } else if end2 < start1 { start1.saturating_sub(end2) } else { 0 // Overlapping ranges } } /// Calculate similarity between two names fn calculate_name_similarity(name1: &str, name2: &str) -> f64 { if name1 == name2 { return 1.0; } let len1 = name1.len(); let len2 = name2.len(); let max_len = len1.max(len2); if max_len == 0 { return 1.0; } // Simple longest common subsequence similarity let common = Self::longest_common_subsequence(name1, name2); common as f64 / max_len as f64 } /// Calculate longest common subsequence length fn longest_common_subsequence(s1: &str, s2: &str) -> usize { let chars1: Vec<char> = s1.chars().collect(); let chars2: Vec<char> = s2.chars().collect(); let len1 = chars1.len(); let len2 = chars2.len(); let mut dp = vec![vec![0; len2 + 1]; len1 + 1]; for i in 1..=len1 { for j in 1..=len2 { if chars1[i - 1] == chars2[j - 1] { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]); } } } dp[len1][len2] } /// Check if two names have a naming relationship (e.g., getter/setter) fn has_naming_relationship(name1: &str, name2: &str) -> bool { let name1_lower = name1.to_lowercase(); let name2_lower = name2.to_lowercase(); // Check for getter/setter patterns if name1_lower.starts_with("get") && name2_lower.starts_with("set") { let suffix1 = &name1_lower[3..]; let suffix2 = &name2_lower[3..]; return suffix1 == suffix2; } // Check for test/implementation patterns if name1_lower.contains("test") || name2_lower.contains("test") { let clean1 = name1_lower.replace("test", ""); let clean2 = name2_lower.replace("test", ""); return clean1 == clean2 || clean1.is_empty() || clean2.is_empty(); } false } /// Check if a function is likely a member of a class fn is_function_in_class(function: &SemanticConcept, class: &SemanticConcept) -> bool { // Same file check if function.file_path != class.file_path { return false; } // Check if function is within class line range (with some tolerance) function.line_range.start >= class.line_range.start && function.line_range.end <= class.line_range.end + 5 } /// Check if a variable is in scope of another concept fn is_variable_in_scope(variable: &SemanticConcept, scope: &SemanticConcept) -> bool { // Same file check if variable.file_path != scope.file_path { return false; } // Check if variable is within scope line range variable.line_range.start >= scope.line_range.start && variable.line_range.end <= scope.line_range.end } /// Add a relationship to the relationships map fn add_relationship( relationships: &mut HashMap<String, Vec<String>>, from_id: &str, to_id: &str, relationship_type: &str, ) { let entry = relationships.entry(from_id.to_string()).or_default(); let relationship = format!("{}:{}", relationship_type, to_id); if !entry.contains(&relationship) { entry.push(relationship); } } } impl Default for RelationshipLearner { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use crate::types::LineRange; fn create_test_concept( id: &str, name: &str, concept_type: &str, file_path: &str, start: u32, end: u32, ) -> SemanticConcept { SemanticConcept { id: id.to_string(), name: name.to_string(), concept_type: concept_type.to_string(), confidence: 0.8, file_path: file_path.to_string(), line_range: LineRange { start, end }, relationships: HashMap::new(), metadata: HashMap::new(), } } #[test] fn test_relationship_learner_creation() { let _learner = RelationshipLearner::new(); assert!(true); // Constructor should work } #[test] fn test_spatial_relationships() { let concepts = vec![ create_test_concept("1", "func1", "function", "test.rs", 1, 5), create_test_concept("2", "func2", "function", "test.rs", 8, 12), create_test_concept("3", "func3", "function", "test.rs", 50, 60), ]; let mut relationships = HashMap::new(); RelationshipLearner::learn_concept_relationships(&concepts, &mut relationships); // func1 and func2 should be spatially related (within 10 lines) assert!(relationships.contains_key("1")); assert!(relationships.contains_key("2")); let func1_rels = relationships.get("1").unwrap(); assert!(func1_rels.iter().any(|r| r.contains("spatial_proximity:2"))); } #[test] fn test_naming_relationships() { let concepts = vec![ create_test_concept("1", "getUserName", "function", "test.rs", 1, 5), create_test_concept("2", "setUserName", "function", "test.rs", 10, 15), create_test_concept("3", "getData", "function", "test.rs", 20, 25), ]; let mut relationships = HashMap::new(); RelationshipLearner::learn_concept_relationships(&concepts, &mut relationships); // getUserName and setUserName should have naming relationship let get_rels = relationships.get("1").unwrap(); assert!(get_rels.iter().any(|r| r.contains("naming_pattern:2"))); } #[test] fn test_type_relationships() { let concepts = vec![ create_test_concept("1", "func1", "function", "test.rs", 1, 5), create_test_concept("2", "func2", "function", "test.rs", 10, 15), create_test_concept("3", "Class1", "class", "test.rs", 20, 30), ]; let mut relationships = HashMap::new(); RelationshipLearner::learn_concept_relationships(&concepts, &mut relationships); // Functions should be related by same type let func1_rels = relationships.get("1").unwrap(); assert!(func1_rels.iter().any(|r| r.contains("same_type:2"))); } #[test] fn test_cross_type_relationships() { let concepts = vec![ create_test_concept("1", "TestClass", "class", "test.rs", 1, 30), create_test_concept("2", "method1", "function", "test.rs", 5, 10), create_test_concept("3", "field1", "variable", "test.rs", 15, 15), ]; let mut relationships = HashMap::new(); RelationshipLearner::learn_concept_relationships(&concepts, &mut relationships); // Method should be member of class let method_rels = relationships.get("2").unwrap(); assert!(method_rels.iter().any(|r| r.contains("member_of:1"))); // Class should contain method let class_rels = relationships.get("1").unwrap(); assert!(class_rels.iter().any(|r| r.contains("contains:2"))); } #[test] fn test_calculate_line_distance() { let concept1 = create_test_concept("1", "func1", "function", "test.rs", 1, 5); let concept2 = create_test_concept("2", "func2", "function", "test.rs", 10, 15); let concept3 = create_test_concept("3", "func3", "function", "other.rs", 1, 5); assert_eq!(RelationshipLearner::calculate_line_distance(&concept1, &concept2), 5); assert_eq!(RelationshipLearner::calculate_line_distance(&concept1, &concept3), u32::MAX); } #[test] fn test_name_similarity() { assert_eq!(RelationshipLearner::calculate_name_similarity("test", "test"), 1.0); assert!(RelationshipLearner::calculate_name_similarity("test", "testing") > 0.7); assert!(RelationshipLearner::calculate_name_similarity("abc", "xyz") < 0.3); } #[test] fn test_naming_patterns() { assert!(RelationshipLearner::has_naming_relationship("getName", "setName")); assert!(RelationshipLearner::has_naming_relationship("testFunction", "function")); assert!(!RelationshipLearner::has_naming_relationship("foo", "bar")); } #[test] fn test_function_in_class_detection() { let class = create_test_concept("1", "TestClass", "class", "test.rs", 1, 30); let method_inside = create_test_concept("2", "method1", "function", "test.rs", 5, 10); let method_outside = create_test_concept("3", "method2", "function", "test.rs", 35, 40); assert!(RelationshipLearner::is_function_in_class(&method_inside, &class)); assert!(!RelationshipLearner::is_function_in_class(&method_outside, &class)); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pi22by7/In-Memoria'

If you have feedback or need assistance with the MCP directory API, please join our Discord server