Skip to main content

subcog/services/
graph.rs

1//! Graph service for high-level knowledge graph operations.
2//!
3//! Provides a service layer wrapping [`GraphBackend`] with business logic,
4//! entity deduplication, and relationship inference.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use subcog::services::GraphService;
10//! use subcog::storage::graph::SqliteGraphBackend;
11//! use subcog::models::graph::{Entity, EntityType, EntityQuery};
12//!
13//! let backend = SqliteGraphBackend::new("graph.db")?;
14//! let service = GraphService::new(backend);
15//!
16//! // Store an entity
17//! let entity = Entity::new(EntityType::Person, "Alice", Domain::for_user());
18//! service.store_entity(&entity)?;
19//!
20//! // Query entities
21//! let people = service.find_by_type(EntityType::Person, 10)?;
22//! ```
23
24use crate::models::graph::{
25    Entity, EntityId, EntityMention, EntityQuery, EntityType, Relationship, RelationshipQuery,
26    RelationshipType, TraversalResult,
27};
28use crate::models::temporal::BitemporalPoint;
29use crate::models::{Domain, MemoryId};
30use crate::storage::traits::graph::{GraphBackend, GraphStats};
31use crate::{Error, Result};
32use std::sync::Arc;
33
34/// High-level service for knowledge graph operations.
35///
36/// Wraps a [`GraphBackend`] and provides:
37/// - Entity CRUD with deduplication hints
38/// - Relationship management
39/// - Graph traversal and path finding
40/// - Integration with memory system
41///
42/// # Thread Safety
43///
44/// The service is thread-safe when the underlying backend is thread-safe.
45/// Both [`SqliteGraphBackend`](crate::storage::graph::SqliteGraphBackend) and
46/// [`InMemoryGraphBackend`](crate::storage::graph::InMemoryGraphBackend) are thread-safe.
47pub struct GraphService<B: GraphBackend> {
48    backend: Arc<B>,
49}
50
51impl<B: GraphBackend> GraphService<B> {
52    /// Creates a new graph service with the given backend.
53    pub fn new(backend: B) -> Self {
54        Self {
55            backend: Arc::new(backend),
56        }
57    }
58
59    /// Creates a new graph service with a shared backend.
60    #[must_use]
61    pub const fn with_shared_backend(backend: Arc<B>) -> Self {
62        Self { backend }
63    }
64
65    /// Returns a reference to the underlying backend.
66    #[must_use]
67    pub fn backend(&self) -> &B {
68        &self.backend
69    }
70
71    // =========================================================================
72    // Entity Operations
73    // =========================================================================
74
75    /// Stores an entity in the graph.
76    ///
77    /// If an entity with the same ID exists, it will be updated.
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if the storage operation fails.
82    pub fn store_entity(&self, entity: &Entity) -> Result<()> {
83        self.backend.store_entity(entity)
84    }
85
86    /// Stores an entity with automatic deduplication.
87    ///
88    /// Checks for an existing entity with the same name and type (case-insensitive).
89    /// If found:
90    /// - Updates confidence if the new entity has higher confidence
91    /// - Merges aliases from both entities
92    /// - Returns the existing entity's ID
93    ///
94    /// If not found, stores the new entity and returns its ID.
95    ///
96    /// # Arguments
97    ///
98    /// * `entity` - The entity to store or merge
99    ///
100    /// # Returns
101    ///
102    /// The ID of the stored or existing entity.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if the storage or lookup operation fails.
107    pub fn store_entity_deduped(&self, entity: &Entity) -> Result<EntityId> {
108        // Look for existing entity with exact name+type match (case-insensitive)
109        let existing = self.backend.find_entities_by_name(
110            &entity.name,
111            Some(entity.entity_type),
112            Some(&entity.domain),
113            10, // Small limit since we're looking for exact matches
114        )?;
115
116        // Find exact case-insensitive match
117        let name_lower = entity.name.to_lowercase();
118        let exact_match = existing
119            .into_iter()
120            .find(|e| e.name.to_lowercase() == name_lower);
121
122        if let Some(mut existing_entity) = exact_match {
123            // Update confidence if new extraction has higher confidence
124            if entity.confidence > existing_entity.confidence {
125                existing_entity.confidence = entity.confidence;
126            }
127
128            // Merge aliases (add new aliases that don't already exist)
129            // Build set of existing aliases (lowercased) for efficient lookup
130            let existing_lower: std::collections::HashSet<String> = existing_entity
131                .aliases
132                .iter()
133                .map(|a| a.to_lowercase())
134                .chain(std::iter::once(existing_entity.name.to_lowercase()))
135                .collect();
136
137            let new_aliases: Vec<String> = entity
138                .aliases
139                .iter()
140                .filter(|alias| !existing_lower.contains(&alias.to_lowercase()))
141                .cloned()
142                .collect();
143            existing_entity.aliases.extend(new_aliases);
144
145            // Increment mention count
146            existing_entity.mention_count = existing_entity.mention_count.saturating_add(1);
147
148            // Store the updated entity
149            self.backend.store_entity(&existing_entity)?;
150
151            Ok(existing_entity.id)
152        } else {
153            // No duplicate found, store the new entity
154            self.backend.store_entity(entity)?;
155            Ok(entity.id.clone())
156        }
157    }
158
159    /// Retrieves an entity by ID.
160    ///
161    /// # Returns
162    ///
163    /// `Some(entity)` if found, `None` otherwise.
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if the retrieval fails.
168    pub fn get_entity(&self, id: &EntityId) -> Result<Option<Entity>> {
169        self.backend.get_entity(id)
170    }
171
172    /// Queries entities matching the given criteria.
173    ///
174    /// # Errors
175    ///
176    /// Returns an error if the query fails.
177    pub fn query_entities(&self, query: &EntityQuery) -> Result<Vec<Entity>> {
178        self.backend.query_entities(query)
179    }
180
181    /// Finds entities by type with a limit.
182    ///
183    /// Convenience method for common entity type queries.
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if the query fails.
188    pub fn find_by_type(&self, entity_type: EntityType, limit: usize) -> Result<Vec<Entity>> {
189        let query = EntityQuery::new().with_type(entity_type).with_limit(limit);
190        self.backend.query_entities(&query)
191    }
192
193    /// Finds entities by name with optional type and domain filtering.
194    ///
195    /// Performs case-insensitive partial matching on entity names and aliases.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if the search fails.
200    pub fn find_by_name(
201        &self,
202        name: &str,
203        entity_type: Option<EntityType>,
204        domain: Option<&Domain>,
205        limit: usize,
206    ) -> Result<Vec<Entity>> {
207        self.backend
208            .find_entities_by_name(name, entity_type, domain, limit)
209    }
210
211    /// Deletes an entity and its relationships/mentions.
212    ///
213    /// # Returns
214    ///
215    /// `true` if the entity existed and was deleted.
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if the deletion fails.
220    pub fn delete_entity(&self, id: &EntityId) -> Result<bool> {
221        self.backend.delete_entity(id)
222    }
223
224    /// Merges multiple entities into one canonical entity.
225    ///
226    /// The first entity ID becomes the canonical entity. All relationships
227    /// and mentions from other entities are redirected to the canonical entity.
228    ///
229    /// # Arguments
230    ///
231    /// * `entity_ids` - IDs of entities to merge (first is canonical)
232    /// * `canonical_name` - The name for the merged entity
233    ///
234    /// # Returns
235    ///
236    /// The merged entity.
237    ///
238    /// # Errors
239    ///
240    /// Returns an error if:
241    /// - No entity IDs provided
242    /// - Canonical entity not found
243    /// - Merge operation fails
244    pub fn merge_entities(&self, entity_ids: &[EntityId], canonical_name: &str) -> Result<Entity> {
245        if entity_ids.is_empty() {
246            return Err(Error::OperationFailed {
247                operation: "merge_entities".to_string(),
248                cause: "No entity IDs provided".to_string(),
249            });
250        }
251
252        self.backend.merge_entities(entity_ids, canonical_name)
253    }
254
255    /// Finds potential duplicate entities based on name similarity.
256    ///
257    /// Returns entities that may be duplicates of the given entity,
258    /// useful for deduplication workflows.
259    ///
260    /// # Errors
261    ///
262    /// Returns an error if the search fails.
263    pub fn find_duplicates(&self, entity: &Entity, threshold: f32) -> Result<Vec<Entity>> {
264        // Find entities with similar names
265        let candidates = self.backend.find_entities_by_name(
266            &entity.name,
267            Some(entity.entity_type),
268            Some(&entity.domain),
269            20,
270        )?;
271
272        // Filter by confidence threshold (simple heuristic)
273        let duplicates: Vec<Entity> = candidates
274            .into_iter()
275            .filter(|e| e.id != entity.id && name_similarity(&e.name, &entity.name) >= threshold)
276            .collect();
277
278        Ok(duplicates)
279    }
280
281    // =========================================================================
282    // Relationship Operations
283    // =========================================================================
284
285    /// Stores a relationship between entities.
286    ///
287    /// If a relationship with the same (from, to, type) exists, it will be updated.
288    ///
289    /// # Errors
290    ///
291    /// Returns an error if the storage operation fails.
292    pub fn store_relationship(&self, relationship: &Relationship) -> Result<()> {
293        self.backend.store_relationship(relationship)
294    }
295
296    /// Creates a relationship between two entities.
297    ///
298    /// Convenience method that creates and stores a relationship.
299    ///
300    /// # Errors
301    ///
302    /// Returns an error if:
303    /// - Either entity doesn't exist
304    /// - Storage operation fails
305    pub fn relate(
306        &self,
307        from: &EntityId,
308        to: &EntityId,
309        relationship_type: RelationshipType,
310    ) -> Result<Relationship> {
311        // Verify entities exist
312        if self.backend.get_entity(from)?.is_none() {
313            return Err(Error::OperationFailed {
314                operation: "relate".to_string(),
315                cause: format!("From entity not found: {}", from.as_str()),
316            });
317        }
318        if self.backend.get_entity(to)?.is_none() {
319            return Err(Error::OperationFailed {
320                operation: "relate".to_string(),
321                cause: format!("To entity not found: {}", to.as_str()),
322            });
323        }
324
325        let relationship = Relationship::new(from.clone(), to.clone(), relationship_type);
326        self.backend.store_relationship(&relationship)?;
327        Ok(relationship)
328    }
329
330    /// Queries relationships matching the given criteria.
331    ///
332    /// # Errors
333    ///
334    /// Returns an error if the query fails.
335    pub fn query_relationships(&self, query: &RelationshipQuery) -> Result<Vec<Relationship>> {
336        self.backend.query_relationships(query)
337    }
338
339    /// Gets all relationships from an entity.
340    ///
341    /// # Errors
342    ///
343    /// Returns an error if the query fails.
344    pub fn get_outgoing_relationships(&self, entity_id: &EntityId) -> Result<Vec<Relationship>> {
345        let query = RelationshipQuery::new().from(entity_id.clone());
346        self.backend.query_relationships(&query)
347    }
348
349    /// Gets all relationships to an entity.
350    ///
351    /// # Errors
352    ///
353    /// Returns an error if the query fails.
354    pub fn get_incoming_relationships(&self, entity_id: &EntityId) -> Result<Vec<Relationship>> {
355        let query = RelationshipQuery::new().to(entity_id.clone());
356        self.backend.query_relationships(&query)
357    }
358
359    /// Deletes relationships matching the query.
360    ///
361    /// # Returns
362    ///
363    /// Number of relationships deleted.
364    ///
365    /// # Errors
366    ///
367    /// Returns an error if the deletion fails.
368    pub fn delete_relationships(&self, query: &RelationshipQuery) -> Result<usize> {
369        self.backend.delete_relationships(query)
370    }
371
372    /// Gets all relationship types between two entities.
373    ///
374    /// # Errors
375    ///
376    /// Returns an error if the query fails.
377    pub fn get_relationship_types(
378        &self,
379        from: &EntityId,
380        to: &EntityId,
381    ) -> Result<Vec<RelationshipType>> {
382        self.backend.get_relationship_types(from, to)
383    }
384
385    // =========================================================================
386    // Mention Operations
387    // =========================================================================
388
389    /// Records an entity mention in a memory.
390    ///
391    /// # Errors
392    ///
393    /// Returns an error if the storage operation fails.
394    pub fn record_mention(&self, entity_id: &EntityId, memory_id: &MemoryId) -> Result<()> {
395        let mention = EntityMention::new(entity_id.clone(), memory_id.clone());
396        self.backend.store_mention(&mention)
397    }
398
399    /// Gets all mentions of an entity.
400    ///
401    /// # Errors
402    ///
403    /// Returns an error if the query fails.
404    pub fn get_mentions(&self, entity_id: &EntityId) -> Result<Vec<EntityMention>> {
405        self.backend.get_mentions_for_entity(entity_id)
406    }
407
408    /// Gets all entities mentioned in a memory.
409    ///
410    /// # Errors
411    ///
412    /// Returns an error if the query fails.
413    pub fn get_entities_in_memory(&self, memory_id: &MemoryId) -> Result<Vec<Entity>> {
414        self.backend.get_entities_in_memory(memory_id)
415    }
416
417    /// Removes entity mentions when a memory is deleted.
418    ///
419    /// # Returns
420    ///
421    /// Number of mentions removed.
422    ///
423    /// # Errors
424    ///
425    /// Returns an error if the deletion fails.
426    pub fn remove_mentions_for_memory(&self, memory_id: &MemoryId) -> Result<usize> {
427        self.backend.delete_mentions_for_memory(memory_id)
428    }
429
430    // =========================================================================
431    // Graph Traversal
432    // =========================================================================
433
434    /// Traverses the graph from a starting entity.
435    ///
436    /// Performs breadth-first search up to the specified depth.
437    ///
438    /// # Arguments
439    ///
440    /// * `start` - Starting entity ID
441    /// * `max_depth` - Maximum traversal depth
442    /// * `relationship_types` - Optional filter for relationship types
443    /// * `min_confidence` - Optional minimum confidence threshold
444    ///
445    /// # Returns
446    ///
447    /// Entities and relationships discovered during traversal.
448    ///
449    /// # Errors
450    ///
451    /// Returns an error if the traversal fails.
452    pub fn traverse(
453        &self,
454        start: &EntityId,
455        max_depth: u32,
456        relationship_types: Option<&[RelationshipType]>,
457        min_confidence: Option<f32>,
458    ) -> Result<TraversalResult> {
459        self.backend
460            .traverse(start, max_depth, relationship_types, min_confidence)
461    }
462
463    /// Finds the shortest path between two entities.
464    ///
465    /// # Arguments
466    ///
467    /// * `from` - Starting entity ID
468    /// * `to` - Target entity ID
469    /// * `max_depth` - Maximum path length
470    ///
471    /// # Returns
472    ///
473    /// `Some(result)` with the path if found, `None` otherwise.
474    ///
475    /// # Errors
476    ///
477    /// Returns an error if the search fails.
478    pub fn find_path(
479        &self,
480        from: &EntityId,
481        to: &EntityId,
482        max_depth: u32,
483    ) -> Result<Option<TraversalResult>> {
484        self.backend.find_path(from, to, max_depth)
485    }
486
487    /// Gets neighbors of an entity within a given depth.
488    ///
489    /// Convenience method for single-depth traversal.
490    ///
491    /// # Errors
492    ///
493    /// Returns an error if the traversal fails.
494    pub fn get_neighbors(&self, entity_id: &EntityId, depth: u32) -> Result<Vec<Entity>> {
495        let result = self.backend.traverse(entity_id, depth, None, None)?;
496        // Exclude the starting entity
497        Ok(result
498            .entities
499            .into_iter()
500            .filter(|e| e.id != *entity_id)
501            .collect())
502    }
503
504    // =========================================================================
505    // Temporal Queries
506    // =========================================================================
507
508    /// Queries entities at a specific point in time.
509    ///
510    /// Uses bitemporal filtering to find entities that were:
511    /// - Valid at the specified time (`valid_at`)
512    /// - Known in the system at the specified time (`as_of`)
513    ///
514    /// # Errors
515    ///
516    /// Returns an error if the query fails.
517    pub fn query_entities_at(
518        &self,
519        query: &EntityQuery,
520        point: &BitemporalPoint,
521    ) -> Result<Vec<Entity>> {
522        self.backend.query_entities_at(query, point)
523    }
524
525    /// Queries relationships at a specific point in time.
526    ///
527    /// # Errors
528    ///
529    /// Returns an error if the query fails.
530    pub fn query_relationships_at(
531        &self,
532        query: &RelationshipQuery,
533        point: &BitemporalPoint,
534    ) -> Result<Vec<Relationship>> {
535        self.backend.query_relationships_at(query, point)
536    }
537
538    /// Closes an entity's valid time (marks it as no longer valid).
539    ///
540    /// Used when an entity is superseded or becomes invalid.
541    ///
542    /// # Errors
543    ///
544    /// Returns an error if the entity doesn't exist or update fails.
545    pub fn close_entity_valid_time(&self, id: &EntityId, end_time: i64) -> Result<()> {
546        self.backend.close_entity_valid_time(id, end_time)
547    }
548
549    /// Closes a relationship's valid time.
550    ///
551    /// # Errors
552    ///
553    /// Returns an error if the relationship doesn't exist or update fails.
554    pub fn close_relationship_valid_time(
555        &self,
556        from: &EntityId,
557        to: &EntityId,
558        relationship_type: RelationshipType,
559        end_time: i64,
560    ) -> Result<()> {
561        self.backend
562            .close_relationship_valid_time(from, to, relationship_type, end_time)
563    }
564
565    // =========================================================================
566    // Statistics
567    // =========================================================================
568
569    /// Gets graph statistics.
570    ///
571    /// # Errors
572    ///
573    /// Returns an error if the statistics cannot be retrieved.
574    pub fn get_stats(&self) -> Result<GraphStats> {
575        self.backend.get_stats()
576    }
577
578    /// Clears all graph data.
579    ///
580    /// **Warning**: This permanently deletes all entities, relationships, and mentions.
581    ///
582    /// # Errors
583    ///
584    /// Returns an error if the clear operation fails.
585    pub fn clear(&self) -> Result<()> {
586        self.backend.clear()
587    }
588}
589
590/// Simple name similarity using Jaccard index on character bigrams.
591fn name_similarity(a: &str, b: &str) -> f32 {
592    let a_lower = a.to_lowercase();
593    let b_lower = b.to_lowercase();
594
595    if a_lower == b_lower {
596        return 1.0;
597    }
598
599    let a_bigrams: std::collections::HashSet<_> = a_lower
600        .chars()
601        .collect::<Vec<_>>()
602        .windows(2)
603        .map(|w| (w[0], w[1]))
604        .collect();
605
606    let b_bigrams: std::collections::HashSet<_> = b_lower
607        .chars()
608        .collect::<Vec<_>>()
609        .windows(2)
610        .map(|w| (w[0], w[1]))
611        .collect();
612
613    if a_bigrams.is_empty() || b_bigrams.is_empty() {
614        return 0.0;
615    }
616
617    let intersection = a_bigrams.intersection(&b_bigrams).count();
618    let union = a_bigrams.union(&b_bigrams).count();
619
620    if union == 0 {
621        0.0
622    } else {
623        intersection as f32 / union as f32
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630    use crate::storage::graph::InMemoryGraphBackend;
631
632    fn create_service() -> GraphService<InMemoryGraphBackend> {
633        GraphService::new(InMemoryGraphBackend::new())
634    }
635
636    fn create_entity(name: &str, entity_type: EntityType) -> Entity {
637        Entity::new(entity_type, name, Domain::for_user())
638    }
639
640    #[test]
641    fn test_store_and_get_entity() {
642        let service = create_service();
643        let entity = create_entity("Alice", EntityType::Person);
644
645        service.store_entity(&entity).unwrap();
646        let retrieved = service.get_entity(&entity.id).unwrap();
647
648        assert!(retrieved.is_some());
649        assert_eq!(retrieved.unwrap().name, "Alice");
650    }
651
652    #[test]
653    fn test_find_by_type() {
654        let service = create_service();
655
656        service
657            .store_entity(&create_entity("Alice", EntityType::Person))
658            .unwrap();
659        service
660            .store_entity(&create_entity("Bob", EntityType::Person))
661            .unwrap();
662        service
663            .store_entity(&create_entity("Acme", EntityType::Organization))
664            .unwrap();
665
666        let people = service.find_by_type(EntityType::Person, 10).unwrap();
667        assert_eq!(people.len(), 2);
668
669        let orgs = service.find_by_type(EntityType::Organization, 10).unwrap();
670        assert_eq!(orgs.len(), 1);
671    }
672
673    #[test]
674    fn test_relate_entities() {
675        let service = create_service();
676
677        let alice = create_entity("Alice", EntityType::Person);
678        let acme = create_entity("Acme", EntityType::Organization);
679
680        service.store_entity(&alice).unwrap();
681        service.store_entity(&acme).unwrap();
682
683        let rel = service
684            .relate(&alice.id, &acme.id, RelationshipType::WorksAt)
685            .unwrap();
686
687        assert_eq!(rel.from_entity, alice.id);
688        assert_eq!(rel.to_entity, acme.id);
689        assert_eq!(rel.relationship_type, RelationshipType::WorksAt);
690    }
691
692    #[test]
693    fn test_relate_nonexistent_entity() {
694        let service = create_service();
695
696        let alice = create_entity("Alice", EntityType::Person);
697        service.store_entity(&alice).unwrap();
698
699        let fake_id = EntityId::generate();
700        let result = service.relate(&alice.id, &fake_id, RelationshipType::WorksAt);
701
702        assert!(result.is_err());
703    }
704
705    #[test]
706    fn test_get_neighbors() {
707        let service = create_service();
708
709        let alice = create_entity("Alice", EntityType::Person);
710        let bob = create_entity("Bob", EntityType::Person);
711        let acme = create_entity("Acme", EntityType::Organization);
712
713        service.store_entity(&alice).unwrap();
714        service.store_entity(&bob).unwrap();
715        service.store_entity(&acme).unwrap();
716
717        service
718            .relate(&alice.id, &bob.id, RelationshipType::RelatesTo)
719            .unwrap();
720        service
721            .relate(&alice.id, &acme.id, RelationshipType::WorksAt)
722            .unwrap();
723
724        let neighbors = service.get_neighbors(&alice.id, 1).unwrap();
725        assert_eq!(neighbors.len(), 2);
726    }
727
728    #[test]
729    fn test_find_path() {
730        let service = create_service();
731
732        let a = create_entity("A", EntityType::Concept);
733        let b = create_entity("B", EntityType::Concept);
734        let c = create_entity("C", EntityType::Concept);
735
736        service.store_entity(&a).unwrap();
737        service.store_entity(&b).unwrap();
738        service.store_entity(&c).unwrap();
739
740        service
741            .relate(&a.id, &b.id, RelationshipType::RelatesTo)
742            .unwrap();
743        service
744            .relate(&b.id, &c.id, RelationshipType::RelatesTo)
745            .unwrap();
746
747        let path = service.find_path(&a.id, &c.id, 5).unwrap();
748        assert!(path.is_some());
749        assert_eq!(path.unwrap().entities.len(), 3);
750    }
751
752    #[test]
753    fn test_record_and_get_mentions() {
754        let service = create_service();
755
756        let alice = create_entity("Alice", EntityType::Person);
757        service.store_entity(&alice).unwrap();
758
759        let mem1 = MemoryId::new("mem_1");
760        let mem2 = MemoryId::new("mem_2");
761
762        service.record_mention(&alice.id, &mem1).unwrap();
763        service.record_mention(&alice.id, &mem2).unwrap();
764
765        let mentions = service.get_mentions(&alice.id).unwrap();
766        assert_eq!(mentions.len(), 2);
767    }
768
769    #[test]
770    fn test_get_stats() {
771        let service = create_service();
772
773        service
774            .store_entity(&create_entity("Alice", EntityType::Person))
775            .unwrap();
776        service
777            .store_entity(&create_entity("Bob", EntityType::Person))
778            .unwrap();
779
780        let stats = service.get_stats().unwrap();
781        assert_eq!(stats.entity_count, 2);
782    }
783
784    #[test]
785    fn test_name_similarity() {
786        assert!((name_similarity("Alice", "Alice") - 1.0).abs() < f32::EPSILON);
787        assert!((name_similarity("alice", "ALICE") - 1.0).abs() < f32::EPSILON);
788        // "Alice" vs "Alicia" share 3 bigrams out of 6 unique = 0.5 Jaccard
789        assert!(name_similarity("Alice", "Alicia") >= 0.5);
790        assert!(name_similarity("Alice", "Bob") < 0.3);
791    }
792
793    #[test]
794    fn test_find_duplicates() {
795        let service = create_service();
796
797        let alice1 = create_entity("Alice Smith", EntityType::Person);
798        let alice2 = create_entity("Alice Smithson", EntityType::Person);
799        let bob = create_entity("Bob Jones", EntityType::Person);
800
801        service.store_entity(&alice1).unwrap();
802        service.store_entity(&alice2).unwrap();
803        service.store_entity(&bob).unwrap();
804
805        let duplicates = service.find_duplicates(&alice1, 0.5).unwrap();
806        assert_eq!(duplicates.len(), 1);
807        assert_eq!(duplicates[0].name, "Alice Smithson");
808    }
809
810    #[test]
811    fn test_store_entity_upsert() {
812        let service = create_service();
813        let mut entity = create_entity("Alice", EntityType::Person);
814        service.store_entity(&entity).unwrap();
815
816        // Update and store again (upsert behavior)
817        entity.aliases = vec!["Ali".to_string()];
818        entity.confidence = 0.95;
819        service.store_entity(&entity).unwrap();
820
821        let retrieved = service.get_entity(&entity.id).unwrap().unwrap();
822        assert_eq!(retrieved.aliases, vec!["Ali".to_string()]);
823        assert!((retrieved.confidence - 0.95).abs() < f32::EPSILON);
824    }
825
826    #[test]
827    fn test_delete_entity() {
828        let service = create_service();
829        let entity = create_entity("Alice", EntityType::Person);
830        service.store_entity(&entity).unwrap();
831
832        service.delete_entity(&entity.id).unwrap();
833        let retrieved = service.get_entity(&entity.id).unwrap();
834        assert!(retrieved.is_none());
835    }
836
837    #[test]
838    fn test_relationship_with_properties() {
839        let service = create_service();
840
841        let rust = create_entity("Rust", EntityType::Technology);
842        let cargo = create_entity("Cargo", EntityType::Technology);
843
844        service.store_entity(&rust).unwrap();
845        service.store_entity(&cargo).unwrap();
846
847        let rel = service
848            .relate(&rust.id, &cargo.id, RelationshipType::Uses)
849            .unwrap();
850
851        // Relationship should have the correct type
852        assert_eq!(rel.relationship_type, RelationshipType::Uses);
853        assert_eq!(rel.from_entity, rust.id);
854        assert_eq!(rel.to_entity, cargo.id);
855    }
856
857    #[test]
858    fn test_get_outgoing_relationships() {
859        let service = create_service();
860
861        let alice = create_entity("Alice", EntityType::Person);
862        let bob = create_entity("Bob", EntityType::Person);
863        let acme = create_entity("Acme", EntityType::Organization);
864
865        service.store_entity(&alice).unwrap();
866        service.store_entity(&bob).unwrap();
867        service.store_entity(&acme).unwrap();
868
869        service
870            .relate(&alice.id, &bob.id, RelationshipType::RelatesTo)
871            .unwrap();
872        service
873            .relate(&alice.id, &acme.id, RelationshipType::WorksAt)
874            .unwrap();
875
876        let rels = service.get_outgoing_relationships(&alice.id).unwrap();
877        assert_eq!(rels.len(), 2);
878    }
879
880    #[test]
881    fn test_no_path_found() {
882        let service = create_service();
883
884        let a = create_entity("A", EntityType::Concept);
885        let b = create_entity("B", EntityType::Concept);
886
887        service.store_entity(&a).unwrap();
888        service.store_entity(&b).unwrap();
889
890        // No relationship between A and B
891        let path = service.find_path(&a.id, &b.id, 5).unwrap();
892        assert!(path.is_none());
893    }
894
895    #[test]
896    fn test_traversal_depth_limit() {
897        let service = create_service();
898
899        // Create chain: A -> B -> C -> D
900        let a = create_entity("A", EntityType::Concept);
901        let b = create_entity("B", EntityType::Concept);
902        let c = create_entity("C", EntityType::Concept);
903        let d = create_entity("D", EntityType::Concept);
904
905        service.store_entity(&a).unwrap();
906        service.store_entity(&b).unwrap();
907        service.store_entity(&c).unwrap();
908        service.store_entity(&d).unwrap();
909
910        service
911            .relate(&a.id, &b.id, RelationshipType::RelatesTo)
912            .unwrap();
913        service
914            .relate(&b.id, &c.id, RelationshipType::RelatesTo)
915            .unwrap();
916        service
917            .relate(&c.id, &d.id, RelationshipType::RelatesTo)
918            .unwrap();
919
920        // Depth 1 should only find B
921        let neighbors = service.get_neighbors(&a.id, 1).unwrap();
922        assert_eq!(neighbors.len(), 1);
923        assert_eq!(neighbors[0].name, "B");
924
925        // Depth 2 should find B and C
926        let neighbors = service.get_neighbors(&a.id, 2).unwrap();
927        assert_eq!(neighbors.len(), 2);
928    }
929
930    #[test]
931    fn test_merge_entities() {
932        let service = create_service();
933
934        let alice1 = create_entity("Alice Smith", EntityType::Person);
935        let alice2 = create_entity("A. Smith", EntityType::Person);
936        let bob = create_entity("Bob", EntityType::Person);
937
938        service.store_entity(&alice1).unwrap();
939        service.store_entity(&alice2).unwrap();
940        service.store_entity(&bob).unwrap();
941
942        // Create relationship with alice2
943        service
944            .relate(&alice2.id, &bob.id, RelationshipType::RelatesTo)
945            .unwrap();
946
947        // Merge alice1 and alice2 into canonical "Alice Smith"
948        let merged = service
949            .merge_entities(&[alice1.id, alice2.id.clone()], "Alice Smith")
950            .unwrap();
951
952        // Merged entity should have the canonical name
953        assert_eq!(merged.name, "Alice Smith");
954
955        // alice2 should be gone (merged)
956        let retrieved = service.get_entity(&alice2.id).unwrap();
957        assert!(retrieved.is_none());
958    }
959
960    #[test]
961    fn test_find_by_name() {
962        let service = create_service();
963
964        let alice = create_entity("Alice", EntityType::Person);
965        let bob = create_entity("Bob", EntityType::Person);
966
967        service.store_entity(&alice).unwrap();
968        service.store_entity(&bob).unwrap();
969
970        let found = service.find_by_name("Alice", None, None, 10).unwrap();
971        assert_eq!(found.len(), 1);
972        assert_eq!(found[0].name, "Alice");
973    }
974
975    #[test]
976    fn test_find_by_name_with_type_filter() {
977        let service = create_service();
978
979        let alice_person = create_entity("Alice", EntityType::Person);
980        let alice_tech = create_entity("Alice", EntityType::Technology);
981
982        service.store_entity(&alice_person).unwrap();
983        service.store_entity(&alice_tech).unwrap();
984
985        let found = service
986            .find_by_name("Alice", Some(EntityType::Person), None, 10)
987            .unwrap();
988        assert_eq!(found.len(), 1);
989        assert_eq!(found[0].entity_type, EntityType::Person);
990    }
991
992    #[test]
993    fn test_empty_graph_stats() {
994        let service = create_service();
995        let stats = service.get_stats().unwrap();
996
997        assert_eq!(stats.entity_count, 0);
998        assert_eq!(stats.relationship_count, 0);
999    }
1000
1001    #[test]
1002    fn test_record_duplicate_mention() {
1003        let service = create_service();
1004
1005        let alice = create_entity("Alice", EntityType::Person);
1006        service.store_entity(&alice).unwrap();
1007
1008        let mem = MemoryId::new("mem_1");
1009
1010        // Record same mention twice
1011        service.record_mention(&alice.id, &mem).unwrap();
1012        service.record_mention(&alice.id, &mem).unwrap();
1013
1014        // Should not create duplicates (implementation may dedupe)
1015        let mentions = service.get_mentions(&alice.id).unwrap();
1016        assert!(!mentions.is_empty());
1017    }
1018}