1use std::collections::{BTreeMap, BTreeSet};
17
18use serde::{Deserialize, Serialize};
19use time::OffsetDateTime;
20
21use super::{
22 ceremony_definition::CeremonyDefinition, CeremonyEvent, CeremonyEvidencePack,
23 CeremonyIntervention, PublishedCeremonyDefinition,
24};
25use crate::error::DomainError;
26use crate::ports::CeremonyEvidenceRequest;
27use crate::value_objects::{
28 AuditActorKind, BudgetAccountId, CeremonyContext, CeremonyDeadline, CeremonyDefinitionDigest,
29 CeremonyEvidenceSourceId, CeremonyGuardApproval, CeremonyGuardDeferral,
30 CeremonyGuardDeferralContent, CeremonyId, CeremonyInterventionContent, CeremonyInterventionId,
31 CeremonyInterventionKind, CeremonyInterventionProvenance, CeremonyInterventionTarget,
32 CeremonyLifecycle, CeremonyLineage, CeremonyName, CeremonyParticipantBinding, CeremonyReason,
33 CeremonyReasonKind, CeremonyRecordRef, CeremonyTransitionRecord, CeremonyVersion, ChildGroupId,
34 ChildGroupState, ExecutionOperationId, ExecutionReceiptLink, GuardName, IdempotencyKey,
35 LateStepResult, MemoryConfidence, RoleAction, RoleId, SessionRecollection, Specialty,
36 StateDeadline, StateId, StateIteration, StateVisit, StepAttempt, StepClaimFence, StepDeadline,
37 StepExecutionRecord, StepId, StepLease, StepResult, TransitionTrigger,
38};
39
40mod children;
41mod decisions;
42mod fold;
43mod guard_decisions;
44mod interventions;
45mod invariants;
46mod participant_bindings;
47mod role_resolution;
48#[cfg(test)]
49mod role_resolution_tests;
50mod step_claims;
51mod step_execution;
52mod transitions;
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct CeremonyInstance {
56 id: CeremonyId,
57 definition_name: CeremonyName,
58 definition_version: CeremonyVersion,
59 current_state: StateId,
60 #[serde(default, skip_serializing_if = "StateIteration::is_first")]
61 current_state_iteration: StateIteration,
62 #[serde(default, skip_serializing_if = "StateVisit::is_first")]
63 current_state_visit: StateVisit,
64 step_records: BTreeMap<StepId, StepExecutionRecord>,
65 #[serde(default)]
72 step_record_history: BTreeMap<StepId, Vec<StepExecutionRecord>>,
73 #[serde(default)]
74 interventions: Vec<CeremonyIntervention>,
75 #[serde(default)]
76 guard_deferrals: Vec<CeremonyGuardDeferral>,
77 #[serde(default)]
87 guard_approvals: Vec<CeremonyGuardApproval>,
88 #[serde(default)]
95 transitions: Vec<CeremonyTransitionRecord>,
96 #[serde(default)]
103 reasons: Vec<CeremonyReason>,
104 #[serde(default)]
108 participant_bindings: BTreeMap<RoleId, CeremonyParticipantBinding>,
109 context: CeremonyContext,
110 #[serde(default)]
120 recollection: Option<SessionRecollection>,
121 idempotency_keys: BTreeSet<IdempotencyKey>,
122 #[serde(with = "time::serde::rfc3339")]
123 created_at: OffsetDateTime,
124 #[serde(with = "time::serde::rfc3339")]
125 updated_at: OffsetDateTime,
126 #[serde(with = "time::serde::rfc3339::option")]
127 completed_at: Option<OffsetDateTime>,
128 #[serde(default)]
139 bound_definition: Option<CeremonyDefinitionDigest>,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 lineage: Option<CeremonyLineage>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 budget_account_id: Option<BudgetAccountId>,
144 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
145 child_groups: BTreeMap<ChildGroupId, ChildGroupState>,
146 #[serde(default, skip_serializing_if = "CeremonyLifecycle::is_default")]
147 lifecycle: CeremonyLifecycle,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
149 ceremony_deadline: Option<CeremonyDeadline>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 state_deadline: Option<StateDeadline>,
152 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
153 step_deadlines: BTreeMap<StepId, StepDeadline>,
154 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
155 retired_deadline_claims: BTreeMap<StepClaimFence, StepDeadline>,
156 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
157 late_step_results: BTreeMap<StepClaimFence, LateStepResult>,
158 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
159 execution_receipt_links: BTreeMap<ExecutionOperationId, ExecutionReceiptLink>,
160 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
161 execution_receipt_adoptions:
162 BTreeMap<ExecutionOperationId, BTreeMap<StepClaimFence, ExecutionReceiptLink>>,
163}
164
165impl CeremonyInstance {
166 pub fn start(
172 id: CeremonyId,
173 definition: &CeremonyDefinition,
174 context: CeremonyContext,
175 now: OffsetDateTime,
176 ) -> Result<Self, DomainError> {
177 Ok(Self::from_started(&Self::opening(
178 id, definition, context, now, None, None, None,
179 )?))
180 }
181
182 pub fn start_bound(
188 id: CeremonyId,
189 published: &PublishedCeremonyDefinition,
190 context: CeremonyContext,
191 now: OffsetDateTime,
192 ) -> Result<Self, DomainError> {
193 Ok(Self::from_started(&Self::opening(
194 id,
195 published.definition(),
196 context,
197 now,
198 Some(published.digest()),
199 None,
200 None,
201 )?))
202 }
203
204 #[must_use]
207 pub fn bound_definition(&self) -> Option<CeremonyDefinitionDigest> {
208 self.bound_definition
209 }
210
211 #[must_use]
212 pub fn lineage(&self) -> Option<&CeremonyLineage> {
213 self.lineage.as_ref()
214 }
215
216 #[must_use]
217 pub fn budget_account_id(&self) -> Option<&BudgetAccountId> {
218 self.budget_account_id.as_ref()
219 }
220
221 #[must_use]
222 pub fn child_groups(&self) -> &BTreeMap<ChildGroupId, ChildGroupState> {
223 &self.child_groups
224 }
225
226 #[must_use]
227 pub fn child_group(&self, group_id: &ChildGroupId) -> Option<&ChildGroupState> {
228 self.child_groups.get(group_id)
229 }
230
231 #[must_use]
234 pub fn recollection(&self) -> Option<&SessionRecollection> {
235 self.recollection.as_ref()
236 }
237
238 #[must_use]
241 pub fn is_bound_to_a_published_definition(&self) -> bool {
242 self.bound_definition.is_some()
243 }
244
245 #[must_use]
246 pub fn id(&self) -> &CeremonyId {
247 &self.id
248 }
249
250 #[must_use]
251 pub fn definition_name(&self) -> &CeremonyName {
252 &self.definition_name
253 }
254
255 #[must_use]
256 pub fn definition_version(&self) -> &CeremonyVersion {
257 &self.definition_version
258 }
259
260 #[must_use]
261 pub fn current_state(&self) -> &StateId {
262 &self.current_state
263 }
264
265 #[must_use]
266 pub fn current_state_visit(&self) -> StateVisit {
267 self.current_state_visit
268 }
269
270 #[must_use]
271 pub fn current_state_iteration(&self) -> StateIteration {
272 self.current_state_iteration
273 }
274
275 #[must_use]
276 pub fn state_work_is_complete(&self, definition: &CeremonyDefinition) -> bool {
277 definition.steps_for_state(&self.current_state).all(|step| {
278 self.step_records
279 .get(step.id())
280 .is_some_and(|record| record.status().is_success())
281 }) && definition.repeat_requirements_are_satisfied(&self.current_state, &self.step_records)
282 }
283
284 #[must_use]
285 pub fn state_repeat_condition_is_satisfied(&self, definition: &CeremonyDefinition) -> bool {
286 let Some(policy) = definition
287 .state(&self.current_state)
288 .and_then(|state| state.repeat_policy())
289 else {
290 return true;
291 };
292 self.step_records
293 .get(policy.until().step_id())
294 .is_some_and(|record| policy.is_satisfied(record))
295 }
296
297 #[must_use]
298 pub fn state_repeat_permits_transition(&self, definition: &CeremonyDefinition) -> bool {
299 definition
300 .state(&self.current_state)
301 .and_then(|state| state.repeat_policy())
302 .is_none()
303 || (self.state_work_is_complete(definition)
304 && self.state_repeat_condition_is_satisfied(definition))
305 }
306
307 #[must_use]
308 pub fn state_repeat_limit_reached(&self, definition: &CeremonyDefinition) -> bool {
309 let Some(policy) = definition
310 .state(&self.current_state)
311 .and_then(|state| state.repeat_policy())
312 else {
313 return false;
314 };
315 self.state_work_is_complete(definition)
316 && !self.state_repeat_condition_is_satisfied(definition)
317 && !policy.permits_another_iteration(self.current_state_iteration)
318 }
319
320 #[must_use]
321 pub fn step_records(&self) -> &BTreeMap<StepId, StepExecutionRecord> {
322 &self.step_records
323 }
324
325 #[must_use]
326 pub fn step_record(&self, step_id: &StepId) -> Option<&StepExecutionRecord> {
327 self.step_records.get(step_id)
328 }
329
330 #[must_use]
331 pub fn execution_receipt_links(&self) -> &BTreeMap<ExecutionOperationId, ExecutionReceiptLink> {
332 &self.execution_receipt_links
333 }
334
335 #[must_use]
336 pub fn execution_receipt_link(
337 &self,
338 operation_id: &ExecutionOperationId,
339 ) -> Option<&ExecutionReceiptLink> {
340 self.execution_receipt_links.get(operation_id)
341 }
342
343 #[must_use]
344 pub fn execution_receipt_adoptions(
345 &self,
346 ) -> &BTreeMap<ExecutionOperationId, BTreeMap<StepClaimFence, ExecutionReceiptLink>> {
347 &self.execution_receipt_adoptions
348 }
349
350 #[must_use]
351 pub fn execution_receipt_adoption(
352 &self,
353 operation_id: &ExecutionOperationId,
354 applied_claim_fence: &StepClaimFence,
355 ) -> Option<&ExecutionReceiptLink> {
356 self.execution_receipt_adoptions
357 .get(operation_id)
358 .and_then(|adoptions| adoptions.get(applied_claim_fence))
359 }
360
361 #[must_use]
363 pub fn step_record_history(&self, step_id: &StepId) -> &[StepExecutionRecord] {
364 self.step_record_history
365 .get(step_id)
366 .map(Vec::as_slice)
367 .unwrap_or_default()
368 }
369
370 #[must_use]
373 pub fn step_repeat_limit_reached(
374 &self,
375 definition: &CeremonyDefinition,
376 step_id: &StepId,
377 ) -> bool {
378 let Some(step) = definition.step(step_id) else {
379 return false;
380 };
381 let Some(policy) = step.repeat_policy() else {
382 return false;
383 };
384 let Some(record) = self.step_record(step_id) else {
385 return false;
386 };
387 record.status().is_success()
388 && !policy.is_satisfied(record.output())
389 && !policy.permits_another_iteration(record.iteration())
390 }
391
392 #[must_use]
393 pub fn interventions(&self) -> &[CeremonyIntervention] {
394 &self.interventions
395 }
396
397 #[must_use]
398 pub fn guard_deferrals(&self) -> &[CeremonyGuardDeferral] {
399 &self.guard_deferrals
400 }
401
402 #[must_use]
408 pub fn guard_approvals(&self) -> &[CeremonyGuardApproval] {
409 &self.guard_approvals
410 }
411
412 #[must_use]
414 pub fn transitions(&self) -> &[CeremonyTransitionRecord] {
415 &self.transitions
416 }
417
418 #[must_use]
420 pub fn reasons(&self) -> &[CeremonyReason] {
421 &self.reasons
422 }
423
424 #[must_use]
425 pub fn intervention(
426 &self,
427 intervention_id: &CeremonyInterventionId,
428 ) -> Option<&CeremonyIntervention> {
429 self.interventions
430 .iter()
431 .find(|intervention| intervention.id() == intervention_id)
432 }
433
434 #[must_use]
435 pub fn context(&self) -> &CeremonyContext {
436 &self.context
437 }
438
439 #[must_use]
440 pub fn idempotency_keys(&self) -> &BTreeSet<IdempotencyKey> {
441 &self.idempotency_keys
442 }
443
444 #[must_use]
445 pub fn created_at(&self) -> OffsetDateTime {
446 self.created_at
447 }
448
449 #[must_use]
450 pub fn updated_at(&self) -> OffsetDateTime {
451 self.updated_at
452 }
453
454 #[must_use]
455 pub fn completed_at(&self) -> Option<OffsetDateTime> {
456 self.completed_at
457 }
458
459 #[must_use]
460 pub fn lifecycle(&self) -> CeremonyLifecycle {
461 self.completed_at
462 .filter(|_| !self.lifecycle.is_explicit())
463 .map_or_else(|| self.lifecycle.clone(), CeremonyLifecycle::completed)
464 }
465 #[must_use]
466 pub fn ceremony_deadline(&self) -> Option<CeremonyDeadline> {
467 self.ceremony_deadline
468 }
469 #[must_use]
470 pub fn state_deadline(&self) -> Option<&StateDeadline> {
471 self.state_deadline.as_ref()
472 }
473 #[must_use]
474 pub fn step_deadlines(&self) -> &BTreeMap<StepId, StepDeadline> {
475 &self.step_deadlines
476 }
477 #[must_use]
478 pub fn late_step_results(&self) -> &BTreeMap<StepClaimFence, LateStepResult> {
479 &self.late_step_results
480 }
481 #[must_use]
482 pub fn is_paused(&self) -> bool {
483 self.lifecycle().is_paused()
484 }
485 #[must_use]
486 pub fn is_ended(&self) -> bool {
487 self.lifecycle().is_ended()
488 }
489 #[must_use]
490 pub fn admits_new_work(&self) -> bool {
491 !self.is_ended() && self.lifecycle.admits_new_work()
492 }
493
494 #[must_use]
495 pub fn is_terminal(&self, definition: &CeremonyDefinition) -> bool {
496 self.matches_definition(definition) && definition.is_terminal_state(&self.current_state)
497 }
498
499 #[must_use]
500 pub fn is_completed(&self, definition: &CeremonyDefinition) -> bool {
501 self.is_terminal(definition) && self.completed_at.is_some()
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use crate::value_objects::{
509 Attributes, CeremonyGuard, CeremonyState, CeremonyStep, CeremonyTransition, GuardCondition,
510 GuardName, LeaseOwnerId, RepeatUntilCondition, RetryPolicy, StepHandlerConfig,
511 StepHandlerKind, StepIteration, StepOutput, StepOutputField, StepRepeatPolicy, StepStatus,
512 };
513 use serde_json::json;
514 use time::macros::datetime;
515
516 fn now() -> OffsetDateTime {
517 datetime!(2026-06-06 12:00:00 UTC)
518 }
519
520 fn state_id(raw: &str) -> StateId {
521 StateId::new(raw).unwrap()
522 }
523
524 fn step_id(raw: &str) -> StepId {
525 StepId::new(raw).unwrap()
526 }
527
528 fn trigger(raw: &str) -> TransitionTrigger {
529 TransitionTrigger::new(raw).unwrap()
530 }
531
532 fn role_id(raw: &str) -> RoleId {
533 RoleId::new(raw).unwrap()
534 }
535
536 fn guard_name(raw: &str) -> GuardName {
537 GuardName::new(raw).unwrap()
538 }
539
540 fn handler_kind() -> StepHandlerKind {
541 StepHandlerKind::new("multiagent_round").unwrap()
542 }
543
544 fn retrying_step(raw_step_id: &str, raw_state_id: &str) -> CeremonyStep {
545 CeremonyStep::new(
546 step_id(raw_step_id),
547 state_id(raw_state_id),
548 handler_kind(),
549 StepHandlerConfig::empty(),
550 RetryPolicy::new(
551 StepAttempt::new(3).unwrap(),
552 crate::value_objects::DurationMs::ZERO,
553 ),
554 None,
555 )
556 }
557
558 fn single_attempt_step(raw_step_id: &str, raw_state_id: &str) -> CeremonyStep {
559 CeremonyStep::new(
560 step_id(raw_step_id),
561 state_id(raw_state_id),
562 handler_kind(),
563 StepHandlerConfig::empty(),
564 RetryPolicy::single_attempt(),
565 None,
566 )
567 }
568
569 fn repeating_plan(max_iterations: u32) -> CeremonyStep {
570 retrying_step("plan", "drafting").with_repeat_policy(StepRepeatPolicy::new(
571 RepeatUntilCondition::output_field_equals(
572 StepOutputField::new("ready").unwrap(),
573 json!(true),
574 ),
575 StepIteration::new(max_iterations).unwrap(),
576 ))
577 }
578
579 fn readiness_output(ready: bool) -> StepOutput {
580 StepOutput::new(
581 Attributes::new(std::collections::BTreeMap::from([(
582 "ready".to_owned(),
583 json!(ready),
584 )]))
585 .unwrap(),
586 )
587 }
588
589 fn lease(
590 raw_owner_id: &str,
591 raw_key: &str,
592 acquired_at: OffsetDateTime,
593 expires_at: OffsetDateTime,
594 ) -> StepLease {
595 StepLease::new(
596 LeaseOwnerId::new(raw_owner_id).unwrap(),
597 IdempotencyKey::new(raw_key).unwrap(),
598 acquired_at,
599 expires_at,
600 )
601 .unwrap()
602 }
603
604 fn role(actions: Vec<RoleAction>) -> crate::value_objects::CeremonyRole {
605 crate::value_objects::CeremonyRole::new(role_id("facilitator"), actions).unwrap()
606 }
607
608 fn definition_with_steps(steps: Vec<CeremonyStep>) -> CeremonyDefinition {
609 let plan_done = CeremonyGuard::new(
610 guard_name("plan_done"),
611 GuardCondition::StepStatus {
612 step_id: step_id("plan"),
613 status: StepStatus::Completed,
614 },
615 );
616 let finish = CeremonyTransition::new(
617 state_id("drafting"),
618 state_id("done"),
619 trigger("finish"),
620 vec![plan_done.name().clone()],
621 )
622 .unwrap();
623 let role = role(vec![
624 RoleAction::step(step_id("plan")),
625 RoleAction::transition(finish.trigger().clone()),
626 RoleAction::request_intervention(),
627 ]);
628 let observer = crate::value_objects::CeremonyRole::new(
629 role_id("observer"),
630 vec![RoleAction::respond_to_intervention()],
631 )
632 .unwrap();
633
634 CeremonyDefinition::new(
635 crate::value_objects::CeremonyName::new("planning_ceremony").unwrap(),
636 CeremonyVersion::v1(),
637 None,
638 Vec::new(),
639 Vec::new(),
640 vec![
641 CeremonyState::initial(state_id("drafting")),
642 CeremonyState::intermediate(state_id("review")),
643 CeremonyState::terminal(state_id("done")),
644 ],
645 vec![finish],
646 steps,
647 vec![plan_done],
648 vec![role, observer],
649 )
650 .unwrap()
651 }
652
653 fn definition() -> CeremonyDefinition {
654 definition_with_steps(vec![
655 retrying_step("plan", "drafting"),
656 single_attempt_step("review_step", "review"),
657 ])
658 }
659
660 fn definition_with_human_guard(approval: &CeremonyGuard) -> CeremonyDefinition {
663 let finish = CeremonyTransition::new(
664 state_id("drafting"),
665 state_id("done"),
666 trigger("approve"),
667 vec![approval.name().clone()],
668 )
669 .unwrap();
670 CeremonyDefinition::new(
671 crate::value_objects::CeremonyName::new("approval_ceremony").unwrap(),
672 CeremonyVersion::v1(),
673 None,
674 Vec::new(),
675 Vec::new(),
676 vec![
677 CeremonyState::initial(state_id("drafting")),
678 CeremonyState::terminal(state_id("done")),
679 ],
680 vec![finish.clone()],
681 Vec::new(),
682 vec![approval.clone()],
683 vec![role(vec![RoleAction::transition(finish.trigger().clone())])],
684 )
685 .unwrap()
686 }
687
688 fn instance(definition: &CeremonyDefinition) -> CeremonyInstance {
689 CeremonyInstance::start(
690 CeremonyId::new("ceremony-1").unwrap(),
691 definition,
692 CeremonyContext::empty(),
693 now(),
694 )
695 .expect("required ceremony inputs")
696 }
697
698 #[test]
699 fn starts_in_initial_state_with_pending_records() {
700 let definition = definition();
701 let instance = instance(&definition);
702
703 assert_eq!(instance.current_state(), &state_id("drafting"));
704 assert_eq!(
705 instance.step_record(&step_id("plan")).unwrap().status(),
706 StepStatus::Pending
707 );
708 assert_eq!(
709 instance
710 .step_record(&step_id("review_step"))
711 .unwrap()
712 .status(),
713 StepStatus::Pending
714 );
715 }
716
717 #[test]
718 fn instances_without_iteration_fields_load_as_the_first_iteration() {
719 let definition = definition();
720 let mut value = serde_json::to_value(instance(&definition)).unwrap();
721 value.as_object_mut().unwrap().remove("step_record_history");
722 for record in value["step_records"].as_object_mut().unwrap().values_mut() {
723 record.as_object_mut().unwrap().remove("iteration");
724 }
725
726 let restored: CeremonyInstance = serde_json::from_value(value).unwrap();
727
728 assert!(restored.step_record_history(&step_id("plan")).is_empty());
729 assert_eq!(
730 restored.step_record(&step_id("plan")).unwrap().iteration(),
731 StepIteration::FIRST
732 );
733 }
734
735 #[test]
736 fn dynamic_intervention_collects_role_scoped_response_and_requester_closes_it() {
737 let definition = definition();
738 let mut instance = instance(&definition);
739 let intervention_id = CeremonyInterventionId::new("queue-check").unwrap();
740 let facilitator = role_id("facilitator");
741 let observer = role_id("observer");
742
743 instance
744 .request_intervention_as(
745 &definition,
746 intervention_id.clone(),
747 facilitator.clone(),
748 CeremonyInterventionKind::Investigation,
749 CeremonyInterventionTarget::roles([observer.clone()]).unwrap(),
750 CeremonyInterventionContent::new(
751 "Inspect the queue without consuming messages.",
752 Attributes::empty(),
753 )
754 .unwrap(),
755 now(),
756 )
757 .unwrap();
758 instance
759 .respond_to_intervention_as(
760 &definition,
761 &intervention_id,
762 observer.clone(),
763 CeremonyInterventionContent::new("Queue depth is stable.", Attributes::empty())
764 .unwrap(),
765 now(),
766 )
767 .unwrap();
768 let selected_intervention_id = CeremonyInterventionId::new("selected-check").unwrap();
769 instance
770 .request_intervention_with_provenance_as(
771 &definition,
772 selected_intervention_id.clone(),
773 facilitator.clone(),
774 CeremonyInterventionKind::Investigation,
775 CeremonyInterventionTarget::roles([observer.clone()]).unwrap(),
776 CeremonyInterventionContent::new(
777 "Inspect the proposed signal.",
778 Attributes::empty(),
779 )
780 .unwrap(),
781 Some(CeremonyInterventionProvenance::selected_from(
782 intervention_id.clone(),
783 observer.clone(),
784 observer.clone(),
785 )),
786 now(),
787 )
788 .unwrap();
789 instance
790 .close_intervention_as(&definition, &intervention_id, &facilitator, now())
791 .unwrap();
792
793 let intervention = instance.intervention(&intervention_id).unwrap();
794 assert_eq!(intervention.responses().len(), 1);
795 assert_eq!(
796 intervention.status(),
797 crate::value_objects::CeremonyInterventionStatus::Closed
798 );
799 let provenance = instance
800 .intervention(&selected_intervention_id)
801 .unwrap()
802 .provenance()
803 .unwrap();
804 assert_eq!(provenance.source_intervention_id(), &intervention_id);
805 assert_eq!(provenance.selected_role_id(), &observer);
806 }
807
808 #[test]
809 fn intervention_rejects_roles_without_the_required_capability() {
810 let definition = definition();
811 let mut instance = instance(&definition);
812
813 let error = instance
814 .request_intervention_as(
815 &definition,
816 CeremonyInterventionId::new("not-allowed").unwrap(),
817 role_id("observer"),
818 CeremonyInterventionKind::Opinion,
819 CeremonyInterventionTarget::table(),
820 CeremonyInterventionContent::new("What do you think?", Attributes::empty())
821 .unwrap(),
822 now(),
823 )
824 .unwrap_err();
825
826 assert!(matches!(error, DomainError::InvariantViolated { .. }));
827 }
828
829 #[test]
830 fn rejects_step_execution_outside_current_state() {
831 let definition = definition();
832 let mut instance = instance(&definition);
833
834 let err = instance
835 .start_step(
836 &definition,
837 &step_id("review_step"),
838 lease(
839 "runner-1",
840 "key-1",
841 now(),
842 datetime!(2026-06-06 12:05:00 UTC),
843 ),
844 now(),
845 )
846 .unwrap_err();
847
848 assert!(matches!(err, DomainError::InvalidTransition { .. }));
849 }
850
851 #[test]
852 fn completed_step_unlocks_guarded_transition() {
853 let definition = definition();
854 let mut instance = instance(&definition);
855
856 instance
857 .start_step_as(
858 &definition,
859 &role_id("facilitator"),
860 &step_id("plan"),
861 lease(
862 "runner-1",
863 "key-1",
864 now(),
865 datetime!(2026-06-06 12:05:00 UTC),
866 ),
867 now(),
868 )
869 .unwrap();
870 instance
871 .apply_step_result(
872 &definition,
873 &step_id("plan"),
874 instance.step_claim_fence(&step_id("plan")).unwrap(),
875 StepResult::completed(StepOutput::empty()).unwrap(),
876 datetime!(2026-06-06 12:01:00 UTC),
877 )
878 .unwrap();
879 let state = instance
880 .apply_transition_as(
881 &definition,
882 &role_id("facilitator"),
883 &trigger("finish"),
884 datetime!(2026-06-06 12:02:00 UTC),
885 )
886 .unwrap();
887
888 assert_eq!(state, state_id("done"));
889 assert!(instance.is_completed(&definition));
890 }
891
892 #[test]
893 fn false_repeat_condition_archives_iteration_and_schedules_the_next() {
894 let definition = definition_with_steps(vec![repeating_plan(3)]);
895 let mut instance = instance(&definition);
896
897 instance
898 .start_step(
899 &definition,
900 &step_id("plan"),
901 lease(
902 "runner-1",
903 "repeat-1",
904 now(),
905 datetime!(2026-06-06 12:05:00 UTC),
906 ),
907 now(),
908 )
909 .unwrap();
910 instance
911 .apply_step_result(
912 &definition,
913 &step_id("plan"),
914 instance.step_claim_fence(&step_id("plan")).unwrap(),
915 StepResult::completed(readiness_output(false)).unwrap(),
916 datetime!(2026-06-06 12:01:00 UTC),
917 )
918 .unwrap();
919
920 let current = instance.step_record(&step_id("plan")).unwrap();
921 assert_eq!(current.status(), StepStatus::Pending);
922 assert_eq!(current.iteration().get(), 2);
923 assert_eq!(current.attempt(), StepAttempt::FIRST);
924 let history = instance.step_record_history(&step_id("plan"));
925 assert_eq!(history.len(), 1);
926 assert_eq!(history[0].iteration(), StepIteration::FIRST);
927 assert_eq!(history[0].output(), &readiness_output(false));
928 assert!(instance
929 .apply_transition(&definition, &trigger("finish"), now())
930 .is_err());
931
932 instance
933 .start_step(
934 &definition,
935 &step_id("plan"),
936 lease(
937 "runner-1",
938 "repeat-2",
939 datetime!(2026-06-06 12:02:00 UTC),
940 datetime!(2026-06-06 12:07:00 UTC),
941 ),
942 datetime!(2026-06-06 12:02:00 UTC),
943 )
944 .unwrap();
945 instance
946 .apply_step_result(
947 &definition,
948 &step_id("plan"),
949 instance.step_claim_fence(&step_id("plan")).unwrap(),
950 StepResult::completed(readiness_output(true)).unwrap(),
951 datetime!(2026-06-06 12:03:00 UTC),
952 )
953 .unwrap();
954
955 let current = instance.step_record(&step_id("plan")).unwrap();
956 assert_eq!(current.status(), StepStatus::Completed);
957 assert_eq!(current.iteration().get(), 2);
958 assert!(!instance.step_repeat_limit_reached(&definition, &step_id("plan")));
959 assert_eq!(
960 instance
961 .apply_transition(&definition, &trigger("finish"), now())
962 .unwrap(),
963 state_id("done")
964 );
965 }
966
967 #[test]
968 fn repeat_limit_is_terminal_for_the_step_and_blocks_transition() {
969 let definition = definition_with_steps(vec![repeating_plan(2)]);
970 let mut instance = instance(&definition);
971
972 for iteration in 1..=2 {
973 instance
974 .start_step(
975 &definition,
976 &step_id("plan"),
977 lease(
978 "runner-1",
979 &format!("limit-{iteration}"),
980 now(),
981 datetime!(2026-06-06 12:05:00 UTC),
982 ),
983 now(),
984 )
985 .unwrap();
986 instance
987 .apply_step_result(
988 &definition,
989 &step_id("plan"),
990 instance.step_claim_fence(&step_id("plan")).unwrap(),
991 StepResult::completed(readiness_output(false)).unwrap(),
992 now(),
993 )
994 .unwrap();
995 }
996
997 assert!(instance.step_repeat_limit_reached(&definition, &step_id("plan")));
998 assert_eq!(
999 instance
1000 .step_record(&step_id("plan"))
1001 .unwrap()
1002 .iteration()
1003 .get(),
1004 2
1005 );
1006 assert_eq!(instance.step_record_history(&step_id("plan")).len(), 1);
1007 assert!(instance
1008 .apply_transition(&definition, &trigger("finish"), now())
1009 .is_err());
1010 assert!(instance
1011 .start_step(
1012 &definition,
1013 &step_id("plan"),
1014 lease(
1015 "runner-1",
1016 "limit-3",
1017 now(),
1018 datetime!(2026-06-06 12:05:00 UTC),
1019 ),
1020 now(),
1021 )
1022 .is_err());
1023 }
1024
1025 #[test]
1026 fn active_lease_blocks_failover_takeover() {
1027 let definition = definition();
1028 let mut instance = instance(&definition);
1029
1030 instance
1031 .start_step(
1032 &definition,
1033 &step_id("plan"),
1034 lease(
1035 "runner-1",
1036 "key-1",
1037 now(),
1038 datetime!(2026-06-06 12:05:00 UTC),
1039 ),
1040 now(),
1041 )
1042 .unwrap();
1043 let err = instance
1044 .start_step(
1045 &definition,
1046 &step_id("plan"),
1047 lease(
1048 "runner-2",
1049 "key-2",
1050 datetime!(2026-06-06 12:01:00 UTC),
1051 datetime!(2026-06-06 12:06:00 UTC),
1052 ),
1053 datetime!(2026-06-06 12:01:00 UTC),
1054 )
1055 .unwrap_err();
1056
1057 assert!(matches!(err, DomainError::InvariantViolated { .. }));
1058 assert_eq!(
1059 instance
1060 .step_record(&step_id("plan"))
1061 .unwrap()
1062 .lease()
1063 .unwrap()
1064 .owner_id()
1065 .as_str(),
1066 "runner-1"
1067 );
1068 }
1069
1070 #[test]
1071 fn expired_lease_allows_failover_takeover_with_next_attempt() {
1072 let definition = definition();
1073 let mut instance = instance(&definition);
1074
1075 instance
1076 .start_step(
1077 &definition,
1078 &step_id("plan"),
1079 lease(
1080 "runner-1",
1081 "key-1",
1082 now(),
1083 datetime!(2026-06-06 12:05:00 UTC),
1084 ),
1085 now(),
1086 )
1087 .unwrap();
1088 let attempt = instance
1089 .start_step(
1090 &definition,
1091 &step_id("plan"),
1092 lease(
1093 "runner-2",
1094 "key-2",
1095 datetime!(2026-06-06 12:06:00 UTC),
1096 datetime!(2026-06-06 12:11:00 UTC),
1097 ),
1098 datetime!(2026-06-06 12:06:00 UTC),
1099 )
1100 .unwrap();
1101
1102 assert_eq!(attempt, StepAttempt::new(2).unwrap());
1103 let record = instance.step_record(&step_id("plan")).unwrap();
1104 assert_eq!(record.attempt(), StepAttempt::new(2).unwrap());
1105 assert_eq!(record.lease().unwrap().owner_id().as_str(), "runner-2");
1106 }
1107
1108 #[test]
1109 fn approving_a_guard_the_ceremony_never_declared_is_refused() {
1110 let approval =
1111 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1112 let finish = CeremonyTransition::new(
1113 state_id("drafting"),
1114 state_id("done"),
1115 trigger("approve"),
1116 vec![approval.name().clone()],
1117 )
1118 .unwrap();
1119 let definition = CeremonyDefinition::new(
1120 crate::value_objects::CeremonyName::new("approval_ceremony").unwrap(),
1121 CeremonyVersion::v1(),
1122 None,
1123 Vec::new(),
1124 Vec::new(),
1125 vec![
1126 CeremonyState::initial(state_id("drafting")),
1127 CeremonyState::terminal(state_id("done")),
1128 ],
1129 vec![finish.clone()],
1130 Vec::new(),
1131 vec![approval],
1132 vec![role(vec![RoleAction::transition(finish.trigger().clone())])],
1133 )
1134 .unwrap();
1135 let mut instance = instance(&definition);
1136
1137 assert!(matches!(
1142 instance.approve_guard(
1143 &definition,
1144 &guard_name("not_a_guard"),
1145 role_id("facilitator"),
1146 AuditActorKind::Human,
1147 now()
1148 ),
1149 Err(DomainError::NotFound {
1150 what: "ceremony_guard"
1151 })
1152 ));
1153 assert!(!instance
1154 .context()
1155 .is_guard_approved(&guard_name("not_a_guard")));
1156 }
1157
1158 #[test]
1159 fn human_approval_guard_uses_typed_context() {
1160 let approval =
1161 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1162 let finish = CeremonyTransition::new(
1163 state_id("drafting"),
1164 state_id("done"),
1165 trigger("approve"),
1166 vec![approval.name().clone()],
1167 )
1168 .unwrap();
1169 let definition = CeremonyDefinition::new(
1170 crate::value_objects::CeremonyName::new("approval_ceremony").unwrap(),
1171 CeremonyVersion::v1(),
1172 None,
1173 Vec::new(),
1174 Vec::new(),
1175 vec![
1176 CeremonyState::initial(state_id("drafting")),
1177 CeremonyState::terminal(state_id("done")),
1178 ],
1179 vec![finish.clone()],
1180 Vec::new(),
1181 vec![approval.clone()],
1182 vec![role(vec![RoleAction::transition(finish.trigger().clone())])],
1183 )
1184 .unwrap();
1185 let mut instance = instance(&definition);
1186
1187 assert!(matches!(
1188 instance.apply_transition(&definition, &trigger("approve"), now()),
1189 Err(DomainError::InvariantViolated { .. })
1190 ));
1191 instance
1192 .approve_guard(
1193 &definition,
1194 approval.name(),
1195 role_id("facilitator"),
1196 AuditActorKind::Human,
1197 datetime!(2026-06-06 12:01:00 UTC),
1198 )
1199 .unwrap();
1200 instance
1201 .apply_transition(
1202 &definition,
1203 &trigger("approve"),
1204 datetime!(2026-06-06 12:02:00 UTC),
1205 )
1206 .unwrap();
1207
1208 assert!(instance.is_completed(&definition));
1209 }
1210
1211 #[test]
1212 fn human_guard_deferral_preserves_uncertainty_without_approving() {
1213 let approval =
1214 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1215 let finish = CeremonyTransition::new(
1216 state_id("drafting"),
1217 state_id("done"),
1218 trigger("approve"),
1219 vec![approval.name().clone()],
1220 )
1221 .unwrap();
1222 let definition = CeremonyDefinition::new(
1223 crate::value_objects::CeremonyName::new("deferral_ceremony").unwrap(),
1224 CeremonyVersion::v1(),
1225 None,
1226 Vec::new(),
1227 Vec::new(),
1228 vec![
1229 CeremonyState::initial(state_id("drafting")),
1230 CeremonyState::terminal(state_id("done")),
1231 ],
1232 vec![finish.clone()],
1233 Vec::new(),
1234 vec![approval.clone()],
1235 vec![role(vec![RoleAction::transition(finish.trigger().clone())])],
1236 )
1237 .unwrap();
1238 let mut instance = instance(&definition);
1239
1240 instance
1241 .defer_guard(
1242 &definition,
1243 approval.name().clone(),
1244 CeremonyGuardDeferralContent::new(
1245 "I do not know.",
1246 "I cannot explain how the issue was resolved.",
1247 vec!["New evidence explains the resolution.".to_owned()],
1248 )
1249 .unwrap(),
1250 role_id("facilitator"),
1251 AuditActorKind::Human,
1252 datetime!(2026-06-06 12:01:00 UTC),
1253 )
1254 .unwrap();
1255
1256 assert!(!instance.context().is_guard_approved(approval.name()));
1257 assert!(instance
1258 .apply_transition(&definition, &trigger("approve"), now())
1259 .is_err());
1260 let deferral = &instance.guard_deferrals()[0];
1261 assert_eq!(deferral.guard_name(), approval.name());
1262 assert_eq!(deferral.content().statement(), "I do not know.");
1263 }
1264 #[test]
1267 fn approving_a_human_guard_records_the_seat_that_did_it() {
1268 let approval =
1269 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1270 let definition = definition_with_human_guard(&approval);
1271 let mut instance = instance(&definition);
1272
1273 instance
1274 .approve_guard(
1275 &definition,
1276 approval.name(),
1277 role_id("facilitator"),
1278 AuditActorKind::Human,
1279 datetime!(2026-06-06 12:01:00 UTC),
1280 )
1281 .unwrap();
1282
1283 let [recorded] = instance.guard_approvals() else {
1284 panic!(
1285 "expected one approval, got {:?}",
1286 instance.guard_approvals()
1287 );
1288 };
1289 assert_eq!(recorded.guard_name(), approval.name());
1290 assert_eq!(recorded.approved_by(), &role_id("facilitator"));
1291 assert_eq!(recorded.approved_at(), datetime!(2026-06-06 12:01:00 UTC));
1292 assert!(instance.context().is_guard_approved(approval.name()));
1293 }
1294
1295 #[test]
1299 fn a_seat_the_definition_does_not_declare_cannot_approve() {
1300 let approval =
1301 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1302 let definition = definition_with_human_guard(&approval);
1303 let mut instance = instance(&definition);
1304
1305 let outcome = instance.approve_guard(
1306 &definition,
1307 approval.name(),
1308 role_id("someone-who-is-not-here"),
1309 AuditActorKind::Human,
1310 now(),
1311 );
1312
1313 assert!(matches!(
1314 outcome,
1315 Err(DomainError::NotFound {
1316 what: "ceremony_role"
1317 })
1318 ));
1319 assert!(instance.guard_approvals().is_empty());
1320 assert!(!instance.context().is_guard_approved(approval.name()));
1321 }
1322 fn session_with_a_contribution(
1325 definition: &CeremonyDefinition,
1326 ) -> (CeremonyInstance, CeremonyInterventionId) {
1327 let mut instance = instance(definition);
1328 let agenda_item = CeremonyInterventionId::new("queue-check").unwrap();
1329 instance
1330 .request_intervention_as(
1331 definition,
1332 agenda_item.clone(),
1333 role_id("facilitator"),
1334 CeremonyInterventionKind::Investigation,
1335 CeremonyInterventionTarget::roles([role_id("observer")]).unwrap(),
1336 CeremonyInterventionContent::new("Inspect the queue.", Attributes::empty())
1337 .unwrap(),
1338 now(),
1339 )
1340 .unwrap();
1341 instance
1342 .respond_to_intervention_as(
1343 definition,
1344 &agenda_item,
1345 role_id("observer"),
1346 CeremonyInterventionContent::new("Queue depth is stable.", Attributes::empty())
1347 .unwrap(),
1348 now(),
1349 )
1350 .unwrap();
1351 (instance, agenda_item)
1352 }
1353
1354 #[test]
1357 fn a_contribution_is_recorded_as_answering_its_agenda_item() {
1358 let definition = definition();
1359 let (instance, agenda_item) = session_with_a_contribution(&definition);
1360
1361 let [answered] = instance.reasons() else {
1362 panic!("expected exactly one reason, got {:?}", instance.reasons());
1363 };
1364 assert_eq!(answered.kind(), CeremonyReasonKind::Answers);
1365 assert_eq!(
1366 answered.from(),
1367 &CeremonyRecordRef::contribution(agenda_item.clone(), 0)
1368 );
1369 assert_eq!(answered.to(), &CeremonyRecordRef::agenda_item(agenda_item));
1370 assert_eq!(
1371 answered.asserted_by(),
1372 None,
1373 "the engine observed it; naming a seat would be inventing one"
1374 );
1375 }
1376
1377 #[test]
1380 fn a_seat_cannot_assert_what_only_the_engine_observes() {
1381 let definition = definition();
1382 let (mut instance, agenda_item) = session_with_a_contribution(&definition);
1383
1384 let outcome = instance.assert_reason_as(
1385 &definition,
1386 role_id("observer"),
1387 CeremonyRecordRef::contribution(agenda_item.clone(), 0),
1388 CeremonyRecordRef::agenda_item(agenda_item),
1389 CeremonyReasonKind::Answers,
1390 "because I say it does",
1391 MemoryConfidence::High,
1392 now(),
1393 );
1394
1395 assert!(matches!(
1396 outcome,
1397 Err(DomainError::InvariantViolated { .. })
1398 ));
1399 }
1400
1401 #[test]
1404 fn only_whoever_contributed_may_say_why_they_did() {
1405 let definition = definition();
1406 let (mut instance, agenda_item) = session_with_a_contribution(&definition);
1407 let contribution = CeremonyRecordRef::contribution(agenda_item.clone(), 0);
1408 let item = CeremonyRecordRef::agenda_item(agenda_item);
1409
1410 let by_someone_else = instance.assert_reason_as(
1411 &definition,
1412 role_id("facilitator"),
1413 contribution.clone(),
1414 item.clone(),
1415 CeremonyReasonKind::ChosenBecause,
1416 "they must have thought the queue mattered",
1417 MemoryConfidence::Low,
1418 now(),
1419 );
1420 assert!(matches!(
1421 by_someone_else,
1422 Err(DomainError::InvariantViolated { .. })
1423 ));
1424
1425 instance
1426 .assert_reason_as(
1427 &definition,
1428 role_id("observer"),
1429 contribution,
1430 item,
1431 CeremonyReasonKind::ChosenBecause,
1432 "the depth graph had been flat for an hour",
1433 MemoryConfidence::High,
1434 now(),
1435 )
1436 .expect("its author may say why");
1437 assert_eq!(instance.reasons().len(), 2);
1438 }
1439
1440 #[test]
1443 fn any_seat_may_claim_that_one_thing_came_from_another() {
1444 let definition = definition();
1445 let (mut instance, agenda_item) = session_with_a_contribution(&definition);
1446
1447 instance
1448 .assert_reason_as(
1449 &definition,
1450 role_id("facilitator"),
1451 CeremonyRecordRef::agenda_item(agenda_item.clone()),
1452 CeremonyRecordRef::contribution(agenda_item, 0),
1453 CeremonyReasonKind::FollowsFrom,
1454 "the item stayed open because the answer raised a new question",
1455 MemoryConfidence::Medium,
1456 now(),
1457 )
1458 .expect("a claim about the world is open to any seat");
1459
1460 let asserted = instance.reasons().last().unwrap();
1461 assert_eq!(asserted.confidence(), MemoryConfidence::Medium);
1462 assert_eq!(asserted.asserted_by(), Some(&role_id("facilitator")));
1463 }
1464
1465 #[test]
1468 fn a_reason_cannot_cite_something_that_never_happened() {
1469 let definition = definition();
1470 let (mut instance, agenda_item) = session_with_a_contribution(&definition);
1471
1472 let outcome = instance.assert_reason_as(
1473 &definition,
1474 role_id("observer"),
1475 CeremonyRecordRef::contribution(agenda_item.clone(), 7),
1476 CeremonyRecordRef::agenda_item(agenda_item),
1477 CeremonyReasonKind::FollowsFrom,
1478 "a contribution nobody made",
1479 MemoryConfidence::Low,
1480 now(),
1481 );
1482
1483 assert!(matches!(
1484 outcome,
1485 Err(DomainError::NotFound {
1486 what: "ceremony_record"
1487 })
1488 ));
1489 }
1490
1491 #[test]
1494 fn a_move_is_recorded_with_whoever_made_it() {
1495 let approval =
1496 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1497 let definition = definition_with_human_guard(&approval);
1498 let mut instance = instance(&definition);
1499 instance
1500 .approve_guard(
1501 &definition,
1502 approval.name(),
1503 role_id("facilitator"),
1504 AuditActorKind::Human,
1505 now(),
1506 )
1507 .unwrap();
1508
1509 instance
1510 .apply_transition_as(
1511 &definition,
1512 &role_id("facilitator"),
1513 &trigger("approve"),
1514 datetime!(2026-06-06 12:05:00 UTC),
1515 )
1516 .unwrap();
1517
1518 let [moved] = instance.transitions() else {
1519 panic!("expected one move, got {:?}", instance.transitions());
1520 };
1521 assert_eq!(moved.trigger(), &trigger("approve"));
1522 assert_eq!(moved.from_state(), &state_id("drafting"));
1523 assert_eq!(moved.to_state(), &state_id("done"));
1524 assert_eq!(moved.applied_by(), Some(&role_id("facilitator")));
1525 }
1526
1527 #[test]
1531 fn a_move_the_engine_took_names_nobody() {
1532 let approval =
1533 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1534 let definition = definition_with_human_guard(&approval);
1535 let mut instance = instance(&definition);
1536 instance
1537 .approve_guard(
1538 &definition,
1539 approval.name(),
1540 role_id("facilitator"),
1541 AuditActorKind::Human,
1542 now(),
1543 )
1544 .unwrap();
1545
1546 instance
1547 .apply_transition(&definition, &trigger("approve"), now())
1548 .unwrap();
1549
1550 assert_eq!(instance.transitions()[0].applied_by(), None);
1551 }
1552 #[test]
1562 fn an_approval_records_the_kind_it_was_told_not_the_one_the_guard_wanted() {
1563 let approval =
1564 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1565 let definition = definition_with_human_guard(&approval);
1566 let mut instance = instance(&definition);
1567
1568 instance
1569 .approve_guard(
1570 &definition,
1571 approval.name(),
1572 role_id("facilitator"),
1573 AuditActorKind::Agent,
1574 now(),
1575 )
1576 .unwrap();
1577
1578 let [recorded] = instance.guard_approvals() else {
1579 panic!("expected one approval");
1580 };
1581 assert_eq!(
1582 recorded.approved_by_kind(),
1583 AuditActorKind::Agent,
1584 "the guard asked for a human and an agent answered; saying otherwise \
1585 would be the engine vouching for something it cannot see"
1586 );
1587 assert!(instance.context().is_guard_approved(approval.name()));
1588 }
1589}