Skip to main content

made_core/entities/
ceremony_instance.rs

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