1#![allow(clippy::missing_const_for_fn)]
3
4use crate::models::temporal::{TransactionTime, ValidTimeRange};
57use crate::models::{Domain, MemoryId};
58use serde::{Deserialize, Serialize};
59use std::fmt;
60
61#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
63pub struct EntityId(String);
64
65impl EntityId {
66 #[must_use]
68 pub fn new(id: impl Into<String>) -> Self {
69 Self(id.into())
70 }
71
72 #[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 let random: u32 = rand_simple();
82 Self(format!("ent_{timestamp:x}_{random:08x}"))
83 }
84
85 #[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#[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 .map(|d| d.as_nanos() as u64)
117 .unwrap_or(12345)
118 );
119 }
120
121 STATE.with(|state| {
122 let mut s = state.get();
124 s ^= s << 13;
125 s ^= s >> 7;
126 s ^= s << 17;
127 state.set(s);
128 s as u32
130 })
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
135#[serde(rename_all = "lowercase")]
136pub enum EntityType {
137 Person,
139 Organization,
141 Concept,
143 Technology,
145 File,
147}
148
149impl EntityType {
150 #[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 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
204#[serde(rename_all = "snake_case")]
205pub enum RelationshipType {
206 WorksAt,
208 Created,
210 Uses,
212 Implements,
214 PartOf,
216 RelatesTo,
218 MentionedIn,
220 Supersedes,
222 ConflictsWith,
224}
225
226impl RelationshipType {
227 #[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 #[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 #[must_use]
265 pub const fn inverse(&self) -> Option<Self> {
266 match self {
267 Self::RelatesTo => Some(Self::RelatesTo),
269 Self::ConflictsWith => Some(Self::ConflictsWith),
270 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 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
318pub struct Entity {
319 pub id: EntityId,
321 pub entity_type: EntityType,
323 pub name: String,
325 pub aliases: Vec<String>,
327 pub domain: Domain,
329 pub confidence: f32,
331 pub valid_time: ValidTimeRange,
333 pub transaction_time: TransactionTime,
335 pub properties: std::collections::HashMap<String, String>,
337 pub mention_count: u32,
339}
340
341impl Entity {
342 #[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 #[must_use]
361 pub fn with_id(mut self, id: EntityId) -> Self {
362 self.id = id;
363 self
364 }
365
366 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
411 pub fn is_valid_at(&self, timestamp: i64) -> bool {
412 self.valid_time.contains(timestamp)
413 }
414}
415
416#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
418pub struct Relationship {
419 pub from_entity: EntityId,
421 pub to_entity: EntityId,
423 pub relationship_type: RelationshipType,
425 pub confidence: f32,
427 pub valid_time: ValidTimeRange,
429 pub transaction_time: TransactionTime,
431 pub properties: std::collections::HashMap<String, String>,
433}
434
435impl Relationship {
436 #[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 #[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 #[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 #[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 #[must_use]
477 pub fn is_valid_at(&self, timestamp: i64) -> bool {
478 self.valid_time.contains(timestamp)
479 }
480}
481
482#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
486pub struct EntityMention {
487 pub entity_id: EntityId,
489 pub memory_id: MemoryId,
491 pub confidence: f32,
493 pub start_offset: Option<usize>,
495 pub end_offset: Option<usize>,
497 pub matched_text: Option<String>,
499 pub transaction_time: TransactionTime,
501}
502
503impl EntityMention {
504 #[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 #[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 #[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#[derive(Debug, Clone, Default)]
537pub struct EntityQuery {
538 pub entity_type: Option<EntityType>,
540 pub name: Option<String>,
542 pub domain: Option<Domain>,
544 pub min_confidence: Option<f32>,
546 pub valid_at: Option<i64>,
548 pub limit: Option<usize>,
550 pub offset: Option<usize>,
552}
553
554impl EntityQuery {
555 #[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 #[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 #[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 #[must_use]
585 pub fn with_domain(mut self, domain: Domain) -> Self {
586 self.domain = Some(domain);
587 self
588 }
589
590 #[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 #[must_use]
599 pub const fn valid_at(mut self, timestamp: i64) -> Self {
600 self.valid_at = Some(timestamp);
601 self
602 }
603
604 #[must_use]
606 pub const fn with_limit(mut self, limit: usize) -> Self {
607 self.limit = Some(limit);
608 self
609 }
610
611 #[must_use]
613 pub const fn with_offset(mut self, offset: usize) -> Self {
614 self.offset = Some(offset);
615 self
616 }
617}
618
619#[derive(Debug, Clone, Default)]
621pub struct RelationshipQuery {
622 pub from_entity: Option<EntityId>,
624 pub to_entity: Option<EntityId>,
626 pub relationship_type: Option<RelationshipType>,
628 pub max_depth: Option<u32>,
630 pub min_confidence: Option<f32>,
632 pub valid_at: Option<i64>,
634 pub limit: Option<usize>,
636}
637
638impl RelationshipQuery {
639 #[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 #[must_use]
655 pub fn from(mut self, entity_id: EntityId) -> Self {
656 self.from_entity = Some(entity_id);
657 self
658 }
659
660 #[must_use]
662 pub fn to(mut self, entity_id: EntityId) -> Self {
663 self.to_entity = Some(entity_id);
664 self
665 }
666
667 #[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 #[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 #[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 #[must_use]
690 pub const fn valid_at(mut self, timestamp: i64) -> Self {
691 self.valid_at = Some(timestamp);
692 self
693 }
694
695 #[must_use]
697 pub const fn with_limit(mut self, limit: usize) -> Self {
698 self.limit = Some(limit);
699 self
700 }
701}
702
703#[derive(Debug, Clone, Default)]
705pub struct TraversalResult {
706 pub entities: Vec<Entity>,
708 pub relationships: Vec<Relationship>,
710 pub total_count: usize,
712}
713
714impl TraversalResult {
715 #[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 #[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); assert_eq!(entity.confidence, 1.0);
847
848 let entity2 =
849 Entity::new(EntityType::Concept, "Test", Domain::for_user()).with_confidence(-0.5); 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}