Skip to main content

made_core/entities/
ceremony_event.rs

1//! [`CeremonyEvent`] — one thing that happened to a ceremony, with
2//! everything it changed.
3//!
4//! The journal used to record *that* something happened; the event is
5//! *what*. Sealed inside an [`AuditRecord`](super::AuditRecord), it is
6//! what a fold replays and what the chain's digest now covers.
7
8use serde::{Deserialize, Serialize};
9
10use crate::value_objects::{AuditEventType, EventSchemaVersion};
11
12use super::ceremony_events::{
13    CeremonyCancelled, CeremonyCompleted, CeremonyDeadlineExceeded, CeremonyInstanceStarted,
14    CeremonyPaused, CeremonyResumed, ChildCompletionAccepted, ChildSpawnPlanAdopted,
15    ChildSpawnPlanned, ContextWritten, EvidenceCollected, ExecutionReceiptLinked,
16    HumanApprovalRecorded, HumanDeferralRecorded, InstanceImported, InterventionClosed,
17    InterventionRequested, InterventionResponded, LateStepResultObserved, MemoryRecalled,
18    ParticipantsBound, ReasonAsserted, StateDeadlineExceeded, StateIterationStarted, StepCompleted,
19    StepDeadlineExceeded, StepFailed, StepStarted, TransitionApplied,
20};
21
22/// A fact a ceremony's stream can hold, with its full payload.
23///
24/// One variant per [`AuditEventType`] the engine seals today, under the
25/// same name, and tagged on the wire with the same snake_case string
26/// `AuditEventType::as_str` returns. Three types of the catalogue have
27/// no variant because nothing produces them: `CeremonyDefinitionValidated`
28/// and `CeremonyDefinitionPublished` belong to the definition catalogue,
29/// not to a ceremony's stream, and `CeremonyFailed` has no producer —
30/// a ceremony reaches its end only by a transition into a terminal
31/// state, which is a completion. They join when something can emit
32/// them.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(tag = "type", rename_all = "snake_case")]
35pub enum CeremonyEvent {
36    CeremonyInstanceStarted(CeremonyInstanceStarted),
37    ParticipantsBound(ParticipantsBound),
38    StepStarted(StepStarted),
39    StepCompleted(StepCompleted),
40    StepFailed(StepFailed),
41    ContextWritten(ContextWritten),
42    StateIterationStarted(StateIterationStarted),
43    TransitionApplied(TransitionApplied),
44    InterventionRequested(InterventionRequested),
45    InterventionResponded(InterventionResponded),
46    InterventionClosed(InterventionClosed),
47    EvidenceCollected(EvidenceCollected),
48    ReasonAsserted(ReasonAsserted),
49    HumanApprovalRecorded(HumanApprovalRecorded),
50    HumanDeferralRecorded(HumanDeferralRecorded),
51    CeremonyCompleted(CeremonyCompleted),
52    InstanceImported(InstanceImported),
53    MemoryRecalled(MemoryRecalled),
54    ChildSpawnPlanned(ChildSpawnPlanned),
55    ChildSpawnPlanAdopted(ChildSpawnPlanAdopted),
56    ChildCompletionAccepted(ChildCompletionAccepted),
57    CeremonyPaused(CeremonyPaused),
58    CeremonyResumed(CeremonyResumed),
59    CeremonyCancelled(CeremonyCancelled),
60    CeremonyDeadlineExceeded(CeremonyDeadlineExceeded),
61    StateDeadlineExceeded(StateDeadlineExceeded),
62    StepDeadlineExceeded(StepDeadlineExceeded),
63    LateStepResultObserved(LateStepResultObserved),
64    ExecutionReceiptLinked(ExecutionReceiptLinked),
65}
66
67impl CeremonyEvent {
68    /// The catalogue entry this event is an instance of.
69    #[must_use]
70    pub fn event_type(&self) -> AuditEventType {
71        match self {
72            Self::CeremonyInstanceStarted(_) => AuditEventType::CeremonyInstanceStarted,
73            Self::ParticipantsBound(_) => AuditEventType::ParticipantsBound,
74            Self::StepStarted(_) => AuditEventType::StepStarted,
75            Self::StepCompleted(_) => AuditEventType::StepCompleted,
76            Self::StepFailed(_) => AuditEventType::StepFailed,
77            Self::ContextWritten(_) => AuditEventType::ContextWritten,
78            Self::StateIterationStarted(_) => AuditEventType::StateIterationStarted,
79            Self::TransitionApplied(_) => AuditEventType::TransitionApplied,
80            Self::InterventionRequested(_) => AuditEventType::InterventionRequested,
81            Self::InterventionResponded(_) => AuditEventType::InterventionResponded,
82            Self::InterventionClosed(_) => AuditEventType::InterventionClosed,
83            Self::EvidenceCollected(_) => AuditEventType::EvidenceCollected,
84            Self::ReasonAsserted(_) => AuditEventType::ReasonAsserted,
85            Self::HumanApprovalRecorded(_) => AuditEventType::HumanApprovalRecorded,
86            Self::HumanDeferralRecorded(_) => AuditEventType::HumanDeferralRecorded,
87            Self::CeremonyCompleted(_) => AuditEventType::CeremonyCompleted,
88            Self::InstanceImported(_) => AuditEventType::InstanceImported,
89            Self::MemoryRecalled(_) => AuditEventType::MemoryRecalled,
90            Self::ChildSpawnPlanned(_) => AuditEventType::ChildSpawnPlanned,
91            Self::ChildSpawnPlanAdopted(_) => AuditEventType::ChildSpawnPlanAdopted,
92            Self::ChildCompletionAccepted(_) => AuditEventType::ChildCompletionAccepted,
93            Self::CeremonyPaused(_) => AuditEventType::CeremonyPaused,
94            Self::CeremonyResumed(_) => AuditEventType::CeremonyResumed,
95            Self::CeremonyCancelled(_) => AuditEventType::CeremonyCancelled,
96            Self::CeremonyDeadlineExceeded(_) => AuditEventType::CeremonyDeadlineExceeded,
97            Self::StateDeadlineExceeded(_) => AuditEventType::StateDeadlineExceeded,
98            Self::StepDeadlineExceeded(_) => AuditEventType::StepDeadlineExceeded,
99            Self::LateStepResultObserved(_) => AuditEventType::LateStepResultObserved,
100            Self::ExecutionReceiptLinked(_) => AuditEventType::ExecutionReceiptLinked,
101        }
102    }
103
104    /// The shape this event's payload is written in.
105    ///
106    /// Listed per variant rather than as one constant, so a payload
107    /// that changes shape bumps its own version and nobody else's.
108    #[must_use]
109    pub fn schema_version(&self) -> EventSchemaVersion {
110        match self {
111            Self::StepStarted(event) => {
112                if event.budget_reservation_id.is_some() {
113                    EventSchemaVersion::V6
114                } else if event.deadline.is_some() {
115                    EventSchemaVersion::V5
116                } else if event.state_visit.is_some() {
117                    EventSchemaVersion::V4
118                } else if event.role_from.is_some() || event.sealed_role.is_some() {
119                    EventSchemaVersion::V3
120                } else {
121                    event
122                        .state_iteration
123                        .map_or(EventSchemaVersion::V1, |_| EventSchemaVersion::V2)
124                }
125            }
126            Self::StepCompleted(event) => {
127                if event.state_visit.is_some() {
128                    EventSchemaVersion::V3
129                } else {
130                    event
131                        .state_iteration
132                        .map_or(EventSchemaVersion::V1, |_| EventSchemaVersion::V2)
133                }
134            }
135            Self::StepFailed(event) => {
136                if event.result.failure_kind().is_some() {
137                    EventSchemaVersion::V4
138                } else if event.state_visit.is_some() {
139                    EventSchemaVersion::V3
140                } else {
141                    event
142                        .state_iteration
143                        .map_or(EventSchemaVersion::V1, |_| EventSchemaVersion::V2)
144                }
145            }
146            Self::TransitionApplied(event) => {
147                if event
148                    .destination
149                    .as_ref()
150                    .is_some_and(|destination| destination.deadline.is_some())
151                {
152                    EventSchemaVersion::V4
153                } else if event.destination.is_some() || event.transition.has_explicit_state_visit()
154                {
155                    EventSchemaVersion::V3
156                } else if event.transition.has_explicit_state_iteration() {
157                    EventSchemaVersion::V2
158                } else {
159                    EventSchemaVersion::V1
160                }
161            }
162            Self::StateIterationStarted(event) => event
163                .state_visit
164                .map_or(EventSchemaVersion::V1, |_| EventSchemaVersion::V2),
165            Self::ContextWritten(event) => event
166                .state_visit
167                .map_or(EventSchemaVersion::V1, |_| EventSchemaVersion::V2),
168            Self::CeremonyInstanceStarted(event) if event.budget_account_id.is_some() => {
169                EventSchemaVersion::V4
170            }
171            Self::CeremonyInstanceStarted(event)
172                if event.ceremony_deadline.is_some() || event.state_deadline.is_some() =>
173            {
174                EventSchemaVersion::V3
175            }
176            Self::CeremonyInstanceStarted(event) if event.lineage.is_some() => {
177                EventSchemaVersion::V2
178            }
179            Self::CeremonyInstanceStarted(_)
180            | Self::ParticipantsBound(_)
181            | Self::InterventionRequested(_)
182            | Self::InterventionResponded(_)
183            | Self::InterventionClosed(_)
184            | Self::EvidenceCollected(_)
185            | Self::ReasonAsserted(_)
186            | Self::HumanApprovalRecorded(_)
187            | Self::HumanDeferralRecorded(_)
188            | Self::CeremonyCompleted(_)
189            | Self::InstanceImported(_)
190            | Self::MemoryRecalled(_)
191            | Self::ChildSpawnPlanned(_)
192            | Self::ChildSpawnPlanAdopted(_)
193            | Self::ChildCompletionAccepted(_)
194            | Self::CeremonyPaused(_)
195            | Self::CeremonyResumed(_)
196            | Self::CeremonyCancelled(_)
197            | Self::CeremonyDeadlineExceeded(_)
198            | Self::StateDeadlineExceeded(_)
199            | Self::StepDeadlineExceeded(_)
200            | Self::LateStepResultObserved(_)
201            | Self::ExecutionReceiptLinked(_) => EventSchemaVersion::V1,
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::value_objects::StateId;
210    use time::macros::datetime;
211
212    fn completed() -> CeremonyEvent {
213        CeremonyEvent::CeremonyCompleted(CeremonyCompleted {
214            final_state: StateId::new("DONE").unwrap(),
215            completed_at: datetime!(2026-07-29 09:00:00 UTC),
216        })
217    }
218
219    #[test]
220    fn the_wire_tag_is_the_event_type_name() {
221        let json = serde_json::to_value(completed()).unwrap();
222
223        assert_eq!(json["type"], AuditEventType::CeremonyCompleted.as_str());
224        assert_eq!(json["final_state"], "DONE");
225        assert_eq!(json["completed_at"], "2026-07-29T09:00:00Z");
226    }
227
228    #[test]
229    fn every_payload_is_at_version_one_today() {
230        assert_eq!(completed().schema_version(), EventSchemaVersion::V1);
231        assert_eq!(completed().event_type(), AuditEventType::CeremonyCompleted);
232    }
233
234    #[test]
235    fn a_tag_with_a_foreign_shape_does_not_deserialize() {
236        let json = serde_json::json!({ "type": "ceremony_completed", "final_state": "DONE" });
237
238        assert!(serde_json::from_value::<CeremonyEvent>(json).is_err());
239    }
240}