cellstate_core/
event_sourcing.rs

1//! Systematic Event Sourcing
2//!
3//! Provides `from_events()` reconstruction for all core entities,
4//! extending the pattern established in `agent_state.rs`.
5//!
6//! # Design
7//!
8//! The `EventSourced` trait defines a common interface for entities that can be
9//! reconstructed from their event history. Each entity implementation:
10//!
11//! 1. Sorts events by timestamp to handle out-of-order delivery
12//! 2. Filters for relevant event kinds using `relevant_event_kinds()`
13//! 3. Applies events in order via `apply_event()` to build up state
14//! 4. Returns `None` from `from_events()` if the creation event is missing
15//!
16//! # Entities
17//!
18//! - **Trajectory**: TRAJECTORY_CREATED -> TRAJECTORY_UPDATED -> TRAJECTORY_COMPLETED/FAILED/DELETED
19//! - **Scope**: SCOPE_CREATED -> SCOPE_UPDATED -> SCOPE_CLOSED
20//! - **Artifact**: ARTIFACT_CREATED -> ARTIFACT_UPDATED -> ARTIFACT_DELETED
21//! - **Note**: NOTE_CREATED -> NOTE_UPDATED -> NOTE_DELETED
22
23use crate::entities::{Artifact, Note, Provenance, Scope, Trajectory, TrajectoryOutcome};
24use crate::event::{Event, EventKind};
25use crate::*;
26
27/// Trait for entities that can be reconstructed from their event history.
28pub trait EventSourced: Sized {
29    /// Reconstruct entity state from a sequence of events.
30    ///
31    /// Events are sorted by timestamp before processing. Returns `None` if
32    /// the creation event is missing or the entity has been deleted.
33    fn from_events(events: &[Event<serde_json::Value>]) -> Option<Self>;
34
35    /// Apply a single event to update state (used for incremental updates).
36    fn apply_event(&mut self, event: &Event<serde_json::Value>);
37
38    /// The event kinds this entity cares about.
39    fn relevant_event_kinds() -> &'static [EventKind];
40}
41
42/// Sort events by timestamp (ascending), then filter for relevant kinds.
43fn sorted_relevant_events<'a>(
44    events: &'a [Event<serde_json::Value>],
45    relevant: &[EventKind],
46) -> Vec<&'a Event<serde_json::Value>> {
47    let mut filtered: Vec<&Event<serde_json::Value>> = events
48        .iter()
49        .filter(|e| relevant.contains(&e.header.event_kind))
50        .collect();
51    filtered.sort_by_key(|e| e.header.timestamp);
52    filtered
53}
54
55// ============================================================================
56// Helper extractors
57// ============================================================================
58
59/// Extract a typed ID from a JSON payload field.
60fn extract_id<T: identity::EntityIdType>(payload: &serde_json::Value, field: &str) -> Option<T> {
61    payload
62        .get(field)
63        .and_then(|v| serde_json::from_value(v.clone()).ok())
64}
65
66/// Extract a string from a JSON payload field.
67fn extract_string(payload: &serde_json::Value, field: &str) -> Option<String> {
68    payload
69        .get(field)
70        .and_then(|v| v.as_str())
71        .map(|s| s.to_string())
72}
73
74/// Extract an optional string from a JSON payload field.
75fn extract_optional_string(payload: &serde_json::Value, field: &str) -> Option<String> {
76    payload.get(field).and_then(|v| {
77        if v.is_null() {
78            None
79        } else {
80            v.as_str().map(|s| s.to_string())
81        }
82    })
83}
84
85/// Extract an i32 from a JSON payload field, with a default.
86fn extract_i32(payload: &serde_json::Value, field: &str, default: i32) -> i32 {
87    payload
88        .get(field)
89        .and_then(|v| v.as_i64())
90        .map(|v| v as i32)
91        .unwrap_or(default)
92}
93
94/// Extract an i32 from a JSON payload field, returning None if absent.
95fn extract_optional_i32(payload: &serde_json::Value, field: &str) -> Option<i32> {
96    payload
97        .get(field)
98        .and_then(|v| v.as_i64())
99        .map(|v| v as i32)
100}
101
102/// Convert a microsecond timestamp to a chrono DateTime.
103fn timestamp_to_datetime(ts: i64) -> Timestamp {
104    chrono::DateTime::from_timestamp_micros(ts).unwrap_or_else(chrono::Utc::now)
105}
106
107// ============================================================================
108// Trajectory EventSourced
109// ============================================================================
110
111impl EventSourced for Trajectory {
112    fn from_events(events: &[Event<serde_json::Value>]) -> Option<Self> {
113        let relevant = sorted_relevant_events(events, Self::relevant_event_kinds());
114
115        // Find the creation event
116        let create_event = relevant
117            .iter()
118            .find(|e| e.header.event_kind == EventKind::TRAJECTORY_CREATED)?;
119
120        let payload = &create_event.payload;
121        let trajectory_id = extract_id::<TrajectoryId>(payload, "trajectory_id")?;
122        let name = extract_string(payload, "name").unwrap_or_default();
123        let created_at = timestamp_to_datetime(create_event.header.timestamp);
124
125        let mut trajectory = Trajectory {
126            trajectory_id,
127            name,
128            description: extract_optional_string(payload, "description"),
129            status: TrajectoryStatus::Active,
130            parent_trajectory_id: extract_id(payload, "parent_trajectory_id"),
131            root_trajectory_id: extract_id(payload, "root_trajectory_id"),
132            agent_id: extract_id(payload, "agent_id"),
133            created_at,
134            updated_at: created_at,
135            completed_at: None,
136            outcome: None,
137            metadata: payload.get("metadata").cloned(),
138        };
139
140        // Apply remaining events in order
141        for event in &relevant {
142            if event.header.event_kind != EventKind::TRAJECTORY_CREATED {
143                trajectory.apply_event(event);
144            }
145        }
146
147        // Even deleted trajectories can be reconstructed from events
148        Some(trajectory)
149    }
150
151    fn apply_event(&mut self, event: &Event<serde_json::Value>) {
152        let payload = &event.payload;
153        let ts = timestamp_to_datetime(event.header.timestamp);
154
155        match event.header.event_kind {
156            EventKind::TRAJECTORY_UPDATED => {
157                if let Some(name) = extract_string(payload, "name") {
158                    self.name = name;
159                }
160                if let Some(desc) = payload.get("description") {
161                    self.description = if desc.is_null() {
162                        None
163                    } else {
164                        desc.as_str().map(|s| s.to_string())
165                    };
166                }
167                if let Some(status) = payload
168                    .get("status")
169                    .and_then(|v| serde_json::from_value(v.clone()).ok())
170                {
171                    self.status = status;
172                }
173                if let Some(meta) = payload.get("metadata") {
174                    self.metadata = Some(meta.clone());
175                }
176                self.updated_at = ts;
177            }
178            EventKind::TRAJECTORY_COMPLETED => {
179                self.status = TrajectoryStatus::Completed;
180                self.completed_at = Some(ts);
181                self.updated_at = ts;
182
183                // Extract outcome if present
184                if let Some(outcome_val) = payload.get("outcome") {
185                    if let Ok(outcome) =
186                        serde_json::from_value::<TrajectoryOutcome>(outcome_val.clone())
187                    {
188                        self.outcome = Some(outcome);
189                    }
190                }
191            }
192            EventKind::TRAJECTORY_FAILED => {
193                self.status = TrajectoryStatus::Failed;
194                self.completed_at = Some(ts);
195                self.updated_at = ts;
196
197                // Build a failure outcome from the error message
198                if let Some(error) = extract_string(payload, "error") {
199                    self.outcome = Some(TrajectoryOutcome {
200                        status: OutcomeStatus::Failure,
201                        summary: error,
202                        produced_artifacts: vec![],
203                        produced_notes: vec![],
204                        error: extract_optional_string(payload, "error"),
205                    });
206                }
207            }
208            EventKind::TRAJECTORY_SUSPENDED => {
209                self.status = TrajectoryStatus::Suspended;
210                self.updated_at = ts;
211            }
212            EventKind::TRAJECTORY_RESUMED => {
213                self.status = TrajectoryStatus::Active;
214                self.updated_at = ts;
215            }
216            EventKind::TRAJECTORY_DELETED => {
217                // Mark as completed with a deletion timestamp.
218                // Callers can check completed_at to determine if deleted.
219                self.completed_at = Some(ts);
220                self.updated_at = ts;
221            }
222            _ => {} // Skip unknown event kinds
223        }
224    }
225
226    fn relevant_event_kinds() -> &'static [EventKind] {
227        &[
228            EventKind::TRAJECTORY_CREATED,
229            EventKind::TRAJECTORY_UPDATED,
230            EventKind::TRAJECTORY_COMPLETED,
231            EventKind::TRAJECTORY_FAILED,
232            EventKind::TRAJECTORY_SUSPENDED,
233            EventKind::TRAJECTORY_RESUMED,
234            EventKind::TRAJECTORY_DELETED,
235        ]
236    }
237}
238
239// ============================================================================
240// Scope EventSourced
241// ============================================================================
242
243impl EventSourced for Scope {
244    fn from_events(events: &[Event<serde_json::Value>]) -> Option<Self> {
245        let relevant = sorted_relevant_events(events, Self::relevant_event_kinds());
246
247        let create_event = relevant
248            .iter()
249            .find(|e| e.header.event_kind == EventKind::SCOPE_CREATED)?;
250
251        let payload = &create_event.payload;
252        let scope_id = extract_id::<ScopeId>(payload, "scope_id")?;
253        let trajectory_id = extract_id::<TrajectoryId>(payload, "trajectory_id")?;
254        let created_at = timestamp_to_datetime(create_event.header.timestamp);
255
256        let mut scope = Scope {
257            scope_id,
258            trajectory_id,
259            parent_scope_id: extract_id(payload, "parent_scope_id"),
260            name: extract_string(payload, "name").unwrap_or_default(),
261            purpose: extract_optional_string(payload, "purpose"),
262            is_active: true,
263            created_at,
264            closed_at: None,
265            checkpoint: None,
266            token_budget: extract_i32(payload, "token_budget", 8000),
267            tokens_used: extract_i32(payload, "tokens_used", 0),
268            metadata: payload.get("metadata").cloned(),
269        };
270
271        for event in &relevant {
272            if event.header.event_kind != EventKind::SCOPE_CREATED {
273                scope.apply_event(event);
274            }
275        }
276
277        Some(scope)
278    }
279
280    fn apply_event(&mut self, event: &Event<serde_json::Value>) {
281        let payload = &event.payload;
282        let ts = timestamp_to_datetime(event.header.timestamp);
283
284        match event.header.event_kind {
285            EventKind::SCOPE_UPDATED => {
286                if let Some(name) = extract_string(payload, "name") {
287                    self.name = name;
288                }
289                if let Some(purpose) = payload.get("purpose") {
290                    self.purpose = if purpose.is_null() {
291                        None
292                    } else {
293                        purpose.as_str().map(|s| s.to_string())
294                    };
295                }
296                if let Some(budget) = extract_optional_i32(payload, "token_budget") {
297                    self.token_budget = budget;
298                }
299                if let Some(used) = extract_optional_i32(payload, "tokens_used") {
300                    self.tokens_used = used;
301                }
302                if let Some(meta) = payload.get("metadata") {
303                    self.metadata = Some(meta.clone());
304                }
305            }
306            EventKind::SCOPE_CLOSED => {
307                self.is_active = false;
308                self.closed_at = Some(ts);
309            }
310            EventKind::SCOPE_CHECKPOINTED => {
311                // Checkpoint data is in the payload; extract if present
312                if let Some(cp_val) = payload.get("checkpoint") {
313                    if let Ok(cp) = serde_json::from_value(cp_val.clone()) {
314                        self.checkpoint = Some(cp);
315                    }
316                }
317            }
318            _ => {} // Skip unknown event kinds
319        }
320    }
321
322    fn relevant_event_kinds() -> &'static [EventKind] {
323        &[
324            EventKind::SCOPE_CREATED,
325            EventKind::SCOPE_UPDATED,
326            EventKind::SCOPE_CLOSED,
327            EventKind::SCOPE_CHECKPOINTED,
328        ]
329    }
330}
331
332// ============================================================================
333// Artifact EventSourced
334// ============================================================================
335
336impl EventSourced for Artifact {
337    fn from_events(events: &[Event<serde_json::Value>]) -> Option<Self> {
338        let relevant = sorted_relevant_events(events, Self::relevant_event_kinds());
339
340        let create_event = relevant
341            .iter()
342            .find(|e| e.header.event_kind == EventKind::ARTIFACT_CREATED)?;
343
344        let payload = &create_event.payload;
345        let artifact_id = extract_id::<ArtifactId>(payload, "artifact_id")?;
346        let trajectory_id = extract_id::<TrajectoryId>(payload, "trajectory_id")?;
347        let scope_id = extract_id::<ScopeId>(payload, "scope_id")?;
348        let created_at = timestamp_to_datetime(create_event.header.timestamp);
349
350        let artifact_type = payload
351            .get("artifact_type")
352            .and_then(|v| serde_json::from_value(v.clone()).ok())
353            .unwrap_or(ArtifactType::Custom);
354
355        let content_hash: ContentHash = payload
356            .get("content_hash")
357            .and_then(|v| serde_json::from_value(v.clone()).ok())
358            .unwrap_or_default();
359
360        let provenance = payload
361            .get("provenance")
362            .and_then(|v| serde_json::from_value(v.clone()).ok())
363            .unwrap_or(Provenance {
364                source_turn: 0,
365                extraction_method: ExtractionMethod::Unknown,
366                confidence: None,
367            });
368
369        let ttl = payload
370            .get("ttl")
371            .and_then(|v| serde_json::from_value(v.clone()).ok())
372            .unwrap_or(TTL::Persistent);
373
374        let mut artifact = Artifact {
375            artifact_id,
376            trajectory_id,
377            scope_id,
378            artifact_type,
379            name: extract_string(payload, "name").unwrap_or_default(),
380            content: extract_string(payload, "content").unwrap_or_default(),
381            content_hash,
382            embedding: None,
383            provenance,
384            ttl,
385            created_at,
386            updated_at: created_at,
387            superseded_by: None,
388            metadata: payload.get("metadata").cloned(),
389        };
390
391        // Track if deleted
392        let mut deleted = false;
393
394        for event in &relevant {
395            if event.header.event_kind == EventKind::ARTIFACT_CREATED {
396                continue;
397            }
398            if event.header.event_kind == EventKind::ARTIFACT_DELETED {
399                deleted = true;
400            }
401            artifact.apply_event(event);
402        }
403
404        if deleted {
405            None
406        } else {
407            Some(artifact)
408        }
409    }
410
411    fn apply_event(&mut self, event: &Event<serde_json::Value>) {
412        let payload = &event.payload;
413        let ts = timestamp_to_datetime(event.header.timestamp);
414
415        match event.header.event_kind {
416            EventKind::ARTIFACT_UPDATED => {
417                if let Some(name) = extract_string(payload, "name") {
418                    self.name = name;
419                }
420                if let Some(content) = extract_string(payload, "content") {
421                    self.content = content;
422                }
423                if let Some(hash) = payload
424                    .get("content_hash")
425                    .and_then(|v| serde_json::from_value(v.clone()).ok())
426                {
427                    self.content_hash = hash;
428                }
429                if let Some(meta) = payload.get("metadata") {
430                    self.metadata = Some(meta.clone());
431                }
432                self.updated_at = ts;
433            }
434            EventKind::ARTIFACT_SUPERSEDED => {
435                self.superseded_by = extract_id(payload, "superseded_by");
436                self.updated_at = ts;
437            }
438            EventKind::ARTIFACT_DELETED => {
439                // Mark updated_at for audit; caller checks deletion via from_events returning None
440                self.updated_at = ts;
441            }
442            _ => {} // Skip unknown event kinds
443        }
444    }
445
446    fn relevant_event_kinds() -> &'static [EventKind] {
447        &[
448            EventKind::ARTIFACT_CREATED,
449            EventKind::ARTIFACT_UPDATED,
450            EventKind::ARTIFACT_SUPERSEDED,
451            EventKind::ARTIFACT_DELETED,
452        ]
453    }
454}
455
456// ============================================================================
457// Note EventSourced
458// ============================================================================
459
460impl EventSourced for Note {
461    fn from_events(events: &[Event<serde_json::Value>]) -> Option<Self> {
462        let relevant = sorted_relevant_events(events, Self::relevant_event_kinds());
463
464        let create_event = relevant
465            .iter()
466            .find(|e| e.header.event_kind == EventKind::NOTE_CREATED)?;
467
468        let payload = &create_event.payload;
469        let note_id = extract_id::<NoteId>(payload, "note_id")?;
470        let created_at = timestamp_to_datetime(create_event.header.timestamp);
471
472        let note_type = payload
473            .get("note_type")
474            .and_then(|v| serde_json::from_value(v.clone()).ok())
475            .unwrap_or(NoteType::Insight);
476
477        let content_hash: ContentHash = payload
478            .get("content_hash")
479            .and_then(|v| serde_json::from_value(v.clone()).ok())
480            .unwrap_or_default();
481
482        let ttl = payload
483            .get("ttl")
484            .and_then(|v| serde_json::from_value(v.clone()).ok())
485            .unwrap_or(TTL::Persistent);
486
487        let abstraction_level = payload
488            .get("abstraction_level")
489            .and_then(|v| serde_json::from_value(v.clone()).ok())
490            .unwrap_or(AbstractionLevel::Raw);
491
492        let source_trajectory_ids: Vec<TrajectoryId> = payload
493            .get("source_trajectory_ids")
494            .and_then(|v| serde_json::from_value(v.clone()).ok())
495            .unwrap_or_default();
496
497        let source_artifact_ids: Vec<ArtifactId> = payload
498            .get("source_artifact_ids")
499            .and_then(|v| serde_json::from_value(v.clone()).ok())
500            .unwrap_or_default();
501
502        let source_note_ids: Vec<NoteId> = payload
503            .get("source_note_ids")
504            .and_then(|v| serde_json::from_value(v.clone()).ok())
505            .unwrap_or_default();
506
507        let mut note = Note {
508            note_id,
509            note_type,
510            title: extract_string(payload, "title").unwrap_or_default(),
511            content: extract_string(payload, "content").unwrap_or_default(),
512            content_hash,
513            embedding: None,
514            source_trajectory_ids,
515            source_artifact_ids,
516            ttl,
517            created_at,
518            updated_at: created_at,
519            accessed_at: created_at,
520            access_count: 0,
521            superseded_by: None,
522            metadata: payload.get("metadata").cloned(),
523            abstraction_level,
524            source_note_ids,
525        };
526
527        let mut deleted = false;
528
529        for event in &relevant {
530            if event.header.event_kind == EventKind::NOTE_CREATED {
531                continue;
532            }
533            if event.header.event_kind == EventKind::NOTE_DELETED {
534                deleted = true;
535            }
536            note.apply_event(event);
537        }
538
539        if deleted {
540            None
541        } else {
542            Some(note)
543        }
544    }
545
546    fn apply_event(&mut self, event: &Event<serde_json::Value>) {
547        let payload = &event.payload;
548        let ts = timestamp_to_datetime(event.header.timestamp);
549
550        match event.header.event_kind {
551            EventKind::NOTE_UPDATED => {
552                if let Some(title) = extract_string(payload, "title") {
553                    self.title = title;
554                }
555                if let Some(content) = extract_string(payload, "content") {
556                    self.content = content;
557                }
558                if let Some(hash) = payload
559                    .get("content_hash")
560                    .and_then(|v| serde_json::from_value(v.clone()).ok())
561                {
562                    self.content_hash = hash;
563                }
564                if let Some(meta) = payload.get("metadata") {
565                    self.metadata = Some(meta.clone());
566                }
567                self.updated_at = ts;
568            }
569            EventKind::NOTE_SUPERSEDED => {
570                self.superseded_by = extract_id(payload, "superseded_by");
571                self.updated_at = ts;
572            }
573            EventKind::NOTE_ACCESSED => {
574                self.accessed_at = ts;
575                self.access_count += 1;
576            }
577            EventKind::NOTE_DELETED => {
578                self.updated_at = ts;
579            }
580            _ => {} // Skip unknown event kinds
581        }
582    }
583
584    fn relevant_event_kinds() -> &'static [EventKind] {
585        &[
586            EventKind::NOTE_CREATED,
587            EventKind::NOTE_UPDATED,
588            EventKind::NOTE_SUPERSEDED,
589            EventKind::NOTE_ACCESSED,
590            EventKind::NOTE_DELETED,
591        ]
592    }
593}
594
595// ============================================================================
596// TESTS
597// ============================================================================
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602    use crate::event::{DagPosition, EventFlags, EventHeader};
603    use crate::EventId;
604    use serde_json::json;
605
606    // Helper to create a test event with specific kind, timestamp, and payload
607    fn make_event(
608        event_kind: EventKind,
609        timestamp_micros: i64,
610        payload: serde_json::Value,
611    ) -> Event<serde_json::Value> {
612        Event::new(
613            EventHeader::new(
614                EventId::now_v7(),
615                EventId::now_v7(),
616                timestamp_micros,
617                DagPosition::root(),
618                0,
619                event_kind,
620                EventFlags::empty(),
621                None,
622            ),
623            payload,
624        )
625    }
626
627    // Helper: base timestamp for tests (2025-01-01T00:00:00Z in microseconds)
628    fn base_ts() -> i64 {
629        1_735_689_600_000_000
630    }
631
632    // ========================================================================
633    // Trajectory Tests
634    // ========================================================================
635
636    #[test]
637    fn test_trajectory_from_events_empty() {
638        let events: Vec<Event<serde_json::Value>> = vec![];
639        assert!(Trajectory::from_events(&events).is_none());
640    }
641
642    #[test]
643    fn test_trajectory_from_events_missing_creation() {
644        let events = vec![make_event(
645            EventKind::TRAJECTORY_UPDATED,
646            base_ts(),
647            json!({"trajectory_id": TrajectoryId::now_v7(), "name": "updated"}),
648        )];
649        assert!(Trajectory::from_events(&events).is_none());
650    }
651
652    #[test]
653    fn test_trajectory_from_creation_event() {
654        let tid = TrajectoryId::now_v7();
655        let events = vec![make_event(
656            EventKind::TRAJECTORY_CREATED,
657            base_ts(),
658            json!({
659                "trajectory_id": tid,
660                "name": "Test Trajectory",
661                "description": "A test",
662            }),
663        )];
664
665        let trajectory = Trajectory::from_events(&events).unwrap();
666        assert_eq!(trajectory.trajectory_id, tid);
667        assert_eq!(trajectory.name, "Test Trajectory");
668        assert_eq!(trajectory.description.as_deref(), Some("A test"));
669        assert_eq!(trajectory.status, TrajectoryStatus::Active);
670        assert!(trajectory.completed_at.is_none());
671    }
672
673    #[test]
674    fn test_trajectory_updated() {
675        let tid = TrajectoryId::now_v7();
676        let events = vec![
677            make_event(
678                EventKind::TRAJECTORY_CREATED,
679                base_ts(),
680                json!({
681                    "trajectory_id": tid,
682                    "name": "Original",
683                }),
684            ),
685            make_event(
686                EventKind::TRAJECTORY_UPDATED,
687                base_ts() + 1_000_000,
688                json!({
689                    "name": "Renamed",
690                    "description": "Now has a description",
691                }),
692            ),
693        ];
694
695        let trajectory = Trajectory::from_events(&events).unwrap();
696        assert_eq!(trajectory.name, "Renamed");
697        assert_eq!(
698            trajectory.description.as_deref(),
699            Some("Now has a description")
700        );
701    }
702
703    #[test]
704    fn test_trajectory_completed() {
705        let tid = TrajectoryId::now_v7();
706        let events = vec![
707            make_event(
708                EventKind::TRAJECTORY_CREATED,
709                base_ts(),
710                json!({ "trajectory_id": tid, "name": "Task" }),
711            ),
712            make_event(
713                EventKind::TRAJECTORY_COMPLETED,
714                base_ts() + 2_000_000,
715                json!({
716                    "outcome": {
717                        "status": "success",
718                        "summary": "Done",
719                        "produced_artifacts": [],
720                        "produced_notes": [],
721                        "error": null,
722                    }
723                }),
724            ),
725        ];
726
727        let trajectory = Trajectory::from_events(&events).unwrap();
728        assert_eq!(trajectory.status, TrajectoryStatus::Completed);
729        assert!(trajectory.completed_at.is_some());
730        assert!(trajectory.outcome.is_some());
731        assert_eq!(trajectory.outcome.unwrap().summary, "Done");
732    }
733
734    #[test]
735    fn test_trajectory_failed() {
736        let tid = TrajectoryId::now_v7();
737        let events = vec![
738            make_event(
739                EventKind::TRAJECTORY_CREATED,
740                base_ts(),
741                json!({ "trajectory_id": tid, "name": "Failing Task" }),
742            ),
743            make_event(
744                EventKind::TRAJECTORY_FAILED,
745                base_ts() + 1_000_000,
746                json!({ "error": "Something went wrong" }),
747            ),
748        ];
749
750        let trajectory = Trajectory::from_events(&events).unwrap();
751        assert_eq!(trajectory.status, TrajectoryStatus::Failed);
752        assert!(trajectory.completed_at.is_some());
753        assert!(trajectory.outcome.is_some());
754    }
755
756    #[test]
757    fn test_trajectory_out_of_order_events() {
758        let tid = TrajectoryId::now_v7();
759        // Events delivered out of order (completed before created in array)
760        let events = vec![
761            make_event(
762                EventKind::TRAJECTORY_COMPLETED,
763                base_ts() + 2_000_000,
764                json!({}),
765            ),
766            make_event(
767                EventKind::TRAJECTORY_CREATED,
768                base_ts(),
769                json!({ "trajectory_id": tid, "name": "Out of order" }),
770            ),
771            make_event(
772                EventKind::TRAJECTORY_UPDATED,
773                base_ts() + 1_000_000,
774                json!({ "name": "Updated name" }),
775            ),
776        ];
777
778        let trajectory = Trajectory::from_events(&events).unwrap();
779        // Should have processed in timestamp order: created -> updated -> completed
780        assert_eq!(trajectory.status, TrajectoryStatus::Completed);
781        assert_eq!(trajectory.name, "Updated name");
782    }
783
784    #[test]
785    fn test_trajectory_ignores_irrelevant_events() {
786        let tid = TrajectoryId::now_v7();
787        let events = vec![
788            make_event(
789                EventKind::TRAJECTORY_CREATED,
790                base_ts(),
791                json!({ "trajectory_id": tid, "name": "Test" }),
792            ),
793            make_event(
794                EventKind::SCOPE_CREATED,
795                base_ts() + 500_000,
796                json!({ "scope_id": ScopeId::now_v7() }),
797            ),
798            make_event(
799                EventKind::SYSTEM_HEARTBEAT,
800                base_ts() + 1_000_000,
801                json!({}),
802            ),
803        ];
804
805        let trajectory = Trajectory::from_events(&events).unwrap();
806        assert_eq!(trajectory.name, "Test");
807        assert_eq!(trajectory.status, TrajectoryStatus::Active);
808    }
809
810    #[test]
811    fn test_trajectory_suspend_resume() {
812        let tid = TrajectoryId::now_v7();
813        let events = vec![
814            make_event(
815                EventKind::TRAJECTORY_CREATED,
816                base_ts(),
817                json!({ "trajectory_id": tid, "name": "Suspendable" }),
818            ),
819            make_event(
820                EventKind::TRAJECTORY_SUSPENDED,
821                base_ts() + 1_000_000,
822                json!({}),
823            ),
824            make_event(
825                EventKind::TRAJECTORY_RESUMED,
826                base_ts() + 2_000_000,
827                json!({}),
828            ),
829        ];
830
831        let trajectory = Trajectory::from_events(&events).unwrap();
832        assert_eq!(trajectory.status, TrajectoryStatus::Active);
833    }
834
835    // ========================================================================
836    // Scope Tests
837    // ========================================================================
838
839    #[test]
840    fn test_scope_from_events_empty() {
841        let events: Vec<Event<serde_json::Value>> = vec![];
842        assert!(Scope::from_events(&events).is_none());
843    }
844
845    #[test]
846    fn test_scope_from_creation_event() {
847        let sid = ScopeId::now_v7();
848        let tid = TrajectoryId::now_v7();
849        let events = vec![make_event(
850            EventKind::SCOPE_CREATED,
851            base_ts(),
852            json!({
853                "scope_id": sid,
854                "trajectory_id": tid,
855                "name": "Main Scope",
856                "purpose": "Primary workspace",
857                "token_budget": 4096,
858            }),
859        )];
860
861        let scope = Scope::from_events(&events).unwrap();
862        assert_eq!(scope.scope_id, sid);
863        assert_eq!(scope.trajectory_id, tid);
864        assert_eq!(scope.name, "Main Scope");
865        assert_eq!(scope.purpose.as_deref(), Some("Primary workspace"));
866        assert!(scope.is_active);
867        assert!(scope.closed_at.is_none());
868        assert_eq!(scope.token_budget, 4096);
869    }
870
871    #[test]
872    fn test_scope_updated_then_closed() {
873        let sid = ScopeId::now_v7();
874        let tid = TrajectoryId::now_v7();
875        let events = vec![
876            make_event(
877                EventKind::SCOPE_CREATED,
878                base_ts(),
879                json!({
880                    "scope_id": sid,
881                    "trajectory_id": tid,
882                    "name": "Scope",
883                    "token_budget": 4096,
884                }),
885            ),
886            make_event(
887                EventKind::SCOPE_UPDATED,
888                base_ts() + 1_000_000,
889                json!({
890                    "name": "Updated Scope",
891                    "tokens_used": 1024,
892                }),
893            ),
894            make_event(EventKind::SCOPE_CLOSED, base_ts() + 2_000_000, json!({})),
895        ];
896
897        let scope = Scope::from_events(&events).unwrap();
898        assert_eq!(scope.name, "Updated Scope");
899        assert_eq!(scope.tokens_used, 1024);
900        assert!(!scope.is_active);
901        assert!(scope.closed_at.is_some());
902    }
903
904    #[test]
905    fn test_scope_out_of_order() {
906        let sid = ScopeId::now_v7();
907        let tid = TrajectoryId::now_v7();
908        let events = vec![
909            make_event(EventKind::SCOPE_CLOSED, base_ts() + 2_000_000, json!({})),
910            make_event(
911                EventKind::SCOPE_CREATED,
912                base_ts(),
913                json!({
914                    "scope_id": sid,
915                    "trajectory_id": tid,
916                    "name": "Scope",
917                }),
918            ),
919        ];
920
921        let scope = Scope::from_events(&events).unwrap();
922        assert!(!scope.is_active);
923        assert!(scope.closed_at.is_some());
924    }
925
926    // ========================================================================
927    // Artifact Tests
928    // ========================================================================
929
930    #[test]
931    fn test_artifact_from_events_empty() {
932        let events: Vec<Event<serde_json::Value>> = vec![];
933        assert!(Artifact::from_events(&events).is_none());
934    }
935
936    #[test]
937    fn test_artifact_from_creation_event() {
938        let aid = ArtifactId::now_v7();
939        let tid = TrajectoryId::now_v7();
940        let sid = ScopeId::now_v7();
941        let events = vec![make_event(
942            EventKind::ARTIFACT_CREATED,
943            base_ts(),
944            json!({
945                "artifact_id": aid,
946                "trajectory_id": tid,
947                "scope_id": sid,
948                "artifact_type": "fact",
949                "name": "Test Artifact",
950                "content": "Some content",
951                "ttl": "persistent",
952            }),
953        )];
954
955        let artifact = Artifact::from_events(&events).unwrap();
956        assert_eq!(artifact.artifact_id, aid);
957        assert_eq!(artifact.trajectory_id, tid);
958        assert_eq!(artifact.scope_id, sid);
959        assert_eq!(artifact.name, "Test Artifact");
960        assert_eq!(artifact.content, "Some content");
961        assert_eq!(artifact.artifact_type, ArtifactType::Fact);
962    }
963
964    #[test]
965    fn test_artifact_updated() {
966        let aid = ArtifactId::now_v7();
967        let tid = TrajectoryId::now_v7();
968        let sid = ScopeId::now_v7();
969        let events = vec![
970            make_event(
971                EventKind::ARTIFACT_CREATED,
972                base_ts(),
973                json!({
974                    "artifact_id": aid,
975                    "trajectory_id": tid,
976                    "scope_id": sid,
977                    "name": "Original",
978                    "content": "Original content",
979                }),
980            ),
981            make_event(
982                EventKind::ARTIFACT_UPDATED,
983                base_ts() + 1_000_000,
984                json!({
985                    "name": "Updated",
986                    "content": "New content",
987                }),
988            ),
989        ];
990
991        let artifact = Artifact::from_events(&events).unwrap();
992        assert_eq!(artifact.name, "Updated");
993        assert_eq!(artifact.content, "New content");
994    }
995
996    #[test]
997    fn test_artifact_deleted_returns_none() {
998        let aid = ArtifactId::now_v7();
999        let tid = TrajectoryId::now_v7();
1000        let sid = ScopeId::now_v7();
1001        let events = vec![
1002            make_event(
1003                EventKind::ARTIFACT_CREATED,
1004                base_ts(),
1005                json!({
1006                    "artifact_id": aid,
1007                    "trajectory_id": tid,
1008                    "scope_id": sid,
1009                    "name": "Doomed",
1010                    "content": "Will be deleted",
1011                }),
1012            ),
1013            make_event(
1014                EventKind::ARTIFACT_DELETED,
1015                base_ts() + 1_000_000,
1016                json!({}),
1017            ),
1018        ];
1019
1020        assert!(Artifact::from_events(&events).is_none());
1021    }
1022
1023    #[test]
1024    fn test_artifact_superseded() {
1025        let aid = ArtifactId::now_v7();
1026        let new_aid = ArtifactId::now_v7();
1027        let tid = TrajectoryId::now_v7();
1028        let sid = ScopeId::now_v7();
1029        let events = vec![
1030            make_event(
1031                EventKind::ARTIFACT_CREATED,
1032                base_ts(),
1033                json!({
1034                    "artifact_id": aid,
1035                    "trajectory_id": tid,
1036                    "scope_id": sid,
1037                    "name": "V1",
1038                    "content": "Version 1",
1039                }),
1040            ),
1041            make_event(
1042                EventKind::ARTIFACT_SUPERSEDED,
1043                base_ts() + 1_000_000,
1044                json!({ "superseded_by": new_aid }),
1045            ),
1046        ];
1047
1048        let artifact = Artifact::from_events(&events).unwrap();
1049        assert_eq!(artifact.superseded_by, Some(new_aid));
1050    }
1051
1052    // ========================================================================
1053    // Note Tests
1054    // ========================================================================
1055
1056    #[test]
1057    fn test_note_from_events_empty() {
1058        let events: Vec<Event<serde_json::Value>> = vec![];
1059        assert!(Note::from_events(&events).is_none());
1060    }
1061
1062    #[test]
1063    fn test_note_from_creation_event() {
1064        let nid = NoteId::now_v7();
1065        let events = vec![make_event(
1066            EventKind::NOTE_CREATED,
1067            base_ts(),
1068            json!({
1069                "note_id": nid,
1070                "note_type": "insight",
1071                "title": "Test Note",
1072                "content": "Some insight",
1073                "abstraction_level": "raw",
1074            }),
1075        )];
1076
1077        let note = Note::from_events(&events).unwrap();
1078        assert_eq!(note.note_id, nid);
1079        assert_eq!(note.title, "Test Note");
1080        assert_eq!(note.content, "Some insight");
1081        assert_eq!(note.note_type, NoteType::Insight);
1082        assert_eq!(note.abstraction_level, AbstractionLevel::Raw);
1083        assert_eq!(note.access_count, 0);
1084    }
1085
1086    #[test]
1087    fn test_note_updated() {
1088        let nid = NoteId::now_v7();
1089        let events = vec![
1090            make_event(
1091                EventKind::NOTE_CREATED,
1092                base_ts(),
1093                json!({
1094                    "note_id": nid,
1095                    "title": "Original",
1096                    "content": "Original content",
1097                }),
1098            ),
1099            make_event(
1100                EventKind::NOTE_UPDATED,
1101                base_ts() + 1_000_000,
1102                json!({
1103                    "title": "Updated Title",
1104                    "content": "New content",
1105                }),
1106            ),
1107        ];
1108
1109        let note = Note::from_events(&events).unwrap();
1110        assert_eq!(note.title, "Updated Title");
1111        assert_eq!(note.content, "New content");
1112    }
1113
1114    #[test]
1115    fn test_note_accessed_increments_count() {
1116        let nid = NoteId::now_v7();
1117        let events = vec![
1118            make_event(
1119                EventKind::NOTE_CREATED,
1120                base_ts(),
1121                json!({
1122                    "note_id": nid,
1123                    "title": "Popular Note",
1124                    "content": "Accessed often",
1125                }),
1126            ),
1127            make_event(EventKind::NOTE_ACCESSED, base_ts() + 1_000_000, json!({})),
1128            make_event(EventKind::NOTE_ACCESSED, base_ts() + 2_000_000, json!({})),
1129            make_event(EventKind::NOTE_ACCESSED, base_ts() + 3_000_000, json!({})),
1130        ];
1131
1132        let note = Note::from_events(&events).unwrap();
1133        assert_eq!(note.access_count, 3);
1134    }
1135
1136    #[test]
1137    fn test_note_deleted_returns_none() {
1138        let nid = NoteId::now_v7();
1139        let events = vec![
1140            make_event(
1141                EventKind::NOTE_CREATED,
1142                base_ts(),
1143                json!({
1144                    "note_id": nid,
1145                    "title": "Doomed Note",
1146                    "content": "Will be deleted",
1147                }),
1148            ),
1149            make_event(EventKind::NOTE_DELETED, base_ts() + 1_000_000, json!({})),
1150        ];
1151
1152        assert!(Note::from_events(&events).is_none());
1153    }
1154
1155    #[test]
1156    fn test_note_superseded() {
1157        let nid = NoteId::now_v7();
1158        let new_nid = NoteId::now_v7();
1159        let events = vec![
1160            make_event(
1161                EventKind::NOTE_CREATED,
1162                base_ts(),
1163                json!({
1164                    "note_id": nid,
1165                    "title": "V1",
1166                    "content": "Version 1",
1167                }),
1168            ),
1169            make_event(
1170                EventKind::NOTE_SUPERSEDED,
1171                base_ts() + 1_000_000,
1172                json!({ "superseded_by": new_nid }),
1173            ),
1174        ];
1175
1176        let note = Note::from_events(&events).unwrap();
1177        assert_eq!(note.superseded_by, Some(new_nid));
1178    }
1179
1180    #[test]
1181    fn test_note_out_of_order_events() {
1182        let nid = NoteId::now_v7();
1183        let events = vec![
1184            // Accessed event before creation in the array
1185            make_event(EventKind::NOTE_ACCESSED, base_ts() + 2_000_000, json!({})),
1186            make_event(
1187                EventKind::NOTE_UPDATED,
1188                base_ts() + 1_000_000,
1189                json!({ "title": "Updated" }),
1190            ),
1191            make_event(
1192                EventKind::NOTE_CREATED,
1193                base_ts(),
1194                json!({
1195                    "note_id": nid,
1196                    "title": "Original",
1197                    "content": "Content",
1198                }),
1199            ),
1200        ];
1201
1202        let note = Note::from_events(&events).unwrap();
1203        // Should process in timestamp order: created -> updated -> accessed
1204        assert_eq!(note.title, "Updated");
1205        assert_eq!(note.access_count, 1);
1206    }
1207
1208    // ========================================================================
1209    // EventSourced trait tests
1210    // ========================================================================
1211
1212    #[test]
1213    fn test_trajectory_relevant_event_kinds() {
1214        let kinds = Trajectory::relevant_event_kinds();
1215        assert!(kinds.contains(&EventKind::TRAJECTORY_CREATED));
1216        assert!(kinds.contains(&EventKind::TRAJECTORY_UPDATED));
1217        assert!(kinds.contains(&EventKind::TRAJECTORY_COMPLETED));
1218        assert!(kinds.contains(&EventKind::TRAJECTORY_FAILED));
1219        assert!(kinds.contains(&EventKind::TRAJECTORY_DELETED));
1220        assert!(!kinds.contains(&EventKind::SCOPE_CREATED));
1221    }
1222
1223    #[test]
1224    fn test_scope_relevant_event_kinds() {
1225        let kinds = Scope::relevant_event_kinds();
1226        assert!(kinds.contains(&EventKind::SCOPE_CREATED));
1227        assert!(kinds.contains(&EventKind::SCOPE_UPDATED));
1228        assert!(kinds.contains(&EventKind::SCOPE_CLOSED));
1229        assert!(kinds.contains(&EventKind::SCOPE_CHECKPOINTED));
1230        assert!(!kinds.contains(&EventKind::TRAJECTORY_CREATED));
1231    }
1232
1233    #[test]
1234    fn test_artifact_relevant_event_kinds() {
1235        let kinds = Artifact::relevant_event_kinds();
1236        assert!(kinds.contains(&EventKind::ARTIFACT_CREATED));
1237        assert!(kinds.contains(&EventKind::ARTIFACT_UPDATED));
1238        assert!(kinds.contains(&EventKind::ARTIFACT_SUPERSEDED));
1239        assert!(kinds.contains(&EventKind::ARTIFACT_DELETED));
1240        assert!(!kinds.contains(&EventKind::NOTE_CREATED));
1241    }
1242
1243    #[test]
1244    fn test_note_relevant_event_kinds() {
1245        let kinds = Note::relevant_event_kinds();
1246        assert!(kinds.contains(&EventKind::NOTE_CREATED));
1247        assert!(kinds.contains(&EventKind::NOTE_UPDATED));
1248        assert!(kinds.contains(&EventKind::NOTE_SUPERSEDED));
1249        assert!(kinds.contains(&EventKind::NOTE_ACCESSED));
1250        assert!(kinds.contains(&EventKind::NOTE_DELETED));
1251        assert!(!kinds.contains(&EventKind::ARTIFACT_CREATED));
1252    }
1253}