Skip to main content

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}