pulsedb/relation/types.rs
1//! Data types for experience relations.
2//!
3//! Relations connect two experiences within the same collective, forming
4//! a knowledge graph that agents can traverse to understand how concepts
5//! relate to each other.
6
7use serde::{Deserialize, Serialize};
8
9use crate::types::{ExperienceId, RelationId, Timestamp};
10
11/// Type of relationship between two experiences.
12///
13/// Relations are directed: the semantics describe how the **source**
14/// experience relates to the **target** experience.
15///
16/// # Example
17///
18/// ```rust
19/// use pulsedb::RelationType;
20///
21/// let rel = RelationType::Supports;
22/// // "Experience A supports Experience B"
23/// ```
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub enum RelationType {
26 /// Source experience supports or reinforces the target.
27 Supports,
28 /// Source experience contradicts the target.
29 Contradicts,
30 /// Source experience elaborates on or adds detail to the target.
31 Elaborates,
32 /// Source experience supersedes or replaces the target.
33 Supersedes,
34 /// Source experience implies the target.
35 Implies,
36 /// General relationship with no specific semantics.
37 RelatedTo,
38}
39
40/// Direction for querying relations from a given experience.
41///
42/// Relations are directed graphs. When querying, you can ask for:
43/// - **Outgoing**: "What does this experience point to?"
44/// - **Incoming**: "What points to this experience?"
45/// - **Both**: All connections regardless of direction
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum RelationDirection {
48 /// Relations where the experience is the source (source → target).
49 Outgoing,
50 /// Relations where the experience is the target (source → target).
51 Incoming,
52 /// Both outgoing and incoming relations.
53 Both,
54}
55
56/// A stored relationship between two experiences.
57///
58/// Relations are always within the same collective and are directed
59/// from `source_id` to `target_id`. The `relation_type` describes
60/// the semantic meaning of the connection.
61///
62/// # Uniqueness
63///
64/// The combination `(source_id, target_id, relation_type)` must be
65/// unique — you cannot have two "Supports" relations between the
66/// same pair of experiences.
67#[derive(Clone, Debug, Serialize, Deserialize)]
68pub struct ExperienceRelation {
69 /// Unique identifier for this relation.
70 pub id: RelationId,
71
72 /// The experience this relation originates from.
73 pub source_id: ExperienceId,
74
75 /// The experience this relation points to.
76 pub target_id: ExperienceId,
77
78 /// The type of relationship.
79 pub relation_type: RelationType,
80
81 /// Strength of the relation (0.0 = weak, 1.0 = strong).
82 pub strength: f32,
83
84 /// Optional JSON metadata (max 10KB).
85 pub metadata: Option<String>,
86
87 /// When this relation was created.
88 pub created_at: Timestamp,
89}
90
91/// Input for creating a new relation between two experiences.
92///
93/// # Example
94///
95/// ```rust
96/// # fn main() -> pulsedb::Result<()> {
97/// # let dir = tempfile::tempdir().unwrap();
98/// # let db = pulsedb::PulseDB::open(dir.path().join("test.db"), pulsedb::Config::default())?;
99/// # let cid = db.create_collective("example")?;
100/// # let emb = vec![0.1f32; 384];
101/// # let exp_a = db.record_experience(pulsedb::NewExperience {
102/// # collective_id: cid, content: "a".into(), embedding: Some(emb.clone()), ..Default::default()
103/// # })?;
104/// # let exp_b = db.record_experience(pulsedb::NewExperience {
105/// # collective_id: cid, content: "b".into(), embedding: Some(emb.clone()), ..Default::default()
106/// # })?;
107/// use pulsedb::{NewExperienceRelation, RelationType};
108///
109/// let rel = NewExperienceRelation {
110/// source_id: exp_a,
111/// target_id: exp_b,
112/// relation_type: RelationType::Supports,
113/// strength: 0.9,
114/// metadata: None,
115/// };
116/// let id = db.store_relation(rel)?;
117/// # Ok(())
118/// # }
119/// ```
120pub struct NewExperienceRelation {
121 /// The experience this relation originates from.
122 pub source_id: ExperienceId,
123
124 /// The experience this relation points to.
125 pub target_id: ExperienceId,
126
127 /// The type of relationship.
128 pub relation_type: RelationType,
129
130 /// Strength of the relation (0.0 - 1.0).
131 pub strength: f32,
132
133 /// Optional JSON metadata (max 10KB).
134 pub metadata: Option<String>,
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn test_relation_type_bincode_roundtrip() {
143 let types = [
144 RelationType::Supports,
145 RelationType::Contradicts,
146 RelationType::Elaborates,
147 RelationType::Supersedes,
148 RelationType::Implies,
149 RelationType::RelatedTo,
150 ];
151 for rt in &types {
152 let bytes = bincode::serialize(rt).unwrap();
153 let restored: RelationType = bincode::deserialize(&bytes).unwrap();
154 assert_eq!(*rt, restored);
155 }
156 }
157
158 #[test]
159 fn test_experience_relation_bincode_roundtrip() {
160 let relation = ExperienceRelation {
161 id: RelationId::new(),
162 source_id: ExperienceId::new(),
163 target_id: ExperienceId::new(),
164 relation_type: RelationType::Supports,
165 strength: 0.85,
166 metadata: Some(r#"{"context": "test"}"#.to_string()),
167 created_at: Timestamp::now(),
168 };
169
170 let bytes = bincode::serialize(&relation).unwrap();
171 let restored: ExperienceRelation = bincode::deserialize(&bytes).unwrap();
172
173 assert_eq!(relation.id, restored.id);
174 assert_eq!(relation.source_id, restored.source_id);
175 assert_eq!(relation.target_id, restored.target_id);
176 assert_eq!(relation.relation_type, restored.relation_type);
177 assert_eq!(relation.strength, restored.strength);
178 assert_eq!(relation.metadata, restored.metadata);
179 }
180
181 #[test]
182 fn test_relation_type_copy_and_eq() {
183 let a = RelationType::Contradicts;
184 let b = a; // Copy
185 assert_eq!(a, b);
186 }
187
188 #[test]
189 fn test_relation_direction_variants() {
190 // Ensure all 3 variants are distinct
191 assert_ne!(RelationDirection::Outgoing, RelationDirection::Incoming);
192 assert_ne!(RelationDirection::Outgoing, RelationDirection::Both);
193 assert_ne!(RelationDirection::Incoming, RelationDirection::Both);
194 }
195}