Skip to main content

subcog/models/
graph.rs

1// Allow non-const functions that use f32::clamp (not const-stable yet)
2#![allow(clippy::missing_const_for_fn)]
3
4//! Graph memory types for knowledge graph construction.
5//!
6//! This module provides types for representing entities extracted from memories
7//! and relationships between them, forming a temporal knowledge graph.
8//!
9//! # Entity Types
10//!
11//! Entities are categorized into five types:
12//!
13//! | Type | Description | Examples |
14//! |------|-------------|----------|
15//! | `Person` | Named individuals | "Alice Johnson", "@username" |
16//! | `Organization` | Companies, teams, groups | "Anthropic", "Backend Team" |
17//! | `Concept` | Abstract ideas, patterns | "REST API", "Event Sourcing" |
18//! | `Technology` | Tools, frameworks, languages | "Rust", "`SQLite`", "Docker" |
19//! | `File` | Code files, documents | "src/main.rs", "README.md" |
20//!
21//! # Relationship Types
22//!
23//! Relationships between entities include:
24//!
25//! - `WorksAt` - Person → Organization
26//! - `Created` - Entity → Entity (authorship)
27//! - `Uses` - Entity → Entity (dependency)
28//! - `Implements` - Entity → Entity (realization)
29//! - `PartOf` - Entity → Entity (composition)
30//! - `RelatesTo` - Entity → Entity (general association)
31//! - `MentionedIn` - Entity → Memory (provenance)
32//! - `Supersedes` - Entity → Entity (versioning)
33//! - `ConflictsWith` - Entity → Entity (contradiction)
34//!
35//! # Example
36//!
37//! ```rust
38//! use subcog::models::graph::{Entity, EntityType, Relationship, RelationshipType, EntityId};
39//! use subcog::models::Domain;
40//!
41//! // Create an entity for a technology
42//! let rust_entity = Entity::new(
43//!     EntityType::Technology,
44//!     "Rust",
45//!     Domain::for_user(),
46//! );
47//!
48//! // Create a relationship
49//! let relationship = Relationship::new(
50//!     EntityId::new("person_alice"),
51//!     EntityId::new("tech_rust"),
52//!     RelationshipType::Uses,
53//! );
54//! ```
55
56use crate::models::temporal::{TransactionTime, ValidTimeRange};
57use crate::models::{Domain, MemoryId};
58use serde::{Deserialize, Serialize};
59use std::fmt;
60
61/// Unique identifier for a graph entity.
62#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
63pub struct EntityId(String);
64
65impl EntityId {
66    /// Creates a new entity ID from a string.
67    #[must_use]
68    pub fn new(id: impl Into<String>) -> Self {
69        Self(id.into())
70    }
71
72    /// Generates a new unique entity ID.
73    #[must_use]
74    pub fn generate() -> Self {
75        use std::time::{SystemTime, UNIX_EPOCH};
76        let timestamp = SystemTime::now()
77            .duration_since(UNIX_EPOCH)
78            .map(|d| d.as_nanos())
79            .unwrap_or(0);
80        // Use a simple hash-like format: ent_<timestamp_hex>_<random>
81        let random: u32 = rand_simple();
82        Self(format!("ent_{timestamp:x}_{random:08x}"))
83    }
84
85    /// Returns the entity ID as a string slice.
86    #[must_use]
87    pub fn as_str(&self) -> &str {
88        &self.0
89    }
90}
91
92impl fmt::Display for EntityId {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "{}", self.0)
95    }
96}
97
98impl AsRef<str> for EntityId {
99    fn as_ref(&self) -> &str {
100        &self.0
101    }
102}
103
104/// Simple pseudo-random number generator for ID generation.
105/// Uses thread-local state with system time seeding.
106#[allow(clippy::cast_possible_truncation)]
107fn rand_simple() -> u32 {
108    use std::cell::Cell;
109    use std::time::{SystemTime, UNIX_EPOCH};
110
111    thread_local! {
112        static STATE: Cell<u64> = Cell::new(
113            SystemTime::now()
114                .duration_since(UNIX_EPOCH)
115                // Truncation is intentional - we only need lower bits for randomness
116                .map(|d| d.as_nanos() as u64)
117                .unwrap_or(12345)
118        );
119    }
120
121    STATE.with(|state| {
122        // Simple xorshift64 PRNG
123        let mut s = state.get();
124        s ^= s << 13;
125        s ^= s >> 7;
126        s ^= s << 17;
127        state.set(s);
128        // Truncation is intentional - we only need 32 bits for the ID
129        s as u32
130    })
131}
132
133/// Type of entity in the knowledge graph.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
135#[serde(rename_all = "lowercase")]
136pub enum EntityType {
137    /// Named individual (people, users, contributors).
138    Person,
139    /// Company, team, group, or collective.
140    Organization,
141    /// Abstract idea, pattern, or methodology.
142    Concept,
143    /// Tool, framework, language, or library.
144    Technology,
145    /// Code file, document, or artifact.
146    File,
147}
148
149impl EntityType {
150    /// Returns all entity type variants.
151    #[must_use]
152    pub const fn all() -> &'static [Self] {
153        &[
154            Self::Person,
155            Self::Organization,
156            Self::Concept,
157            Self::Technology,
158            Self::File,
159        ]
160    }
161
162    /// Returns the entity type as a string slice.
163    #[must_use]
164    pub const fn as_str(&self) -> &'static str {
165        match self {
166            Self::Person => "person",
167            Self::Organization => "organization",
168            Self::Concept => "concept",
169            Self::Technology => "technology",
170            Self::File => "file",
171        }
172    }
173
174    /// Parses an entity type from a string.
175    #[must_use]
176    pub fn parse(s: &str) -> Option<Self> {
177        match s.to_lowercase().as_str() {
178            "person" | "people" | "user" => Some(Self::Person),
179            "organization" | "org" | "company" | "team" => Some(Self::Organization),
180            "concept" | "idea" | "pattern" => Some(Self::Concept),
181            "technology" | "tech" | "tool" | "framework" | "language" => Some(Self::Technology),
182            "file" | "document" | "artifact" => Some(Self::File),
183            _ => None,
184        }
185    }
186}
187
188impl fmt::Display for EntityType {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        write!(f, "{}", self.as_str())
191    }
192}
193
194impl std::str::FromStr for EntityType {
195    type Err = String;
196
197    fn from_str(s: &str) -> Result<Self, Self::Err> {
198        Self::parse(s).ok_or_else(|| format!("unknown entity type: {s}"))
199    }
200}
201
202/// Type of relationship between entities.
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
204#[serde(rename_all = "snake_case")]
205pub enum RelationshipType {
206    /// Person works at an organization.
207    WorksAt,
208    /// Entity created another entity.
209    Created,
210    /// Entity uses/depends on another entity.
211    Uses,
212    /// Entity implements a concept or interface.
213    Implements,
214    /// Entity is part of another entity.
215    PartOf,
216    /// General association between entities.
217    RelatesTo,
218    /// Entity is mentioned in a memory.
219    MentionedIn,
220    /// Entity supersedes/replaces another entity.
221    Supersedes,
222    /// Entity conflicts with another entity.
223    ConflictsWith,
224}
225
226impl RelationshipType {
227    /// Returns all relationship type variants.
228    #[must_use]
229    pub const fn all() -> &'static [Self] {
230        &[
231            Self::WorksAt,
232            Self::Created,
233            Self::Uses,
234            Self::Implements,
235            Self::PartOf,
236            Self::RelatesTo,
237            Self::MentionedIn,
238            Self::Supersedes,
239            Self::ConflictsWith,
240        ]
241    }
242
243    /// Returns the relationship type as a string slice.
244    #[must_use]
245    pub const fn as_str(&self) -> &'static str {
246        match self {
247            Self::WorksAt => "works_at",
248            Self::Created => "created",
249            Self::Uses => "uses",
250            Self::Implements => "implements",
251            Self::PartOf => "part_of",
252            Self::RelatesTo => "relates_to",
253            Self::MentionedIn => "mentioned_in",
254            Self::Supersedes => "supersedes",
255            Self::ConflictsWith => "conflicts_with",
256        }
257    }
258
259    /// Returns the inverse relationship type, if defined.
260    ///
261    /// Some relationships have natural inverses (e.g., `PartOf` ↔ `HasPart`),
262    /// while others are symmetric (e.g., `RelatesTo`) or asymmetric without
263    /// a defined inverse (e.g., `Created`).
264    #[must_use]
265    pub const fn inverse(&self) -> Option<Self> {
266        match self {
267            // Symmetric relationships
268            Self::RelatesTo => Some(Self::RelatesTo),
269            Self::ConflictsWith => Some(Self::ConflictsWith),
270            // Asymmetric relationships without defined inverses
271            Self::PartOf
272            | Self::WorksAt
273            | Self::Created
274            | Self::Uses
275            | Self::Implements
276            | Self::MentionedIn
277            | Self::Supersedes => None,
278        }
279    }
280
281    /// Parses a relationship type from a string.
282    #[must_use]
283    pub fn parse(s: &str) -> Option<Self> {
284        match s.to_lowercase().replace('-', "_").as_str() {
285            "works_at" | "worksat" | "employed_by" => Some(Self::WorksAt),
286            "created" | "authored" | "wrote" => Some(Self::Created),
287            "uses" | "depends_on" | "requires" => Some(Self::Uses),
288            "implements" | "realizes" | "extends" => Some(Self::Implements),
289            "part_of" | "partof" | "belongs_to" | "member_of" => Some(Self::PartOf),
290            "relates_to" | "relatesto" | "related" | "associated" => Some(Self::RelatesTo),
291            "mentioned_in" | "mentionedin" | "referenced_in" => Some(Self::MentionedIn),
292            "supersedes" | "replaces" | "upgrades" => Some(Self::Supersedes),
293            "conflicts_with" | "conflictswith" | "contradicts" => Some(Self::ConflictsWith),
294            _ => None,
295        }
296    }
297}
298
299impl fmt::Display for RelationshipType {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        write!(f, "{}", self.as_str())
302    }
303}
304
305impl std::str::FromStr for RelationshipType {
306    type Err = String;
307
308    fn from_str(s: &str) -> Result<Self, Self::Err> {
309        Self::parse(s).ok_or_else(|| format!("unknown relationship type: {s}"))
310    }
311}
312
313/// An entity in the knowledge graph.
314///
315/// Entities represent real-world concepts extracted from memories, such as
316/// people, organizations, technologies, and files.
317#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
318pub struct Entity {
319    /// Unique identifier for this entity.
320    pub id: EntityId,
321    /// Type of entity.
322    pub entity_type: EntityType,
323    /// Canonical name for the entity.
324    pub name: String,
325    /// Alternative names or aliases.
326    pub aliases: Vec<String>,
327    /// Domain scope for the entity.
328    pub domain: Domain,
329    /// Confidence score from extraction (0.0 to 1.0).
330    pub confidence: f32,
331    /// Bitemporal: when this entity was valid in the real world.
332    pub valid_time: ValidTimeRange,
333    /// Bitemporal: when this entity was recorded in the system.
334    pub transaction_time: TransactionTime,
335    /// Optional properties as key-value pairs.
336    pub properties: std::collections::HashMap<String, String>,
337    /// Number of times this entity has been mentioned.
338    pub mention_count: u32,
339}
340
341impl Entity {
342    /// Creates a new entity with default temporal values.
343    #[must_use]
344    pub fn new(entity_type: EntityType, name: impl Into<String>, domain: Domain) -> Self {
345        Self {
346            id: EntityId::generate(),
347            entity_type,
348            name: name.into(),
349            aliases: Vec::new(),
350            domain,
351            confidence: 1.0,
352            valid_time: ValidTimeRange::unbounded(),
353            transaction_time: TransactionTime::now(),
354            properties: std::collections::HashMap::new(),
355            mention_count: 1,
356        }
357    }
358
359    /// Creates an entity with a specific ID.
360    #[must_use]
361    pub fn with_id(mut self, id: EntityId) -> Self {
362        self.id = id;
363        self
364    }
365
366    /// Sets the confidence score.
367    #[must_use]
368    pub fn with_confidence(mut self, confidence: f32) -> Self {
369        self.confidence = confidence.clamp(0.0, 1.0);
370        self
371    }
372
373    /// Adds an alias to the entity.
374    #[must_use]
375    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
376        self.aliases.push(alias.into());
377        self
378    }
379
380    /// Adds multiple aliases to the entity.
381    #[must_use]
382    pub fn with_aliases(mut self, aliases: impl IntoIterator<Item = impl Into<String>>) -> Self {
383        self.aliases.extend(aliases.into_iter().map(Into::into));
384        self
385    }
386
387    /// Sets the valid time range.
388    #[must_use]
389    pub fn with_valid_time(mut self, valid_time: ValidTimeRange) -> Self {
390        self.valid_time = valid_time;
391        self
392    }
393
394    /// Adds a property to the entity.
395    #[must_use]
396    pub fn with_property(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
397        self.properties.insert(key.into(), value.into());
398        self
399    }
400
401    /// Returns true if this entity matches a name (canonical or alias).
402    #[must_use]
403    pub fn matches_name(&self, name: &str) -> bool {
404        let name_lower = name.to_lowercase();
405        self.name.to_lowercase() == name_lower
406            || self.aliases.iter().any(|a| a.to_lowercase() == name_lower)
407    }
408
409    /// Returns true if this entity is valid at the given time.
410    #[must_use]
411    pub fn is_valid_at(&self, timestamp: i64) -> bool {
412        self.valid_time.contains(timestamp)
413    }
414}
415
416/// A relationship between two entities in the knowledge graph.
417#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
418pub struct Relationship {
419    /// Source entity ID.
420    pub from_entity: EntityId,
421    /// Target entity ID.
422    pub to_entity: EntityId,
423    /// Type of relationship.
424    pub relationship_type: RelationshipType,
425    /// Confidence score (0.0 to 1.0).
426    pub confidence: f32,
427    /// Bitemporal: when this relationship was valid.
428    pub valid_time: ValidTimeRange,
429    /// Bitemporal: when this relationship was recorded.
430    pub transaction_time: TransactionTime,
431    /// Optional properties as key-value pairs.
432    pub properties: std::collections::HashMap<String, String>,
433}
434
435impl Relationship {
436    /// Creates a new relationship with default temporal values.
437    #[must_use]
438    pub fn new(
439        from_entity: EntityId,
440        to_entity: EntityId,
441        relationship_type: RelationshipType,
442    ) -> Self {
443        Self {
444            from_entity,
445            to_entity,
446            relationship_type,
447            confidence: 1.0,
448            valid_time: ValidTimeRange::unbounded(),
449            transaction_time: TransactionTime::now(),
450            properties: std::collections::HashMap::new(),
451        }
452    }
453
454    /// Sets the confidence score.
455    #[must_use]
456    pub fn with_confidence(mut self, confidence: f32) -> Self {
457        self.confidence = confidence.clamp(0.0, 1.0);
458        self
459    }
460
461    /// Sets the valid time range.
462    #[must_use]
463    pub fn with_valid_time(mut self, valid_time: ValidTimeRange) -> Self {
464        self.valid_time = valid_time;
465        self
466    }
467
468    /// Adds a property to the relationship.
469    #[must_use]
470    pub fn with_property(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
471        self.properties.insert(key.into(), value.into());
472        self
473    }
474
475    /// Returns true if this relationship is valid at the given time.
476    #[must_use]
477    pub fn is_valid_at(&self, timestamp: i64) -> bool {
478        self.valid_time.contains(timestamp)
479    }
480}
481
482/// A mention of an entity in a memory.
483///
484/// This links entities to their source memories, providing provenance tracking.
485#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
486pub struct EntityMention {
487    /// Entity that was mentioned.
488    pub entity_id: EntityId,
489    /// Memory where the entity was mentioned.
490    pub memory_id: MemoryId,
491    /// Confidence score of this specific mention.
492    pub confidence: f32,
493    /// Character offset where the mention starts (if available).
494    pub start_offset: Option<usize>,
495    /// Character offset where the mention ends (if available).
496    pub end_offset: Option<usize>,
497    /// The exact text that was matched.
498    pub matched_text: Option<String>,
499    /// When this mention was recorded.
500    pub transaction_time: TransactionTime,
501}
502
503impl EntityMention {
504    /// Creates a new entity mention.
505    #[must_use]
506    pub fn new(entity_id: EntityId, memory_id: MemoryId) -> Self {
507        Self {
508            entity_id,
509            memory_id,
510            confidence: 1.0,
511            start_offset: None,
512            end_offset: None,
513            matched_text: None,
514            transaction_time: TransactionTime::now(),
515        }
516    }
517
518    /// Sets the confidence score.
519    #[must_use]
520    pub fn with_confidence(mut self, confidence: f32) -> Self {
521        self.confidence = confidence.clamp(0.0, 1.0);
522        self
523    }
524
525    /// Sets the text span for this mention.
526    #[must_use]
527    pub fn with_span(mut self, start: usize, end: usize, text: impl Into<String>) -> Self {
528        self.start_offset = Some(start);
529        self.end_offset = Some(end);
530        self.matched_text = Some(text.into());
531        self
532    }
533}
534
535/// Query parameters for searching entities.
536#[derive(Debug, Clone, Default)]
537pub struct EntityQuery {
538    /// Filter by entity type.
539    pub entity_type: Option<EntityType>,
540    /// Search by name (fuzzy match).
541    pub name: Option<String>,
542    /// Filter by domain.
543    pub domain: Option<Domain>,
544    /// Minimum confidence threshold.
545    pub min_confidence: Option<f32>,
546    /// Point-in-time query for temporal filtering.
547    pub valid_at: Option<i64>,
548    /// Maximum results to return.
549    pub limit: Option<usize>,
550    /// Offset for pagination.
551    pub offset: Option<usize>,
552}
553
554impl EntityQuery {
555    /// Creates a new empty query.
556    #[must_use]
557    pub const fn new() -> Self {
558        Self {
559            entity_type: None,
560            name: None,
561            domain: None,
562            min_confidence: None,
563            valid_at: None,
564            limit: None,
565            offset: None,
566        }
567    }
568
569    /// Filters by entity type.
570    #[must_use]
571    pub fn with_type(mut self, entity_type: EntityType) -> Self {
572        self.entity_type = Some(entity_type);
573        self
574    }
575
576    /// Searches by name.
577    #[must_use]
578    pub fn with_name(mut self, name: impl Into<String>) -> Self {
579        self.name = Some(name.into());
580        self
581    }
582
583    /// Filters by domain.
584    #[must_use]
585    pub fn with_domain(mut self, domain: Domain) -> Self {
586        self.domain = Some(domain);
587        self
588    }
589
590    /// Sets minimum confidence threshold.
591    #[must_use]
592    pub fn with_min_confidence(mut self, confidence: f32) -> Self {
593        self.min_confidence = Some(confidence.clamp(0.0, 1.0));
594        self
595    }
596
597    /// Sets point-in-time for temporal query.
598    #[must_use]
599    pub const fn valid_at(mut self, timestamp: i64) -> Self {
600        self.valid_at = Some(timestamp);
601        self
602    }
603
604    /// Sets maximum results.
605    #[must_use]
606    pub const fn with_limit(mut self, limit: usize) -> Self {
607        self.limit = Some(limit);
608        self
609    }
610
611    /// Sets offset for pagination.
612    #[must_use]
613    pub const fn with_offset(mut self, offset: usize) -> Self {
614        self.offset = Some(offset);
615        self
616    }
617}
618
619/// Query parameters for traversing relationships.
620#[derive(Debug, Clone, Default)]
621pub struct RelationshipQuery {
622    /// Starting entity for traversal.
623    pub from_entity: Option<EntityId>,
624    /// Target entity for filtering.
625    pub to_entity: Option<EntityId>,
626    /// Filter by relationship type.
627    pub relationship_type: Option<RelationshipType>,
628    /// Maximum traversal depth.
629    pub max_depth: Option<u32>,
630    /// Minimum confidence threshold.
631    pub min_confidence: Option<f32>,
632    /// Point-in-time query for temporal filtering.
633    pub valid_at: Option<i64>,
634    /// Maximum results to return.
635    pub limit: Option<usize>,
636}
637
638impl RelationshipQuery {
639    /// Creates a new empty query.
640    #[must_use]
641    pub const fn new() -> Self {
642        Self {
643            from_entity: None,
644            to_entity: None,
645            relationship_type: None,
646            max_depth: None,
647            min_confidence: None,
648            valid_at: None,
649            limit: None,
650        }
651    }
652
653    /// Sets the starting entity.
654    #[must_use]
655    pub fn from(mut self, entity_id: EntityId) -> Self {
656        self.from_entity = Some(entity_id);
657        self
658    }
659
660    /// Sets the target entity.
661    #[must_use]
662    pub fn to(mut self, entity_id: EntityId) -> Self {
663        self.to_entity = Some(entity_id);
664        self
665    }
666
667    /// Filters by relationship type.
668    #[must_use]
669    pub fn with_type(mut self, relationship_type: RelationshipType) -> Self {
670        self.relationship_type = Some(relationship_type);
671        self
672    }
673
674    /// Sets maximum traversal depth.
675    #[must_use]
676    pub const fn with_max_depth(mut self, depth: u32) -> Self {
677        self.max_depth = Some(depth);
678        self
679    }
680
681    /// Sets minimum confidence threshold.
682    #[must_use]
683    pub fn with_min_confidence(mut self, confidence: f32) -> Self {
684        self.min_confidence = Some(confidence.clamp(0.0, 1.0));
685        self
686    }
687
688    /// Sets point-in-time for temporal query.
689    #[must_use]
690    pub const fn valid_at(mut self, timestamp: i64) -> Self {
691        self.valid_at = Some(timestamp);
692        self
693    }
694
695    /// Sets maximum results.
696    #[must_use]
697    pub const fn with_limit(mut self, limit: usize) -> Self {
698        self.limit = Some(limit);
699        self
700    }
701}
702
703/// Result of a graph traversal operation.
704#[derive(Debug, Clone, Default)]
705pub struct TraversalResult {
706    /// Entities found during traversal.
707    pub entities: Vec<Entity>,
708    /// Relationships traversed.
709    pub relationships: Vec<Relationship>,
710    /// Total count before limit was applied.
711    pub total_count: usize,
712}
713
714impl TraversalResult {
715    /// Creates a new empty traversal result.
716    #[must_use]
717    pub const fn new() -> Self {
718        Self {
719            entities: Vec::new(),
720            relationships: Vec::new(),
721            total_count: 0,
722        }
723    }
724
725    /// Returns true if the result is empty.
726    #[must_use]
727    pub fn is_empty(&self) -> bool {
728        self.entities.is_empty() && self.relationships.is_empty()
729    }
730}
731
732#[cfg(test)]
733#[allow(clippy::float_cmp)]
734mod tests {
735    use super::*;
736
737    #[test]
738    fn test_entity_id_generate() {
739        let id1 = EntityId::generate();
740        let id2 = EntityId::generate();
741        assert_ne!(id1, id2);
742        assert!(id1.as_str().starts_with("ent_"));
743    }
744
745    #[test]
746    fn test_entity_type_parse() {
747        assert_eq!(EntityType::parse("person"), Some(EntityType::Person));
748        assert_eq!(EntityType::parse("PERSON"), Some(EntityType::Person));
749        assert_eq!(EntityType::parse("org"), Some(EntityType::Organization));
750        assert_eq!(EntityType::parse("tech"), Some(EntityType::Technology));
751        assert_eq!(EntityType::parse("unknown"), None);
752    }
753
754    #[test]
755    fn test_relationship_type_parse() {
756        assert_eq!(
757            RelationshipType::parse("works_at"),
758            Some(RelationshipType::WorksAt)
759        );
760        assert_eq!(
761            RelationshipType::parse("uses"),
762            Some(RelationshipType::Uses)
763        );
764        assert_eq!(
765            RelationshipType::parse("part-of"),
766            Some(RelationshipType::PartOf)
767        );
768        assert_eq!(RelationshipType::parse("unknown"), None);
769    }
770
771    #[test]
772    fn test_entity_creation() {
773        let entity = Entity::new(EntityType::Technology, "Rust", Domain::for_user())
774            .with_confidence(0.95)
775            .with_alias("rust-lang")
776            .with_property("version", "1.85");
777
778        assert_eq!(entity.entity_type, EntityType::Technology);
779        assert_eq!(entity.name, "Rust");
780        assert_eq!(entity.confidence, 0.95);
781        assert!(entity.aliases.contains(&"rust-lang".to_string()));
782        assert_eq!(entity.properties.get("version"), Some(&"1.85".to_string()));
783    }
784
785    #[test]
786    fn test_entity_matches_name() {
787        let entity = Entity::new(EntityType::Person, "Alice Johnson", Domain::for_user())
788            .with_alias("alice")
789            .with_alias("AJ");
790
791        assert!(entity.matches_name("Alice Johnson"));
792        assert!(entity.matches_name("alice johnson"));
793        assert!(entity.matches_name("alice"));
794        assert!(entity.matches_name("AJ"));
795        assert!(!entity.matches_name("Bob"));
796    }
797
798    #[test]
799    fn test_relationship_creation() {
800        let rel = Relationship::new(
801            EntityId::new("person_1"),
802            EntityId::new("org_1"),
803            RelationshipType::WorksAt,
804        )
805        .with_confidence(0.9)
806        .with_property("role", "Engineer");
807
808        assert_eq!(rel.from_entity.as_str(), "person_1");
809        assert_eq!(rel.to_entity.as_str(), "org_1");
810        assert_eq!(rel.relationship_type, RelationshipType::WorksAt);
811        assert_eq!(rel.confidence, 0.9);
812    }
813
814    #[test]
815    fn test_entity_query_builder() {
816        let query = EntityQuery::new()
817            .with_type(EntityType::Person)
818            .with_name("Alice")
819            .with_min_confidence(0.8)
820            .with_limit(10);
821
822        assert_eq!(query.entity_type, Some(EntityType::Person));
823        assert_eq!(query.name, Some("Alice".to_string()));
824        assert_eq!(query.min_confidence, Some(0.8));
825        assert_eq!(query.limit, Some(10));
826    }
827
828    #[test]
829    fn test_relationship_query_builder() {
830        let query = RelationshipQuery::new()
831            .from(EntityId::new("ent_1"))
832            .with_type(RelationshipType::Uses)
833            .with_max_depth(2)
834            .with_limit(20);
835
836        assert_eq!(query.from_entity, Some(EntityId::new("ent_1")));
837        assert_eq!(query.relationship_type, Some(RelationshipType::Uses));
838        assert_eq!(query.max_depth, Some(2));
839        assert_eq!(query.limit, Some(20));
840    }
841
842    #[test]
843    fn test_confidence_clamping() {
844        let entity =
845            Entity::new(EntityType::Concept, "Test", Domain::for_user()).with_confidence(1.5); // Should clamp to 1.0
846        assert_eq!(entity.confidence, 1.0);
847
848        let entity2 =
849            Entity::new(EntityType::Concept, "Test", Domain::for_user()).with_confidence(-0.5); // Should clamp to 0.0
850        assert_eq!(entity2.confidence, 0.0);
851    }
852
853    #[test]
854    fn test_entity_mention() {
855        let mention = EntityMention::new(EntityId::new("ent_1"), MemoryId::new("mem_1"))
856            .with_confidence(0.95)
857            .with_span(10, 20, "example text");
858
859        assert_eq!(mention.entity_id.as_str(), "ent_1");
860        assert_eq!(mention.memory_id.as_str(), "mem_1");
861        assert_eq!(mention.confidence, 0.95);
862        assert_eq!(mention.start_offset, Some(10));
863        assert_eq!(mention.end_offset, Some(20));
864        assert_eq!(mention.matched_text, Some("example text".to_string()));
865    }
866
867    #[test]
868    fn test_traversal_result() {
869        let result = TraversalResult::new();
870        assert!(result.is_empty());
871
872        let mut result = TraversalResult::new();
873        result
874            .entities
875            .push(Entity::new(EntityType::Person, "Test", Domain::for_user()));
876        assert!(!result.is_empty());
877    }
878}