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