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