1use 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#[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 #[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 #[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}