1use crate::entities::{Artifact, Note, Provenance, Scope, Trajectory, TrajectoryOutcome};
24use crate::event::{Event, EventKind};
25use crate::*;
26use serde::de::DeserializeOwned;
27
28pub trait EventSourced: Sized {
30 fn from_events(events: &[Event<serde_json::Value>]) -> Option<Self>;
35
36 fn apply_event(&mut self, event: &Event<serde_json::Value>);
38
39 fn relevant_event_kinds() -> &'static [EventKind];
41}
42
43fn 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
56fn 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
67fn 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
75fn extract_required_string(payload: &serde_json::Value, field: &str) -> Option<String> {
77 extract_string(payload, field)
78}
79
80fn 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
91fn 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
104fn 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
120fn timestamp_to_datetime(ts: i64) -> Option<Timestamp> {
122 chrono::DateTime::from_timestamp_micros(ts)
123}
124
125impl 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 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 for event in &relevant {
160 if event.header.event_kind != EventKind::TRAJECTORY_CREATED {
161 trajectory.apply_event(event);
162 }
163 }
164
165 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 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 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 self.completed_at = Some(ts);
240 self.updated_at = ts;
241 }
242 _ => {} }
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
259impl 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 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 _ => {} }
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
354impl 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 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 self.updated_at = ts;
456 }
457 _ => {} }
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
471impl 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 _ => {} }
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#[cfg(test)]
605mod tests {
606 use super::*;
607 use crate::event::{DagPosition, EventHeader};
608 use crate::EventId;
609 use serde_json::json;
610
611 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 fn base_ts() -> i64 {
632 1_735_689_600_000_000
633 }
634
635 #[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 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 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 #[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 #[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 #[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 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 assert_eq!(note.title, "Updated");
1226 assert_eq!(note.access_count, 1);
1227 }
1228
1229 #[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}