Skip to main content

do_memory_core/episode/
relationships.rs

1//! Episode relationship types and data structures.
2//!
3//! This module provides types for modeling relationships between episodes,
4//! enabling hierarchical organization, dependency tracking, and workflow modeling.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11#[cfg(feature = "proptest-arbitrary")]
12use proptest::prelude::{Arbitrary, BoxedStrategy, Just, Strategy, prop_oneof};
13
14/// Type of relationship between two episodes.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum RelationshipType {
18    /// Parent-child hierarchical relationship (e.g., epic → story → subtask)
19    ParentChild,
20    /// Dependency relationship (from depends on to)
21    DependsOn,
22    /// Sequential relationship (from follows to)
23    Follows,
24    /// Loose association between related episodes
25    RelatedTo,
26    /// Blocking relationship (from blocks to)
27    Blocks,
28    /// Marks episodes as duplicates
29    Duplicates,
30    /// General cross-reference
31    References,
32}
33
34impl RelationshipType {
35    /// Check if this relationship type implies directionality
36    #[must_use]
37    pub fn is_directional(&self) -> bool {
38        matches!(
39            self,
40            Self::ParentChild | Self::DependsOn | Self::Follows | Self::Blocks
41        )
42    }
43
44    /// Get the inverse relationship type (for bidirectional tracking)
45    #[must_use]
46    pub fn inverse(&self) -> Option<Self> {
47        match self {
48            Self::ParentChild => Some(Self::ParentChild), // Child knows parent
49            Self::DependsOn => Some(Self::DependsOn),     // Reverse dependency
50            Self::Follows => Some(Self::Follows),         // Preceded by
51            Self::Blocks => Some(Self::Blocks),           // Blocked by
52            Self::RelatedTo => None,                      // Symmetric
53            Self::Duplicates => None,                     // Symmetric
54            Self::References => None,                     // Symmetric
55        }
56    }
57
58    /// Check if this relationship type should prevent cycles
59    #[must_use]
60    pub fn requires_acyclic(&self) -> bool {
61        matches!(self, Self::DependsOn | Self::ParentChild | Self::Blocks)
62    }
63
64    /// Convert to string representation for storage
65    #[must_use]
66    pub fn as_str(&self) -> &'static str {
67        match self {
68            Self::ParentChild => "parent_child",
69            Self::DependsOn => "depends_on",
70            Self::Follows => "follows",
71            Self::RelatedTo => "related_to",
72            Self::Blocks => "blocks",
73            Self::Duplicates => "duplicates",
74            Self::References => "references",
75        }
76    }
77
78    /// Parse from string representation
79    ///
80    /// # Errors
81    ///
82    /// Returns an error if the string doesn't match a known relationship type.
83    pub fn parse(s: &str) -> Result<Self, String> {
84        match s {
85            "parent_child" => Ok(Self::ParentChild),
86            "depends_on" => Ok(Self::DependsOn),
87            "follows" => Ok(Self::Follows),
88            "related_to" => Ok(Self::RelatedTo),
89            "blocks" => Ok(Self::Blocks),
90            "duplicates" => Ok(Self::Duplicates),
91            "references" => Ok(Self::References),
92            _ => Err(format!("Unknown relationship type: {s}")),
93        }
94    }
95
96    /// Get all relationship types
97    #[must_use]
98    pub fn all() -> Vec<Self> {
99        vec![
100            Self::ParentChild,
101            Self::DependsOn,
102            Self::Follows,
103            Self::RelatedTo,
104            Self::Blocks,
105            Self::Duplicates,
106            Self::References,
107        ]
108    }
109}
110
111impl std::fmt::Display for RelationshipType {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        write!(f, "{}", self.as_str())
114    }
115}
116
117#[cfg(feature = "proptest-arbitrary")]
118impl Arbitrary for RelationshipType {
119    type Parameters = ();
120    type Strategy = BoxedStrategy<Self>;
121
122    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
123        prop_oneof![
124            Just(Self::ParentChild),
125            Just(Self::DependsOn),
126            Just(Self::Follows),
127            Just(Self::RelatedTo),
128            Just(Self::Blocks),
129            Just(Self::Duplicates),
130            Just(Self::References),
131        ]
132        .boxed()
133    }
134}
135
136/// Additional metadata for a relationship.
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
138pub struct RelationshipMetadata {
139    /// Human-readable reason for the relationship
140    pub reason: Option<String>,
141    /// Who created this relationship
142    pub created_by: Option<String>,
143    /// Priority/importance (1-10, higher is more important)
144    pub priority: Option<u8>,
145    /// Weight for temporal or semantic significance (0.0 to 1.0)
146    pub weight: Option<f32>,
147    /// Custom fields for extensibility
148    #[serde(default)]
149    pub custom_fields: HashMap<String, String>,
150}
151
152impl RelationshipMetadata {
153    /// Create new empty metadata
154    #[must_use]
155    pub fn new() -> Self {
156        Self::default()
157    }
158
159    /// Create metadata with a reason
160    #[must_use]
161    pub fn with_reason(reason: String) -> Self {
162        Self {
163            reason: Some(reason),
164            ..Default::default()
165        }
166    }
167
168    /// Create metadata with reason and `created_by`
169    #[must_use]
170    pub fn with_creator(reason: String, created_by: String) -> Self {
171        Self {
172            reason: Some(reason),
173            created_by: Some(created_by),
174            ..Default::default()
175        }
176    }
177
178    /// Add or update a custom field
179    pub fn set_field(&mut self, key: String, value: String) {
180        self.custom_fields.insert(key, value);
181    }
182
183    /// Get a custom field value
184    #[must_use]
185    pub fn get_field(&self, key: &str) -> Option<&String> {
186        self.custom_fields.get(key)
187    }
188}
189
190/// A relationship between two episodes.
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub struct EpisodeRelationship {
193    /// Unique identifier for this relationship
194    pub id: Uuid,
195    /// Source episode ID
196    pub from_episode_id: Uuid,
197    /// Target episode ID
198    pub to_episode_id: Uuid,
199    /// Type of relationship
200    pub relationship_type: RelationshipType,
201    /// Additional metadata
202    pub metadata: RelationshipMetadata,
203    /// When this relationship was created
204    pub created_at: DateTime<Utc>,
205}
206
207/// A relationship between an episode and a pattern.
208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
209pub struct EpisodePatternRelationship {
210    /// Unique identifier for this relationship
211    pub id: Uuid,
212    /// Source episode ID
213    pub episode_id: Uuid,
214    /// Target pattern ID
215    pub pattern_id: Uuid,
216    /// Type of relationship
217    pub relationship_type: RelationshipType,
218    /// Additional metadata
219    pub metadata: RelationshipMetadata,
220    /// When this relationship was created
221    pub created_at: DateTime<Utc>,
222}
223
224impl EpisodePatternRelationship {
225    /// Create a new episode-pattern relationship
226    #[must_use]
227    pub fn new(
228        episode_id: Uuid,
229        pattern_id: Uuid,
230        relationship_type: RelationshipType,
231        metadata: RelationshipMetadata,
232    ) -> Self {
233        Self {
234            id: Uuid::new_v4(),
235            episode_id,
236            pattern_id,
237            relationship_type,
238            metadata,
239            created_at: Utc::now(),
240        }
241    }
242}
243
244impl EpisodeRelationship {
245    /// Create a new relationship
246    #[must_use]
247    pub fn new(
248        from_episode_id: Uuid,
249        to_episode_id: Uuid,
250        relationship_type: RelationshipType,
251        metadata: RelationshipMetadata,
252    ) -> Self {
253        Self {
254            id: Uuid::new_v4(),
255            from_episode_id,
256            to_episode_id,
257            relationship_type,
258            metadata,
259            created_at: Utc::now(),
260        }
261    }
262
263    /// Create a simple relationship with just a reason
264    #[must_use]
265    pub fn with_reason(
266        from_episode_id: Uuid,
267        to_episode_id: Uuid,
268        relationship_type: RelationshipType,
269        reason: String,
270    ) -> Self {
271        Self::new(
272            from_episode_id,
273            to_episode_id,
274            relationship_type,
275            RelationshipMetadata::with_reason(reason),
276        )
277    }
278
279    /// Check if this relationship is directional
280    #[must_use]
281    pub fn is_directional(&self) -> bool {
282        self.relationship_type.is_directional()
283    }
284
285    /// Get the inverse of this relationship (swap from/to)
286    #[must_use]
287    pub fn inverse(&self) -> Option<Self> {
288        self.relationship_type.inverse().map(|inv_type| Self {
289            id: Uuid::new_v4(),
290            from_episode_id: self.to_episode_id,
291            to_episode_id: self.from_episode_id,
292            relationship_type: inv_type,
293            metadata: self.metadata.clone(),
294            created_at: self.created_at,
295        })
296    }
297}
298
299/// Direction for querying relationships.
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum Direction {
302    /// Outgoing relationships (this episode → others)
303    Outgoing,
304    /// Incoming relationships (others → this episode)
305    Incoming,
306    /// Both directions
307    Both,
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn test_relationship_type_directional() {
316        assert!(RelationshipType::ParentChild.is_directional());
317        assert!(RelationshipType::DependsOn.is_directional());
318        assert!(RelationshipType::Follows.is_directional());
319        assert!(RelationshipType::Blocks.is_directional());
320        assert!(!RelationshipType::RelatedTo.is_directional());
321        assert!(!RelationshipType::Duplicates.is_directional());
322        assert!(!RelationshipType::References.is_directional());
323    }
324
325    #[test]
326    fn test_relationship_type_acyclic() {
327        assert!(RelationshipType::DependsOn.requires_acyclic());
328        assert!(RelationshipType::ParentChild.requires_acyclic());
329        assert!(RelationshipType::Blocks.requires_acyclic());
330        assert!(!RelationshipType::Follows.requires_acyclic());
331        assert!(!RelationshipType::RelatedTo.requires_acyclic());
332    }
333
334    #[test]
335    fn test_relationship_type_str_conversion() {
336        for rel_type in RelationshipType::all() {
337            let s = rel_type.as_str();
338            let parsed = RelationshipType::parse(s).unwrap();
339            assert_eq!(rel_type, parsed);
340        }
341    }
342
343    #[test]
344    fn test_relationship_type_from_str_invalid() {
345        let result = RelationshipType::parse("invalid_type");
346        assert!(result.is_err());
347        assert!(result.unwrap_err().contains("Unknown relationship type"));
348    }
349
350    #[test]
351    fn test_relationship_creation() {
352        let from_id = Uuid::new_v4();
353        let to_id = Uuid::new_v4();
354        let metadata = RelationshipMetadata::with_reason("Subtask of parent".to_string());
355
356        let rel = EpisodeRelationship::new(
357            from_id,
358            to_id,
359            RelationshipType::ParentChild,
360            metadata.clone(),
361        );
362
363        assert_eq!(rel.from_episode_id, from_id);
364        assert_eq!(rel.to_episode_id, to_id);
365        assert_eq!(rel.relationship_type, RelationshipType::ParentChild);
366        assert_eq!(rel.metadata.reason, Some("Subtask of parent".to_string()));
367    }
368
369    #[test]
370    fn test_relationship_with_reason() {
371        let from_id = Uuid::new_v4();
372        let to_id = Uuid::new_v4();
373
374        let rel = EpisodeRelationship::with_reason(
375            from_id,
376            to_id,
377            RelationshipType::DependsOn,
378            "Requires API design".to_string(),
379        );
380
381        assert_eq!(rel.from_episode_id, from_id);
382        assert_eq!(rel.to_episode_id, to_id);
383        assert_eq!(rel.metadata.reason, Some("Requires API design".to_string()));
384    }
385
386    #[test]
387    fn test_relationship_inverse() {
388        let from_id = Uuid::new_v4();
389        let to_id = Uuid::new_v4();
390
391        let rel = EpisodeRelationship::with_reason(
392            from_id,
393            to_id,
394            RelationshipType::ParentChild,
395            "Child task".to_string(),
396        );
397
398        let inverse = rel.inverse().expect("Should have inverse");
399        assert_eq!(inverse.from_episode_id, to_id);
400        assert_eq!(inverse.to_episode_id, from_id);
401        assert_eq!(inverse.relationship_type, RelationshipType::ParentChild);
402    }
403
404    #[test]
405    fn test_relationship_metadata() {
406        let mut metadata = RelationshipMetadata::new();
407        assert!(metadata.reason.is_none());
408        assert!(metadata.created_by.is_none());
409        assert!(metadata.priority.is_none());
410
411        metadata.set_field("project".to_string(), "memory-system".to_string());
412        assert_eq!(
413            metadata.get_field("project"),
414            Some(&"memory-system".to_string())
415        );
416    }
417
418    #[test]
419    fn test_relationship_metadata_with_creator() {
420        let metadata =
421            RelationshipMetadata::with_creator("Bug fix".to_string(), "alice".to_string());
422
423        assert_eq!(metadata.reason, Some("Bug fix".to_string()));
424        assert_eq!(metadata.created_by, Some("alice".to_string()));
425    }
426
427    #[test]
428    fn test_relationship_serialization() {
429        let from_id = Uuid::new_v4();
430        let to_id = Uuid::new_v4();
431        let rel = EpisodeRelationship::with_reason(
432            from_id,
433            to_id,
434            RelationshipType::DependsOn,
435            "Test reason".to_string(),
436        );
437
438        let json = serde_json::to_string(&rel).unwrap();
439        let deserialized: EpisodeRelationship = serde_json::from_str(&json).unwrap();
440
441        assert_eq!(rel.from_episode_id, deserialized.from_episode_id);
442        assert_eq!(rel.to_episode_id, deserialized.to_episode_id);
443        assert_eq!(rel.relationship_type, deserialized.relationship_type);
444        assert_eq!(rel.metadata.reason, deserialized.metadata.reason);
445    }
446
447    #[test]
448    fn test_direction_enum() {
449        // Just ensure the Direction enum variants compile
450        let _ = Direction::Outgoing;
451        let _ = Direction::Incoming;
452        let _ = Direction::Both;
453    }
454}