1use std::collections::{BTreeMap, BTreeSet};
8
9use serde::{Deserialize, Serialize};
10use time::OffsetDateTime;
11
12use super::{
13 ceremony_definition::CeremonyDefinition, CeremonyIntervention, PublishedCeremonyDefinition,
14};
15use crate::error::DomainError;
16use crate::ports::CeremonyEvidenceRequest;
17use crate::value_objects::{
18 AuditActorKind, CeremonyContext, CeremonyDefinitionDigest, CeremonyDefinitionDigestMigration,
19 CeremonyEvidenceSourceId, CeremonyGuardApproval, CeremonyGuardDeferral,
20 CeremonyGuardDeferralContent, CeremonyId, CeremonyInterventionContent, CeremonyInterventionId,
21 CeremonyInterventionKind, CeremonyInterventionProvenance, CeremonyInterventionResponse,
22 CeremonyInterventionTarget, CeremonyName, CeremonyParticipantBinding, CeremonyReason,
23 CeremonyReasonKind, CeremonyRecordRef, CeremonyTransitionRecord, CeremonyVersion,
24 GuardCondition, GuardName, IdempotencyKey, MemoryConfidence, ReasonAsserter, RoleAction,
25 RoleId, Specialty, StateId, StepAttempt, StepExecutionRecord, StepId, StepLease, StepResult,
26 StepStatus, TransitionTrigger,
27};
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct CeremonyInstance {
31 id: CeremonyId,
32 definition_name: CeremonyName,
33 definition_version: CeremonyVersion,
34 current_state: StateId,
35 step_records: BTreeMap<StepId, StepExecutionRecord>,
36 #[serde(default)]
43 step_record_history: BTreeMap<StepId, Vec<StepExecutionRecord>>,
44 #[serde(default)]
45 interventions: Vec<CeremonyIntervention>,
46 #[serde(default)]
47 guard_deferrals: Vec<CeremonyGuardDeferral>,
48 #[serde(default)]
58 guard_approvals: Vec<CeremonyGuardApproval>,
59 #[serde(default)]
66 transitions: Vec<CeremonyTransitionRecord>,
67 #[serde(default)]
74 reasons: Vec<CeremonyReason>,
75 #[serde(default)]
79 participant_bindings: BTreeMap<RoleId, CeremonyParticipantBinding>,
80 context: CeremonyContext,
81 idempotency_keys: BTreeSet<IdempotencyKey>,
82 #[serde(with = "time::serde::rfc3339")]
83 created_at: OffsetDateTime,
84 #[serde(with = "time::serde::rfc3339")]
85 updated_at: OffsetDateTime,
86 #[serde(with = "time::serde::rfc3339::option")]
87 completed_at: Option<OffsetDateTime>,
88 #[serde(default)]
99 bound_definition: Option<CeremonyDefinitionDigest>,
100}
101
102impl CeremonyInstance {
103 #[must_use]
108 pub fn start(
109 id: CeremonyId,
110 definition: &CeremonyDefinition,
111 context: CeremonyContext,
112 now: OffsetDateTime,
113 ) -> Self {
114 Self::open(id, definition, context, now, None)
115 }
116
117 #[must_use]
123 pub fn start_bound(
124 id: CeremonyId,
125 published: &PublishedCeremonyDefinition,
126 context: CeremonyContext,
127 now: OffsetDateTime,
128 ) -> Self {
129 Self::open(
130 id,
131 published.definition(),
132 context,
133 now,
134 Some(published.digest()),
135 )
136 }
137
138 fn open(
139 id: CeremonyId,
140 definition: &CeremonyDefinition,
141 context: CeremonyContext,
142 now: OffsetDateTime,
143 bound_definition: Option<CeremonyDefinitionDigest>,
144 ) -> Self {
145 let step_records = definition
146 .steps()
147 .keys()
148 .map(|step_id| (step_id.clone(), StepExecutionRecord::pending()))
149 .collect();
150
151 Self {
152 id,
153 definition_name: definition.name().clone(),
154 definition_version: definition.version().clone(),
155 current_state: definition.initial_state_id().clone(),
156 step_records,
157 step_record_history: BTreeMap::new(),
158 interventions: Vec::new(),
159 guard_deferrals: Vec::new(),
160 guard_approvals: Vec::new(),
161 transitions: Vec::new(),
162 reasons: Vec::new(),
163 participant_bindings: BTreeMap::new(),
164 context,
165 idempotency_keys: BTreeSet::new(),
166 created_at: now,
167 updated_at: now,
168 completed_at: None,
169 bound_definition,
170 }
171 }
172
173 #[must_use]
176 pub fn bound_definition(&self) -> Option<CeremonyDefinitionDigest> {
177 self.bound_definition
178 }
179
180 #[must_use]
183 pub fn is_bound_to_a_published_definition(&self) -> bool {
184 self.bound_definition.is_some()
185 }
186
187 pub fn migrate_definition_binding(
195 &mut self,
196 migration: &CeremonyDefinitionDigestMigration,
197 ) -> Result<bool, DomainError> {
198 if self.definition_name != *migration.definition_name()
199 || self.definition_version != *migration.definition_version()
200 {
201 return Err(DomainError::InvariantViolated {
202 reason: "a definition binding migration cannot change name or version",
203 });
204 }
205
206 match self.bound_definition {
207 Some(current) if current == migration.destination() => Ok(false),
208 Some(current) if current == migration.source() => {
209 self.bound_definition = Some(migration.destination());
210 Ok(true)
211 }
212 _ => Err(DomainError::InvariantViolated {
213 reason: "a definition binding migration did not match the stored identity",
214 }),
215 }
216 }
217
218 #[must_use]
219 pub fn id(&self) -> &CeremonyId {
220 &self.id
221 }
222
223 #[must_use]
224 pub fn definition_name(&self) -> &CeremonyName {
225 &self.definition_name
226 }
227
228 #[must_use]
229 pub fn definition_version(&self) -> &CeremonyVersion {
230 &self.definition_version
231 }
232
233 #[must_use]
234 pub fn current_state(&self) -> &StateId {
235 &self.current_state
236 }
237
238 #[must_use]
239 pub fn step_records(&self) -> &BTreeMap<StepId, StepExecutionRecord> {
240 &self.step_records
241 }
242
243 #[must_use]
244 pub fn step_record(&self, step_id: &StepId) -> Option<&StepExecutionRecord> {
245 self.step_records.get(step_id)
246 }
247
248 #[must_use]
250 pub fn step_record_history(&self, step_id: &StepId) -> &[StepExecutionRecord] {
251 self.step_record_history
252 .get(step_id)
253 .map(Vec::as_slice)
254 .unwrap_or_default()
255 }
256
257 #[must_use]
260 pub fn step_repeat_limit_reached(
261 &self,
262 definition: &CeremonyDefinition,
263 step_id: &StepId,
264 ) -> bool {
265 let Some(step) = definition.step(step_id) else {
266 return false;
267 };
268 let Some(policy) = step.repeat_policy() else {
269 return false;
270 };
271 let Some(record) = self.step_record(step_id) else {
272 return false;
273 };
274 record.status().is_success()
275 && !policy.is_satisfied(record.output())
276 && !policy.permits_another_iteration(record.iteration())
277 }
278
279 #[must_use]
280 pub fn interventions(&self) -> &[CeremonyIntervention] {
281 &self.interventions
282 }
283
284 #[must_use]
285 pub fn guard_deferrals(&self) -> &[CeremonyGuardDeferral] {
286 &self.guard_deferrals
287 }
288
289 #[must_use]
295 pub fn guard_approvals(&self) -> &[CeremonyGuardApproval] {
296 &self.guard_approvals
297 }
298
299 #[must_use]
301 pub fn transitions(&self) -> &[CeremonyTransitionRecord] {
302 &self.transitions
303 }
304
305 #[must_use]
307 pub fn reasons(&self) -> &[CeremonyReason] {
308 &self.reasons
309 }
310
311 #[must_use]
312 pub fn intervention(
313 &self,
314 intervention_id: &CeremonyInterventionId,
315 ) -> Option<&CeremonyIntervention> {
316 self.interventions
317 .iter()
318 .find(|intervention| intervention.id() == intervention_id)
319 }
320
321 #[must_use]
322 pub fn context(&self) -> &CeremonyContext {
323 &self.context
324 }
325
326 #[must_use]
327 pub fn idempotency_keys(&self) -> &BTreeSet<IdempotencyKey> {
328 &self.idempotency_keys
329 }
330
331 #[must_use]
332 pub fn created_at(&self) -> OffsetDateTime {
333 self.created_at
334 }
335
336 #[must_use]
337 pub fn updated_at(&self) -> OffsetDateTime {
338 self.updated_at
339 }
340
341 #[must_use]
342 pub fn completed_at(&self) -> Option<OffsetDateTime> {
343 self.completed_at
344 }
345
346 #[must_use]
347 pub fn is_terminal(&self, definition: &CeremonyDefinition) -> bool {
348 self.matches_definition(definition) && definition.is_terminal_state(&self.current_state)
349 }
350
351 #[must_use]
352 pub fn is_completed(&self, definition: &CeremonyDefinition) -> bool {
353 self.is_terminal(definition) && self.completed_at.is_some()
354 }
355
356 pub fn start_step_as(
357 &mut self,
358 definition: &CeremonyDefinition,
359 role_id: &RoleId,
360 step_id: &StepId,
361 lease: StepLease,
362 now: OffsetDateTime,
363 ) -> Result<StepAttempt, DomainError> {
364 self.require_role(definition, role_id, &RoleAction::step(step_id.clone()))?;
365 self.start_step(definition, step_id, lease, now)
366 }
367
368 pub fn start_step(
369 &mut self,
370 definition: &CeremonyDefinition,
371 step_id: &StepId,
372 lease: StepLease,
373 now: OffsetDateTime,
374 ) -> Result<StepAttempt, DomainError> {
375 self.require_definition(definition)?;
376 if self.is_terminal(definition) {
377 return Err(DomainError::InvariantViolated {
378 reason: "terminal ceremony instances cannot start steps",
379 });
380 }
381
382 let step = definition.step(step_id).ok_or(DomainError::NotFound {
383 what: "ceremony_instance.step",
384 })?;
385 if step.state_id() != &self.current_state {
386 return Err(DomainError::InvalidTransition {
387 from: "ceremony_instance.current_state",
388 to: "ceremony_step.state",
389 });
390 }
391
392 let record = self
393 .step_records
394 .get(step_id)
395 .cloned()
396 .ok_or(DomainError::NotFound {
397 what: "ceremony_instance.step_record",
398 })?;
399 if !record.can_be_started_at(now) {
400 return Err(DomainError::InvariantViolated {
401 reason: "step lease is still active",
402 });
403 }
404
405 let next_attempt = next_attempt_for_start(&record)?;
406 if !step.retry_policy().allows_attempt(next_attempt) {
407 return Err(DomainError::InvariantViolated {
408 reason: "step retry policy exhausted",
409 });
410 }
411 if !self
412 .idempotency_keys
413 .insert(lease.idempotency_key().clone())
414 {
415 return Err(DomainError::AlreadyExists {
416 what: "ceremony_instance.idempotency_key",
417 });
418 }
419
420 self.step_records
421 .insert(step_id.clone(), record.with_started(lease, next_attempt));
422 self.updated_at = now;
423 Ok(next_attempt)
424 }
425
426 pub fn apply_step_result(
427 &mut self,
428 definition: &CeremonyDefinition,
429 step_id: &StepId,
430 result: StepResult,
431 now: OffsetDateTime,
432 ) -> Result<(), DomainError> {
433 self.require_definition(definition)?;
434 let step = definition.step(step_id).ok_or(DomainError::NotFound {
435 what: "ceremony_instance.step",
436 })?;
437 if step.state_id() != &self.current_state {
438 return Err(DomainError::InvalidTransition {
439 from: "ceremony_instance.current_state",
440 to: "ceremony_step.state",
441 });
442 }
443
444 let record = self
445 .step_records
446 .get(step_id)
447 .cloned()
448 .ok_or(DomainError::NotFound {
449 what: "ceremony_instance.step_record",
450 })?;
451 if record.status() != StepStatus::InProgress {
452 return Err(DomainError::InvariantViolated {
453 reason: "step result requires an in-progress step",
454 });
455 }
456
457 let finished = record.with_result(result);
458 let repeat = step.repeat_policy().filter(|policy| {
459 finished.status().is_success() && !policy.is_satisfied(finished.output())
460 });
461 if repeat.is_some_and(|policy| policy.permits_another_iteration(finished.iteration())) {
462 let next_iteration = finished.iteration().next()?;
463 self.step_record_history
464 .entry(step_id.clone())
465 .or_default()
466 .push(finished);
467 self.step_records.insert(
468 step_id.clone(),
469 StepExecutionRecord::pending_iteration(next_iteration),
470 );
471 } else {
472 self.step_records.insert(step_id.clone(), finished);
473 }
474 self.updated_at = now;
475 Ok(())
476 }
477
478 pub fn approve_guard(
487 &mut self,
488 definition: &CeremonyDefinition,
489 guard_name: &GuardName,
490 approved_by: RoleId,
491 approved_by_kind: AuditActorKind,
492 now: OffsetDateTime,
493 ) -> Result<(), DomainError> {
494 self.require_active(
495 definition,
496 "terminal ceremony instances cannot approve guards",
497 )?;
498 let guard = definition
499 .guards()
500 .get(guard_name)
501 .ok_or(DomainError::NotFound {
502 what: "ceremony_guard",
503 })?;
504 if !matches!(guard.condition(), GuardCondition::HumanApproval) {
505 return Err(DomainError::InvariantViolated {
506 reason: "only human approval guards can be approved",
507 });
508 }
509 self.require_declared_role(definition, &approved_by)?;
510 self.context = self.context.clone().with_guard_approval(guard_name)?;
511 self.guard_approvals.push(CeremonyGuardApproval::record(
512 guard_name.clone(),
513 approved_by,
514 approved_by_kind,
515 now,
516 ));
517 self.updated_at = now;
518 Ok(())
519 }
520
521 pub fn bind_participant(
530 &mut self,
531 definition: &CeremonyDefinition,
532 role_id: RoleId,
533 specialty: Specialty,
534 now: OffsetDateTime,
535 ) -> Result<(), DomainError> {
536 self.require_active(
537 definition,
538 "terminal ceremony instances cannot be re-seated",
539 )?;
540 if definition.role(&role_id).is_none() {
542 return Err(DomainError::NotFound {
543 what: "ceremony_role",
544 });
545 }
546 self.participant_bindings.insert(
547 role_id.clone(),
548 CeremonyParticipantBinding::record(role_id, specialty, now),
549 );
550 self.updated_at = now;
551 Ok(())
552 }
553
554 #[must_use]
555 pub fn participant_bindings(&self) -> &BTreeMap<RoleId, CeremonyParticipantBinding> {
556 &self.participant_bindings
557 }
558
559 #[must_use]
562 pub fn bound_specialty(&self, role_id: &RoleId) -> Option<&Specialty> {
563 self.participant_bindings
564 .get(role_id)
565 .map(CeremonyParticipantBinding::specialty)
566 }
567
568 pub fn defer_guard(
569 &mut self,
570 definition: &CeremonyDefinition,
571 guard_name: GuardName,
572 content: CeremonyGuardDeferralContent,
573 deferred_by: RoleId,
574 deferred_by_kind: AuditActorKind,
575 now: OffsetDateTime,
576 ) -> Result<(), DomainError> {
577 self.require_active(
578 definition,
579 "terminal ceremony instances cannot defer guard decisions",
580 )?;
581 let guard = definition
582 .guards()
583 .get(&guard_name)
584 .ok_or(DomainError::NotFound {
585 what: "ceremony_guard",
586 })?;
587 if !matches!(guard.condition(), GuardCondition::HumanApproval) {
588 return Err(DomainError::InvariantViolated {
589 reason: "only human approval guards can be deferred",
590 });
591 }
592 if self.context.is_guard_approved(&guard_name) {
593 return Err(DomainError::InvariantViolated {
594 reason: "approved human guards cannot be deferred",
595 });
596 }
597 let is_currently_required = definition
598 .available_transitions(&self.current_state)
599 .any(|transition| transition.required_guards().contains(&guard_name));
600 if !is_currently_required {
601 return Err(DomainError::InvariantViolated {
602 reason: "human guard is not required from the current state",
603 });
604 }
605
606 self.require_declared_role(definition, &deferred_by)?;
607 self.guard_deferrals.push(CeremonyGuardDeferral::record(
608 guard_name,
609 deferred_by,
610 deferred_by_kind,
611 content,
612 now,
613 ));
614 self.updated_at = now;
615 Ok(())
616 }
617
618 #[allow(clippy::too_many_arguments)]
619 pub fn request_intervention_as(
620 &mut self,
621 definition: &CeremonyDefinition,
622 intervention_id: CeremonyInterventionId,
623 role_id: RoleId,
624 kind: CeremonyInterventionKind,
625 target: CeremonyInterventionTarget,
626 content: CeremonyInterventionContent,
627 now: OffsetDateTime,
628 ) -> Result<(), DomainError> {
629 self.request_intervention_with_provenance_as(
630 definition,
631 intervention_id,
632 role_id,
633 kind,
634 target,
635 content,
636 None,
637 now,
638 )
639 }
640
641 #[allow(clippy::too_many_arguments)]
642 pub fn request_intervention_with_provenance_as(
643 &mut self,
644 definition: &CeremonyDefinition,
645 intervention_id: CeremonyInterventionId,
646 role_id: RoleId,
647 kind: CeremonyInterventionKind,
648 target: CeremonyInterventionTarget,
649 content: CeremonyInterventionContent,
650 provenance: Option<CeremonyInterventionProvenance>,
651 now: OffsetDateTime,
652 ) -> Result<(), DomainError> {
653 self.require_active(
654 definition,
655 "terminal ceremony instances cannot accept interventions",
656 )?;
657 self.require_role(definition, &role_id, &RoleAction::request_intervention())?;
658 Self::require_intervention_target(definition, &target)?;
659 if let Some(provenance) = provenance.as_ref() {
660 self.require_intervention_provenance(definition, &role_id, &target, provenance)?;
661 }
662 if self
663 .interventions
664 .iter()
665 .any(|intervention| intervention.id() == &intervention_id)
666 {
667 return Err(DomainError::AlreadyExists {
668 what: "ceremony_intervention",
669 });
670 }
671 let intervention = CeremonyIntervention::open_with_provenance(
672 intervention_id,
673 kind,
674 role_id,
675 target,
676 content,
677 provenance,
678 now,
679 );
680 self.interventions.push(intervention);
681 self.updated_at = now;
682 Ok(())
683 }
684
685 pub fn respond_to_intervention_as(
686 &mut self,
687 definition: &CeremonyDefinition,
688 intervention_id: &CeremonyInterventionId,
689 role_id: RoleId,
690 content: CeremonyInterventionContent,
691 now: OffsetDateTime,
692 ) -> Result<(), DomainError> {
693 self.require_active(
694 definition,
695 "terminal ceremony instances cannot receive intervention responses",
696 )?;
697 self.require_role(definition, &role_id, &RoleAction::respond_to_intervention())?;
698 self.interventions
699 .iter_mut()
700 .find(|intervention| intervention.id() == intervention_id)
701 .ok_or(DomainError::NotFound {
702 what: "ceremony_intervention",
703 })?
704 .respond(role_id, content, now)?;
705 self.record_that_it_answers(intervention_id, now);
706 self.updated_at = now;
707 Ok(())
708 }
709
710 pub fn prepare_evidence_request_as(
711 &self,
712 definition: &CeremonyDefinition,
713 intervention_id: CeremonyInterventionId,
714 role_id: RoleId,
715 source_id: CeremonyEvidenceSourceId,
716 query: CeremonyInterventionContent,
717 ) -> Result<CeremonyEvidenceRequest, DomainError> {
718 self.require_active(
719 definition,
720 "terminal ceremony instances cannot collect intervention evidence",
721 )?;
722 self.require_role(definition, &role_id, &RoleAction::respond_to_intervention())?;
723 self.intervention(&intervention_id)
724 .ok_or(DomainError::NotFound {
725 what: "ceremony_intervention",
726 })?
727 .ensure_can_respond(&role_id)?;
728 Ok(CeremonyEvidenceRequest::new(
729 self.id.clone(),
730 intervention_id,
731 role_id,
732 source_id,
733 query,
734 self.context.clone(),
735 ))
736 }
737
738 pub fn respond_to_intervention_with_evidence_as(
739 &mut self,
740 definition: &CeremonyDefinition,
741 intervention_id: &CeremonyInterventionId,
742 role_id: RoleId,
743 evidence_pack: super::CeremonyEvidencePack,
744 now: OffsetDateTime,
745 ) -> Result<(), DomainError> {
746 self.require_active(
747 definition,
748 "terminal ceremony instances cannot receive intervention evidence",
749 )?;
750 self.require_role(definition, &role_id, &RoleAction::respond_to_intervention())?;
751 self.interventions
752 .iter_mut()
753 .find(|intervention| intervention.id() == intervention_id)
754 .ok_or(DomainError::NotFound {
755 what: "ceremony_intervention",
756 })?
757 .respond_with_evidence(role_id, evidence_pack, now)?;
758 self.record_that_it_answers(intervention_id, now);
759 self.updated_at = now;
760 Ok(())
761 }
762
763 #[allow(clippy::too_many_arguments)]
778 pub fn assert_reason_as(
779 &mut self,
780 definition: &CeremonyDefinition,
781 role_id: RoleId,
782 from: CeremonyRecordRef,
783 to: CeremonyRecordRef,
784 kind: CeremonyReasonKind,
785 why: impl Into<String>,
786 confidence: MemoryConfidence,
787 now: OffsetDateTime,
788 ) -> Result<(), DomainError> {
789 self.require_declared_role(definition, &role_id)?;
790 self.require_record(&from)?;
791 self.require_record(&to)?;
792
793 match kind.asserter() {
794 ReasonAsserter::TheEngine => {
795 return Err(DomainError::InvariantViolated {
796 reason:
797 "this kind of reason states the shape of the session, not a judgement, \
798 and only the engine may assert it",
799 });
800 }
801 ReasonAsserter::ItsAuthor => {
802 if self.author_of(&from) != Some(&role_id) {
803 return Err(DomainError::InvariantViolated {
804 reason: "only whoever produced something may say why they decided it or \
805 how they did it",
806 });
807 }
808 }
809 ReasonAsserter::AnySeat => {}
810 }
811
812 self.reasons.push(CeremonyReason::new(
813 from,
814 to,
815 kind,
816 why,
817 confidence,
818 Some(role_id),
819 now,
820 )?);
821 self.updated_at = now;
822 Ok(())
823 }
824
825 fn author_of(&self, record: &CeremonyRecordRef) -> Option<&RoleId> {
832 match record {
833 CeremonyRecordRef::Step { .. } => None,
834 CeremonyRecordRef::AgendaItem { agenda_item } => self
835 .intervention(agenda_item)
836 .map(CeremonyIntervention::requested_by),
837 CeremonyRecordRef::Contribution {
838 agenda_item,
839 ordinal,
840 } => self
841 .intervention(agenda_item)
842 .and_then(|item| item.responses().get(*ordinal as usize))
843 .map(CeremonyInterventionResponse::role_id),
844 CeremonyRecordRef::GuardDecision { guard_name } => self
845 .guard_approvals
846 .iter()
847 .find(|approval| approval.guard_name() == guard_name)
848 .map(CeremonyGuardApproval::approved_by)
849 .or_else(|| {
850 self.guard_deferrals
851 .iter()
852 .find(|deferral| deferral.guard_name() == guard_name)
853 .map(CeremonyGuardDeferral::deferred_by)
854 }),
855 CeremonyRecordRef::Transition { ordinal } => self
856 .transitions
857 .get(ordinal.saturating_sub(1) as usize)
858 .and_then(CeremonyTransitionRecord::applied_by),
859 }
860 }
861
862 fn require_record(&self, record: &CeremonyRecordRef) -> Result<(), DomainError> {
869 let exists = match record {
870 CeremonyRecordRef::Step { step_id } => self.step_records.contains_key(step_id),
871 CeremonyRecordRef::AgendaItem { agenda_item } => {
872 self.intervention(agenda_item).is_some()
873 }
874 CeremonyRecordRef::Contribution {
875 agenda_item,
876 ordinal,
877 } => self
878 .intervention(agenda_item)
879 .is_some_and(|item| item.responses().len() > *ordinal as usize),
880 CeremonyRecordRef::GuardDecision { guard_name } => {
881 self.guard_approvals
882 .iter()
883 .any(|approval| approval.guard_name() == guard_name)
884 || self
885 .guard_deferrals
886 .iter()
887 .any(|deferral| deferral.guard_name() == guard_name)
888 }
889 CeremonyRecordRef::Transition { ordinal } => {
890 *ordinal >= 1 && (*ordinal as usize) <= self.transitions.len()
891 }
892 };
893 if exists {
894 Ok(())
895 } else {
896 Err(DomainError::NotFound {
897 what: "ceremony_record",
898 })
899 }
900 }
901
902 fn record_that_it_answers(
909 &mut self,
910 agenda_item: &CeremonyInterventionId,
911 now: OffsetDateTime,
912 ) {
913 let Some(ordinal) = self
914 .intervention(agenda_item)
915 .map(|item| item.responses().len())
916 .and_then(|count| u32::try_from(count.checked_sub(1)?).ok())
917 else {
918 return;
919 };
920 if let Ok(reason) = CeremonyReason::new(
921 CeremonyRecordRef::contribution(agenda_item.clone(), ordinal),
922 CeremonyRecordRef::agenda_item(agenda_item.clone()),
923 CeremonyReasonKind::Answers,
924 "a contribution made against this agenda item",
925 MemoryConfidence::High,
926 None,
927 now,
928 ) {
929 self.reasons.push(reason);
930 }
931 }
932
933 pub fn close_intervention_as(
934 &mut self,
935 definition: &CeremonyDefinition,
936 intervention_id: &CeremonyInterventionId,
937 role_id: &RoleId,
938 now: OffsetDateTime,
939 ) -> Result<(), DomainError> {
940 self.require_active(
941 definition,
942 "terminal ceremony instances cannot close interventions",
943 )?;
944 self.require_role(definition, role_id, &RoleAction::request_intervention())?;
945 self.interventions
946 .iter_mut()
947 .find(|intervention| intervention.id() == intervention_id)
948 .ok_or(DomainError::NotFound {
949 what: "ceremony_intervention",
950 })?
951 .close(role_id, now)?;
952 self.updated_at = now;
953 Ok(())
954 }
955
956 pub fn apply_transition_as(
957 &mut self,
958 definition: &CeremonyDefinition,
959 role_id: &RoleId,
960 trigger: &TransitionTrigger,
961 now: OffsetDateTime,
962 ) -> Result<StateId, DomainError> {
963 self.require_role(
964 definition,
965 role_id,
966 &RoleAction::transition(trigger.clone()),
967 )?;
968 self.move_on(definition, trigger, Some(role_id.clone()), now)
969 }
970
971 pub fn apply_transition(
972 &mut self,
973 definition: &CeremonyDefinition,
974 trigger: &TransitionTrigger,
975 now: OffsetDateTime,
976 ) -> Result<StateId, DomainError> {
977 self.move_on(definition, trigger, None, now)
978 }
979
980 fn move_on(
985 &mut self,
986 definition: &CeremonyDefinition,
987 trigger: &TransitionTrigger,
988 applied_by: Option<RoleId>,
989 now: OffsetDateTime,
990 ) -> Result<StateId, DomainError> {
991 self.require_definition(definition)?;
992 if self.is_terminal(definition) {
993 return Err(DomainError::InvariantViolated {
994 reason: "terminal ceremony instances cannot transition",
995 });
996 }
997
998 let transition = definition
999 .transition_for_trigger(&self.current_state, trigger)
1000 .ok_or(DomainError::InvalidTransition {
1001 from: "ceremony_instance.current_state",
1002 to: "transition_trigger",
1003 })?;
1004 if !definition.repeat_requirements_are_satisfied(&self.current_state, &self.step_records) {
1005 return Err(DomainError::InvariantViolated {
1006 reason: "ceremony step repeat condition is not satisfied",
1007 });
1008 }
1009 if !definition.guards_are_satisfied(transition, &self.step_records, &self.context) {
1010 return Err(DomainError::InvariantViolated {
1011 reason: "ceremony transition guards are not satisfied",
1012 });
1013 }
1014
1015 let from_state = self.current_state.clone();
1016 self.current_state = transition.to().clone();
1017 self.transitions.push(CeremonyTransitionRecord::record(
1018 trigger.clone(),
1019 from_state,
1020 self.current_state.clone(),
1021 applied_by,
1022 now,
1023 ));
1024 self.updated_at = now;
1025 if definition.is_terminal_state(&self.current_state) {
1026 self.completed_at = Some(now);
1027 }
1028 Ok(self.current_state.clone())
1029 }
1030
1031 fn matches_definition(&self, definition: &CeremonyDefinition) -> bool {
1032 self.definition_name == *definition.name()
1033 && self.definition_version == *definition.version()
1034 }
1035
1036 fn require_definition(&self, definition: &CeremonyDefinition) -> Result<(), DomainError> {
1037 if self.matches_definition(definition) {
1038 Ok(())
1039 } else {
1040 Err(DomainError::InvariantViolated {
1041 reason: "ceremony instance definition mismatch",
1042 })
1043 }
1044 }
1045
1046 fn require_active(
1047 &self,
1048 definition: &CeremonyDefinition,
1049 terminal_reason: &'static str,
1050 ) -> Result<(), DomainError> {
1051 self.require_definition(definition)?;
1052 if self.is_terminal(definition) {
1053 Err(DomainError::InvariantViolated {
1054 reason: terminal_reason,
1055 })
1056 } else {
1057 Ok(())
1058 }
1059 }
1060
1061 fn require_intervention_target(
1062 definition: &CeremonyDefinition,
1063 target: &CeremonyInterventionTarget,
1064 ) -> Result<(), DomainError> {
1065 let Some(role_ids) = target.role_ids() else {
1066 return Ok(());
1067 };
1068 for role_id in role_ids {
1069 if definition.role(role_id).is_none() {
1070 return Err(DomainError::NotFound {
1071 what: "ceremony_intervention.target_role",
1072 });
1073 }
1074 if !definition.role_allows(role_id, &RoleAction::respond_to_intervention()) {
1075 return Err(DomainError::InvariantViolated {
1076 reason: "target role cannot respond to ceremony interventions",
1077 });
1078 }
1079 }
1080 Ok(())
1081 }
1082
1083 fn require_intervention_provenance(
1084 &self,
1085 definition: &CeremonyDefinition,
1086 requested_by: &RoleId,
1087 target: &CeremonyInterventionTarget,
1088 provenance: &CeremonyInterventionProvenance,
1089 ) -> Result<(), DomainError> {
1090 let source = self
1091 .intervention(provenance.source_intervention_id())
1092 .ok_or(DomainError::NotFound {
1093 what: "ceremony_intervention.provenance_source",
1094 })?;
1095 if source.requested_by() != requested_by {
1096 return Err(DomainError::InvariantViolated {
1097 reason: "only the source requester can select an intervention response",
1098 });
1099 }
1100 if !source
1101 .responses()
1102 .iter()
1103 .any(|response| response.role_id() == provenance.source_response_role_id())
1104 {
1105 return Err(DomainError::NotFound {
1106 what: "ceremony_intervention.provenance_response",
1107 });
1108 }
1109 if definition.role(provenance.selected_role_id()).is_none() {
1110 return Err(DomainError::NotFound {
1111 what: "ceremony_intervention.provenance_selected_role",
1112 });
1113 }
1114 if !definition.role_allows(
1115 provenance.selected_role_id(),
1116 &RoleAction::respond_to_intervention(),
1117 ) {
1118 return Err(DomainError::InvariantViolated {
1119 reason: "selected intervention role cannot respond",
1120 });
1121 }
1122 if !target.accepts(provenance.selected_role_id()) {
1123 return Err(DomainError::InvariantViolated {
1124 reason: "intervention target does not include the selected role",
1125 });
1126 }
1127 Ok(())
1128 }
1129
1130 fn require_declared_role(
1141 &self,
1142 definition: &CeremonyDefinition,
1143 role_id: &RoleId,
1144 ) -> Result<(), DomainError> {
1145 self.require_definition(definition)?;
1146 if definition.role(role_id).is_some() {
1147 Ok(())
1148 } else {
1149 Err(DomainError::NotFound {
1150 what: "ceremony_role",
1151 })
1152 }
1153 }
1154
1155 fn require_role(
1156 &self,
1157 definition: &CeremonyDefinition,
1158 role_id: &RoleId,
1159 action: &RoleAction,
1160 ) -> Result<(), DomainError> {
1161 self.require_definition(definition)?;
1162 if definition.role_allows(role_id, action) {
1163 Ok(())
1164 } else {
1165 Err(DomainError::InvariantViolated {
1166 reason: "ceremony role is not allowed to perform action",
1167 })
1168 }
1169 }
1170}
1171
1172fn next_attempt_for_start(record: &StepExecutionRecord) -> Result<StepAttempt, DomainError> {
1173 if matches!(record.status(), StepStatus::Failed | StepStatus::InProgress) {
1174 record.attempt().next()
1175 } else {
1176 Ok(record.attempt())
1177 }
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182 use super::*;
1183 use crate::value_objects::{
1184 Attributes, CeremonyGuard, CeremonyState, CeremonyStep, CeremonyTransition, GuardCondition,
1185 GuardName, LeaseOwnerId, RepeatUntilCondition, RetryPolicy, StepHandlerConfig,
1186 StepHandlerKind, StepIteration, StepOutput, StepOutputField, StepRepeatPolicy,
1187 };
1188 use serde_json::json;
1189 use time::macros::datetime;
1190
1191 fn now() -> OffsetDateTime {
1192 datetime!(2026-06-06 12:00:00 UTC)
1193 }
1194
1195 fn state_id(raw: &str) -> StateId {
1196 StateId::new(raw).unwrap()
1197 }
1198
1199 fn step_id(raw: &str) -> StepId {
1200 StepId::new(raw).unwrap()
1201 }
1202
1203 fn trigger(raw: &str) -> TransitionTrigger {
1204 TransitionTrigger::new(raw).unwrap()
1205 }
1206
1207 fn role_id(raw: &str) -> RoleId {
1208 RoleId::new(raw).unwrap()
1209 }
1210
1211 fn guard_name(raw: &str) -> GuardName {
1212 GuardName::new(raw).unwrap()
1213 }
1214
1215 fn handler_kind() -> StepHandlerKind {
1216 StepHandlerKind::new("multiagent_round").unwrap()
1217 }
1218
1219 fn retrying_step(raw_step_id: &str, raw_state_id: &str) -> CeremonyStep {
1220 CeremonyStep::new(
1221 step_id(raw_step_id),
1222 state_id(raw_state_id),
1223 handler_kind(),
1224 StepHandlerConfig::empty(),
1225 RetryPolicy::new(
1226 StepAttempt::new(3).unwrap(),
1227 crate::value_objects::DurationMs::ZERO,
1228 ),
1229 None,
1230 )
1231 }
1232
1233 fn single_attempt_step(raw_step_id: &str, raw_state_id: &str) -> CeremonyStep {
1234 CeremonyStep::new(
1235 step_id(raw_step_id),
1236 state_id(raw_state_id),
1237 handler_kind(),
1238 StepHandlerConfig::empty(),
1239 RetryPolicy::single_attempt(),
1240 None,
1241 )
1242 }
1243
1244 fn repeating_plan(max_iterations: u32) -> CeremonyStep {
1245 retrying_step("plan", "drafting").with_repeat_policy(StepRepeatPolicy::new(
1246 RepeatUntilCondition::output_field_equals(
1247 StepOutputField::new("ready").unwrap(),
1248 json!(true),
1249 ),
1250 StepIteration::new(max_iterations).unwrap(),
1251 ))
1252 }
1253
1254 fn readiness_output(ready: bool) -> StepOutput {
1255 StepOutput::new(
1256 Attributes::new(std::collections::BTreeMap::from([(
1257 "ready".to_owned(),
1258 json!(ready),
1259 )]))
1260 .unwrap(),
1261 )
1262 }
1263
1264 fn lease(
1265 raw_owner_id: &str,
1266 raw_key: &str,
1267 acquired_at: OffsetDateTime,
1268 expires_at: OffsetDateTime,
1269 ) -> StepLease {
1270 StepLease::new(
1271 LeaseOwnerId::new(raw_owner_id).unwrap(),
1272 IdempotencyKey::new(raw_key).unwrap(),
1273 acquired_at,
1274 expires_at,
1275 )
1276 .unwrap()
1277 }
1278
1279 fn role(actions: Vec<RoleAction>) -> crate::value_objects::CeremonyRole {
1280 crate::value_objects::CeremonyRole::new(role_id("facilitator"), actions).unwrap()
1281 }
1282
1283 fn definition_with_steps(steps: Vec<CeremonyStep>) -> CeremonyDefinition {
1284 let plan_done = CeremonyGuard::new(
1285 guard_name("plan_done"),
1286 GuardCondition::StepStatus {
1287 step_id: step_id("plan"),
1288 status: StepStatus::Completed,
1289 },
1290 );
1291 let finish = CeremonyTransition::new(
1292 state_id("drafting"),
1293 state_id("done"),
1294 trigger("finish"),
1295 vec![plan_done.name().clone()],
1296 )
1297 .unwrap();
1298 let role = role(vec![
1299 RoleAction::step(step_id("plan")),
1300 RoleAction::transition(finish.trigger().clone()),
1301 RoleAction::request_intervention(),
1302 ]);
1303 let observer = crate::value_objects::CeremonyRole::new(
1304 role_id("observer"),
1305 vec![RoleAction::respond_to_intervention()],
1306 )
1307 .unwrap();
1308
1309 CeremonyDefinition::new(
1310 crate::value_objects::CeremonyName::new("planning_ceremony").unwrap(),
1311 CeremonyVersion::v1(),
1312 None,
1313 Vec::new(),
1314 Vec::new(),
1315 vec![
1316 CeremonyState::initial(state_id("drafting")),
1317 CeremonyState::intermediate(state_id("review")),
1318 CeremonyState::terminal(state_id("done")),
1319 ],
1320 vec![finish],
1321 steps,
1322 vec![plan_done],
1323 vec![role, observer],
1324 )
1325 .unwrap()
1326 }
1327
1328 fn definition() -> CeremonyDefinition {
1329 definition_with_steps(vec![
1330 retrying_step("plan", "drafting"),
1331 single_attempt_step("review_step", "review"),
1332 ])
1333 }
1334
1335 #[test]
1336 fn a_verified_digest_migration_rebinds_only_its_exact_definition() {
1337 let definition = definition();
1338 let published = PublishedCeremonyDefinition::seal(definition.clone()).unwrap();
1339 let migration = definition.choreographer_v1_digest_migration().unwrap();
1340 let mut value = serde_json::to_value(CeremonyInstance::start_bound(
1341 CeremonyId::new("legacy-bound").unwrap(),
1342 &published,
1343 CeremonyContext::empty(),
1344 now(),
1345 ))
1346 .unwrap();
1347 value["bound_definition"] = serde_json::to_value(migration.source()).unwrap();
1348 let mut instance: CeremonyInstance = serde_json::from_value(value).unwrap();
1349
1350 assert!(instance.migrate_definition_binding(&migration).unwrap());
1351 assert_eq!(instance.bound_definition(), Some(migration.destination()));
1352 assert!(!instance.migrate_definition_binding(&migration).unwrap());
1353 }
1354
1355 #[test]
1356 fn a_digest_migration_for_another_definition_is_rejected() {
1357 let definition = definition();
1358 let published = PublishedCeremonyDefinition::seal(definition.clone()).unwrap();
1359 let mut instance = CeremonyInstance::start_bound(
1360 CeremonyId::new("still-bound").unwrap(),
1361 &published,
1362 CeremonyContext::empty(),
1363 now(),
1364 );
1365 let other = CeremonyDefinition::new(
1366 CeremonyName::new("another_ceremony").unwrap(),
1367 CeremonyVersion::v1(),
1368 None,
1369 [],
1370 [],
1371 [CeremonyState::initial(state_id("OPEN"))],
1372 [],
1373 [],
1374 [],
1375 [],
1376 )
1377 .unwrap()
1378 .choreographer_v1_digest_migration()
1379 .unwrap();
1380
1381 assert!(instance.migrate_definition_binding(&other).is_err());
1382 assert_eq!(instance.bound_definition(), Some(published.digest()));
1383 }
1384
1385 fn definition_with_human_guard(approval: &CeremonyGuard) -> CeremonyDefinition {
1388 let finish = CeremonyTransition::new(
1389 state_id("drafting"),
1390 state_id("done"),
1391 trigger("approve"),
1392 vec![approval.name().clone()],
1393 )
1394 .unwrap();
1395 CeremonyDefinition::new(
1396 crate::value_objects::CeremonyName::new("approval_ceremony").unwrap(),
1397 CeremonyVersion::v1(),
1398 None,
1399 Vec::new(),
1400 Vec::new(),
1401 vec![
1402 CeremonyState::initial(state_id("drafting")),
1403 CeremonyState::terminal(state_id("done")),
1404 ],
1405 vec![finish.clone()],
1406 Vec::new(),
1407 vec![approval.clone()],
1408 vec![role(vec![RoleAction::transition(finish.trigger().clone())])],
1409 )
1410 .unwrap()
1411 }
1412
1413 fn instance(definition: &CeremonyDefinition) -> CeremonyInstance {
1414 CeremonyInstance::start(
1415 CeremonyId::new("ceremony-1").unwrap(),
1416 definition,
1417 CeremonyContext::empty(),
1418 now(),
1419 )
1420 }
1421
1422 #[test]
1423 fn starts_in_initial_state_with_pending_records() {
1424 let definition = definition();
1425 let instance = instance(&definition);
1426
1427 assert_eq!(instance.current_state(), &state_id("drafting"));
1428 assert_eq!(
1429 instance.step_record(&step_id("plan")).unwrap().status(),
1430 StepStatus::Pending
1431 );
1432 assert_eq!(
1433 instance
1434 .step_record(&step_id("review_step"))
1435 .unwrap()
1436 .status(),
1437 StepStatus::Pending
1438 );
1439 }
1440
1441 #[test]
1442 fn instances_without_iteration_fields_load_as_the_first_iteration() {
1443 let definition = definition();
1444 let mut value = serde_json::to_value(instance(&definition)).unwrap();
1445 value.as_object_mut().unwrap().remove("step_record_history");
1446 for record in value["step_records"].as_object_mut().unwrap().values_mut() {
1447 record.as_object_mut().unwrap().remove("iteration");
1448 }
1449
1450 let restored: CeremonyInstance = serde_json::from_value(value).unwrap();
1451
1452 assert!(restored.step_record_history(&step_id("plan")).is_empty());
1453 assert_eq!(
1454 restored.step_record(&step_id("plan")).unwrap().iteration(),
1455 StepIteration::FIRST
1456 );
1457 }
1458
1459 #[test]
1460 fn dynamic_intervention_collects_role_scoped_response_and_requester_closes_it() {
1461 let definition = definition();
1462 let mut instance = instance(&definition);
1463 let intervention_id = CeremonyInterventionId::new("queue-check").unwrap();
1464 let facilitator = role_id("facilitator");
1465 let observer = role_id("observer");
1466
1467 instance
1468 .request_intervention_as(
1469 &definition,
1470 intervention_id.clone(),
1471 facilitator.clone(),
1472 CeremonyInterventionKind::Investigation,
1473 CeremonyInterventionTarget::roles([observer.clone()]).unwrap(),
1474 CeremonyInterventionContent::new(
1475 "Inspect the queue without consuming messages.",
1476 Attributes::empty(),
1477 )
1478 .unwrap(),
1479 now(),
1480 )
1481 .unwrap();
1482 instance
1483 .respond_to_intervention_as(
1484 &definition,
1485 &intervention_id,
1486 observer.clone(),
1487 CeremonyInterventionContent::new("Queue depth is stable.", Attributes::empty())
1488 .unwrap(),
1489 now(),
1490 )
1491 .unwrap();
1492 let selected_intervention_id = CeremonyInterventionId::new("selected-check").unwrap();
1493 instance
1494 .request_intervention_with_provenance_as(
1495 &definition,
1496 selected_intervention_id.clone(),
1497 facilitator.clone(),
1498 CeremonyInterventionKind::Investigation,
1499 CeremonyInterventionTarget::roles([observer.clone()]).unwrap(),
1500 CeremonyInterventionContent::new(
1501 "Inspect the proposed signal.",
1502 Attributes::empty(),
1503 )
1504 .unwrap(),
1505 Some(CeremonyInterventionProvenance::selected_from(
1506 intervention_id.clone(),
1507 observer.clone(),
1508 observer.clone(),
1509 )),
1510 now(),
1511 )
1512 .unwrap();
1513 instance
1514 .close_intervention_as(&definition, &intervention_id, &facilitator, now())
1515 .unwrap();
1516
1517 let intervention = instance.intervention(&intervention_id).unwrap();
1518 assert_eq!(intervention.responses().len(), 1);
1519 assert_eq!(
1520 intervention.status(),
1521 crate::value_objects::CeremonyInterventionStatus::Closed
1522 );
1523 let provenance = instance
1524 .intervention(&selected_intervention_id)
1525 .unwrap()
1526 .provenance()
1527 .unwrap();
1528 assert_eq!(provenance.source_intervention_id(), &intervention_id);
1529 assert_eq!(provenance.selected_role_id(), &observer);
1530 }
1531
1532 #[test]
1533 fn intervention_rejects_roles_without_the_required_capability() {
1534 let definition = definition();
1535 let mut instance = instance(&definition);
1536
1537 let error = instance
1538 .request_intervention_as(
1539 &definition,
1540 CeremonyInterventionId::new("not-allowed").unwrap(),
1541 role_id("observer"),
1542 CeremonyInterventionKind::Opinion,
1543 CeremonyInterventionTarget::table(),
1544 CeremonyInterventionContent::new("What do you think?", Attributes::empty())
1545 .unwrap(),
1546 now(),
1547 )
1548 .unwrap_err();
1549
1550 assert!(matches!(error, DomainError::InvariantViolated { .. }));
1551 }
1552
1553 #[test]
1554 fn rejects_step_execution_outside_current_state() {
1555 let definition = definition();
1556 let mut instance = instance(&definition);
1557
1558 let err = instance
1559 .start_step(
1560 &definition,
1561 &step_id("review_step"),
1562 lease(
1563 "runner-1",
1564 "key-1",
1565 now(),
1566 datetime!(2026-06-06 12:05:00 UTC),
1567 ),
1568 now(),
1569 )
1570 .unwrap_err();
1571
1572 assert!(matches!(err, DomainError::InvalidTransition { .. }));
1573 }
1574
1575 #[test]
1576 fn completed_step_unlocks_guarded_transition() {
1577 let definition = definition();
1578 let mut instance = instance(&definition);
1579
1580 instance
1581 .start_step_as(
1582 &definition,
1583 &role_id("facilitator"),
1584 &step_id("plan"),
1585 lease(
1586 "runner-1",
1587 "key-1",
1588 now(),
1589 datetime!(2026-06-06 12:05:00 UTC),
1590 ),
1591 now(),
1592 )
1593 .unwrap();
1594 instance
1595 .apply_step_result(
1596 &definition,
1597 &step_id("plan"),
1598 StepResult::completed(StepOutput::empty()).unwrap(),
1599 datetime!(2026-06-06 12:01:00 UTC),
1600 )
1601 .unwrap();
1602 let state = instance
1603 .apply_transition_as(
1604 &definition,
1605 &role_id("facilitator"),
1606 &trigger("finish"),
1607 datetime!(2026-06-06 12:02:00 UTC),
1608 )
1609 .unwrap();
1610
1611 assert_eq!(state, state_id("done"));
1612 assert!(instance.is_completed(&definition));
1613 }
1614
1615 #[test]
1616 fn false_repeat_condition_archives_iteration_and_schedules_the_next() {
1617 let definition = definition_with_steps(vec![repeating_plan(3)]);
1618 let mut instance = instance(&definition);
1619
1620 instance
1621 .start_step(
1622 &definition,
1623 &step_id("plan"),
1624 lease(
1625 "runner-1",
1626 "repeat-1",
1627 now(),
1628 datetime!(2026-06-06 12:05:00 UTC),
1629 ),
1630 now(),
1631 )
1632 .unwrap();
1633 instance
1634 .apply_step_result(
1635 &definition,
1636 &step_id("plan"),
1637 StepResult::completed(readiness_output(false)).unwrap(),
1638 datetime!(2026-06-06 12:01:00 UTC),
1639 )
1640 .unwrap();
1641
1642 let current = instance.step_record(&step_id("plan")).unwrap();
1643 assert_eq!(current.status(), StepStatus::Pending);
1644 assert_eq!(current.iteration().get(), 2);
1645 assert_eq!(current.attempt(), StepAttempt::FIRST);
1646 let history = instance.step_record_history(&step_id("plan"));
1647 assert_eq!(history.len(), 1);
1648 assert_eq!(history[0].iteration(), StepIteration::FIRST);
1649 assert_eq!(history[0].output(), &readiness_output(false));
1650 assert!(instance
1651 .apply_transition(&definition, &trigger("finish"), now())
1652 .is_err());
1653
1654 instance
1655 .start_step(
1656 &definition,
1657 &step_id("plan"),
1658 lease(
1659 "runner-1",
1660 "repeat-2",
1661 datetime!(2026-06-06 12:02:00 UTC),
1662 datetime!(2026-06-06 12:07:00 UTC),
1663 ),
1664 datetime!(2026-06-06 12:02:00 UTC),
1665 )
1666 .unwrap();
1667 instance
1668 .apply_step_result(
1669 &definition,
1670 &step_id("plan"),
1671 StepResult::completed(readiness_output(true)).unwrap(),
1672 datetime!(2026-06-06 12:03:00 UTC),
1673 )
1674 .unwrap();
1675
1676 let current = instance.step_record(&step_id("plan")).unwrap();
1677 assert_eq!(current.status(), StepStatus::Completed);
1678 assert_eq!(current.iteration().get(), 2);
1679 assert!(!instance.step_repeat_limit_reached(&definition, &step_id("plan")));
1680 assert_eq!(
1681 instance
1682 .apply_transition(&definition, &trigger("finish"), now())
1683 .unwrap(),
1684 state_id("done")
1685 );
1686 }
1687
1688 #[test]
1689 fn repeat_limit_is_terminal_for_the_step_and_blocks_transition() {
1690 let definition = definition_with_steps(vec![repeating_plan(2)]);
1691 let mut instance = instance(&definition);
1692
1693 for iteration in 1..=2 {
1694 instance
1695 .start_step(
1696 &definition,
1697 &step_id("plan"),
1698 lease(
1699 "runner-1",
1700 &format!("limit-{iteration}"),
1701 now(),
1702 datetime!(2026-06-06 12:05:00 UTC),
1703 ),
1704 now(),
1705 )
1706 .unwrap();
1707 instance
1708 .apply_step_result(
1709 &definition,
1710 &step_id("plan"),
1711 StepResult::completed(readiness_output(false)).unwrap(),
1712 now(),
1713 )
1714 .unwrap();
1715 }
1716
1717 assert!(instance.step_repeat_limit_reached(&definition, &step_id("plan")));
1718 assert_eq!(
1719 instance
1720 .step_record(&step_id("plan"))
1721 .unwrap()
1722 .iteration()
1723 .get(),
1724 2
1725 );
1726 assert_eq!(instance.step_record_history(&step_id("plan")).len(), 1);
1727 assert!(instance
1728 .apply_transition(&definition, &trigger("finish"), now())
1729 .is_err());
1730 assert!(instance
1731 .start_step(
1732 &definition,
1733 &step_id("plan"),
1734 lease(
1735 "runner-1",
1736 "limit-3",
1737 now(),
1738 datetime!(2026-06-06 12:05:00 UTC),
1739 ),
1740 now(),
1741 )
1742 .is_err());
1743 }
1744
1745 #[test]
1746 fn active_lease_blocks_failover_takeover() {
1747 let definition = definition();
1748 let mut instance = instance(&definition);
1749
1750 instance
1751 .start_step(
1752 &definition,
1753 &step_id("plan"),
1754 lease(
1755 "runner-1",
1756 "key-1",
1757 now(),
1758 datetime!(2026-06-06 12:05:00 UTC),
1759 ),
1760 now(),
1761 )
1762 .unwrap();
1763 let err = instance
1764 .start_step(
1765 &definition,
1766 &step_id("plan"),
1767 lease(
1768 "runner-2",
1769 "key-2",
1770 datetime!(2026-06-06 12:01:00 UTC),
1771 datetime!(2026-06-06 12:06:00 UTC),
1772 ),
1773 datetime!(2026-06-06 12:01:00 UTC),
1774 )
1775 .unwrap_err();
1776
1777 assert!(matches!(err, DomainError::InvariantViolated { .. }));
1778 assert_eq!(
1779 instance
1780 .step_record(&step_id("plan"))
1781 .unwrap()
1782 .lease()
1783 .unwrap()
1784 .owner_id()
1785 .as_str(),
1786 "runner-1"
1787 );
1788 }
1789
1790 #[test]
1791 fn expired_lease_allows_failover_takeover_with_next_attempt() {
1792 let definition = definition();
1793 let mut instance = instance(&definition);
1794
1795 instance
1796 .start_step(
1797 &definition,
1798 &step_id("plan"),
1799 lease(
1800 "runner-1",
1801 "key-1",
1802 now(),
1803 datetime!(2026-06-06 12:05:00 UTC),
1804 ),
1805 now(),
1806 )
1807 .unwrap();
1808 let attempt = instance
1809 .start_step(
1810 &definition,
1811 &step_id("plan"),
1812 lease(
1813 "runner-2",
1814 "key-2",
1815 datetime!(2026-06-06 12:06:00 UTC),
1816 datetime!(2026-06-06 12:11:00 UTC),
1817 ),
1818 datetime!(2026-06-06 12:06:00 UTC),
1819 )
1820 .unwrap();
1821
1822 assert_eq!(attempt, StepAttempt::new(2).unwrap());
1823 let record = instance.step_record(&step_id("plan")).unwrap();
1824 assert_eq!(record.attempt(), StepAttempt::new(2).unwrap());
1825 assert_eq!(record.lease().unwrap().owner_id().as_str(), "runner-2");
1826 }
1827
1828 #[test]
1829 fn approving_a_guard_the_ceremony_never_declared_is_refused() {
1830 let approval =
1831 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1832 let finish = CeremonyTransition::new(
1833 state_id("drafting"),
1834 state_id("done"),
1835 trigger("approve"),
1836 vec![approval.name().clone()],
1837 )
1838 .unwrap();
1839 let definition = CeremonyDefinition::new(
1840 crate::value_objects::CeremonyName::new("approval_ceremony").unwrap(),
1841 CeremonyVersion::v1(),
1842 None,
1843 Vec::new(),
1844 Vec::new(),
1845 vec![
1846 CeremonyState::initial(state_id("drafting")),
1847 CeremonyState::terminal(state_id("done")),
1848 ],
1849 vec![finish.clone()],
1850 Vec::new(),
1851 vec![approval],
1852 vec![role(vec![RoleAction::transition(finish.trigger().clone())])],
1853 )
1854 .unwrap();
1855 let mut instance = instance(&definition);
1856
1857 assert!(matches!(
1862 instance.approve_guard(
1863 &definition,
1864 &guard_name("not_a_guard"),
1865 role_id("facilitator"),
1866 AuditActorKind::Human,
1867 now()
1868 ),
1869 Err(DomainError::NotFound {
1870 what: "ceremony_guard"
1871 })
1872 ));
1873 assert!(!instance
1874 .context()
1875 .is_guard_approved(&guard_name("not_a_guard")));
1876 }
1877
1878 #[test]
1879 fn human_approval_guard_uses_typed_context() {
1880 let approval =
1881 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1882 let finish = CeremonyTransition::new(
1883 state_id("drafting"),
1884 state_id("done"),
1885 trigger("approve"),
1886 vec![approval.name().clone()],
1887 )
1888 .unwrap();
1889 let definition = CeremonyDefinition::new(
1890 crate::value_objects::CeremonyName::new("approval_ceremony").unwrap(),
1891 CeremonyVersion::v1(),
1892 None,
1893 Vec::new(),
1894 Vec::new(),
1895 vec![
1896 CeremonyState::initial(state_id("drafting")),
1897 CeremonyState::terminal(state_id("done")),
1898 ],
1899 vec![finish.clone()],
1900 Vec::new(),
1901 vec![approval.clone()],
1902 vec![role(vec![RoleAction::transition(finish.trigger().clone())])],
1903 )
1904 .unwrap();
1905 let mut instance = instance(&definition);
1906
1907 assert!(matches!(
1908 instance.apply_transition(&definition, &trigger("approve"), now()),
1909 Err(DomainError::InvariantViolated { .. })
1910 ));
1911 instance
1912 .approve_guard(
1913 &definition,
1914 approval.name(),
1915 role_id("facilitator"),
1916 AuditActorKind::Human,
1917 datetime!(2026-06-06 12:01:00 UTC),
1918 )
1919 .unwrap();
1920 instance
1921 .apply_transition(
1922 &definition,
1923 &trigger("approve"),
1924 datetime!(2026-06-06 12:02:00 UTC),
1925 )
1926 .unwrap();
1927
1928 assert!(instance.is_completed(&definition));
1929 }
1930
1931 #[test]
1932 fn human_guard_deferral_preserves_uncertainty_without_approving() {
1933 let approval =
1934 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1935 let finish = CeremonyTransition::new(
1936 state_id("drafting"),
1937 state_id("done"),
1938 trigger("approve"),
1939 vec![approval.name().clone()],
1940 )
1941 .unwrap();
1942 let definition = CeremonyDefinition::new(
1943 crate::value_objects::CeremonyName::new("deferral_ceremony").unwrap(),
1944 CeremonyVersion::v1(),
1945 None,
1946 Vec::new(),
1947 Vec::new(),
1948 vec![
1949 CeremonyState::initial(state_id("drafting")),
1950 CeremonyState::terminal(state_id("done")),
1951 ],
1952 vec![finish.clone()],
1953 Vec::new(),
1954 vec![approval.clone()],
1955 vec![role(vec![RoleAction::transition(finish.trigger().clone())])],
1956 )
1957 .unwrap();
1958 let mut instance = instance(&definition);
1959
1960 instance
1961 .defer_guard(
1962 &definition,
1963 approval.name().clone(),
1964 CeremonyGuardDeferralContent::new(
1965 "I do not know.",
1966 "I cannot explain how the issue was resolved.",
1967 vec!["New evidence explains the resolution.".to_owned()],
1968 )
1969 .unwrap(),
1970 role_id("facilitator"),
1971 AuditActorKind::Human,
1972 datetime!(2026-06-06 12:01:00 UTC),
1973 )
1974 .unwrap();
1975
1976 assert!(!instance.context().is_guard_approved(approval.name()));
1977 assert!(instance
1978 .apply_transition(&definition, &trigger("approve"), now())
1979 .is_err());
1980 let deferral = &instance.guard_deferrals()[0];
1981 assert_eq!(deferral.guard_name(), approval.name());
1982 assert_eq!(deferral.content().statement(), "I do not know.");
1983 }
1984 #[test]
1987 fn approving_a_human_guard_records_the_seat_that_did_it() {
1988 let approval =
1989 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1990 let definition = definition_with_human_guard(&approval);
1991 let mut instance = instance(&definition);
1992
1993 instance
1994 .approve_guard(
1995 &definition,
1996 approval.name(),
1997 role_id("facilitator"),
1998 AuditActorKind::Human,
1999 datetime!(2026-06-06 12:01:00 UTC),
2000 )
2001 .unwrap();
2002
2003 let [recorded] = instance.guard_approvals() else {
2004 panic!(
2005 "expected one approval, got {:?}",
2006 instance.guard_approvals()
2007 );
2008 };
2009 assert_eq!(recorded.guard_name(), approval.name());
2010 assert_eq!(recorded.approved_by(), &role_id("facilitator"));
2011 assert_eq!(recorded.approved_at(), datetime!(2026-06-06 12:01:00 UTC));
2012 assert!(instance.context().is_guard_approved(approval.name()));
2013 }
2014
2015 #[test]
2019 fn a_seat_the_definition_does_not_declare_cannot_approve() {
2020 let approval =
2021 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
2022 let definition = definition_with_human_guard(&approval);
2023 let mut instance = instance(&definition);
2024
2025 let outcome = instance.approve_guard(
2026 &definition,
2027 approval.name(),
2028 role_id("someone-who-is-not-here"),
2029 AuditActorKind::Human,
2030 now(),
2031 );
2032
2033 assert!(matches!(
2034 outcome,
2035 Err(DomainError::NotFound {
2036 what: "ceremony_role"
2037 })
2038 ));
2039 assert!(instance.guard_approvals().is_empty());
2040 assert!(!instance.context().is_guard_approved(approval.name()));
2041 }
2042 fn session_with_a_contribution(
2045 definition: &CeremonyDefinition,
2046 ) -> (CeremonyInstance, CeremonyInterventionId) {
2047 let mut instance = instance(definition);
2048 let agenda_item = CeremonyInterventionId::new("queue-check").unwrap();
2049 instance
2050 .request_intervention_as(
2051 definition,
2052 agenda_item.clone(),
2053 role_id("facilitator"),
2054 CeremonyInterventionKind::Investigation,
2055 CeremonyInterventionTarget::roles([role_id("observer")]).unwrap(),
2056 CeremonyInterventionContent::new("Inspect the queue.", Attributes::empty())
2057 .unwrap(),
2058 now(),
2059 )
2060 .unwrap();
2061 instance
2062 .respond_to_intervention_as(
2063 definition,
2064 &agenda_item,
2065 role_id("observer"),
2066 CeremonyInterventionContent::new("Queue depth is stable.", Attributes::empty())
2067 .unwrap(),
2068 now(),
2069 )
2070 .unwrap();
2071 (instance, agenda_item)
2072 }
2073
2074 #[test]
2077 fn a_contribution_is_recorded_as_answering_its_agenda_item() {
2078 let definition = definition();
2079 let (instance, agenda_item) = session_with_a_contribution(&definition);
2080
2081 let [answered] = instance.reasons() else {
2082 panic!("expected exactly one reason, got {:?}", instance.reasons());
2083 };
2084 assert_eq!(answered.kind(), CeremonyReasonKind::Answers);
2085 assert_eq!(
2086 answered.from(),
2087 &CeremonyRecordRef::contribution(agenda_item.clone(), 0)
2088 );
2089 assert_eq!(answered.to(), &CeremonyRecordRef::agenda_item(agenda_item));
2090 assert_eq!(
2091 answered.asserted_by(),
2092 None,
2093 "the engine observed it; naming a seat would be inventing one"
2094 );
2095 }
2096
2097 #[test]
2100 fn a_seat_cannot_assert_what_only_the_engine_observes() {
2101 let definition = definition();
2102 let (mut instance, agenda_item) = session_with_a_contribution(&definition);
2103
2104 let outcome = instance.assert_reason_as(
2105 &definition,
2106 role_id("observer"),
2107 CeremonyRecordRef::contribution(agenda_item.clone(), 0),
2108 CeremonyRecordRef::agenda_item(agenda_item),
2109 CeremonyReasonKind::Answers,
2110 "because I say it does",
2111 MemoryConfidence::High,
2112 now(),
2113 );
2114
2115 assert!(matches!(
2116 outcome,
2117 Err(DomainError::InvariantViolated { .. })
2118 ));
2119 }
2120
2121 #[test]
2124 fn only_whoever_contributed_may_say_why_they_did() {
2125 let definition = definition();
2126 let (mut instance, agenda_item) = session_with_a_contribution(&definition);
2127 let contribution = CeremonyRecordRef::contribution(agenda_item.clone(), 0);
2128 let item = CeremonyRecordRef::agenda_item(agenda_item);
2129
2130 let by_someone_else = instance.assert_reason_as(
2131 &definition,
2132 role_id("facilitator"),
2133 contribution.clone(),
2134 item.clone(),
2135 CeremonyReasonKind::ChosenBecause,
2136 "they must have thought the queue mattered",
2137 MemoryConfidence::Low,
2138 now(),
2139 );
2140 assert!(matches!(
2141 by_someone_else,
2142 Err(DomainError::InvariantViolated { .. })
2143 ));
2144
2145 instance
2146 .assert_reason_as(
2147 &definition,
2148 role_id("observer"),
2149 contribution,
2150 item,
2151 CeremonyReasonKind::ChosenBecause,
2152 "the depth graph had been flat for an hour",
2153 MemoryConfidence::High,
2154 now(),
2155 )
2156 .expect("its author may say why");
2157 assert_eq!(instance.reasons().len(), 2);
2158 }
2159
2160 #[test]
2163 fn any_seat_may_claim_that_one_thing_came_from_another() {
2164 let definition = definition();
2165 let (mut instance, agenda_item) = session_with_a_contribution(&definition);
2166
2167 instance
2168 .assert_reason_as(
2169 &definition,
2170 role_id("facilitator"),
2171 CeremonyRecordRef::agenda_item(agenda_item.clone()),
2172 CeremonyRecordRef::contribution(agenda_item, 0),
2173 CeremonyReasonKind::FollowsFrom,
2174 "the item stayed open because the answer raised a new question",
2175 MemoryConfidence::Medium,
2176 now(),
2177 )
2178 .expect("a claim about the world is open to any seat");
2179
2180 let asserted = instance.reasons().last().unwrap();
2181 assert_eq!(asserted.confidence(), MemoryConfidence::Medium);
2182 assert_eq!(asserted.asserted_by(), Some(&role_id("facilitator")));
2183 }
2184
2185 #[test]
2188 fn a_reason_cannot_cite_something_that_never_happened() {
2189 let definition = definition();
2190 let (mut instance, agenda_item) = session_with_a_contribution(&definition);
2191
2192 let outcome = instance.assert_reason_as(
2193 &definition,
2194 role_id("observer"),
2195 CeremonyRecordRef::contribution(agenda_item.clone(), 7),
2196 CeremonyRecordRef::agenda_item(agenda_item),
2197 CeremonyReasonKind::FollowsFrom,
2198 "a contribution nobody made",
2199 MemoryConfidence::Low,
2200 now(),
2201 );
2202
2203 assert!(matches!(
2204 outcome,
2205 Err(DomainError::NotFound {
2206 what: "ceremony_record"
2207 })
2208 ));
2209 }
2210
2211 #[test]
2214 fn a_move_is_recorded_with_whoever_made_it() {
2215 let approval =
2216 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
2217 let definition = definition_with_human_guard(&approval);
2218 let mut instance = instance(&definition);
2219 instance
2220 .approve_guard(
2221 &definition,
2222 approval.name(),
2223 role_id("facilitator"),
2224 AuditActorKind::Human,
2225 now(),
2226 )
2227 .unwrap();
2228
2229 instance
2230 .apply_transition_as(
2231 &definition,
2232 &role_id("facilitator"),
2233 &trigger("approve"),
2234 datetime!(2026-06-06 12:05:00 UTC),
2235 )
2236 .unwrap();
2237
2238 let [moved] = instance.transitions() else {
2239 panic!("expected one move, got {:?}", instance.transitions());
2240 };
2241 assert_eq!(moved.trigger(), &trigger("approve"));
2242 assert_eq!(moved.from_state(), &state_id("drafting"));
2243 assert_eq!(moved.to_state(), &state_id("done"));
2244 assert_eq!(moved.applied_by(), Some(&role_id("facilitator")));
2245 }
2246
2247 #[test]
2251 fn a_move_the_engine_took_names_nobody() {
2252 let approval =
2253 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
2254 let definition = definition_with_human_guard(&approval);
2255 let mut instance = instance(&definition);
2256 instance
2257 .approve_guard(
2258 &definition,
2259 approval.name(),
2260 role_id("facilitator"),
2261 AuditActorKind::Human,
2262 now(),
2263 )
2264 .unwrap();
2265
2266 instance
2267 .apply_transition(&definition, &trigger("approve"), now())
2268 .unwrap();
2269
2270 assert_eq!(instance.transitions()[0].applied_by(), None);
2271 }
2272 #[test]
2282 fn an_approval_records_the_kind_it_was_told_not_the_one_the_guard_wanted() {
2283 let approval =
2284 CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
2285 let definition = definition_with_human_guard(&approval);
2286 let mut instance = instance(&definition);
2287
2288 instance
2289 .approve_guard(
2290 &definition,
2291 approval.name(),
2292 role_id("facilitator"),
2293 AuditActorKind::Agent,
2294 now(),
2295 )
2296 .unwrap();
2297
2298 let [recorded] = instance.guard_approvals() else {
2299 panic!("expected one approval");
2300 };
2301 assert_eq!(
2302 recorded.approved_by_kind(),
2303 AuditActorKind::Agent,
2304 "the guard asked for a human and an agent answered; saying otherwise \
2305 would be the engine vouching for something it cannot see"
2306 );
2307 assert!(instance.context().is_guard_approved(approval.name()));
2308 }
2309}