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